Guide

Solana Anchor framework explained

Solana programs are Rust (or C) compiled to BPF bytecode and invoked by transactions. You can write them by hand — manually deserializing account data, checking owners and signers, and building CPI calls with raw byte slices. Most teams do not, because the error surface is enormous and one missed check becomes a nine-figure exploit. Anchor is the dominant Rust framework that wraps Solana's runtime with declarative macros: it validates accounts before your instruction body runs, generates a machine-readable IDL for clients, and standardizes testing and deployment. This guide explains how Anchor fits together, what it actually enforces, and where it still leaves security work to you.

What Anchor adds on top of native Solana

At runtime, every Solana instruction receives a flat list of AccountInfo handles — public keys, lamport balances, owners, and opaque byte blobs. Native code must verify each account's role: Is this the expected mint? Did the user sign? Is this PDA derived from the right seeds? Anchor moves that verification into #[derive(Accounts)] structs annotated with constraints, so your handler starts only after the macro layer passes.

Anchor also provides:

  • Borsh serialization for instruction data and account state — predictable layouts without hand-rolled byte offsets.
  • Discriminators — 8-byte type prefixes so clients and programs cannot confuse one account struct with another.
  • IDL output — a JSON schema describing instructions, accounts, and types; TypeScript and Python clients are generated from it.
  • CPI helpersCpiContext wrappers that carry signer seeds for PDA-signed inner calls.
  • Integrated toolchainanchor build, anchor test, and anchor deploy wired to your Anchor.toml clusters.

Anchor does not change Solana's execution model. Accounts are still passed explicitly, compute units still meter every branch, and upgrade authority still controls whether bytecode can change. Anchor is ergonomics and guardrails — not a different chain.

Project layout and the program entrypoint

A typical Anchor workspace contains:

  • programs/<name>/src/lib.rs — your on-chain logic.
  • Anchor.toml — program ID, cluster URLs, and test wallet paths.
  • tests/ — TypeScript integration tests using @coral-xyz/anchor.
  • target/idl/ and target/types/ — generated artifacts after anchor build.

The root of lib.rs declares the program ID and wires instructions:

declare_id!("YourProgramPubkey...");

#[program]
pub mod my_program {
    use super::*;
    pub fn initialize(ctx: Context<Initialize>, amount: u64) -> Result<()> {
        ctx.accounts.vault.amount = amount;
        Ok(())
    }
}

The #[program] module expands into dispatch logic that routes incoming instruction data to your functions. Each public function becomes one instruction variant with a matching discriminator in the serialized payload.

The Accounts macro — where security actually lives

Every instruction pairs with a struct describing which accounts it needs and what must be true about them:

#[derive(Accounts)]
pub struct Initialize<'info> {
    #[account(mut)]
    pub payer: Signer<'info>,
    #[account(
        init,
        payer = payer,
        space = 8 + Vault::INIT_SPACE,
        seeds = [b"vault", payer.key().as_ref()],
        bump
    )]
    pub vault: Account<'info, Vault>,
    pub system_program: Program<'info, System>,
}

Read this struct as a checklist enforced before your Rust body executes:

  • Signer — the account must have signed the transaction.
  • mut — the account will be written; without it, Anchor rejects writes.
  • init — create a new account, charge rent to payer, assign owner to this program.
  • seeds + bump — derive and verify a PDA so only this program can sign for the vault address.
  • Account<'info, Vault> — deserialize bytes into your Vault struct and verify the owner is this program.
  • Program<'info, System> — ensure the System Program address is the real one, not a malicious substitute.

Common constraint keywords you will see in production code:

  • has_one — a field inside account data must match another account's key (e.g. mint authority).
  • constraint = ... — arbitrary boolean expressions for business rules.
  • close — drain lamports to a destination and zero the account (reclaim rent).
  • realloc — grow or shrink account data with explicit payer funding.
  • token:: / associated_token:: — SPL Token helpers for mints, ATAs, and transfers.

Most Anchor exploits are not macro bugs — they are missing or wrong constraints. If you accept a token account without checking its mint, or forget has_one on an authority field, attackers pass valid accounts that belong to them, not your protocol. Treat every account as hostile until constrained.

Account state: #[account] structs

Persistent data lives in accounts owned by your program. Anchor models that with #[account] structs:

#[account]
#[derive(InitSpace)]
pub struct Vault {
    pub authority: Pubkey,
    pub amount: u64,
}

The 8-byte discriminator Anchor prepends is separate from your fields — always allocate 8 + Type::INIT_SPACE (or use space = 8 + ... in constraints) when calling init. For variable-length vectors or strings, use InitSpace carefully or compute space manually; overallocation wastes rent, underallocation fails at runtime.

Our account model guide explains rent exemption, owners, and why PDAs cannot sign without invoke_signed — Anchor's seeds constraints wire that pattern for you on inbound accounts and on CPI calls.

Cross-program invocations inside Anchor

DeFi programs rarely stand alone. Swapping tokens means CPI into the SPL Token program; creating ATAs means CPI into Associated Token. Anchor wraps these with typed helpers:

let cpi_accounts = Transfer {
    from: ctx.accounts.from_ata.to_account_info(),
    to: ctx.accounts.to_ata.to_account_info(),
    authority: ctx.accounts.authority.to_account_info(),
};
let cpi_ctx = CpiContext::new(
    ctx.accounts.token_program.to_account_info(),
    cpi_accounts,
);
token::transfer(cpi_ctx, amount)?;

When the authority is a PDA your program owns, switch to CpiContext::new_with_signer and pass the same seed slices used in #[account(seeds = ..., bump)]. That is how escrow releases funds or how a vault moves tokens without a human keypair. See our CPI guide for the underlying invoke semantics Anchor abstracts.

Errors, events, and the IDL

Anchor encourages structured errors instead of bare ProgramError codes:

#[error_code]
pub enum MyError {
    #[msg("Amount exceeds vault cap")]
    AmountTooLarge,
}

Clients map these to readable messages via the IDL. #[event] structs emit logs parsers can subscribe to — useful for indexers tracking deposits or game outcomes without simulating full state diffs.

After anchor build, target/idl/my_program.json lists every instruction, account metas (mutability, signer, optional), and custom types. Frontend code imports the IDL and a Program instance:

const program = new Program(idl, provider);
await program.methods
  .initialize(new BN(1_000))
  .accounts({ vault: vaultPda, payer: wallet.publicKey })
  .rpc();

The IDL is a contract between on-chain and off-chain teams. Version it in git, regenerate on every release, and reject client builds when discriminators drift. Wallets that simulate before signing rely on the same account list the IDL documents.

Testing and deployment workflow

anchor test spins up a local validator (or hits devnet if configured), deploys your program, runs TypeScript tests, and shuts down. Tests typically:

  1. Airdrop SOL to a payer keypair.
  2. Derive PDAs with PublicKey.findProgramAddressSync.
  3. Call instructions through the typed Program API.
  4. Assert on-chain account data via program.account.vault.fetch.

Devnet practice should precede mainnet. Our program deployment guide covers upgrade authority, buffer accounts, and the checklist before you point anchor deploy at production RPC. Pair deployment with cluster verification so you never debug "program not found" on the wrong network.

Anchor vs native Rust — when to choose which

Use Anchor when: you are building application logic (DeFi pools, NFT mints, games, payment routers), want fast iteration, need a typed client IDL, and your team values declarative account checks over minimal binary size.

Consider native Rust when: you are writing infrastructure consumed by other programs, need every last compute unit (high-frequency arbitrage, oracle updates), or must avoid macro magic for auditability. Some teams split: Anchor for product programs, native for performance-critical libraries.

Anchor's macro layer adds compile time and a dependency footprint. That trade-off is usually correct for product teams; it is less obvious for protocol primitives where hundreds of downstream programs pay your CU bill.

Security checklist for Anchor programs

  • Account substitution — every account that can drain funds must be tied to expected pubkeys via has_one, seeds, or explicit constraint.
  • Signer gaps — if an instruction moves lamports or tokens, confirm the right Signer is required; PDAs use seeds + CPI signers instead.
  • Arbitrary CPI — do not let users pass unvalidated program IDs into your CPI path unless that is the explicit feature.
  • Reinitialization — use init only once; for resets prefer close + new init or explicit version fields.
  • Integer overflow — enable overflow-checks in release profile; use checked_add for token math.
  • Upgrade authority — multisig or burn it when the code should be immutable; document who can patch live logic.
  • IDL drift — ship IDL hashes with frontends; verify before mainnet launches.

Anchor removes boilerplate; it does not remove threat modeling. Read external audits of similar programs and mirror their account layouts before you invent a new pattern.

Key takeaways

  • Anchor is a Rust framework that validates accounts declaratively, serializes state with Borsh, and emits an IDL for typed clients.
  • Security lives in #[derive(Accounts)] constraints — signers, mutability, seeds, has_one, and token helpers.
  • CpiContext carries PDA signer seeds for composable calls into SPL Token and other programs.
  • anchor build / test / deploy standardizes the dev loop from local validator to devnet to mainnet.
  • Choose Anchor for product velocity; choose native Rust when compute and audit surface demand minimal abstraction.

Related reading