All articles
Solana·April 15, 2026·6 min read

Trident Fuzz Testing vs Manual Review for Anchor Programs

Trident fuzz testing anchor programs catches overflow and invariant bugs fast, but manual review still finds access-control and design flaws it can't.

Two Bugs, Two Methods

A lending protocol on Solana lost $2.6M last year to a bug that a single line-by-line read would have caught in ten minutes: a missing check on which account could call the liquidation instruction. The same protocol's fuzz suite ran for 40 million iterations without finding it, because nobody wrote an invariant that said "only accounts flagged as under-collateralized can be liquidated." Meanwhile, a different Anchor program shipped with an integer overflow in its reward-distribution math that three separate human reviewers missed across two audits — Trident found it in under six hours of fuzzing because it doesn't get bored mutating u64 values near their ceiling.

That's the real split between coverage-guided fuzzing and manual review on Anchor programs. They fail differently, and knowing which failure mode you're exposed to matters more than picking a "better" method.

How Trident Actually Exercises an Anchor Program

Trident wraps honggfuzz (and can be pointed at libFuzzer) around your Anchor instruction handlers, using the IDL to generate typed instruction data and account inputs automatically. It doesn't need you to hand-write a corpus — it mutates instruction arguments, account orderings, and PDA derivations, then replays sequences of instructions against a local solana-program-test validator or LiteSVM, checking your declared invariants after each transaction.

A minimal fuzz target for a vault deposit instruction looks like this:

#[derive(Arbitrary, Debug)]
pub struct FuzzInstruction {
    pub amount: u64,
    pub depositor_seed: u8,
}

impl FuzzTestExecutor<FuzzTestState> for FuzzInstruction {
    fn check_invariants(&self, state: &FuzzTestState) -> Result<(), FuzzingError> {
        let vault = state.get_account_data::<Vault>("vault")?;
        if vault.total_shares == 0 && vault.total_assets != 0 {
            return Err(FuzzingError::custom("shares zero but assets nonzero"));
        }
        if vault.total_assets < vault.reserved_assets {
            return Err(FuzzingError::custom("reserved exceeds total"));
        }
        Ok(())
    }
}

Run this for a few hours with a reasonable corpus and Trident will happily discover that depositing u64::MAX twice wraps total_assets, or that a zero-amount deposit mints shares at a price that breaks your rounding assumption. This is exactly the class of bug that's tedious for a human to spot by inspection because it only manifests at specific numeric boundaries the reviewer has to think to construct by hand.

What Coverage-Guided Fuzzing Is Genuinely Good At

  • Arithmetic edge cases — overflow, underflow, off-by-one in fee math, division truncation, and rounding direction errors that favor the wrong party.
  • State invariant violations after long instruction sequences — Trident can chain hundreds of instructions in randomized order and catch drift between, say, total_supply and the sum of individual balances, something no reviewer manually traces past a handful of steps.
  • Account substitution bugs — because Trident mutates which accounts get passed where, it surfaces missing has_one or owner checks faster than eyeballing constraint macros, especially in programs with a dozen-plus accounts per instruction.
  • Regression locking — once a bug is found and fixed, the crashing input becomes a permanent regression test in CI, which manual review can't offer by itself.

What It Structurally Cannot Find

Fuzzing only checks the properties you told it to check. If your invariant list doesn't include "the DAO treasury signer must match the multisig PDA," Trident will fuzz that instruction forever and report a clean bill of health while a governance bypass sits in plain sight. This is the core limitation, and it's not a tooling gap — it's inherent to property-based testing. A fuzzer proves your invariants hold under the inputs it tried; it says nothing about invariants nobody thought to write.

Manual review catches a different category entirely:

  • Missing or wrong access control that doesn't violate any state invariant, just business logic (e.g., an instruction technically balances the books but lets the wrong party trigger it).
  • Economic and MEV-adjacent design flaws — sandwichable price updates, oracle staleness windows, front-runnable liquidation ordering — that require reasoning about off-chain actors and mempool behavior, not just on-chain state transitions.
  • Spec-vs-implementation mismatches, where the code is internally consistent but doesn't do what the whitepaper or product spec promised.
  • PDA seed collisions and account confusion across instructions that only show up when a reviewer cross-references every seeds = [...] constraint in the codebase against every other one.

This is also where reading how a firm structures its audit process pays off — the way we approach a Solana program audit before launch leans on this manual pass specifically because fuzzing alone leaves these gaps open.

Trident vs. Manual Review, Side by Side

Dimension Trident (coverage-guided fuzzing) Manual line-by-line review
Finds arithmetic edge cases Excellent, exhaustive within run time Inconsistent, depends on reviewer diligence
Finds access-control gaps Only if invariant explicitly checks it Primary strength
Finds economic/MEV design flaws No — no concept of off-chain actors Yes, with experience in the area
Setup cost Hours to days to write harness + invariants Zero setup, starts on read #1
Ongoing value Runs in CI forever, cheap per-commit One-time unless re-engaged
Output quality Crashing inputs, deterministic repro Written rationale, severity, fix guidance
Blind spots Bounded by invariants you wrote Bounded by reviewer time and attention

Which to Pick When

If you're shipping incremental changes to an existing, already-audited program — CPI additions, new instruction variants, fee-parameter updates — wire Trident into CI and let it chew on every PR. It's cheap once the harness exists and it will catch the numeric regressions that slip through code review because nobody re-derives the overflow math on every diff.

If you're pushing a new program to mainnet, especially one moving user funds, fuzzing is not a substitute for a human pass. Pair both: use fuzzing to harden the arithmetic and invariant surface, and get a manual review to validate access control, PDA design, and the economic assumptions baked into the protocol. This is roughly the same conclusion we lay out when people ask how to choose a Solana smart contract auditor — the firms worth hiring run both, not one instead of the other, and they document exactly which invariants the fuzz suite covers so you know where the manual review needs to focus.

One more practical note: fuzzing findings are cheap to fix pre-launch and expensive post-launch. If Trident turns up an invariant break after you've already deployed, follow a structured post-audit remediation process for Solana programs rather than patching live and hoping — a rushed fix under a running program with real deposits is how a fuzzing win turns into a second incident.

Programs with complex account graphs — think an MEV/arbitrage bot juggling multiple CPI calls per transaction, or a sniper bot racing block inclusion — benefit disproportionately from fuzzing because the account-substitution and ordering bugs multiply with every additional CPI hop. Simpler single-purpose vaults get proportionally more value from the manual pass, since their attack surface is mostly logic, not combinatorics.

Need both run against your program before mainnet, with a fuzz harness you actually keep afterward. Get a Solana smart contract audit that includes a Trident-based fuzz suite as a deliverable, not just a PDF.

Need a bot like this built?

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

Start a project
#Solana#Trident#Fuzz Testing#Anchor#Smart Contract Audit