State management in Anchor programs involves creating, reading, updating, and closing accounts that store your program’s data. Anchor provides powerful abstractions through the #[account] macro and account constraints to make state management safe and ergonomic.
The #[account] Macro
The #[account] macro is applied to structs to create custom account types for your program. It automatically implements several traits:
- Owner: Sets the account owner to the program ID (from
declare_id!)
- AccountSerialize and AccountDeserialize: Handles serialization
- Discriminator: Adds an 8-byte discriminator to distinguish account types
Basic Account Definition
#[account]
pub struct MyAccount {
pub data: u64,
pub authority: Pubkey,
pub created_at: i64,
}
The #[account] macro adds an 8-byte discriminator at the start, so when calculating space:
// Space = 8 (discriminator) + account data size
space = 8 + 8 + 32 + 8 // discriminator + u64 + Pubkey + i64
InitSpace Derive Macro
For easier space calculation, use the InitSpace derive macro:
#[account]
#[derive(InitSpace)]
pub struct MyAccount {
pub data: u64,
pub authority: Pubkey,
#[max_len(50)]
pub name: String,
#[max_len(10)]
pub tags: Vec<u8>,
}
// Usage in accounts struct:
#[account(
init,
payer = authority,
space = 8 + MyAccount::INIT_SPACE
)]
pub my_account: Account<'info, MyAccount>,
Account Initialization
To create and initialize a new account, use the init constraint:
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(
init,
payer = user,
space = 8 + MyAccount::INIT_SPACE
)]
pub my_account: Account<'info, MyAccount>,
#[account(mut)]
pub user: Signer<'info>,
pub system_program: Program<'info, System>,
}
The init constraint:
- Creates the account via CPI to the system program
- Allocates the specified space
- Assigns ownership to your program
- Sets the account discriminator
- Requires
payer and space parameters
- Requires
system_program in the accounts struct
Initialization with PDA
For PDA (Program Derived Address) accounts:
#[derive(Accounts)]
pub struct InitializePda<'info> {
#[account(
init,
payer = user,
space = 8 + MyAccount::INIT_SPACE,
seeds = [b"my_account", user.key().as_ref()],
bump
)]
pub my_account: Account<'info, MyAccount>,
#[account(mut)]
pub user: Signer<'info>,
pub system_program: Program<'info, System>,
}
Reading Account Data
Access account data through the Context:
pub fn read_data(ctx: Context<ReadData>) -> Result<()> {
let account = &ctx.accounts.my_account;
msg!("Data: {}", account.data);
msg!("Authority: {}", account.authority);
Ok(())
}
#[derive(Accounts)]
pub struct ReadData<'info> {
pub my_account: Account<'info, MyAccount>,
}
Updating Account Data
To modify account data, mark the account as mut (mutable):
pub fn update_data(ctx: Context<UpdateData>, new_value: u64) -> Result<()> {
let account = &mut ctx.accounts.my_account;
account.data = new_value;
msg!("Updated data to: {}", new_value);
Ok(())
}
#[derive(Accounts)]
pub struct UpdateData<'info> {
#[account(mut)]
pub my_account: Account<'info, MyAccount>,
}
Anchor automatically serializes the account data back to the account when the instruction completes successfully.
Account Constraints
Use constraints to enforce security and business logic:
has_one Constraint
Verifies that an account field matches a provided account:
#[derive(Accounts)]
pub struct UpdateData<'info> {
#[account(
mut,
has_one = authority
)]
pub my_account: Account<'info, MyAccount>,
pub authority: Signer<'info>,
}
This checks that my_account.authority == authority.key().
constraint Constraint
Custom validation logic:
#[derive(Accounts)]
pub struct UpdateData<'info> {
#[account(
mut,
constraint = my_account.data < 100 @ ErrorCode::DataTooLarge
)]
pub my_account: Account<'info, MyAccount>,
}
Closing Accounts
Reclaim SOL rent by closing accounts with the close constraint:
pub fn close_account(ctx: Context<CloseAccount>) -> Result<()> {
msg!("Closing account");
Ok(())
}
#[derive(Accounts)]
pub struct CloseAccount<'info> {
#[account(
mut,
close = authority,
has_one = authority
)]
pub my_account: Account<'info, MyAccount>,
#[account(mut)]
pub authority: Signer<'info>,
}
The close constraint:
- Transfers all lamports to the specified account
- Zeroes out the account data
- Marks the account for garbage collection
Reallocating Accounts
Increase or decrease account size with realloc:
#[derive(Accounts)]
pub struct ReallocAccount<'info> {
#[account(
mut,
realloc = 8 + MyAccount::INIT_SPACE + 100,
realloc::payer = authority,
realloc::zero = true,
)]
pub my_account: Account<'info, MyAccount>,
#[account(mut)]
pub authority: Signer<'info>,
pub system_program: Program<'info, System>,
}
Complete Example
Here’s a complete counter program demonstrating state management:
use anchor_lang::prelude::*;
declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");
#[program]
pub mod counter {
use super::*;
pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
let counter = &mut ctx.accounts.counter;
counter.authority = ctx.accounts.authority.key();
counter.count = 0;
msg!("Counter initialized");
Ok(())
}
pub fn increment(ctx: Context<Update>) -> Result<()> {
let counter = &mut ctx.accounts.counter;
counter.count = counter.count.checked_add(1)
.ok_or(ErrorCode::Overflow)?;
msg!("Counter: {}", counter.count);
Ok(())
}
pub fn decrement(ctx: Context<Update>) -> Result<()> {
let counter = &mut ctx.accounts.counter;
counter.count = counter.count.checked_sub(1)
.ok_or(ErrorCode::Underflow)?;
msg!("Counter: {}", counter.count);
Ok(())
}
pub fn close(ctx: Context<Close>) -> Result<()> {
msg!("Counter closed");
Ok(())
}
}
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(
init,
payer = authority,
space = 8 + Counter::INIT_SPACE,
seeds = [b"counter", authority.key().as_ref()],
bump
)]
pub counter: Account<'info, Counter>,
#[account(mut)]
pub authority: Signer<'info>,
pub system_program: Program<'info, System>,
}
#[derive(Accounts)]
pub struct Update<'info> {
#[account(
mut,
has_one = authority
)]
pub counter: Account<'info, Counter>,
pub authority: Signer<'info>,
}
#[derive(Accounts)]
pub struct Close<'info> {
#[account(
mut,
close = authority,
has_one = authority
)]
pub counter: Account<'info, Counter>,
#[account(mut)]
pub authority: Signer<'info>,
}
#[account]
#[derive(InitSpace)]
pub struct Counter {
pub authority: Pubkey,
pub count: u64,
}
#[error_code]
pub enum ErrorCode {
#[msg("Counter overflow")]
Overflow,
#[msg("Counter underflow")]
Underflow,
}
init_if_needed
For accounts that might already exist, use init_if_needed:
#[account(
init_if_needed,
payer = user,
space = 8 + MyAccount::INIT_SPACE
)]
pub my_account: Account<'info, MyAccount>,
Be careful with init_if_needed as it can introduce security vulnerabilities if not used properly. Always validate the account state after initialization.
Best Practices
- Use InitSpace: Leverage
#[derive(InitSpace)] for automatic space calculation
- Validate ownership: Always use
has_one or other constraints to verify account relationships
- Check arithmetic: Use checked math operations to prevent overflows
- Close unused accounts: Reclaim rent by closing accounts when done
- Use PDAs for deterministic addresses: Prefer PDAs over keypair-based accounts
- Minimize account size: Only store necessary data to reduce rent costs