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 helpers —
CpiContextwrappers that carry signer seeds for PDA-signed inner calls. - Integrated toolchain —
anchor build,anchor test, andanchor deploywired to yourAnchor.tomlclusters.
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/andtarget/types/— generated artifacts afteranchor 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 topayer, 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 yourVaultstruct 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:
- Airdrop SOL to a payer keypair.
- Derive PDAs with
PublicKey.findProgramAddressSync. - Call instructions through the typed
ProgramAPI. - 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 explicitconstraint. - Signer gaps — if an instruction moves lamports or tokens, confirm the right
Signeris required; PDAs useseeds+ 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
initonly once; for resets preferclose+ newinitor explicit version fields. - Integer overflow — enable
overflow-checksin release profile; usechecked_addfor 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 / deploystandardizes 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
- How to deploy a Solana program — Anchor deploy, upgrade authority, and mainnet checklist
- Solana PDAs explained — seeds, bumps, and vault patterns Anchor automates
- Cross-program invocation (CPI) — what happens under Anchor's CPI helpers
- Smart contracts explained — EVM vs Solana program models and shared security themes