This week I built my first Solana program with Anchor: a simple counter that starts at 0 and can only be incremented by its owner.
The first thing that helped me understand Anchor was the Initialize accounts struct:
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(
init,
payer = authority,
space = 8 + Counter::INIT_SPACE,
)]
pub counter: Account<'info, Counter>,
#[account(mut)]
pub authority: Signer<'info>,
pub system_program: Program<'info, System>,
}
The counter account is the on-chain account that stores the data. The authority account is the wallet creating the counter and paying for the account creation. The system_program is needed because Solana uses it to create new accounts.
The initialize handler is very short:
pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
let counter = &mut ctx.accounts.counter;
counter.authority = ctx.accounts.authority.key();
counter.count = 0;
Ok(())
}
ctx.accounts gives me access to all the validated accounts from the Initialize struct. Here I simply save the owner's wallet address and set the counter value to zero.
The increment instruction is also small:
pub fn increment(ctx: Context<Increment>) -> Result<()> {
let counter = &mut ctx.accounts.counter;
counter.count = counter.count
.checked_add(1)
.ok_or(ProgramError::ArithmeticOverflow)?;
Ok(())
}
The important security check is actually here:
#[derive(Accounts)]
pub struct Increment<'info> {
#[account(mut, has_one = authority)]
pub counter: Account<'info, Counter>,
pub authority: Signer<'info>,
}
The has_one = authority constraint makes sure only the wallet that owns the counter can increment it. Anchor checks this before the instruction logic runs.
Testing the Happy Path
My main test initializes a counter, increments it, and checks that everything was stored correctly.
#[test]
fn initialize_then_increment() {
// initialize counter
// increment counter
// verify count == 1
// verify authority is correct
}
This test would fail if the counter was not initialized correctly or if incrementing did not update the value as expected.
Testing the Failure Path
I also wrote a test to make sure a different wallet cannot increment someone else's counter.
#[test]
fn increment_fails_when_wrong_authority_signs() {
// create counter with authority_a
// attempt increment with authority_b
// expect transaction to fail
}
This test would fail if my ownership check stopped working and unauthorized users were allowed to modify the counter.
Breaking the Program on Purpose
For Day 61, I intentionally introduced bugs to make sure my tests were actually protecting me.
One experiment changed:
.checked_add(1)
to:
.checked_add(2)
The transaction still succeeded, but the test failed because the counter became 2 instead of 1.
The failing assertion looked like this:
assertion `left == right` failed
left: 2
right: 1
That was a great reminder that a transaction can succeed while still producing the wrong result. Without the test, I might not have noticed the bug.
What I'd Build Next
If I had another week, I'd expand this project by supporting multiple counters per user, and adding more edge-case tests.
The biggest lesson from this project was that writing tests is useful, but intentionally breaking the program and watching the tests catch the bug is what really builds confidence in them.
Top comments (0)