4. Account Constraints
Learn how to validate accounts and enforce security rules using Anchor’s constraint system.
Common Constraints
Source: instructions/config.rs
use anchor_lang::prelude::*;
use crate::{constants::*, state::*, error::ErrorCode};
#[derive(Accounts)]
pub struct UpdateConfig<'info> {
#[account(
mut, // Account is mutable
seeds = [SEED_PROGRAM_CONFIG], // PDA seeds
bump = program_config.bump, // Use stored bump
has_one = admin @ ErrorCode::Unauthorized // Validate admin field
)]
pub program_config: Account<'info, ProgramConfig>,
pub admin: Signer<'info>, // Must sign transaction
}
pub fn update_config_handler(
ctx: Context<UpdateConfig>,
new_fee_destination: Pubkey,
new_fee_basis_points: u64,
) -> Result<()> {
let config = &mut ctx.accounts.program_config;
config.fee_destination = new_fee_destination;
config.fee_basis_points = new_fee_basis_points;
msg!("Config updated by admin: {}", ctx.accounts.admin.key());
Ok(())
}
Constraint Breakdown
| Constraint | Purpose |
|---|---|
mut | Account data can be modified |
seeds = [...] | Validate PDA derivation |
bump = value | Use stored bump seed |
has_one = field | Validate field matches another account |
Built-in Constraints
1. Mutability
#[account(mut)] // Can modify account data
pub user_account: Account<'info, UserAccount>,
#[account(mut)] // Can modify lamports (for rent, transfers)
pub payer: Signer<'info>,
2. Seeds Validation
#[account(
seeds = [SEED_USER_ACCOUNT, authority.key().as_ref()],
bump = user_account.bump
)]
pub user_account: Account<'info, UserAccount>,
3. Field Validation (has_one)
// Validates: user_account.authority == authority.key()
#[account(
has_one = authority @ ErrorCode::Unauthorized
)]
pub user_account: Account<'info, UserAccount>,
pub authority: Signer<'info>,
4. Owner Validation
// Ensures account is owned by specified program
#[account(
owner = token_program.key()
)]
pub token_account: AccountInfo<'info>,
pub token_program: Program<'info, Token>,
Custom Constraints
Source: instructions/treasury.rs
#[derive(Accounts)]
pub struct WithdrawFromTreasury<'info> {
#[account(
mut,
seeds = [SEED_TREASURY],
bump = treasury.bump,
// Custom constraint: circuit breaker must be inactive
constraint = !treasury.circuit_breaker_active @ ErrorCode::ProgramPaused
)]
pub treasury: Account<'info, Treasury>,
#[account(
seeds = [SEED_PROGRAM_CONFIG],
bump = program_config.bump,
)]
pub program_config: Account<'info, ProgramConfig>,
#[account(
mut,
// Custom constraint: signer must be admin
constraint = authority.key() == program_config.admin @ ErrorCode::Unauthorized
)]
pub authority: Signer<'info>,
#[account(mut)]
/// CHECK: Destination for withdrawn funds
pub destination: AccountInfo<'info>,
pub system_program: Program<'info, System>,
}
Constraint Syntax
constraint = <boolean_expression> @ ErrorCode::YourError
Examples:
// Simple comparison
constraint = amount > 0 @ ErrorCode::InvalidAmount
// Multiple conditions with &&
constraint = user.is_active && !user.is_banned @ ErrorCode::UserNotActive
// Range check
constraint = fee_bps <= 10000 @ ErrorCode::FeeTooHigh
// Access other accounts
constraint = signer.key() == config.admin @ ErrorCode::Unauthorized
Account Lifecycle Constraints
Init - Create New Account
#[account(
init, // Create new account
payer = payer, // Who pays rent
space = 8 + UserAccount::LEN, // Size (discriminator + data)
seeds = [SEED_USER_ACCOUNT, authority.key().as_ref()],
bump
)]
pub user_account: Account<'info, UserAccount>,
#[account(mut)]
pub payer: Signer<'info>,
pub system_program: Program<'info, System>,
Required accounts for init:
payer(mutable signer)system_program
Close - Delete Account and Reclaim Rent
Source: instructions/user.rs
#[derive(Accounts)]
pub struct CloseUserAccount<'info> {
#[account(
mut,
close = authority, // Close account, send rent to authority
seeds = [SEED_USER_ACCOUNT, authority.key().as_ref()],
bump = user_account.bump,
has_one = authority @ ErrorCode::Unauthorized
)]
pub user_account: Account<'info, UserAccount>,
#[account(mut)]
pub authority: Signer<'info>,
}
pub fn close_user_account_handler(ctx: Context<CloseUserAccount>) -> Result<()> {
// Account is automatically closed by Anchor
// Rent is sent to 'authority'
msg!("User account closed: {}", ctx.accounts.authority.key());
Ok(())
}
Realloc - Resize Account
#[account(
mut,
realloc = 8 + UserAccount::new_size(),
realloc::payer = payer,
realloc::zero = false, // Don't zero out new space
seeds = [SEED_USER_ACCOUNT, authority.key().as_ref()],
bump = user_account.bump
)]
pub user_account: Account<'info, UserAccount>,
#[account(mut)]
pub payer: Signer<'info>,
pub system_program: Program<'info, System>,
Init If Needed
Creates account if it doesn’t exist, otherwise uses existing:
Source: instructions/token.rs
use anchor_spl::token_interface::{TokenAccount, Mint};
#[derive(Accounts)]
pub struct MintTokens<'info> {
#[account(mut)]
pub signer: Signer<'info>,
#[account(
init_if_needed, // Create if doesn't exist, skip if exists
payer = signer,
associated_token::mint = mint,
associated_token::authority = signer,
associated_token::token_program = token_program,
)]
pub token_account: InterfaceAccount<'info, TokenAccount>,
pub mint: InterfaceAccount<'info, Mint>,
pub token_program: Interface<'info, TokenInterface>,
pub associated_token_program: Program<'info, AssociatedToken>,
pub system_program: Program<'info, System>,
}
⚠️ Warning: init_if_needed can be a security risk if not used carefully. Ensure proper validation.
Token Account Constraints
Associated Token Account
#[account(
associated_token::mint = mint,
associated_token::authority = owner,
associated_token::token_program = token_program,
)]
pub token_account: InterfaceAccount<'info, TokenAccount>,
Token Mint Constraints
#[account(
init,
payer = payer,
mint::decimals = 6,
mint::authority = mint_authority,
mint::token_program = token_program,
seeds = [b"mint"],
bump
)]
pub mint: InterfaceAccount<'info, Mint>,
Unsafe Accounts (/// CHECK:)
When using AccountInfo without type validation:
#[account(mut)]
/// CHECK: This account is validated manually in the instruction
pub arbitrary_account: AccountInfo<'info>,
Always add /// CHECK: comment explaining why it’s safe.
When to use:
- CPI targets that aren’t typed
- Accounts with dynamic types
- System accounts (recipient addresses)
Manual validation example:
// Validate owner
require_keys_eq!(
arbitrary_account.owner,
expected_program_id,
ErrorCode::InvalidOwner
);
// Validate discriminator
let data = arbitrary_account.try_borrow_data()?;
require_eq!(
&data[0..8],
&UserAccount::discriminator(),
ErrorCode::InvalidAccountType
);
Constraint Combinations
Full Example
#[derive(Accounts)]
pub struct ComplexInstruction<'info> {
#[account(
init, // Lifecycle
payer = payer,
space = 8 + MyAccount::LEN,
seeds = [b"my_account", owner.key().as_ref()], // PDA
bump,
constraint = initial_value > 0 @ ErrorCode::InvalidValue // Custom
)]
pub my_account: Account<'info, MyAccount>,
#[account(
mut, // Mutability
has_one = owner @ ErrorCode::Unauthorized, // Field validation
constraint = config.is_active @ ErrorCode::ConfigPaused // Custom
)]
pub config: Account<'info, Config>,
pub owner: Signer<'info>,
#[account(mut)]
pub payer: Signer<'info>,
pub system_program: Program<'info, System>,
}
Common Constraint Patterns
| Use Case | Constraint |
|---|---|
| Admin-only | constraint = signer.key() == config.admin |
| Amount validation | constraint = amount > 0 && amount <= max |
| State check | constraint = !account.is_paused |
| Time-based | constraint = clock.unix_timestamp >= unlock_time |
| Whitelist | constraint = whitelist.contains(&user.key()) |
| Balance check | constraint = account.lamports() >= min_balance |
Best Practices
✅ Use has_one when validating account fields
✅ Always specify error codes with @
✅ Combine multiple constraints for complex validation
✅ Document /// CHECK: for unsafe accounts
✅ Validate early - constraints run before instruction logic
✅ Use constraint = for business logic validation
❌ Don’t skip validation - assume all inputs are malicious
❌ Don’t use init_if_needed without careful consideration
❌ Don’t forget mut on accounts you modify
Debugging Constraints
Common Errors
| Error | Cause | Solution |
|---|---|---|
| “A seeds constraint was violated” | Wrong PDA seeds | Check seed values and order |
| “A has_one constraint was violated” | Field doesn’t match account | Verify account relationships |
| “A raw constraint was violated” | Custom constraint failed | Check constraint logic |
| “Account not mutable” | Missing mut | Add mut to account |
Testing Constraints
// Test should fail with specific error
try {
await program.methods
.restrictedInstruction()
.accounts({...})
.rpc();
expect.fail("Should have thrown Unauthorized error");
} catch (error) {
expect(error.error.errorCode.code).to.equal("Unauthorized");
}
Next: Error Handling →