Viktor Andriichuk

Auditing a Solana program isn't about reading every line top to bottom and hoping something jumps out. It's about knowing the small set of places where Anchor programs actually go wrong and checking each one deliberately. This guide walks through that process in the order an experienced reviewer would follow, and ends with a checklist you can paste into your review notes.

Anchor does a lot of validation for you — but only when you use its typed accounts and constraints. Most Anchor vulnerabilities are places where the author quietly opted out of that protection. So a large part of auditing an Anchor program is finding where the safety rails were removed.

Step 1: Map the instructions and their authority

Before reading any logic, list every instruction and answer one question for each: who is allowed to call this, and how is that enforced?

For every privileged instruction (withdraw, close, update-config, mint, set-authority), find the account that authorizes it and confirm it's declared as Signer<'info> and tied to the state it acts on with has_one or an explicit constraint. If the authority is an AccountInfo or UncheckedAccount, that's your first finding.

Good: authority must sign AND must match the stored authority

#[account(mut, has_one = authority)]
pub config: Account<'info, Config>,
pub authority: Signer<'info>,

Step 2: Grep for the escape hatches

The fastest way to find risk in an Anchor codebase is to search for the places where automatic validation was bypassed:

  • AccountInfo and UncheckedAccount — no owner, type, or discriminator check.
  • /// CHECK: comments — every one marks an account the author told Anchor to trust blindly. Read each and decide whether the accompanying validation is actually sufficient.
  • init_if_needed — reinitialization risk; confirm it can't be abused to reset state.
  • Raw invoke / invoke_signed — manual CPIs where the target program may not be verified.
  • try_from_slice / manual deserialization — bypasses discriminator and owner checks.

Each hit is a place to slow down. Anchor's defaults are safe; these are where safety was turned off.

Step 3: Verify account constraints

Go through each #[derive(Accounts)] struct and confirm the constraints match the intent:

  • Signer checks — is every authorizing account a Signer?
  • has_one — does state that stores an authority/owner/mint enforce it matches the passed account?
  • seeds + bump — are PDAs re-derived from the correct seeds, and is the stored canonical bump validated?
  • owner — for any account not using typed Account, is the owner checked?
  • mut — are exactly the accounts that get written marked mutable, and no more?
  • address = ... — for programs, sysvars, and known accounts, is the expected address pinned?

Step 4: Check the CPIs

For every cross-program invocation, confirm the program being called is verified, not just passed in. Prefer Anchor's typed CPI (CpiContext with a typed program account) over raw invoke. If raw invoke is used, the target program ID must be checked explicitly:

Pin the program to a known ID

require_keys_eq!(token_program.key(), anchor_spl::token::ID);

Also check that invoke_signed uses the correct PDA seeds and that you aren't accidentally signing for an account you don't control.

Step 5: Audit the arithmetic

Solana programs run in release mode, so overflow wraps silently. Search for bare +, -, * on balances, amounts, rewards, shares, and timestamps. Each should be checked_* (returning an error on overflow) or saturating_* where wrapping to a bound is intended. Watch for as casts that can truncate (u64 as u32), and for divisions that can round in the protocol's disfavor.

Step 6: Review account lifecycle

  • Initialization: does init prevent re-initializing existing accounts? Is the payer and space correct?
  • Closing: are accounts closed with Anchor's close constraint (which zeroes data and reclaims rent safely) rather than by hand? Manual closing invites revival attacks.
  • Rent: are accounts that must persist rent-exempt?

Step 7: Look for duplicate-account and account-ordering assumptions

If an instruction takes two accounts of the same type and mutates both, confirm they're required to be distinct (constraint = a.key() != b.key()). If logic assumes a particular relationship between accounts (e.g. "this token account belongs to this user"), confirm that relationship is enforced with a constraint, not just assumed.

The Anchor audit checklist

Copy this into your review:

Anchor program audit checklist

  • [ ] Every privileged instruction has a Signer that is tied to state (has_one / constraint)
  • [ ] No unexplained AccountInfo / UncheckedAccount on authority accounts
  • [ ] Every /// CHECK: is justified and backed by real validation
  • [ ] PDAs use seeds + canonical bump; bump is stored and validated
  • [ ] Typed Account used wherever possible (owner + discriminator checked)
  • [ ] All CPIs verify the target program ID (typed CPI or explicit check)
  • [ ] invoke_signed uses correct, controlled PDA seeds
  • [ ] All arithmetic on values-of-record uses checked_* / saturating_*
  • [ ] No silent truncation in as casts
  • [ ] init prevents reinitialization; init_if_needed justified if present
  • [ ] Accounts closed via Anchor close, not manual lamport draining
  • [ ] Same-type mutable accounts required to be distinct
  • [ ] mut applied only to accounts actually written
  • [ ] Known programs/sysvars pinned with address = ...

Where automation fits

A human auditor is irreplaceable for logic bugs and economic design flaws. But the checklist above is mostly mechanical — exactly the kind of review that's tedious to do by hand on every PR and easy to let slip under deadline. That's where an automated pass earns its keep.

VaultLint runs this style of review automatically: it reads your Anchor and native Solana programs, flags the missing constraints, unverified CPIs, and unchecked arithmetic, and tells you the file, the line, why it's dangerous, and how to fix it — in CI, on every PR. It's a linter, not a replacement for a full audit: it clears out the common, expensive mistakes early so your paid audit can spend its hours on the hard, protocol-specific stuff.