Solana Security

Anchor Program Security: Securing Solana Smart Contracts with the Anchor Framework

Kennedy OwiroOctober 30, 20259 min read

Anchor is Solana's most popular framework for building programs, handling account serialization, instruction routing, and validation boilerplate. But Anchor's convenience can create a false sense of security. Many of the biggest Solana exploits hit Anchor programs where developers relied on defaults without understanding what's actually being validated.

Anchor's Security Model

Anchor validates accounts through the #[derive(Accounts)] struct. Each field's type and constraints determine what gets checked. The key insight: you must explicitly declare what to validate. Anchor doesn't magically know your security requirements.

1. Account Validation: Getting It Right

#[derive(Accounts)]
pub struct TransferFunds<'info> {
    // ✅ Payer & signer: verified automatically
    #[account(mut)]
    pub authority: Signer<'info>,

    // ✅ PDA with seeds validation + owner check
    #[account(
        mut,
        seeds = [b"vault", authority.key().as_ref()],
        bump = vault.bump,
        has_one = authority,  // Ensures vault.authority == authority.key()
    )]
    pub vault: Account<'info, VaultState>,

    // ✅ Token account with mint & authority constraints
    #[account(
        mut,
        associated_token::mint = mint,
        associated_token::authority = authority,
    )]
    pub user_token_account: Account<'info, TokenAccount>,

    pub mint: Account<'info, Mint>,
    pub token_program: Program<'info, Token>,
}

2. Common Anchor Mistakes

Using UncheckedAccount / AccountInfo without validation

// ❌ DANGEROUS: No validation at all
/// CHECK: This is safe (narrator: it was not safe)
pub untrusted_account: UncheckedAccount<'info>,

// ✅ SAFE: Add explicit constraints
/// CHECK: Validated by constraint
#[account(constraint = some_account.key() == expected_key)]
pub some_account: UncheckedAccount<'info>,

Missing has_one Constraints

Without has_one, an attacker can pass a vault owned by someone else and drain it.

Forgetting close Constraints

// When closing accounts, must send rent to someone
#[account(
    mut,
    close = authority,  // Sends lamports to authority and zeros data
    seeds = [b"position", authority.key().as_ref()],
    bump,
)]
pub position: Account<'info, Position>,

3. CPI Security

// ✅ SAFE: Use CpiContext with explicit program
let cpi_ctx = CpiContext::new(
    ctx.accounts.token_program.to_account_info(),
    Transfer {
        from: ctx.accounts.vault_token.to_account_info(),
        to: ctx.accounts.user_token.to_account_info(),
        authority: ctx.accounts.vault_authority.to_account_info(),
    },
);
// Sign with PDA seeds
let seeds = &[b"authority", &[bump]];
token::transfer(cpi_ctx.with_signer(&[seeds]), amount)?;

Anchor Security Checklist

  • ✅ Every account has appropriate constraints (seeds, has_one, token::mint, etc.)
  • ✅ No AccountInfo or UncheckedAccount without documented validation
  • ✅ Use checked_add/checked_sub for all arithmetic
  • ✅ Verify CPI target program IDs
  • ✅ Close accounts properly to prevent rent drain
  • ✅ Use bumps from account state, not recalculated

Anchor handles the boilerplate but not the thinking. Submit your Anchor program for a full security audit.

AnchorSolanaRustprogram securityaccount validationCPI
Share

Written by

Kennedy Owiro

Founder & CTO, Vultbase

14+ years building security and QA systems at scale. Background in fintech security and Web3 smart contract testing. Built Vultbase's Intelligence Engine with 1,200+ exploit patterns from $40B+ in historical DeFi losses.

Protect your protocol before launch.

Submit your smart contracts for automated security analysis powered by 1,200+ real exploit patterns.

Start Your Audit →