Instructions & Context
In Anchor, an instruction is the unit of on-chain work: a public function inside #[program], a matching accounts struct, optional typed arguments, and a Result that tells the runtime whether the transaction path may continue. Clients (including those built with @solana/kit 7.0.0) do not call your Rust functions by name in isolation. They send a program id, an 8-byte discriminator, Borsh-encoded args, and a closed list of accounts. Anchor deserializes that message, validates every account against your #[derive(Accounts)] constraints, then enters your handler with a typed Context<T>.
This page is the umbrella for Instructions & Context. Sibling pages zoom into handlers, arguments, account access, System and Token program slots, return values and CPIs, and multi-instruction ordering. Here you get one coherent model so those pages fit as zooms, not separate stories.
Summary
- An Anchor instruction is a handler plus a validated
Context: constraints run first, then your code reads or mutatesctx.accounts, uses instruction args, and optionally CPIs out or writes results clients can observe. - Insight: Almost every security and integration bug in Anchor programs starts here: wrong account order, missing
mut, args trusted without signers, omitted System/Token program accounts, or handlers that assume prior init without enforcing it. - Key Concepts:
#[program]handler,Context<T>,#[derive(Accounts)], instruction arguments (Borsh),ctx.accounts/ctx.bumps/remaining_accounts,Program<System>/Program<Token>, CPI from handlers, instruction ordering, IDL-driven clients. - When to Use: Designing or reviewing any Anchor 0.32.1 program surface: new instructions, client builders, multi-step user flows, or audits of how accounts and args interact.
- Limitations/Trade-offs: Context validation costs compute; large arg payloads and unbounded
remaining_accountsare attack surfaces; CPIs share the transaction CU budget and depth limit; multi-instruction flows are atomic only inside one transaction. - Related Topics: Instruction Handlers, Instruction Arguments, Accessing Accounts, The System & Token Programs, Return Values & CPIs from Handlers, Instruction Ordering & Dependencies.
Foundations
Solana programs are executables. State lives in accounts. A client builds a transaction with one or more instructions; each instruction names a program, data bytes, and account metas (pubkey, signer, writable). The runtime loads your SBF program, passes the account list and data, and meters compute units under Agave 4.1.1 rules.
Anchor sits on that model and standardizes three layers:
- Dispatch -
#[program]exports named handlers; the IDL and 8-byte discriminators map client methods to those functions. - Validation - For each handler, an accounts struct with constraints (
Signer,mut,seeds,init, token checks, and so on) runs before business logic. - Handler body - Your Rust code receives
Context<T>(and args) with accounts already typed and checked, then returnsResult<()>, or anotherResulttype when you intentionally return data.
Client (Kit / Anchor TS / Codama)
|
v
Instruction: program_id + discriminator + Borsh args
Account metas in IDL order (+ optional remaining_accounts)
|
v
Runtime loads program (.so)
|
v
Anchor: match discriminator
-> build Context (constraints fail => Err)
-> deserialize args
-> call handler(ctx, args...)
|
+--> mutate Account<'info, T> fields
+--> CPI (System, Token, partner programs)
+--> emit! / set_return_data (optional)
|
v
Ok => serialize dirty accounts; Err => whole instruction failsHandlers are ordinary public functions. Convention: first parameter is always Context<AccountsStruct>, then zero or more args that match the IDL order. Keep them thin: validate business rules with require!, update state, call helpers or CPI modules, return.
Context is not a free bag of accounts. After successful constraint evaluation it exposes:
| Field | Role |
|---|---|
ctx.accounts | Typed fields from your accounts struct |
ctx.program_id | This program's pubkey (declare_id!) |
ctx.bumps | Canonical bumps for accounts that used seeds + bump |
ctx.remaining_accounts | Extra AccountInfos after the fixed IDL list |
Arguments vs accounts. Args are pure data (amounts, enums, ids, optional pubkeys). Accounts are on-chain identities with owners, lamports, data, and signer/writable flags the runtime enforces. Prefer accounts when a key must sign, hold state, or be constrained by seeds. Prefer args for scalars and small configuration that is not already on-chain. Never treat an arg pubkey as authenticated authority unless a matching Signer (or PDA CPI signature) proves it.
System and Token in context. Creating accounts, paying rent, transferring SOL, and moving SPL balances require CPI into the System Program or Token (or Token-2022) program. Anchor makes that safe by requiring typed program accounts such as Program<'info, System> and Program<'info, Token> (or token interfaces) so a client cannot substitute a malicious executable. init and associated-token constraints also pull System and token programs into the accounts list automatically in spirit: you still declare them so the transaction can pass them and constraints can verify their ids.
Mechanics & Interactions
Instruction handlers and Context<T>
use anchor_lang::prelude::*;
#[program]
pub mod vault {
use super::*;
pub fn deposit(ctx: Context<Deposit>, amount: u64) -> Result<()> {
require!(amount > 0, VaultError::InvalidAmount);
let vault = &mut ctx.accounts.vault;
vault.total = vault
.total
.checked_add(amount)
.ok_or(VaultError::Overflow)?;
Ok(())
}
}
#[derive(Accounts)]
pub struct Deposit<'info> {
#[account(mut, seeds = [b"vault", owner.key().as_ref()], bump = vault.bump)]
pub vault: Account<'info, VaultState>,
pub owner: Signer<'info>,
}Flow for every call: constraints on Deposit run (owner signed, vault PDA matches seeds, vault is writable and owned by the program), bumps are recorded if derived, then deposit runs. Failures in constraints never reach business logic. That fail-closed order is the primary security advantage of Anchor 0.32.1 over hand-rolled dispatch.
Instruction arguments
After Context, parameters are Borsh-decoded from instruction data (after the discriminator). Order and types must match the client and the IDL. Use #[instruction(...)] on the accounts struct when constraints need arg values (for example PDA seeds that include an id):
#[derive(Accounts)]
#[instruction(id: u64)]
pub struct CreatePool<'info> {
#[account(
init,
payer = payer,
space = 8 + Pool::INIT_SPACE,
seeds = [b"pool", id.to_le_bytes().as_ref()],
bump,
)]
pub pool: Account<'info, Pool>,
#[account(mut)]
pub payer: Signer<'info>,
pub system_program: Program<'info, System>,
}Validate ranges early (fee_bps <= 10_000, non-zero amounts). Keep arg surfaces small: large Vecs blow CU and message size. Generate clients from the IDL so serialization order never drifts.
Accessing accounts inside the handler
&ctx.accounts.field/&mut- Read or write typed state.Account<'info, T>reserializes on drop when mutable and dirty.ctx.bumps.field_name- Use the same field name as the seeds account; pass intoCpiContext::new_with_signerwithout re-searching bumps.ctx.remaining_accounts- Dynamic tails (routes, optional oracles). Document order, validate owner, writability, and discriminators yourself; the IDL does not fully describe them.to_account_info()- Bridge typed wrappers into CPI account lists.
Least privilege still applies: only mark accounts mut when the instruction must write them. Readonly where possible shrinks blast radius if a bug or CPI target misbehaves.
System and Token programs in context
Declare program accounts for every instruction that creates accounts, moves SOL, or touches SPL balances:
pub system_program: Program<'info, System>,
pub token_program: Program<'info, Token>,
// Token-2022 / dual support often uses Interface<'info, TokenInterface>Program<T> checks the executable id. Omitting it (or using raw unchecked infos) invites program substitution. Associated Token Program appears when you create ATAs. Token-2022 paths need the correct program id and often interface types so both classic Token and Token-2022 can be accepted safely. Details and tables live in The System & Token Programs.
Return values and CPIs from handlers
Most handlers return Result<()> and communicate outcomes by writing accounts and emitting events (emit!). Solana also supports instruction return data; use it sparingly for small synchronous results to a CPI caller. Indexers and dApps almost always prefer events or account state over opaque return buffers.
When the handler must move SOL or tokens, or call another program, build a CPI with accounts already present in context:
use anchor_lang::system_program::{transfer, Transfer};
transfer(
CpiContext::new(
ctx.accounts.system_program.to_account_info(),
Transfer {
from: ctx.accounts.payer.to_account_info(),
to: ctx.accounts.recipient.to_account_info(),
},
),
lamports,
)?;PDA authorities use CpiContext::new_with_signer and ctx.bumps. CPI errors propagate with ?. Nested depth and CU are transaction-wide concerns (depth cap 4 on current clusters); flatten when possible. See Return Values & CPIs from Handlers.
Ordering and dependencies across instructions
One handler is one instruction. Clients often chain several: create user PDA, create ATA, then deposit. Within a single transaction, later instructions see account state written by earlier ones. Across transactions, nothing is atomic: design idempotent steps or explicit status flags, and return clear errors when prerequisites are missing (NotInitialized) instead of panicking.
Same transaction (atomic)
1. create_user -> init PDA
2. create_ata -> init token account
3. deposit -> transfer + update vault
Separate transactions (partial completion possible)
tx A: create_user
tx B: deposit // must tolerate or detect missing setupChoose one fat instruction when the program must enforce intermediate invariants without trusting the client. Choose several smaller instructions when CU limits, composability, or UX retries favor thin steps. Document required order in docs and IDL comments so Kit builders do not invert init and use.
Advanced Considerations & Applications
Single-purpose handlers. Split admin, user, and settlement surfaces. Small handlers compose better in multi-ix transactions, keep CU budgets predictable, and simplify audits. Shared logic belongs in modules, not copy-pasted across #[program] functions.
Args in seeds vs accounts in seeds. Stable identities (user wallet, mint) usually come from account fields. Numeric or string ids often come from args via #[instruction]. Mixing both is normal; changing either changes the PDA. Store the bump at init and constrain bump = account.bump afterward to avoid non-canonical bumps.
Dynamic account graphs. Routers and multi-hop flows push extra accounts through remaining_accounts. Version the expected layout, bound length, and validate every entry before CPI. Fixed accounts in the struct remain the default for high-risk authorities.
Client generation. After anchor build, the IDL drives TypeScript and Kit pipelines. Account order in the client must match the accounts struct. Remaining accounts are always a second contract: publish them next to the IDL. Prefer Codama or Anchor client codegen over hand-packed discriminators.
Testing the contract of an instruction. Unit tests (LiteSVM) and integration tests (anchor test against a local validator or Surfpool) should cover constraint failures (wrong signer, bad seeds), arg validation, CPI account mutability, and multi-ix happy paths. Simulate CU on hot handlers under Solana CLI 3.0.10 tooling so production compute budget instructions are realistic.
Security review checklist for this layer. For every instruction ask: which accounts are signers; which are writable and why; which PDAs are derived and where bumps live; whether System/Token program ids are typed; whether args are authenticated or merely parameters; whether state is finalized before external CPI; whether clients can safely retry.
Common Misconceptions
- "Context is just a bag of AccountInfos." Constraints already ran. Treat
ctx.accountsas validated; do not re-open unchecked paths without reason. - "If the client passes the right pubkey in args, the signer is proven." Only
Signeraccounts (or PDAinvoke_signed) prove authority. Args can lie. - "
initmeans I can omit the system program." Creation still goes through System Program checks; declareProgram<System>and a mutable payer. - "Token transfers can be done by writing the token account in my program." Only Token/Token-2022 may rewrite token account data. CPI with a valid authority.
- "Handler return values are how frontends get results." Prefer account state and events. Return data is niche and size-limited.
- "Instruction order in the client is cosmetic." Init-before-use and mutability flags make order load-bearing across multi-ix transactions.
- "remaining_accounts is safe because Anchor typed the rest." Extras are unchecked by default. Validate owners and roles yourself.
- "One giant instruction is always safer." It can enforce atomicity, but it also spikes CU and couples unrelated failures. Match instruction shape to product risk.
FAQs
What is an Anchor instruction in one sentence?
A public #[program] function whose accounts are validated into a Context, whose args are Borsh-decoded from instruction data, and whose Result decides success or failure for that step of the transaction.
What runs first: my handler body or account constraints?
Constraints on the accounts struct always run first. If they fail, the handler never executes.
What does Context contain?
Typed accounts (ctx.accounts), this program's id, PDA bumps from seeds constraints (ctx.bumps), and any trailing remaining_accounts the client appended.
How should I choose between an argument and an account?
Use accounts for keys that must sign, hold state, or be seed-constrained. Use args for small scalars and configuration that is not already represented on-chain.
Why do System and Token appear in so many accounts structs?
init, SOL movement, and SPL operations CPI into those programs. Typed Program accounts pin the correct executable ids so clients cannot swap in a fake program.
When should a handler CPI instead of relying on another top-level instruction?
CPI when your program must enforce intermediate logic and nested state changes atomically under its own control. Use multiple top-level instructions when steps are independent, CU-heavy, or better retried separately inside one client-built transaction.
How do clients know account order and arg types?
From the Anchor IDL produced by anchor build, consumed by Anchor TS, Codama, or hand-written @solana/kit 7.0.0 builders that match the same layout.
Where do bumps used in CPI signing come from?
From ctx.bumps.<field> when the accounts struct declared seeds and bump, or from a stored bump field on the account after init. Do not invent bumps ad hoc in the handler.
Can one transaction call several of my instructions?
Yes. Order them so creators run before consumers. The whole transaction is atomic; partial progress across separate transactions is not.
Should handlers return rich data to the UI?
Usually no. Write state accounts and emit events for indexers and UIs. Keep return data for small CPI-to-CPI signals if you truly need them.
What Anchor and toolchain versions does this section assume?
Anchor 0.32.1, Rust 1.91.1, Agave 4.1.1, Solana CLI 3.0.10, and clients on @solana/kit 7.0.0 unless a page says otherwise.
What should I read next?
Start with Instruction Handlers and Instruction Arguments, then Accessing Accounts and The System & Token Programs. Add Return Values & CPIs from Handlers and Instruction Ordering & Dependencies when you design multi-step or multi-program flows.
Related
- Instruction Handlers - signatures,
Context<T>, and thin handler style - Instruction Arguments - Borsh args,
#[instruction], and validation - Accessing Accounts -
ctx.accounts, bumps, andremaining_accounts - The System & Token Programs - program accounts required for init and token work
- Return Values & CPIs from Handlers - events, return data, and CPI from handlers
- Instruction Ordering & Dependencies - multi-instruction client flows and prerequisites
Stack versions: This page was written for Agave 4.1.1, Solana CLI 3.0.10, Anchor 0.32.1, Rust 1.91.1, and @solana/kit 7.0.0.