Security Basics
8 examples to get you started with program security - 5 basic and 3 intermediate. Covers the Solana threat model, account validation, signer checks, and fail-closed design on Anchor 0.32.1.
Prerequisites
- Anchor 0.32.1 project with
anchor-lang 0.32.1. - Read CPI Basics - many attacks cross program boundaries.
anchor --version # 0.32.1Basic Examples
1. Require a Signer
Any account that moves funds or changes authority must sign.
#[derive(Accounts)]
pub struct Withdraw<'info> {
#[account(mut)]
pub vault: Account<'info, Vault>,
pub authority: Signer<'info>,
}Signer<'info>fails if the account did not sign the transaction.- Missing signer checks are the most common critical finding.
- PDAs cannot be
Signerunless you useinvoke_signed.
Related: Signer & Owner Checks - deeper patterns
2. Validate Account Owner
Only the owning program should interpret account data.
#[account(
mut,
constraint = vault.owner == program_id @ ErrorCode::InvalidOwner,
)]
pub vault: Account<'info, Vault>,- Anchor
Account<'info, T>checks owner == program_id automatically for your types. AccountInfoandUncheckedAccountskip this - validate manually.- Type confusion attacks pass wrong account types without owner checks.
Related: Account Validation Attacks
3. Constrain PDA Seeds
PDAs must be derived with canonical seeds and bump.
#[account(
seeds = [b"vault", authority.key().as_ref()],
bump = vault.bump,
)]
pub vault: Account<'info, Vault>,- Users cannot forge PDAs without the program's seed rules.
- Store bump on init to avoid runtime search cost and seed ambiguity.
- Wrong seeds let attackers substitute their own PDA layout.
Related: PDA & Seed Attacks
4. Use Checked Math
Token amounts and shares overflow silently in release builds without checks.
let new_balance = old_balance
.checked_add(amount)
.ok_or(ErrorCode::Overflow)?;- Solana BPF uses wrapping arithmetic by default in plain
+. - Use
checked_*,saturating_*, oru128intermediates. - Rounding favors the protocol, not the user, on division.
Related: Arithmetic & Overflow
5. Guard Initialization
Prevent reinitialization of already-initialized accounts.
#[account(
init,
payer = payer,
space = 8 + Vault::INIT_SPACE,
)]
pub vault: Account<'info, Vault>,- Use
initonce, thenmuton subsequent instructions. init_if_neededis dangerous without ais_initializedflag guard.- Attacker can re-run init to reset admin fields.
Related: Reinitialization Attacks
Intermediate Examples
6. Validate CPI Program ID
Callees must be the program you expect, not an impostor.
let cpi_program = ctx.accounts.token_program.to_account_info();
require_keys_eq!(cpi_program.key(), anchor_spl::token::ID, ErrorCode::BadProgram);- Never trust user-supplied program accounts without
require_keys_eq!. - SPL Token vs Token-2022 have different program IDs.
- Malicious program can noop and return success.
Related: CPI & Reentrancy Risks
7. Close Account Safely
Drain lamports to destination and zero data to prevent revival.
#[account(
mut,
close = destination,
)]
pub temp: Account<'info, Temp>,- Anchor
closeattribute assigns lamports and zeros discriminator. - Ensure no open references or duplicate closes in same tx.
- Closing to attacker-controlled account leaks rent if authority check missing.
8. Fail Closed on Oracle Staleness
Price-dependent logic must reject stale feeds.
let price = feed
.get_price_no_older_than(clock.unix_timestamp, 60)
.ok_or(ErrorCode::StaleOracle)?;- Open oracle reads enable borrowing against outdated prices.
- Always pair with confidence checks for high-value decisions.
- Test stale paths in LiteSVM 0.6.x.
Related: Oracles - feed integration
Stack versions: This page was written for Agave 4.1.1, Solana CLI 3.0.10, Anchor 0.32.1, anchor-lang 0.32.1, Rust 1.91.1, @solana/kit 7.0.0, Surfpool 0.12.0, and LiteSVM 0.6.x.