new·The score now tells you which way it movedA brain's exam only ever grows: its own material writes questions, and so does every question a real caller asked and did not get answered. The score is a percentage over that growing set, so a brain that learned more could post a smaller number — and this week three did. One of them answered two MORE questions than the week before and showed eighteen points less. Printed as a single percentage, that reads as decline to a reader and as punishment to anyone who contributes material.all news →
mozg.beta
Sign in

Solana & Anchor Auditor · all subjects

Anchor constraints

8 notes, read out of this brain and free to use. Each one was extracted from a source and is re-checked against its exam.

What does each Anchor constraint on this struct actually enforce?

Anchor's `#[account(...)]` attributes are precise and auditors should recite them cold: `init` creates the account (payer, space, rent) and requires it NOT already initialized; `init_if_needed` same but skips if the discriminator already exists (reinit-safe since Anchor 0.25, but space/payer semantics still bite); `seeds` + `bump` re-derives the PDA and compares the address; `has_one = x` asserts `stored_account.x == ctx.accounts.x.key()`; `constraint = expr` is a raw boolean assertion — it checks exactly what you wrote and nothing more; `owner = program_id` asserts account owner (for external-program accounts); `address = PUBKEY` asserts the key itself; `close = dest` drains lamports to dest, zeroes data, and writes the CLOSED discriminator; `realloc` resizes (needs `realloc::payer`, `realloc::zero` policy). What's NOT enforced unless written: signer status, mint/token-account linkage, duplicate-account inequality, business invariants.

What's missing from this Anchor account struct?

A fast Anchor review heuristic: for each field in `#[derive(Accounts)]`, walk a checklist. (1) Is the account TYPE strict — `Account<'info, T>` / `Signer` / `Program` — or a bare `AccountInfo`/`UncheckedAccount` with a `/// CHECK` comment that just waves at validation? (2) If it gates authority, is it `Signer` AND bound by `has_one`/seeds to stored state? (3) If it's a PDA, are seeds complete and bump pinned? (4) Token accounts: is the mint asserted (`token::mint`) and the authority (`token::authority`)? (5) Mutability: `mut` present where writes/CPIs need it — and absent where they don't (extra `mut` widens attack surface via duplicate-mutable bugs)? (6) Close/realloc present — does the destination/payer get validated? Most real Anchor findings are a missing line in this struct, not complex logic.

Does init_if_needed or realloc reopen a reinitialization attack?

Reinitialization = resetting an existing account's state by re-running init logic. Raw programs are nakedly vulnerable: if `initialize` doesn't check that the account is already initialized (discriminator/flag), anyone rewrites the authority field. Anchor's `init` rejects already-initialized accounts, and `init_if_needed` since v0.25 checks the discriminator — but gaps remain: an attacker who can CLOSE the account (via the program's own `close` path, or lamport-draining in raw code) can then re-create it fresh through `init_if_needed` with attacker-chosen fields. `realloc` to a larger size on an account whose data the program reads via `load_init`-adjacent logic can desync zero-copy layouts. Rule: treat (close path) + (init_if_needed path) as a compound attack surface; if a closed account can be re-inited, the re-init must re-derive everything trust-relevant from seeds, not from caller arguments.

Are zero-copy AccountLoader semantics being handled correctly?

Zero-copy accounts (`#[account(zero_copy)]`, `AccountLoader<'info, T>`) skip deserialization — the account data IS the struct via `bytemuck`. Sharp edges: you must use `load()`, `load_mut()`, or `load_init()` and pick correctly — `load_init` on an already-live account or `load()` on a fresh one panics/behaves wrong; fields must be plain-old-data (no `Pubkey` wrappers that bytemuck can't handle is fine — `Pubkey` is POD, but `Vec`/`String` are forbidden, forcing fixed arrays and manual length tracking); alignment and `repr(C)` mistakes corrupt layout; the discriminator is checked by the loader but DUPLICATE zero-copy accounts of different types with colliding interpretation are on you; and mutating through `load_mut` without marking `mut` fails silently at the tx level. Audit: check every `AccountLoader` for the right load flavor, and grep for manual offset reads on zero-copy data.

Is this UncheckedAccount's CHECK comment lying?

`/// CHECK:` comments above `UncheckedAccount`/`AccountInfo` fields are where Anchor audits go to die. The pattern to hunt: a `/// CHECK: validated in the instruction body` claim where the body checks nothing (or checks it on a different code path), or validates only on success paths while an early `?` return skipped it. Legitimate uses exist (purely-written system accounts, fee-payer-only accounts), so don't auto-flag — instead verify: owner checked? key compared to stored state or constant? signer status asserted if authority-like? data read at all (if yes, full deserialization safety needed)? Also check `Box<Account>` vs `Account` equivalence isn't assumed across struct versions. As of early 2026, Anchor lint tooling catches some bare uses, but semantic verification — that the manual checks match what the type system would have enforced — remains manual review.

What does `#[account(mut)]` actually guarantee?

`mut` is a WRITABILITY DECLARATION, not a write guarantee. It tells Anchor to require the account be passed as writable in the transaction (so the runtime acquires a write lock on it) and to serialize the — possibly unchanged — data back at the end. It enforces NOTHING about modification: a `mut` account may pass through the instruction completely untouched, and no rent, initialization, or content check is implied. Audit consequences: (a) unused-`mut` is a smell — it widens the tx's write-lock set (enabling duplicate-mutable-account aliasing and blocking parallelization) and often signals the author meant to add a check they forgot; (b) never infer "this account was updated" from `mut` alone — verify the actual write path; (c) conversely, any account written by the program OR mutated via CPI must be declared `mut` or the runtime rejects the write.

What does realloc leave behind in the new bytes?

Resizing an account does NOT reliably zero the grown region unless you ask: Anchor's `realloc` takes `realloc::zero = true/false`, and raw `AccountInfo::realloc(new_len, zero_init)` makes it an explicit parameter. If new space isn't zeroed, it contains stale bytes left in the account's memory region — an attacker who influences what previously occupied that space (or who controlled the account before realloc) can smuggle crafted data into fields the program later reads as legitimate state: fake owners, fake authorities, inflated counters. The second gap: `realloc` with no initialized/discriminator check can resurrect or reshape an account that was never properly initialized, letting an attacker define its contents wholesale. Rule: default to `realloc::zero = true` unless profiling proves the cost matters AND every new field is overwritten before any read; gate realloc behind the same owner + discriminator + authority validation as init; treat grow-and-interpret as an initialization path, not a resize.

When does init_if_needed still reopen reinitialization?

`init_if_needed` initializes only if the account appears uninitialized — and "appears" is the whole game. Since Anchor 0.25 it checks the account discriminator: an account with a valid discriminator is treated as initialized and init is skipped, closing the classic reinit-to-reset-authority exploit for Anchor-managed accounts. Residual risks remain: an account existing with the RIGHT discriminator but attacker-controlled contents (created via a separate weakly-validated instruction) sails through; a CLOSED account (lamports drained, discriminator zeroed) can be re-initialized through `init_if_needed` with fresh attacker-chosen fields — any close path combined with init_if_needed is a compound vector; and raw-program or pre-0.25 code checking only `lamports > 0` or a manual `is_initialized` flag is fully exploitable when the account exists under a different owner. Rule: after `init_if_needed`, re-verify stored authority/ownership fields against seeds — the discriminator alone is not a trust decision.

Give your agent this brain