Files

11 KiB

title, description
title description
Programs with Anchor Write Solana programs using the Anchor framework for fast iteration, automatic account validation, and built-in TypeScript client generation.

Programs with Anchor (default choice)

Contents

When to use Anchor

Use Anchor by default when:

  • You want fast iteration with reduced boilerplate
  • You want an IDL and TypeScript client story out of the box
  • You want mature testing and workspace tooling
  • You need built-in security through automatic account validation

Core Advantages

  • Reduced Boilerplate: Abstracts repetitive account management, instruction serialization, and error handling
  • Built-in Security: Automatic account-ownership verification and data validation
  • IDL Generation: Automatic interface definition for client generation

Core Macros

declare_id!()

Declares the onchain address where the program resides—a unique public key derived from the project's keypair.

#[program]

Marks the module containing every instruction entrypoint and business-logic function.

#[derive(Accounts)]

Lists accounts an instruction requires and automatically enforces their constraints:

  • Declares all necessary accounts for specific instructions
  • Enforces constraint checks automatically to block bugs and exploits
  • Generates helper methods for safe account access and mutation

#[error_code]

Enables custom, human-readable error types with #[msg(...)] attributes for clearer debugging.

Account Types

Type Purpose
Signer<'info> Verifies the account signed the transaction
SystemAccount<'info> Confirms System Program ownership
Program<'info, T> Validates executable program accounts
Account<'info, T> Typed program account with automatic validation
UncheckedAccount<'info> Raw account requiring manual validation

Account Constraints

Initialization

#[account(
    init,
    payer = payer,
    space = 8 + CustomAccount::INIT_SPACE
)]
pub account: Account<'info, CustomAccount>,

PDA Validation

#[account(
    seeds = [b"vault", owner.key().as_ref()],
    bump
)]
pub vault: SystemAccount<'info>,

Ownership and Relationships

#[account(
    has_one = authority @ CustomError::InvalidAuthority,
    constraint = account.is_active @ CustomError::AccountInactive
)]
pub account: Account<'info, CustomAccount>,

Reallocation

#[account(
    mut,
    realloc = new_space,
    realloc::payer = payer,
    realloc::zero = true  // Clear old data when shrinking
)]
pub account: Account<'info, CustomAccount>,

Closing Accounts

#[account(
    mut,
    close = destination
)]
pub account: Account<'info, CustomAccount>,

Account Discriminators

Default discriminators use sha256("account:<StructName>")[0..8]. Custom discriminators (Anchor 0.31+):

#[account(discriminator = 1)]
pub struct Escrow { ... }

Constraints:

  • Discriminators must be unique across your program
  • Using [1] prevents using [1, 2, ...] which also start with 1
  • [0] conflicts with uninitialized accounts

Instruction Patterns

Basic Structure

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

    pub fn initialize(ctx: Context<Initialize>, data: u64) -> Result<()> {
        ctx.accounts.account.data = data;
        Ok(())
    }
}

Context Implementation Pattern

Move logic to context struct implementations for organization and testability:

impl<'info> Transfer<'info> {
    pub fn transfer_tokens(&mut self, amount: u64) -> Result<()> {
        // Implementation
        Ok(())
    }
}

Cross-Program Invocations (CPIs)

Basic CPI

let cpi_accounts = Transfer {
    from: ctx.accounts.from.to_account_info(),
    to: ctx.accounts.to.to_account_info(),
};
let cpi_ctx = CpiContext::new(System::id(), cpi_accounts);

transfer(cpi_ctx, amount)?;

PDA-Signed CPIs

let seeds = &[b"vault".as_ref(), &[ctx.bumps.vault]];
let signer = &[&seeds[..]];
let cpi_ctx = CpiContext::new_with_signer(System::id(), cpi_accounts, signer);

Error Handling

#[error_code]
pub enum MyError {
    #[msg("Custom error message")]
    CustomError,
    #[msg("Value too large: {0}")]
    ValueError(u64),
}

// Usage
require!(value > 0, MyError::CustomError);
require!(value < 100, MyError::ValueError(value));

Token Accounts

SPL Token

#[account(
    mint::decimals = 9,
    mint::authority = authority,
)]
pub mint: Account<'info, Mint>,

#[account(
    mut,
    associated_token::mint = mint,
    associated_token::authority = owner,
)]
pub token_account: Account<'info, TokenAccount>,

Token2022 Compatibility

Use InterfaceAccount for dual compatibility:

use anchor_spl::token_interface::{Mint, TokenAccount};

pub mint: InterfaceAccount<'info, Mint>,
pub token_account: InterfaceAccount<'info, TokenAccount>,
pub token_program: Interface<'info, TokenInterface>,

LazyAccount (Anchor 0.31+)

Heap-allocated, read-only account access for efficient memory usage:

// Cargo.toml — match the version to your Anchor CLI (1.1.x current)
anchor-lang = { version = "1.1.2", features = ["lazy-account"] }

// Usage
pub account: LazyAccount<'info, CustomAccountType>,

pub fn handler(ctx: Context<MyInstruction>) -> Result<()> {
    let value = ctx.accounts.account.get_value()?;
    Ok(())
}

Note: LazyAccount is read-only. After CPIs, use unload() to refresh cached values.

Zero-Copy Accounts

For accounts exceeding stack/heap limits:

#[account(zero_copy)]
pub struct LargeAccount {
    pub data: [u8; 10000],
}

Accounts under 10,240 bytes use init; larger accounts require external creation then zero constraint initialization.

Remaining Accounts

Pass dynamic accounts beyond fixed instruction structure:

pub fn batch_operation(ctx: Context<BatchOp>, amounts: Vec<u64>) -> Result<()> {
    let remaining = &ctx.remaining_accounts;
    require!(remaining.len() % 2 == 0, BatchError::InvalidSchema);

    for (i, chunk) in remaining.chunks(2).enumerate() {
        process_pair(&chunk[0], &chunk[1], amounts[i])?;
    }
    Ok(())
}

Version Management

  • Current stable: Anchor 1.1.x (latest: 1.1.2, Jun 2026). CI-tested Solana CLI pairing: 3.1.10.
  • Use AVM (Anchor Version Manager) for reproducible builds. Install AVM from git (cargo install --git https://github.com/solana-foundation/anchor avm --force — the avm crate on crates.io is unrelated), then avm install latest / avm use latest. AVM supports avm self-update and pre-releases (avm install latest-pre-release).
  • Keep Solana CLI + Anchor versions aligned in CI and developer setup
  • Pin versions in Anchor.toml
  • Anchor 1.1.2 tightened inter-crate pins: keep all anchor-* crates on the exact same version

Compatibility Notes for Anchor 0.32.0

To resolve build conflicts with certain crates in Anchor 0.32.0, run these cargo update commands in your project root:

cargo update base64ct --precise 1.6.0
cargo update constant_time_eq --precise 0.4.1
cargo update blake3 --precise 1.5.5

Additionally, if you encounter warnings about solana-program conflicts, pin the v2 crate line explicitly by adding solana-program = "2" to the [dependencies] section in your program's Cargo.toml file (e.g., programs/your-program/Cargo.toml). Anchor 0.32.x is on the Solana v2 crate ecosystem — do not pin solana-program = "3" on a 0.32 project; it forces a resolution incompatible with Anchor 0.32's own dependency graph. Bumping all solana-* crates to ^3 happens only as part of the v0.32 → v1 migration (see below).

Security Best Practices

Account Validation

  • Use typed accounts (Account<'info, T>) over UncheckedAccount when possible
  • Always validate signer requirements explicitly
  • Use has_one for ownership relationships
  • Validate PDA seeds and bumps

CPI Safety

  • Use Program<'info, T> to validate CPI targets (prevents arbitrary CPI attacks)
  • Never pass extra privileges to CPI callees
  • Prefer explicit program IDs for known CPIs

Common Gotchas

  • Avoid init_if_needed: Permits reinitialization attacks
  • Legacy IDL formats: Ensure tooling agrees on format (pre-0.30 vs new spec)
  • PDA seeds: Ensure all seed material is stable and canonical

Testing

  • anchor test (and anchor localnet) default to Surfpool as the local network in Anchor 1.0+ — see ../surfpool/overview.md
  • anchor init scaffolds a LiteSVM test template by default in Anchor 1.0+
  • Use NO_DNA=1 anchor test / NO_DNA=1 anchor build when run by an agent
  • Prefer Mollusk or LiteSVM for fast unit tests; Surfpool for integration tests with mainnet state — see ../testing.md

See no-dna.org for the NO_DNA standard.

IDL and Clients

  • Treat the program's IDL as a product artifact
  • IDL management uses Program Metadata in Anchor 1.0+ (anchor idl init / anchor idl upgrade; anchor idl fetch-historical in 1.1+)
  • Prefer generating Kit-native clients via Codama
  • The Anchor TS client (@anchor-lang/core) works alongside Kit code; for new client work prefer Codama-generated Kit clients

Migrations

Anchor v0.32 → v1

  • Dependencies — bump anchor-lang and anchor-spl to ^1 (latest: 1.1.2), and all solana-* crates to ^3.
  • CPI context — CpiContext::new now takes a program ID (Pubkey) instead of a program AccountInfo. Remove the program account from the accounts struct.
  • TypeScript — replace @coral-xyz/anchor with @anchor-lang/core.
  • IDL — IDL management is being moved off program, mandatory actions required.
  • Duplicate mutable accounts — disallowed by default; annotate intentional cases with the dup constraint.

See anchor/migrating-v0.32-to-v1.md for the full checklist and before/after examples.

Anchor 1.0 → 1.1

  • anchor-lang MSRV is Rust 1.89; TS package requires Node ≥ 20.18
  • anchor-client supports versioned transactions
  • New: verifiedBuild (OtterSec verify.osec.io), multiple named scripts in Anchor.toml, anchor idl fetch-historical
  • 1.1.2: keep all anchor-* crates pinned to the same exact version