Solana Smart Contract Security: Best Practices for Anchor
Quick answer. Validate every account's owner, signer and link to the right user or pool. Use canonical bumps and owner-specific seeds, pin CPI targets, use checked math, close with Anchor's close, and keep upgrade authority in a multisig.
Video transcript
A Solana program holds no state. Its classic bug is an account it trusted without proving it was the right one.
Anchor's account types check owners, signers and programs. But none knows a vault belongs to this pool. That link is yours to prove.
In a LayerZero Solana review, a role macro checked the role PDA but not the signature, so always prove the signer.
Use canonical bumps, since a non-canonical bump creates a second valid address. Put the owning key in seeds, take seeds from the right place, and re-derive at every use.
The runtime stops privilege escalation, not calls to the wrong program. Type the token program as a Program, not a plain account info.
Initialize once. In Degen Safe, init if needed let anyone re-run setup and take the authority. Close with Anchor's close, and use checked math.
Keep upgrade authority in a multisig. Admin keys need care too. Chainalysis reports privileged access cost Drift about 285 million dollars.
Track the framework too. The Anchor repository lists three security advisories, including a Critical account substitution bug. Pin anchor lang and upgrade.
Guardian has run nine engagements for Jupiter since November 2025, all on Solana. Jupiter's Yield Store vault was reviewed by two competing teams plus a fuzzing engineer.
Read the full guide for every check, or talk to Guardian about reviewing your Anchor program.
A Solana program holds no state of its own. Every account it reads or writes arrives as an input, chosen by whoever builds the transaction.
So the classic Solana bug is not a math error. It is an account the program trusted without proving it was the right one.
This guide is for Anchor developers. Buying an audit? Our Solana audit guide covers cost and scope.
Validate Every Account
The runtime enforces ownership for you: only an account's owner program can change its data or debit its lamports.
That stops other programs writing to your accounts. It does not stop a caller passing in the wrong one.
Anchor turns most checks into account types. Its docs say what each one proves:
| Anchor type | What it checks |
|---|---|
Account<'info, T> | The owner, when the account is deserialized |
Signer<'info> | The account signed the transaction |
Program<'info, T> | The account is the given program |
SystemAccount<'info> | The System Program owns the account |
UncheckedAccount<'info> | Nothing |
None of these knows that a stake record belongs to this user, or a vault to this pool. That is the job of has_one, seeds and constraint.
Anchor also rejects the same mutable account passed twice in one instruction, unless you opt in with dup.
Prove the signer
Holding a role is not the same as holding the key. In a LayerZero Solana OApp review, a role macro checked the role PDA but not the signature (R1-L-05).
An integrator who typed authority as anything but Signer would let a caller pass a privileged public key. The finding is resolved.
Derive, then compare
Recomputing an address is only half a check. In Degen Safe's protocol review, two instructions derived the expected token account address but never compared it with the account passed in (L-06).
Treat every /// CHECK comment as a claim to prove. Back it with address, has_one, seeds or a constraint.
PDAs, Seeds and Bumps
A program derived address (PDA) comes from hashing seeds, a bump and the program ID.
The search tries bumps from 255 down. The first one that lands off the Ed25519 curve is the canonical bump.
Solana's docs warn that a non-canonical bump "creates a second valid address for the same seeds". An attacker can then substitute a different account.
In Anchor:
- A
seedsconstraint with an emptybumpuses the canonical bump. - Store
ctx.bumpsat init, then checkbump = account.bump. Sealevel-attacks calls this a more efficient check.
Make sure the stored bump is written. In Jupiter's JupUSD program, a Vault.bump field was never set and stayed at 0 (I-06, Informational, resolved).
Seeds decide who shares an account
Leave the owning key out of the seeds and unrelated users share one PDA. Our audit guide shows a Critical of this kind.
Seeds must also come from the right place. In Orderly's Solana vault, a role PDA's seed was read from instruction parameters (H-03, High, resolved).
So a caller holding any role could pass that role's PDA and change the broker settings.
Re-derive at every use, not only at creation. In OneSig on Solana, execution accepted staged buffer accounts without re-checking their seeds against the executing state.
A buffer staged for one OneSig state could be consumed, and closed, by another (L-02, resolved).
CPI to the Right Program
A cross-program invocation (CPI) hands your accounts, and some of your privileges, to another program. Solana's runtime limits what moves:
- Writable privilege cannot escalate.
- Signer privilege passes only from the caller's signers, or PDAs from the caller's seeds.
- Indirect reentrancy (A calls B calls A) is rejected.
Those rules stop escalation. They do not check that you called the program you meant.
Coral's sealevel-attacks repository shows the classic case: a token transfer CPI to a token_program typed as a plain AccountInfo. Its fix is Program<'info, Token>.
Caller-supplied remaining_accounts need the same care. In the LayerZero Solana OApp review, one admin path forwarded them unchecked into an endpoint CPI (R1-L-03, Low, declined).
Init, Close and Arithmetic
Initialize once
Anchor's API reference warns that with init_if_needed, "you need to make sure you properly protect yourself against re-initialization attacks."
In Degen Safe's SOL deposit program, init_if_needed let anyone re-run setup and take the authority (C-05, Critical, resolved).
Guardian's fix was plain init, so setup can succeed only once.
Bind initialize to a known key as well. Deployment and the first call are separate transactions, so an open initialize can be front-run (M-03).
Close with Anchor
Anchor's close constraint sends the lamports to a target, hands the account to the System Program and clears its data.
Sealevel-attacks marks three hand-written closes as insecure, from draining the lamports alone to also zeroing the data. Its fix is close = destination.
Close what users paid for, too. In Degen Safe's March 2026 updates, user deposit PDAs could never be closed, so their rent stayed locked (L-03, resolved).
Use checked math
The Rust Book says release builds wrap on overflow, and calls relying on that wrapping "an error". Anchor's workspace template turns overflow-checks on for release builds.
Write checked_add and checked_sub anyway, and return a clear error. For a withdrawal net of fees, Guardian advised Orderly to use checked_sub, not saturating_sub (I-13).
A failed transaction can still block users. In OneSig on Solana, a u64::MAX SOL allowance overflowed and made valid calls fail (I-10).
How does Rust compare with Solidity and Move? See best smart contract language for security.
Upgrades and Anchor Advisories
The upgrade authority can replace a program's code. Setting it to None makes the program immutable.
Keep the authority in a multisig. Transfer it with SetAuthorityChecked, which requires the new authority to sign too.
Admin keys need the same care. Chainalysis reports that privileged access cost Drift about $285 million on April 1, 2026.
Its Security Council was a 2-of-5 multisig with zero timelock. See the Drift exploit.
Track the framework too. As of November 2026, the Anchor repository lists three security advisories:
| Advisory | Severity | Affected | Fixed in |
|---|---|---|---|
Program<'info, System> accepted any program (May 7, 2026) | High | From 1.0.0 | 1.0.2 |
InterfaceAccount allowed account substitution (May 8, 2026) | Critical | 1.0.0-rc.1 | 1.0.0-rc.2 |
LazyAccount::unload() skipped post-CPI checks (July 13, 2026) | Low | From 0.31.0 | None listed |
In the System advisory's proof of concept, the Compute Budget program stood in for the System Program. The transfer never happened, and the program carried on.
Pin anchor-lang, watch the repository's advisories, and upgrade. The newest version is 1.2.2, released October 28, 2026.
How Guardian Reviews Solana Programs
A Guardian smart contract audit runs the Guardian Standard: two competing teams on one scope, one manual and one driving frontier AI, over an invariant fuzzing suite.
Guardian has run nine engagements for Jupiter since November 2025, all on Solana.
Jupiter's Yield Store vault program, 5,694 source lines, was reviewed by two competing teams plus a fuzzing engineer.
A Vanguard audit adds a $100,000 Guardian-funded contest and six months of update review.
Frequently Asked Questions
What are the most common Solana smart contract vulnerabilities?
The classic ones are missing account checks: a missing signer, owner or relationship check, wrong PDA seeds, re-initialization and CPIs to unpinned programs. Coral's sealevel-attacks repository catalogs 11 such classes, each with a recommended Anchor idiom.
Does Anchor make a Solana program secure?
Anchor checks what you declare: owners, signers, seeds and the constraints you write. It cannot know that a vault belongs to a pool. The framework itself has had advisories, such as a System Program check fixed in anchor-lang 1.0.2.
Why does the canonical bump matter?
Solana's docs warn that a non-canonical bump creates a second valid address for the same seeds, so an attacker can substitute a different account. Anchor's seeds and bump constraint uses the canonical bump when you leave the bump empty.
Should a Solana program's upgrade authority be revoked?
Revoking it makes the program immutable, so bugs can no longer be patched. If you keep it, hold it in a multisig, document who signs, and transfer it only with SetAuthorityChecked.
Sources
- Solana Docs: Account Modification Rules
- Solana Docs: PDA Derivation
- Solana Docs: CPI Execution and Privileges
- Solana Docs: Program Deployment
- Anchor Docs: Account Types
- Anchor Docs: Account Constraints
- anchor-lang 1.2.2 API reference: derive Accounts
- Anchor on GitHub: workspace template, overflow-checks in release
- Anchor advisory GHSA-c6rc-8jpp-2fgc: Program<'info, System> is not properly validated (May 7, 2026)
- Anchor advisory GHSA-429q-fhh4-r6hj: InterfaceAccount allows account substitution (May 8, 2026)
- Anchor advisory GHSA-6px8-6mw3-4hx4: LazyAccount::unload() skips post-CPI revalidation (July 13, 2026)
- Anchor on GitHub: Releases (v1.2.2, October 28, 2026)
- Coral on GitHub: sealevel-attacks, common exploits and protections on Solana
- The Rust Programming Language: Data Types, Integer Overflow
- Chainalysis: The Drift Protocol Hack, How Privileged Access Led to a $285 Million Loss (April 9, 2026)
- Guardian: LayerZero Solana OApp report (July 20, 2026)
- Guardian: LayerZero OneSig on Solana report (August 16, 2026)
- Guardian: Degen Safe Protocol Review report (February 2, 2026)
- Guardian: Degen Safe Contract Updates report (March 18, 2026)
- Guardian: Jupiter JupUSD Stablecoin report (December 25, 2025)
- Guardian: Orderly Solana Vault, Sol-CC and EVM Updates report (November 11, 2025)
- Guardian: Jupiter client story
Get your Anchor program reviewed
Guardian audits Solana programs account by account, from signer checks and PDA seeds to CPIs and upgrades.