Anchor: #[program], #[derive(Accounts)], and Constraints
On the previous page you did the work by hand: iterate the
account slice, check each account is the one you expect, check is_signer, check owner, check writability,
deserialize the bytes, and only then touch state. Every one of those checks was a line you could forget —
and forgetting one was the difference between a program and an exploit. The runtime handed you a positional
slice of accounts and an opaque byte blob, and the correctness of the whole thing lived in checks that were
easy to skip and invisible when missing.
Anchor is the framework that turns those hand checks into declarations. You don’t write the loop; you
write a struct whose fields are the accounts and whose attributes are the checks. This page maps
Anchor’s four core macros onto the native machinery from the last two pages, so that when you read
#[account(mut, has_one = authority)] you see the exact hand-written check it replaced. The throughline of
this book — build a global state machine that runs at hardware speed without falling apart — needs both
halves: the parallel scheduler gives you the speed, and these constraints are how you stop it falling apart
when a single forgotten check would let anyone drain an account.
The four macros, mapped to what you already built
Section titled “The four macros, mapped to what you already built”Anchor is not a new runtime. It compiles to the same SBF bytecode, runs behind the same
process_instruction entrypoint, and receives the same positional
account slice. It is a set of macros that generate the boilerplate you were writing by hand. There are
four you must know:
native (what you wrote by hand) Anchor (what you declare) ───────────────────────────────── ───────────────────────── the program's address, a constant ► declare_id!("...") entrypoint! + a match on the first byte ► #[program] mod (dispatch + arg deserialization) the &[AccountInfo] slice + every check ► #[derive(Accounts)] struct (the footprint + constraints) your own borsh (de)serialize of `data` ► #[account] struct (typed state + 8-byte discriminator)Line them up and Anchor stops looking like magic. declare_id! is the program id you named in a constant.
#[program] is the entrypoint plus the instruction-dispatch match you wrote. #[derive(Accounts)] is the
account slice plus the checks you did in a loop. #[account] is the typed state you (de)serialized by
hand. Each macro replaces one specific piece of the last two pages.
Here is a complete, minimal Anchor program — a counter that can be created and incremented — annotated with the native equivalent of every line:
use anchor_lang::prelude::*;
declare_id!("Counter111111111111111111111111111111111111"); // ← the program's address constant
#[program] // ← entrypoint! + the instruction dispatch matchpub mod counter { use super::*;
pub fn initialize(ctx: Context<Initialize>) -> Result<()> { ctx.accounts.counter.count = 0; // ← write the freshly-created, typed account ctx.accounts.counter.authority = ctx.accounts.user.key(); Ok(()) }
pub fn increment(ctx: Context<Increment>, step: u64) -> Result<()> { // ← `step` = the data blob let c = &mut ctx.accounts.counter; // ← the account slice, but named and typed c.count = c.count.checked_add(step).ok_or(CounterError::Overflow)?; Ok(()) }}
#[derive(Accounts)] // ← the &[AccountInfo] footprint + all the checkspub struct Initialize<'info> { #[account(init, payer = user, space = 8 + 8 + 32)] // ← create the account, charge rent pub counter: Account<'info, Counter>, #[account(mut)] pub user: Signer<'info>, // ← is_signer check, made a type pub system_program: Program<'info, System>, // ← the CPI target for account creation}
#[derive(Accounts)]pub struct Increment<'info> { #[account(mut, has_one = authority)] // ← is_writable check + "authority field matches" pub counter: Account<'info, Counter>, pub authority: Signer<'info>, // ← is_signer check}
#[account] // ← typed state + Borsh + an 8-byte discriminatorpub struct Counter { pub count: u64, pub authority: Pubkey,}
#[error_code]pub enum CounterError { #[msg("counter overflowed")] Overflow,}Nothing here is a new capability. Every annotation is a check or a serialization step you already performed by hand — now written as a declaration the framework enforces.
declare_id! — the program’s address
Section titled “declare_id! — the program’s address”declare_id!("Counter111...") embeds the program’s public key into the binary as a constant. It is the same
address you deploy to, the same one clients name when they build a transaction, and — importantly — the value
Anchor compares against program_id at runtime so a program can tell whether it is the intended callee.
This is the on-chain equivalent of the id() function on the companion crate’s programs: an address that
names the code.
#[program] — dispatch and argument deserialization
Section titled “#[program] — dispatch and argument deserialization”The #[program] module is the entrypoint. In the native program you matched on the first byte of the
instruction data to pick a handler, then deserialized the rest of the bytes into your argument types. Anchor
generates both steps: it prepends an 8-byte instruction discriminator (a hash of the handler name) to
each instruction, uses it to dispatch to the right pub fn, and Borsh-deserializes the remaining bytes into
the handler’s typed arguments (step: u64 above). The data: &[u8] blob you decoded by hand becomes a typed
function parameter.
#[derive(Accounts)] — the footprint and the checks
Section titled “#[derive(Accounts)] — the footprint and the checks”This is the heart of Anchor and the point where the previous page pays off. The struct’s fields are the
accounts, in the order the transaction supplies them — the same positional slice, now with names and
types. Context<Increment> gives your handler ctx.accounts.counter and ctx.accounts.authority instead
of accounts[0] and accounts[1]. And the #[account(...)] attributes on each field are the checks: Anchor
generates the validation code and runs it before your handler body executes. If any constraint fails, the
handler never runs and the transaction aborts with a named error.
Remember from the parallel-execution story that this same struct is what the
scheduler reads to know the transaction’s footprint. #[account(mut)] is is_writable: true; a plain field
is read-only. So writing this struct does two jobs at once: it declares the checks and it feeds Sealevel
the parallelism information. That is why the account list is not an afterthought — it is the contract between
your program and the runtime.
#[account] — typed state with a discriminator
Section titled “#[account] — typed state with a discriminator”#[account] on the Counter struct makes it a persistable account type. Account<'info, Counter> in the
Accounts struct means “deserialize this account’s data buffer into a Counter, and serialize it back if
I mutate it.” This is exactly the by-hand Borsh (de)serialization from the last page, generated for you. The
one addition worth its own section is the discriminator.
Context and ctx.accounts: the slice, named
Section titled “Context and ctx.accounts: the slice, named”The single biggest ergonomic win is that positional bugs largely disappear. Compare the native handler with the Anchor one:
native Anchor ────── ────── let counter = &accounts[0]; let c = &mut ctx.accounts.counter; let authority = &accounts[1]; let a = &ctx.accounts.authority; // wrong index = wrong account, // field name, typed — the wrong // silently, at runtime // account can't compile inContext<T> wraps the deserialized accounts (ctx.accounts), the program id (ctx.program_id), the
remaining unparsed accounts (ctx.remaining_accounts), and the bumps for any PDAs. The ctx.accounts field
is your Accounts struct, fully parsed and validated. You never index a slice by hand and never write the
&[AccountInfo] → typed-struct deserialization loop — Anchor generates it from the struct definition, and
generates the serialize-back on the way out for any account you took mut.
Constraints: the hand checks, made declarative
Section titled “Constraints: the hand checks, made declarative”This is where the previous page’s checklist becomes a set of attributes. Each constraint replaces one specific manual check, and — this is the whole point — makes it impossible to silently forget, because a missing constraint is a visible absence in the struct rather than a missing line in a loop.
| Constraint | Native check it replaces |
|---|---|
#[account(mut)] | ”is this account marked writable?” — and marks the footprint writable for the scheduler |
Signer<'info> | if !account.is_signer { return Err(...) } |
#[account(has_one = authority)] | ”does account.authority == authority.key()?” — the ownership-of-record check |
#[account(constraint = expr)] | any custom boolean check you wrote by hand |
#[account(owner = some_program)] | if account.owner != expected { return Err(...) } |
#[account(init, payer = x, space = n)] | create the account (CPI to System), fund it to rent-exemption, size it, set its owner |
#[account(seeds = [...], bump)] | derive and verify a program-derived address (see the PDA page) |
A few of these deserve a closer look.
Signer<'info>turns the most-forgotten check — did the account that this action affects actually authorize it? — into a type. You cannot read aSignerfield without Anchor having verifiedis_signer. The type system carries the guarantee.has_one = authorityencodes “theauthorityfield stored inside this account must equal theauthorityaccount passed in.” This is the check that stops one user from operating on another user’s account: the stored owner-of-record must match the signer presented. Forgetting it by hand was the classic authorization bug; here it is one attribute.init, payer, spaceis the account-creation trio the native page could only gesture at.initperforms a CPI to the System program to allocate the account,payersays which signer funds it,spacesizes the byte buffer — and you must budget 8 bytes for the discriminator plus the Borsh size of every field. Thespace = 8 + 8 + 32above isdiscriminator + u64 count + Pubkey authority.constraint = expris the escape hatch: an arbitrary boolean the framework checks for you but cannot infer.#[account(constraint = counter.count < MAX @ CounterError::TooLarge)]is domain logic Anchor has no way to know about — you still write it, you just write it as a declaration.
The discriminator: type-confusion defense, on by default
Section titled “The discriminator: type-confusion defense, on by default”On the account-model page the danger was type confusion: two account types with the same byte layout are indistinguishable, so a program tricked into deserializing a config account as a vault account would happily operate on the wrong data. Native programs defend against this by hand — a type tag as the first field, checked on every deserialize. It was, predictably, easy to forget.
Anchor makes the defense the default. Every #[account] type gets an 8-byte discriminator: the first
eight bytes of SHA256("account:Counter"), prepended to the buffer on write. When Anchor deserializes an
Account<'info, Counter>, it first checks those eight bytes match Counter’s discriminator. Hand a program
the wrong account type and deserialization fails immediately, with a AccountDiscriminatorMismatch error,
before your handler sees a single field.
account.data buffer ┌──────────────────┬─────────────────────────────────┐ │ discriminator(8) │ borsh( count, authority, ... ) │ └──────────────────┴─────────────────────────────────┘ │ └─ = SHA256("account:Counter")[..8] Anchor checks this BEFORE reading any field. Wrong type ⇒ mismatch ⇒ abort. Type confusion, defended by default.The same idea appears for instructions (the 8-byte instruction discriminator that drives dispatch) and, in newer Anchor, for events. The lesson from the native page — validate what you’re handed before you trust it — is baked in rather than left to your discipline.
Under the hood — what the macros expand to
Section titled “Under the hood — what the macros expand to”It is worth internalizing that Anchor generates ordinary Rust. #[derive(Accounts)] expands to an
implementation of a try_accounts function that walks the incoming &[AccountInfo] slice in field order,
and for each field runs the deserialization and every constraint’s generated check, returning your typed
struct or an error. #[program] expands to an entrypoint that reads the 8-byte instruction discriminator,
dispatches to the matching handler, deserializes the arguments, calls try_accounts to build the Context,
runs your handler, and — critically — serializes back every mut account at the end. You can read the
expansion with cargo expand; it is the same loop-and-check code from the previous page, mechanically
generated and therefore never accidentally omitted. Anchor’s value is not that it does something you
couldn’t; it is that it does the tedious, forgettable thing every time, identically.
The architect’s lens
Section titled “The architect’s lens”- Why does it exist? Because native Solana programs are correct only if you remember a checklist of manual checks — signer, owner, writability, type tag, deserialization — on every account, every handler, and a single omission is an exploit. Anchor exists to make those checks declarations the compiler and framework enforce.
- What problem does it solve? It converts easy-to-forget runtime checks into visible, framework-run attributes, and generates the Borsh (de)serialization and account-parsing boilerplate you would otherwise write — and mis-write — by hand.
- What are the trade-offs? You accept a heavier dependency, macro-generated code you must occasionally
cargo expandto debug, some compute-unit overhead, and a small buffer for the discriminator on every account — in exchange for defaults that are safe. For most programs that trade is overwhelmingly worth it. - When should I avoid it? When you need byte-level control the macros get in the way of: an extremely compute-constrained program, an unusual account layout, or a case where the discriminator overhead or Anchor’s serialization assumptions don’t fit. Then you drop to native and pay for correctness in discipline.
- What breaks if I remove it? Nothing the runtime cares about — Anchor compiles to the same bytecode — but you lose the automatic checks. The signer check, the owner check, the type-confusion defense, and the serialize-back all become things you must write and never forget, which is exactly the fragility the previous page showed leads to drained accounts.
Anchor’s guarantee, and its limit
Section titled “Anchor’s guarantee, and its limit”Hold both halves of this in your head, because the boundary between them is where real programs are secured or lost.
The guarantee: the checks that are easy to forget become hard to skip. A missing signer check is now a
missing Signer field you’d notice; a missing type check is impossible because the discriminator is
automatic; a forgotten serialize-back can’t happen because Anchor does it for every mut account. The whole
class of “I forgot to validate this” bugs from the native page is designed out.
The limit: Anchor enforces the checks it can infer from structure — the account is writable, is a
signer, has a matching authority field, is the right type. It cannot infer your program’s domain
invariants: that the total supply must not exceed a cap, that this account must be one of three specific
allowed configs, that account A’s stored reference must point at account B. Those you write as
constraint = ... or as plain code in the handler. The framework makes the mechanical checks default; the
logical checks are still your job — and, as the Cashio incident shows, they are where the money is lost.
Next, the PDA and CPI page shows how programs own accounts at deterministic addresses and call each other — the piece that turns these single-program handlers into composable systems — before the IDL and testing page closes the loop from program to tested, callable deployment.
Check your understanding
Section titled “Check your understanding”- Map each of the four core Anchor macros —
declare_id!,#[program],#[derive(Accounts)],#[account]— to the specific piece of a native program it replaces. - What two jobs does
#[account(mut)]do at once, and why does the second one matter to the parallel scheduler? - A program lets any signer increment their own counter but the code omits
has_one = authority. What goes wrong, and which native check did the missing constraint correspond to? - Explain the 8-byte discriminator: where it comes from, when Anchor checks it, and which class of attack from the account-model page it defends against by default.
- State Anchor’s guarantee and its limit in one sentence each, and give an example of a check that falls on the limit side (the framework can’t make it for you).
Show answers
declare_id!→ the program’s address constant (the id clients name and the program checks itself against).#[program]→ the entrypoint plus the instruction-dispatchmatchand argument deserialization.#[derive(Accounts)]→ the positional&[AccountInfo]footprint plus all the per-account checks you did in a loop.#[account]→ the typed state struct with its Borsh (de)serialization, now carrying an 8-byte discriminator.- It marks the account writable so Anchor allows mutation and serializes it back afterward, and it marks the account writable in the transaction’s declared footprint. The second matters because the Sealevel scheduler reads the footprint to decide which transactions conflict; a mislabeled account would let the scheduler run two conflicting transactions in parallel.
- Without
has_one = authority, Anchor no longer checks that theauthoritystored inside the counter matches the signer presented — so any signer can operate on anyone’s counter. It corresponds to the native checkif counter.authority != authority.key() { return Err(...) }, the ownership-of-record / authorization check. - It is the first 8 bytes of
SHA256("account:<TypeName>"), prepended to the account’s data buffer on write. Anchor checks it before deserializing any field of anAccount<'info, T>; a mismatch aborts withAccountDiscriminatorMismatch. It defends against type confusion — being tricked into deserializing one account type as another with the same byte layout — making that defense the default instead of a hand-written type tag you could forget. - Guarantee: the mechanical, easy-to-forget checks (signer, owner, writability, type, serialize-back)
become declarations the framework enforces, so they can’t be silently omitted. Limit: Anchor only
enforces checks it can infer from structure, not your program’s domain invariants. A limit-side example:
“this account must reference that account,” or “total supply must stay under a cap” — you must write it as
constraint = ...(the class of bug behind the Cashio exploit).