All articles
Guides·April 4, 2026·5 min read

Anchor vs Native Rust: Which Is Safer to Audit on Solana

Anchor vs native Rust on Solana: how attack surface, constraint gaps, and audit checklists differ, plus a clear verdict on which to build with.

Two ways to write the same program

Every Solana program does the same three things: check who's calling, deserialize some account data, and mutate state. Anchor and native Rust disagree on how much of that gets automated versus written by hand, and that disagreement is the entire audit conversation. I've reviewed both flavors on client engagements, and the bugs cluster in predictable, framework-specific places once you know where to look.

Native Rust means entrypoint!, a raw &[AccountInfo] slice, and you doing every check yourself — signer, owner, discriminator, rent exemption, PDA derivation. Anchor wraps all of that in macros: #[program], #[derive(Accounts)], #[account(...)] constraints that generate the boilerplate for you and emit an IDL. Same SBF bytecode at the end, wildly different attack surface for a reviewer.

Where native programs get hurt

The classic native bug is a missing check, not a wrong calculation. Nobody forgets to write the math. People forget to verify that the account claiming to be a vault is actually owned by their program.

// native withdraw handler — no owner check on `vault_account`
pub fn withdraw(accounts: &[AccountInfo], amount: u64) -> ProgramResult {
    let account_iter = &mut accounts.iter();
    let vault_account = next_account_info(account_iter)?;
    let authority = next_account_info(account_iter)?;
    let mut vault_data = Vault::try_from_slice(&vault_account.data.borrow())?;

    if !authority.is_signer {
        return Err(ProgramError::MissingRequiredSignature);
    }
    // no vault_account.owner == program_id check
    // attacker passes a fake account with matching layout
    vault_data.balance -= amount;
    vault_account.data.borrow_mut()[..].copy_from_slice(&vault_data.try_to_vec()?);
    Ok(())
}

That's the account-substitution pattern behind more than one real exploit — pass an account that deserializes cleanly but was never created by your program, and the check that should have failed silently gets skipped because nobody wrote it. Native code also puts you on the hook for discriminators (or you don't bother, and now two account types with the same byte layout are interchangeable), rent-exemption checks, and PDA bump verification, all by hand, all invisible in a diff review unless you're checking every next_account_info() call against a mental checklist.

Where Anchor bugs hide instead

Anchor doesn't remove these bug classes, it moves them into the constraint list, where they're easier to audit — if you actually read every constraint. The equivalent function:

#[derive(Accounts)]
pub struct Withdraw<'info> {
    #[account(mut, has_one = authority)]
    pub vault: Account<'info, Vault>,
    pub authority: Signer<'info>,
}

Account<'info, Vault> checks the discriminator and owner automatically. has_one = authority checks the field. Signer<'info> checks the signature. Three failure modes closed in three lines, and Cashio-style "wrong account passed a validation check that looked right" bugs get much harder to write by accident.

Where Anchor bites you is UncheckedAccount and raw AccountInfo fields inside a #[derive(Accounts)] struct — the moment you opt out of typed validation for a "temporary" reason and forget to add a manual # Safety comment and check, you've recreated the native bug inside a framework that made you feel safe. I've also seen auditors skim past close = destination constraints and miss that a program still reads from an account in the same instruction after closing it, reviving state an attacker refunds and reinitializes. Anchor's Discriminator also isn't collision-proof across account types with identical anchor-generated 8-byte hashes if someone hand-rolls custom discriminators — rare, but I've seen it attempted to save 4 bytes of account space.

Integer overflow is the one place both frameworks are equally exposed: overflow-checks = true in Cargo.toml's release profile is not the default, and neither framework turns it on for you. If a program was compiled without it, checked_add and friends are your only real protection, native or Anchor.

The audit checklist actually changes shape

Dimension Native Rust Anchor
Owner/signer checks Manual, easy to omit Automatic via Account<> / Signer<>
Discriminator safety Manual or absent Automatic 8-byte hash
Where bugs cluster Missing checks in handler body Misused/omitted constraints, UncheckedAccount
Reviewer needs macro expansion No Yes — cargo expand before real review
Integer overflow protection Neither framework helps; needs explicit flag Same
Boilerplate / LOC to review High Low, but IDL + constraints need cross-checking
Compute unit overhead Lowest ~5-15% higher from validation and CPI helpers
Time to competent audit Longer, line-by-line Shorter per-function, but requires Anchor-specific expertise
Ecosystem tooling Fewer static checkers apply cleanly Anchor-aware linters, IDL diffing, Sec3 X-Ray

The practical effect: reviewing native code takes longer per instruction because nothing is assumed safe. Reviewing Anchor code is faster per instruction but demands an auditor who's specifically looked for constraint gaps before — someone unfamiliar with has_one semantics will wave through a missing one, because the code visually looks careful even when it isn't. This is also why we run every review through smart contract audits with both a manual constraint-by-constraint pass and expanded-macro review, not just a read of the source as written.

Which to pick when

Default to Anchor for anything handling user funds, LP positions, or governance — the structural checks eliminate the two bug classes (missing owner check, discriminator confusion) that show up most often in postmortems, and the IDL generation pays for itself the moment you need a client SDK or a second engineer joins. The framework overhead in compute units is rarely the bottleneck for a lending market or an order-book program.

Native Rust earns its keep in exactly one situation: compute-unit-starved hot paths where every CU matters and you have the audit budget to match the added scrutiny it needs — think a routing program inside a Solana sniper bot or the settlement leg of a Polymarket spread strategy, where Anchor's per-instruction overhead competes directly with your priority-fee budget and your execution speed against the validator's leader schedule (see our breakdown of Jito vs. Flashbots MEV auctions for why milliseconds and CUs both matter there). Outside of that narrow case, native Rust is a harder, slower, more expensive program to get audited correctly, for no security upside.

If you're deciding this before you've written a line of code, it's worth a framework-selection call with someone who's shipped both — our strategy consultation covers exactly this tradeoff alongside RPC and infrastructure choices like the ones we compare in Yellowstone gRPC vs. Solana RPC websockets and our Firedancer client notes. Talk to us before you pick a framework, not after your audit turns up the gap.

Need a bot like this built?

We design, build and run trading bots on Solana, Hyperliquid and Polymarket.

Start a project
#Solana#Anchor#Rust#Smart Contract Audit#Security