How I Built a Counter Program in Rust and Learned to Trust My Tests
DEV Community

How I Built a Counter Program in Rust and Learned to Trust My Tests

Building smart contracts on Solana using Rust and the Anchor framework requires a mindset shift from traditional Web2 backends. This week, I built a counter program, broke it on purpose, and used my test suite to verify that my security constraints were truly load-bearing. Here is how the program works under the hood and why every test in the suite exists.

The Initialize Accounts Struct

In Anchor, security boundaries are enforced before your instruction logic ever runs. The Initialize context defines three main accounts:

  • counter: Initialized as a new on-chain account allocated with exact bytes (8 bytes for Anchor's discriminator, 32 for the authority's public key, and 8 for the count value).
  • authority: Marked as a mutable signer who pays the account creation rent.
  • system_program: The native Solana System Program required to execute account creation.

Handler Logic & Constraints

Because Anchor handles account creation and validation in the background, handler logic remains minimal.

Initialize Handler

The initialize handler receives the context, sets the counter account's authority field to match the transaction signer's public key, and sets the initial count state to zero.

Increment Instruction with Constraints

For the increment logic, Anchor uses an account constraint: has_one = authority, directly on the account context. This guarantees that the key in counter.authority matches the signer's wallet before any custom code executes. If an unauthorized wallet attempts to trigger an increment, Anchor rejects the transaction immediately at the constraint level.

Testing the Happy and Failure Paths

To prove these security checks work, I wrote unit tests for both valid execution and unauthorized attempts using LiteSVM.

  1. Happy Path: Successful Initialization
    • The Test: Executes the initialize instruction and asserts that the fetched account's count value equals zero.
    • Why it exists: If space allocation fails or account deserialization breaks, this test fails because the on-chain state won't reflect the expected starting value.
  2. Failure Path: Unauthorized Increment Attempt
    • The Test: Attempts to trigger the increment instruction using an unauthorized attacker keypair, expecting the call to throw a ConstraintHasOne error.
    • Why it exists: If someone accidentally removes the has_one = authority check from the struct, this test fails, warning us that arbitrary wallets can mutate state they don't own.

Planting Bugs to Test the Tests

A test suite that passes isn't helpful unless it fails when the program breaks. To test my harness, I deliberately introduced a bug by changing the counter increment logic from adding 1 to adding 2 in the handler. When running the test suite, it immediately caught the regression, throwing an assertion error showing an expected result of 1 against an actual result of 2. Seeing the assertion catch the precise mutation gave me complete confidence that my test suite was active and load-bearing rather than just passing silently.

What’s Next

Writing this post highlighted a gap in my understanding around program derived addresses (PDAs). We will continue working with PDAs and Rust.

Read on DEV Community ↗ ← Back to News

Comments

No comments yet. Start the discussion.