A Solana program receives every account it will read, write, or invoke from the
transaction. That flexibility is useful, but it puts the validation boundary
inside each instruction.
Before mainnet, prove who authorized the action, which program owns every state
account, how each PDA was derived, and which programs a CPI may call. Then test
the failure paths, lock down upgrades, and make the deployed binary traceable to
source.
This guide turns those requirements into a 10-check review. Examples use Anchor
1.x concepts. Anchor constraints reduce repetitive validation code, but they do
not replace threat modeling, adversarial tests, or an independent audit.
New readers should start with
Solana accounts explained first. The
transaction guide explains how signer and
writable privileges enter an instruction.
Start with an instruction-level threat model
Audit one instruction at a time. Record these facts for each account and
argument:
- Who controls the value?
- Which program must own the account?
- Must it sign, be writable, or be executable?
- What state relationship must already hold?
- Which state transition becomes possible after the instruction succeeds?
- Can the same account appear in two roles?
- Which external programs can receive a CPI?
This table is more useful than a generic promise to "validate inputs." It gives
reviewers a contract they can compare with the account constraints, handler,
and tests.
Treat client-side checks as interface feedback. A transaction can be assembled
outside your application, so every authority and state rule must be enforced by
the program.
Validate every account before the handler runs
Check 1: bind authority to both a signature and state
Signer<'info> proves that a key signed the transaction. It does not prove that
the signer is authorized for the vault, position, order, or configuration being
changed.
Bind both facts in the account context:
use anchor_lang::prelude::*;
#[derive(Accounts)]
pub struct Withdraw<'info> {
#[account(
mut,
has_one = authority @ VaultError::WrongAuthority,
)]
pub vault: Account<'info, Vault>,
pub authority: Signer<'info>,
/// CHECK: Receives lamports only. No data is trusted.
#[account(mut)]
pub destination: UncheckedAccount<'info>,
}
#[account]
pub struct Vault {
pub authority: Pubkey,
pub balance: u64,
}Role-based systems should avoid one unrestricted admin key. Separate emergency,
upgrade, treasury, and routine operating powers. Put high-impact actions behind
a multisig or governance process, and test every unauthorized role.
Check 2: verify owner, type, and data relationships
On Solana, an account's owner field names the program allowed to modify its
data. Deserializing arbitrary bytes without confirming ownership lets an
attacker substitute data with the shape your handler expects.
Prefer typed accounts such as Account<'info, T>, Program<'info, T>, and
InterfaceAccount<'info, T> when the type fits. Each UncheckedAccount needs
a documented reason plus validation for every property the handler relies on.
Token checks need more than an account owner. Confirm the token program, mint,
token authority, and any extension rules relevant to the operation. Anchor's
token::, mint::, and associated_token::* constraints express these
relationships close to the account definition.
Check 3: derive every PDA from canonical inputs
A PDA is not valid because it "looks like" program state. Recompute it from
the expected seeds and program ID.
#[derive(Accounts)]
pub struct Claim<'info> {
#[account(
mut,
seeds = [b"reward", user.key().as_ref()],
bump = reward.bump,
has_one = user @ RewardError::WrongUser,
constraint = !reward.claimed @ RewardError::AlreadyClaimed,
)]
pub reward: Account<'info, Reward>,
pub user: Signer<'info>,
}Store the canonical bump when the account is initialized. Keep seed domains
distinct, and include the business identity that prevents two users or two
markets from sharing state accidentally.
When a PDA signs a CPI, derive the signer seeds from validated state. Never
accept arbitrary signer seeds from instruction data.
Constrain every state change and external call
Check 4: constrain every CPI target and forwarded privilege
A CPI extends signer and writable privileges from the caller to the callee.
Allowing an arbitrary program account can turn a legitimate PDA signature into
authority for attacker-controlled code.
Prefer typed Program or Interface accounts. Otherwise, check the
callee address and executable flag before invocation. Review every account
forwarded to the CPI, especially PDA signers and writable treasury accounts.
Do not confuse a successful CPI with a safe business outcome. After the call,
verify the state change your instruction depends on. Token balances, ownership,
and position state may need explicit postconditions.
Check 5: make arithmetic state-aware
Checked arithmetic catches overflow, underflow, and division by zero. Financial
logic also needs domain checks:
- Define rounding direction for deposits, withdrawals, fees, and shares.
- Promote intermediate multiplication to a wider type where needed.
- Bound prices, quantities, timestamps, and basis points.
- Reject stale or invalid oracle data before calculation.
- Test boundary values, not only expected values.
The safe expression is not always the economically correct expression. Write
invariants such as "total claimable value cannot exceed funded value" and test
them across instruction sequences.
Check 6: make initialization and closure one-way transitions
Choose init when an account must be new. Treat init_if_needed as a separate
security decision because the handler must distinguish first initialization
from later use.
Anchor discriminators protect typed account deserialization, while close
transfers lamports and resets account data. Still test whether a closed or
zeroed account can re-enter the workflow in the same transaction or a later
one.
Initialization should set every security-sensitive field explicitly. Avoid
defaults for authorities, status flags, version markers, and asset identities.
Check 7: reject accidental account aliasing
Two mutable roles receiving the same account may cause a transfer or accounting
instruction to read and write one state object under conflicting assumptions.
Current Anchor 1.x rejects duplicate mutable serialized accounts by default.
The dup constraint opts back into duplicates, so each use deserves a written
invariant and a regression test. Other account wrappers may still allow the
same key in multiple roles.
Native programs should compare keys explicitly whenever roles must be distinct.
Add tests where the attacker supplies the same account for source and
destination, buyer and seller, or vault and fee destination.
Test the release and prepare to operate it
Check 8: test hostile sequences, not isolated happy paths
A passing unit test proves one input worked. Security failures often appear
across a sequence: initialize, deposit, change authority, invoke another
program, close, then initialize again.
Build a test matrix around the instruction threat model:
- Wrong signer and correct state.
- Correct signer and wrong authority relation.
- Wrong owner with correctly shaped bytes.
- Wrong PDA seeds or bump.
- Duplicate accounts in conflicting roles.
- Unauthorized CPI program.
- Arithmetic limits and stale external data.
- Repeated, reordered, and partially completed operations.
Property tests or fuzzing can cover wider state spaces. Keep the
failure assertions in the repository after the review; they are regression
controls, not disposable audit artifacts.
Check 9: control upgrades and verify the deployed binary
An audit covers a source revision. A key that can deploy different bytecode
afterward remains part of the security model.
Record the upgrade authority, require a multisig or governed path for changes,
and define when the program should become immutable. Rehearse rollback and
incident procedures before mainnet.
Solana's verified-build workflow compares the deployed executable with a build
from a public source revision. Verification proves source-to-binary
equivalence. It does not prove the code is safe.
Pin the toolchain and Cargo.lock, retain the deployed commit, and verify
again after every upgrade. Add security.txt so researchers can find a current
reporting channel from the program address.
Check 10: operate the program as a security boundary
Mainnet review is a checkpoint, not the end of the control loop. Monitor
authority changes, upgrade activity, treasury movement, error rates, and
high-risk instruction volume. Define who can pause a flow and what evidence is
required to resume it.
RPC observations help detect delivery and state changes, but they do not repair
unsafe program logic. Critical transaction paths should compare status across
independent endpoints and preserve the blockheight-aware rules in our
transaction reliability guide.
RPC Edge is our separate infrastructure venture for
transaction delivery and observation; program review remains a distinct job.
Publish a vulnerability policy, test the reporting channel, and decide how
you will communicate an incident before one happens.
The 10-check mainnet gate
Copy this into the release issue for every program deployment:
## Solana Program Mainnet Security Gate
- [ ] Each instruction has an account and argument threat model.
- [ ] Signatures are bound to state-based authority relationships.
- [ ] Owners, types, mints, token authorities, and executable programs are checked.
- [ ] PDAs use canonical seeds and validated bumps.
- [ ] CPI targets, forwarded accounts, and signer seeds are constrained.
- [ ] Arithmetic invariants, rounding, bounds, and stale-data cases are tested.
- [ ] Initialization, reinitialization, closure, and account aliasing are tested.
- [ ] Hostile instruction sequences and regression cases pass.
- [ ] Upgrade authority, verified build, source revision, and security.txt are recorded.
- [ ] Monitoring, pause authority, disclosure, and incident ownership are active.A team member outside the implementation path should sign off each item with a
link to code, a test, an on-chain record, or an operating document.
FAQ
Do Anchor constraints make a Solana program secure?
No. Constraints encode common account checks and reduce repetitive validation.
Your handler can still contain broken authorization, accounting, oracle,
economic, or state-transition logic.
Is a verified build the same as an audit?
No. A verified build shows that deployed bytecode matches a source revision.
Independent review evaluates the design and implementation of that revision
within a stated scope.
Should an upgrade authority be removed?
It depends on the operating model. Immutability removes upgrade risk but also
removes the ability to patch defects. Programs that retain upgrades should use a
multisig or governance path with explicit review and delay controls.
When should a program receive an independent review?
Before it controls meaningful user assets or privileges, and again after
material changes to authorization, accounting, external integrations, or
upgrade logic.
Primary references
- Anchor account constraints
- Solana accounts
- Solana program-derived addresses
- Solana cross-program invocations
- Solana verified builds and security.txt
- Anchor releases
Turn findings into release evidence
Programs approaching mainnet, or carrying unresolved review findings, can use
the Builderz smart contract auditing service to see
the review boundary. Send the program, deployed or target program ID, source
revision, approved budget, and acceptance conditions through the
Builderz project fit form.



