solana-fuzz: Trident fuzzing and invariants for Solana programs
Turn a Solana program into a property-tested one. This skill writes Trident fuzz
tests: it scaffolds the test layout, generates the flow methods that drive your
instructions with random inputs, and writes invariant methods that assert your
program's state stays correct after every fuzzed transaction sequence. Trident
(Ackee Blockchain, Solana Foundation supported) runs many transactions per second
against the TridentSVM client and catches the runtime logic errors that happy-path
example tests never reach.
Verify before you author. Trident's API changes between minor versions. This
skill pins to v0.12.0 (see metadata.trident-version), the latest stable release;
0.13 is still pre-release (RC) as of this skill's date, so stable is the
reproducible choice. Before generating code, confirm the user's installed version
and re-read the relevant reference file rather than authoring from memory. If the
version differs, check the live source at the URL in metadata.trident-source.
When to use this skill
- The user wants to fuzz a Solana program, or write a Trident fuzz test.
- The user wants property/invariant checks over a program's state transitions.
- The user wants to find runtime bugs that example-based tests miss, or harden a
program before a mainnet deploy.
- The user wants regression fuzzing to catch bugs introduced between versions.
What this skill is NOT
- Not a static security auditor or vulnerability scanner. Route code-audit and
exploit-class detection to a dedicated audit skill. This skill is dynamic,
dev-loop property testing.
- Not a replacement for example-based tests. It complements LiteSVM/Mollusk/Surfpool.
- Not a transaction-landing, signing, or RPC tool. It runs against TridentSVM, not
a live cluster.
The Trident model (v0.12.0)
Anchored to the repo source, not memory:
Install the CLI: cargo install trident-cli --locked. Run a suite from the trident-tests/
directory: trident fuzz run fuzz_0 --with-exit-code. The --with-exit-code flag is
required: without it a failing invariant is silently swallowed in parallel mode and the
run still exits 0 (see references/invariants.md).
A fuzz test lives in trident-tests/fuzz_0/ and is made of:
test_fuzz.rs, fuzz_accounts.rs, types.rs, and Trident.toml.
The test is a struct deriving the framework methods and an impl annotated as a
flow executor:
use trident_fuzz::fuzzing::*;
#[derive(FuzzTestMethods)]
struct FuzzTest {
trident: Trident,
fuzz_accounts: AccountAddresses,
}
#[flow_executor]
impl FuzzTest {
fn new() -> Self { Self { trident: Trident::default(), fuzz_accounts: AccountAddresses::default() } }
#[init] fn start(&mut self) { /* per-iteration setup */ }
#[flow] fn flow1(&mut self) { /* randomly selected each iteration */ }
#[end] fn end(&mut self) { /* per-iteration cleanup */ }
}
fn main() { FuzzTest::fuzz(1000, 100); }
Drive an instruction: build it, then
self.trident.process_transaction(&[ix], Some("Label")), which returns a result
with .is_success() and .get_transaction_timestamp(). Gate invariant checks on
.is_success(): a rejected transaction is the expected-failure path, not a violation.
Random inputs: self.trident.random_from_range(0..u8::MAX). Fund a signer:
self.trident.airdrop(&addr, 10 * LAMPORTS_PER_SOL). Advance the clock:
self.trident.forward_in_time(n).
Insert accounts in fuzz_accounts: self.fuzz_accounts.author.insert(&mut self.trident, None)
for a keypair, or with Some(PdaSeeds { seeds: &[b"seed"], program_id }) for a PDA.
Read typed state: self.trident.get_account_with_type::<MyAccount>(&addr, 8)
(the 8 is the discriminator byte offset for Anchor accounts).
Invariants (the core value)
The pattern: capture state before, execute, capture state after, assert the
expected change in a dedicated invariant method. Handle expected failures
explicitly instead of swallowing them.
let before = match self.trident.get_account_with_type::<MyAccount>(&addr, 8) {
Some(v) => v, // 8 = Anchor discriminator; None until the account is initialized
None => return,
};
let res = self.trident.process_transaction(&[ix], Some("op"));
if res.is_success() {
if let Some(after) = self.trident.get_account_with_type::<MyAccount>(&addr, 8) {
assert!(/* domain property over before/after */, "describe the violation");
}
}
Gate the assert on is_success() so a correctly-rejected transaction is not counted
as a violation. The full worked invariant method, the expected-failure handling, and
the mandatory --with-exit-code flag (without it, a failing assert is silently
swallowed in parallel mode) are in references/invariants.md and the runnable
examples/vault/.
Operating procedure (when invoked)
- Detect the program: read
Anchor.toml and the IDL (for a native program, read its
instruction enum and entrypoint instead); list the instructions and the accounts each touches.
- Scaffold the
trident-tests/fuzz_0/ layout (or use the CLI init flow per
references/setup.md).
- Define
fuzz_accounts (signers and PDAs with their real seeds) in
fuzz_accounts.rs; mirror the program's account types in types.rs.
- Write
#[init] to establish baseline state (create accounts, airdrop, seed PDAs).
- Write one
#[flow] per instruction (and per meaningful multi-instruction
sequence), driving each with random inputs.
- For every state-changing instruction, write an invariant method using the
capture-before/after pattern above. Assert the real domain property, not just
"it didn't error".
- Run
trident fuzz run fuzz_0 --with-exit-code from trident-tests/. The flag is
mandatory or invariant failures do not register. Triage a reported failure with the
printed master seed (trident fuzz run fuzz_0 <SEED>), fix, re-run green.
Do not author flow or invariant code from memory. Read the relevant reference file
first; the API is version-specific.
References (progressive disclosure)
references/setup.md: install, init, Trident.toml, prerequisites, run command
references/flows.md: #[init]/#[flow]/#[end], driving instructions, random inputs, time control
references/invariants.md: capture-before/after, invariant methods, expected-failure handling
references/accounts.md: fuzz_accounts, PDA seeds, get_account_with_type, discriminator offset
references/regression.md: comparing program behavior across versions
Worked example
examples/vault/ is the verified, runnable reference: a tiny Anchor program with a
planted balance-invariant bug plus a Trident suite that catches it (red), and the
require! + checked_sub fix that makes it pass (green). It is compiled and fuzzed against the exact
v0.12 API; when in doubt about a call, copy from there.
Verified vs documented
examples/vault/ is the one flow verified end-to-end against the v0.12.0 API (red on the
planted bug, green on the require! + checked_sub fix). The other capabilities -- native
(non-Anchor) programs, multi-instruction sequences, and the across-versions regression flow
in references/regression.md -- are documented patterns, not separately shipped runnable
examples. Generate them from the references for your program and your installed Trident
version, then verify the output the same way the vault was verified: run it, confirm a real
bug goes red and the fix goes green, before relying on it.
Troubleshooting (common failure modes)
- Every flow shows 0 successful invocations: the
fuzz_accounts PDA seeds do not match the
program's real seeds. Fix them in fuzz_accounts.rs (see references/accounts.md).
- A run exits 0 when you expected a violation:
--with-exit-code was omitted, so a failing
invariant was swallowed in parallel mode. Always run trident fuzz run fuzz_0 --with-exit-code.
- The build dies with an
edition2024 / MSRV error before your code compiles: a transitive
dependency resolved newer than the pinned toolchain supports. Pin it
(cargo update -p <crate> --precise <older>) or use a newer platform-tools
(anchor build -- --tools-version vX.Y). See references/setup.md.
- A native (non-Anchor) account reads as garbage: wrong discriminator offset. Anchor accounts
use offset 8; native accounts have no Anchor discriminator, so pass the program's real
layout offset (often 0). See
references/accounts.md.
Provenance
Trident API in this skill is taken from the v0.12.0 source and docs at
https://github.com/Ackee-Blockchain/trident (examples under
examples/hello_world/trident-tests/ and the invariants-assertions documentation).
Re-verify against that source when the pinned version changes.
1---2name: solana-fuzz3description: Use when fuzzing a Solana program or writing a Trident fuzz test: property and invariant testing of a program's state transitions, finding runtime logic bugs that happy-path and example-based tests (LiteSVM, Mollusk, Surfpool) miss, hardening an Anchor or native program before a mainnet deploy, or adding regression fuzzing across program versions. Trident is the Solana Foundation supported fuzzer by Ackee Blockchain. Dynamic, dev-loop property testing, not a static security auditor.4license: MIT5---67# solana-fuzz: Trident fuzzing and invariants for Solana programs89Turn a Solana program into a property-tested one. This skill writes Trident fuzz10tests: it scaffolds the test layout, generates the flow methods that drive your11instructions with random inputs, and writes invariant methods that assert your12program's state stays correct after every fuzzed transaction sequence. Trident13(Ackee Blockchain, Solana Foundation supported) runs many transactions per second14against the TridentSVM client and catches the runtime logic errors that happy-path15example tests never reach.1617> Verify before you author. Trident's API changes between minor versions. This18> skill pins to v0.12.0 (see `metadata.trident-version`), the latest stable release;19> 0.13 is still pre-release (RC) as of this skill's date, so stable is the20> reproducible choice. Before generating code, confirm the user's installed version21> and re-read the relevant reference file rather than authoring from memory. If the22> version differs, check the live source at the URL in `metadata.trident-source`.2324## When to use this skill2526- The user wants to fuzz a Solana program, or write a Trident fuzz test.27- The user wants property/invariant checks over a program's state transitions.28- The user wants to find runtime bugs that example-based tests miss, or harden a29 program before a mainnet deploy.30- The user wants regression fuzzing to catch bugs introduced between versions.3132## What this skill is NOT3334- Not a static security auditor or vulnerability scanner. Route code-audit and35 exploit-class detection to a dedicated audit skill. This skill is dynamic,36 dev-loop property testing.37- Not a replacement for example-based tests. It complements LiteSVM/Mollusk/Surfpool.38- Not a transaction-landing, signing, or RPC tool. It runs against TridentSVM, not39 a live cluster.4041## The Trident model (v0.12.0)4243Anchored to the repo source, not memory:4445- Install the CLI: `cargo install trident-cli --locked`. Run a suite from the `trident-tests/`46 directory: `trident fuzz run fuzz_0 --with-exit-code`. The `--with-exit-code` flag is47 required: without it a failing invariant is silently swallowed in parallel mode and the48 run still exits 0 (see `references/invariants.md`).49- A fuzz test lives in `trident-tests/fuzz_0/` and is made of:50 `test_fuzz.rs`, `fuzz_accounts.rs`, `types.rs`, and `Trident.toml`.51- The test is a struct deriving the framework methods and an impl annotated as a52 flow executor:5354 ```rust55 use trident_fuzz::fuzzing::*;5657 #[derive(FuzzTestMethods)]58 struct FuzzTest {59 trident: Trident,60 fuzz_accounts: AccountAddresses,61 }6263 #[flow_executor]64 impl FuzzTest {65 fn new() -> Self { Self { trident: Trident::default(), fuzz_accounts: AccountAddresses::default() } }6667 #[init] fn start(&mut self) { /* per-iteration setup */ }68 #[flow] fn flow1(&mut self) { /* randomly selected each iteration */ }69 #[end] fn end(&mut self) { /* per-iteration cleanup */ }70 }7172 fn main() { FuzzTest::fuzz(1000, 100); }73 ```7475- Drive an instruction: build it, then76 `self.trident.process_transaction(&[ix], Some("Label"))`, which returns a result77 with `.is_success()` and `.get_transaction_timestamp()`. Gate invariant checks on78 `.is_success()`: a rejected transaction is the expected-failure path, not a violation.79- Random inputs: `self.trident.random_from_range(0..u8::MAX)`. Fund a signer:80 `self.trident.airdrop(&addr, 10 * LAMPORTS_PER_SOL)`. Advance the clock:81 `self.trident.forward_in_time(n)`.82- Insert accounts in `fuzz_accounts`: `self.fuzz_accounts.author.insert(&mut self.trident, None)`83 for a keypair, or with `Some(PdaSeeds { seeds: &[b"seed"], program_id })` for a PDA.84- Read typed state: `self.trident.get_account_with_type::<MyAccount>(&addr, 8)`85 (the `8` is the discriminator byte offset for Anchor accounts).8687## Invariants (the core value)8889The pattern: capture state before, execute, capture state after, assert the90expected change in a dedicated invariant method. Handle expected failures91explicitly instead of swallowing them.9293```rust94let before = match self.trident.get_account_with_type::<MyAccount>(&addr, 8) {95 Some(v) => v, // 8 = Anchor discriminator; None until the account is initialized96 None => return,97};98let res = self.trident.process_transaction(&[ix], Some("op"));99if res.is_success() {100 if let Some(after) = self.trident.get_account_with_type::<MyAccount>(&addr, 8) {101 assert!(/* domain property over before/after */, "describe the violation");102 }103}104```105106Gate the assert on `is_success()` so a correctly-rejected transaction is not counted107as a violation. The full worked invariant method, the expected-failure handling, and108the mandatory `--with-exit-code` flag (without it, a failing assert is silently109swallowed in parallel mode) are in `references/invariants.md` and the runnable110`examples/vault/`.111112## Operating procedure (when invoked)1131141. Detect the program: read `Anchor.toml` and the IDL (for a native program, read its115 instruction enum and entrypoint instead); list the instructions and the accounts each touches.1162. Scaffold the `trident-tests/fuzz_0/` layout (or use the CLI init flow per117 `references/setup.md`).1183. Define `fuzz_accounts` (signers and PDAs with their real seeds) in119 `fuzz_accounts.rs`; mirror the program's account types in `types.rs`.1204. Write `#[init]` to establish baseline state (create accounts, airdrop, seed PDAs).1215. Write one `#[flow]` per instruction (and per meaningful multi-instruction122 sequence), driving each with random inputs.1236. For every state-changing instruction, write an invariant method using the124 capture-before/after pattern above. Assert the real domain property, not just125 "it didn't error".1267. Run `trident fuzz run fuzz_0 --with-exit-code` from `trident-tests/`. The flag is127 mandatory or invariant failures do not register. Triage a reported failure with the128 printed master seed (`trident fuzz run fuzz_0 <SEED>`), fix, re-run green.129130Do not author flow or invariant code from memory. Read the relevant reference file131first; the API is version-specific.132133## References (progressive disclosure)134135- `references/setup.md`: install, init, `Trident.toml`, prerequisites, run command136- `references/flows.md`: `#[init]`/`#[flow]`/`#[end]`, driving instructions, random inputs, time control137- `references/invariants.md`: capture-before/after, invariant methods, expected-failure handling138- `references/accounts.md`: `fuzz_accounts`, PDA seeds, `get_account_with_type`, discriminator offset139- `references/regression.md`: comparing program behavior across versions140141## Worked example142143`examples/vault/` is the verified, runnable reference: a tiny Anchor program with a144planted balance-invariant bug plus a Trident suite that catches it (red), and the145`require!` + `checked_sub` fix that makes it pass (green). It is compiled and fuzzed against the exact146v0.12 API; when in doubt about a call, copy from there.147148## Verified vs documented149150`examples/vault/` is the one flow verified end-to-end against the v0.12.0 API (red on the151planted bug, green on the `require!` + `checked_sub` fix). The other capabilities -- native152(non-Anchor) programs, multi-instruction sequences, and the across-versions regression flow153in `references/regression.md` -- are documented patterns, not separately shipped runnable154examples. Generate them from the references for your program and your installed Trident155version, then verify the output the same way the vault was verified: run it, confirm a real156bug goes red and the fix goes green, before relying on it.157158## Troubleshooting (common failure modes)159160- Every flow shows 0 successful invocations: the `fuzz_accounts` PDA seeds do not match the161 program's real seeds. Fix them in `fuzz_accounts.rs` (see `references/accounts.md`).162- A run exits 0 when you expected a violation: `--with-exit-code` was omitted, so a failing163 invariant was swallowed in parallel mode. Always run `trident fuzz run fuzz_0 --with-exit-code`.164- The build dies with an `edition2024` / MSRV error before your code compiles: a transitive165 dependency resolved newer than the pinned toolchain supports. Pin it166 (`cargo update -p <crate> --precise <older>`) or use a newer platform-tools167 (`anchor build -- --tools-version vX.Y`). See `references/setup.md`.168- A native (non-Anchor) account reads as garbage: wrong discriminator offset. Anchor accounts169 use offset 8; native accounts have no Anchor discriminator, so pass the program's real170 layout offset (often 0). See `references/accounts.md`.171172## Provenance173174Trident API in this skill is taken from the v0.12.0 source and docs at175`https://github.com/Ackee-Blockchain/trident` (examples under176`examples/hello_world/trident-tests/` and the invariants-assertions documentation).177Re-verify against that source when the pinned version changes.