A pre-deploy checklist I run top to bottom before any Anchor program goes to mainnet. Built from the bugs I reproduced myself.
This checklist is for Anchor developers about to deploy to mainnet. Run it top to bottom before every deploy. It's built from the bugs I reproduced myself over the last few days: forged accounts, arithmetic overflows, and CPI trust failures. Each item is a yes-or-no check. If you can't answer yes, stop and fix it.
1. Account Validation
Every deserialized account has its owner verified. Typed
Account<'info, T>does this automatically. RawAccountInfoorUncheckedAccountdoes not.Account types are distinguished by their 8-byte discriminator, so one account type cannot be passed where another is expected.
Any
remaining_accountspassed in are validated before use.Any PDA is verified with
seedsandbump,not just accepted by address.
Why this matters: In February 2022, $326M left the Wormhole bridge because the program trusted a forged account without verifying it was the real one. The fix was to check the owner before reading the data.
2. Authority and Signer Checks
Every privileged instruction confirms the expected signer with
Signer<'info>orhas_one / constraintchecks.No instruction trusts a pubkey without also checking its signer flag. Comparing a public key only proves someone knew a public key. Public keys are public.
UncheckedAccountfor an authority is a red flag. If you see it, you have no signer check.Admin-only instructions check the admin against stored state AND require the signer.
The vault's owner is set on first deposit and never silently overwritten.
Why this matters: Comparing a public key without checking the signer flag lets an attacker pass any public key. The instruction must require a signature.
3. Arithmetic Safety
Every balance or supply change uses
checked_add,checked_sub, orchecked_mul, never raw+,-, or*on untrusted values.No silent cast that could truncate (e.g.,
u64tou32).Overflow and underflow return a named error, not a panic.
Property tests prove that a deposit never shrinks a balance and a withdrawal never increases it.
Why this matters: A u64 that overflows wraps silently to a tiny number. Without checked math, a deposit of 2 lamports into a balance one lamport below u64::MAX wraps the balance back to 1. The attacker destroys the vault with a tiny deposit.
4. CPI Safety
Every CPI verifies the target program ID is the one expected, not whatever account the caller supplied.
Program<'info, T>is used where possible, so Anchor checks the program ID.Accounts are reloaded after a CPI if their data is read again. The callee may have changed the account.
Signer seeds used for PDA-signed CPIs match the seeds in the
#[account(seeds = ...)]constraint exactly. One byte off and the runtime rejects the PDA.No CPI trusts the callee to validate its own accounts. The caller is responsible for passing the right ones.
Why this matters: A CPI that accepts whatever program ID the caller supplies can be redirected to an attacker's program. The attacker's program returns success and the caller believes the work was done.
5. Account Lifecycle
Closed accounts are emptied and marked so they cannot be revived or reused within the same transaction.
No instruction allows reinitializing an already-initialized account.
initfails on existing accounts.init_if_neededsucceeds silently. Use it deliberately.Closing an account returns the rent to the correct wallet. Never to an arbitrary signer.
close = authorityis used, not manual lamport draining.
Why this matters: An account that is closed but not marked can be reinitialized and reused. An account that is reinitialized loses its old state and can be repurposed by an attacker.
6. Pre-Deploy Hygiene
cargo audithas been run and reports no known advisories in dependencies.Adversarial tests pass, not just the happy path. Every defense has a test that proves it works.
Property tests pass. Rules like "a deposit never shrinks a balance" hold for random inputs.
Fuzz tests pass. Trident has run the program through thousands of random instruction sequences without a panic.
anchor keys synchas been run. The program ID inlib.rsmatchesAnchor.toml.The
overflow-checks = trueflag is set under[profile.release]in the workspace rootCargo.toml.grep -rn "UncheckedAccount\|AccountInfo\|/// CHECK" programs/*/srcreturns only accounts you have manually verified.
Why this matters: The best defense is a test that proves it works. A constraint you cannot see fail is a constraint you are only hoping is there. Run the checklist before every deploy, not just the first one.
This checklist is a living document. Run it before every mainnet deploy, top to bottom. If you find something it misses, open an issue or reach out. Security is a community effort.
This post draws from Days 75-82 of #100DaysOfSolana.
Top comments (0)