Skip to main content
Security is paramount when building Solana programs. Anchor provides powerful tools to help you write secure code, but understanding common vulnerabilities and best practices is essential.

Account Validation

Always Validate Account Ownership

One of the most critical security checks is verifying that accounts are owned by the expected program:
#[derive(Accounts)]
pub struct UpdateData<'info> {
    // Anchor automatically checks that Account<'info, MyAccount>
    // is owned by the current program
    #[account(mut)]
    pub my_account: Account<'info, MyAccount>,
}
The Account<'info, T> type automatically verifies:
  • account.owner == T::owner()
  • Account is not owned by SystemProgram with 0 lamports
  • Account discriminator matches the expected type

Validate Signers

Always verify that accounts expected to authorize operations have signed:
#[derive(Accounts)]
pub struct Transfer<'info> {
    #[account(mut, has_one = authority)]
    pub vault: Account<'info, Vault>,
    // Use Signer type to ensure the account signed
    pub authority: Signer<'info>,
}
Bad - Using AccountInfo without verification:
pub authority: AccountInfo<'info>, // ❌ No signer check!
Good - Using Signer type:
pub authority: Signer<'info>, // ✅ Automatically checked

Use has_one Constraint

The has_one constraint verifies account relationships:
#[account]
pub struct Vault {
    pub authority: Pubkey,
    pub balance: u64,
}

#[derive(Accounts)]
pub struct Withdraw<'info> {
    #[account(
        mut,
        has_one = authority @ ErrorCode::Unauthorized
    )]
    pub vault: Account<'info, Vault>,
    pub authority: Signer<'info>,
}
This checks that vault.authority == authority.key().

Common Security Vulnerabilities

1. Missing Signer Checks

Vulnerable:
pub fn withdraw(ctx: Context<Withdraw>, amount: u64) -> Result<()> {
    // ❌ No check that authority signed!
    ctx.accounts.vault.balance -= amount;
    Ok(())
}

#[derive(Accounts)]
pub struct Withdraw<'info> {
    #[account(mut)]
    pub vault: Account<'info, Vault>,
    pub authority: AccountInfo<'info>, // ❌ Should be Signer
}
Secure:
#[derive(Accounts)]
pub struct Withdraw<'info> {
    #[account(
        mut,
        has_one = authority
    )]
    pub vault: Account<'info, Vault>,
    pub authority: Signer<'info>, // ✅ Enforces signature
}

2. Missing Ownership Checks

Vulnerable:
#[derive(Accounts)]
pub struct Attack<'info> {
    /// CHECK: ❌ This is dangerous! No ownership check
    pub fake_account: AccountInfo<'info>,
}
An attacker could pass an account from a different program. Secure:
#[derive(Accounts)]
pub struct Secure<'info> {
    // ✅ Automatically checks ownership
    pub real_account: Account<'info, MyAccount>,
}

3. Arithmetic Overflow/Underflow

Vulnerable:
pub fn deposit(ctx: Context<Deposit>, amount: u64) -> Result<()> {
    ctx.accounts.vault.balance += amount; // ❌ Could overflow
    Ok(())
}
Secure:
pub fn deposit(ctx: Context<Deposit>, amount: u64) -> Result<()> {
    ctx.accounts.vault.balance = ctx.accounts.vault.balance
        .checked_add(amount)
        .ok_or(ErrorCode::Overflow)?; // ✅ Safe arithmetic
    Ok(())
}

4. Reinitialization Attacks

Vulnerable:
#[derive(Accounts)]
pub struct Initialize<'info> {
    #[account(init, payer = user, space = 8 + 32)]
    pub account: Account<'info, MyAccount>, // ❌ Could reinitialize
}
If an account already exists, init will fail, but without proper checks, you might allow reinitialization. Secure - Use init only once:
#[derive(Accounts)]
pub struct Initialize<'info> {
    #[account(
        init,
        payer = user,
        space = 8 + MyAccount::INIT_SPACE
    )]
    pub account: Account<'info, MyAccount>, // ✅ Fails if exists
}

// Or for updates:
#[derive(Accounts)]
pub struct Update<'info> {
    #[account(mut)]
    pub account: Account<'info, MyAccount>, // ✅ Requires existing account
}

5. PDA Validation

Vulnerable:
#[derive(Accounts)]
pub struct UsePda<'info> {
    /// CHECK: ❌ Not verifying PDA derivation
    pub pda: AccountInfo<'info>,
}
Secure:
#[derive(Accounts)]
pub struct UsePda<'info> {
    #[account(
        seeds = [b"vault", authority.key().as_ref()],
        bump
    )]
    pub pda: Account<'info, Vault>, // ✅ Verifies correct PDA
    pub authority: Signer<'info>,
}

6. Account Closing Vulnerabilities

Vulnerable - Revival attacks:
pub fn close_account(ctx: Context<Close>) -> Result<()> {
    // ❌ Manual closing is dangerous
    **ctx.accounts.account.to_account_info().lamports.borrow_mut() = 0;
    Ok(())
}
Secure:
#[derive(Accounts)]
pub struct Close<'info> {
    #[account(
        mut,
        close = authority, // ✅ Anchor handles closing safely
        has_one = authority
    )]
    pub account: Account<'info, MyAccount>,
    #[account(mut)]
    pub authority: Signer<'info>,
}

7. Duplicate Mutable Accounts

Vulnerable:
#[derive(Accounts)]
pub struct DangerousAccounts<'info> {
    #[account(mut)]
    pub account1: Account<'info, MyAccount>,
    #[account(mut)]
    pub account2: Account<'info, MyAccount>, // ❌ Could be same as account1
}
By default, Anchor prevents this. To allow it intentionally:
#[derive(Accounts)]
pub struct AllowDuplicates<'info> {
    #[account(mut)]
    pub account1: Account<'info, MyAccount>,
    #[account(mut, dup)] // ✅ Explicitly allow duplicates
    pub account2: Account<'info, MyAccount>,
}

Security Checklist

Before deploying your program, verify:
  • All authority accounts use Signer<'info> type
  • All account relationships validated with has_one or constraint
  • All PDAs validated with seeds and bump
  • All arithmetic uses checked operations
  • All accounts use proper types (Account, Signer, etc., not raw AccountInfo)
  • Account discriminators checked (automatic with Account type)
  • Close constraints used instead of manual closing
  • No unintentional duplicate mutable accounts
  • All /// CHECK: comments explain why validation is skipped
  • Token amounts and balances validated
  • Time-based logic uses Clock sysvar correctly

Complete Secure Example

Here’s a secure token vault implementation:
use anchor_lang::prelude::*;
use anchor_spl::token::{self, Token, TokenAccount, Transfer};

declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");

#[program]
pub mod secure_vault {
    use super::*;

    pub fn initialize(
        ctx: Context<Initialize>,
        vault_bump: u8,
    ) -> Result<()> {
        let vault = &mut ctx.accounts.vault;
        vault.authority = ctx.accounts.authority.key();
        vault.token_account = ctx.accounts.vault_token_account.key();
        vault.bump = vault_bump;
        vault.total_deposited = 0;
        
        msg!("Vault initialized");
        Ok(())
    }

    pub fn deposit(
        ctx: Context<Deposit>,
        amount: u64,
    ) -> Result<()> {
        require!(amount > 0, ErrorCode::InvalidAmount);
        
        let vault = &mut ctx.accounts.vault;
        
        // Safe arithmetic
        vault.total_deposited = vault.total_deposited
            .checked_add(amount)
            .ok_or(ErrorCode::Overflow)?;
        
        // Transfer tokens via CPI
        let cpi_accounts = Transfer {
            from: ctx.accounts.user_token_account.to_account_info(),
            to: ctx.accounts.vault_token_account.to_account_info(),
            authority: ctx.accounts.user.to_account_info(),
        };
        let cpi_program = ctx.accounts.token_program.to_account_info();
        let cpi_ctx = CpiContext::new(cpi_program, cpi_accounts);
        
        token::transfer(cpi_ctx, amount)?;
        
        msg!("Deposited {} tokens", amount);
        Ok(())
    }

    pub fn withdraw(
        ctx: Context<Withdraw>,
        amount: u64,
    ) -> Result<()> {
        require!(amount > 0, ErrorCode::InvalidAmount);
        
        let vault = &mut ctx.accounts.vault;
        let available = ctx.accounts.vault_token_account.amount;
        
        require!(amount <= available, ErrorCode::InsufficientFunds);
        
        // Safe arithmetic
        vault.total_deposited = vault.total_deposited
            .checked_sub(amount)
            .ok_or(ErrorCode::Underflow)?;
        
        // PDA signing
        let authority_seeds = &[
            b"vault",
            vault.authority.as_ref(),
            &[vault.bump],
        ];
        let signer_seeds = &[&authority_seeds[..]];
        
        let cpi_accounts = Transfer {
            from: ctx.accounts.vault_token_account.to_account_info(),
            to: ctx.accounts.user_token_account.to_account_info(),
            authority: ctx.accounts.vault_authority.to_account_info(),
        };
        let cpi_program = ctx.accounts.token_program.to_account_info();
        let cpi_ctx = CpiContext::new_with_signer(
            cpi_program,
            cpi_accounts,
            signer_seeds,
        );
        
        token::transfer(cpi_ctx, amount)?;
        
        msg!("Withdrew {} tokens", amount);
        Ok(())
    }
}

#[derive(Accounts)]
pub struct Initialize<'info> {
    #[account(
        init,
        payer = authority,
        space = 8 + Vault::INIT_SPACE,
        seeds = [b"vault", authority.key().as_ref()],
        bump
    )]
    pub vault: Account<'info, Vault>,
    
    #[account(
        constraint = vault_token_account.owner == vault_authority.key()
    )]
    pub vault_token_account: Account<'info, TokenAccount>,
    
    /// CHECK: PDA authority for vault
    #[account(
        seeds = [b"vault", authority.key().as_ref()],
        bump
    )]
    pub vault_authority: AccountInfo<'info>,
    
    #[account(mut)]
    pub authority: Signer<'info>,
    pub system_program: Program<'info, System>,
}

#[derive(Accounts)]
pub struct Deposit<'info> {
    #[account(mut)]
    pub vault: Account<'info, Vault>,
    
    #[account(
        mut,
        constraint = vault_token_account.key() == vault.token_account
    )]
    pub vault_token_account: Account<'info, TokenAccount>,
    
    #[account(mut)]
    pub user_token_account: Account<'info, TokenAccount>,
    
    pub user: Signer<'info>,
    pub token_program: Program<'info, Token>,
}

#[derive(Accounts)]
pub struct Withdraw<'info> {
    #[account(
        mut,
        has_one = authority @ ErrorCode::Unauthorized,
        has_one = token_account @ ErrorCode::InvalidTokenAccount
    )]
    pub vault: Account<'info, Vault>,
    
    #[account(
        mut,
        constraint = vault_token_account.key() == vault.token_account
    )]
    pub vault_token_account: Account<'info, TokenAccount>,
    
    /// CHECK: PDA authority validated via seeds
    #[account(
        seeds = [b"vault", authority.key().as_ref()],
        bump = vault.bump
    )]
    pub vault_authority: AccountInfo<'info>,
    
    #[account(mut)]
    pub user_token_account: Account<'info, TokenAccount>,
    
    pub authority: Signer<'info>,
    pub token_program: Program<'info, Token>,
}

#[account]
#[derive(InitSpace)]
pub struct Vault {
    pub authority: Pubkey,
    pub token_account: Pubkey,
    pub total_deposited: u64,
    pub bump: u8,
}

#[error_code]
pub enum ErrorCode {
    #[msg("Amount must be greater than zero")]
    InvalidAmount,
    #[msg("Insufficient funds")]
    InsufficientFunds,
    #[msg("Unauthorized access")]
    Unauthorized,
    #[msg("Invalid token account")]
    InvalidTokenAccount,
    #[msg("Arithmetic overflow")]
    Overflow,
    #[msg("Arithmetic underflow")]
    Underflow,
}

Additional Security Resources

Auditing

Before deploying to mainnet:
  1. Self-review: Go through this security checklist
  2. Peer review: Have other developers review your code
  3. Testing: Write comprehensive tests including edge cases
  4. Professional audit: Consider hiring a security firm for critical programs
  5. Bug bounty: Run a bug bounty program for additional security
Security is an ongoing process. Stay updated on new vulnerabilities and best practices in the Solana ecosystem.

Build docs developers (and LLMs) love