Tools: Fuzzing Solana Programs with Trident: How Ackee's Open-Source Fuzzer Catches Bugs That Unit Tests Miss

Tools: Fuzzing Solana Programs with Trident: How Ackee's Open-Source Fuzzer Catches Bugs That Unit Tests Miss

Why Solana Programs Need Fuzzing

1. Missing Signer Checks on Authority Transfers

2. Arithmetic Overflows in Fee Calculations

3. Cross-Instruction State Corruption

Setting Up Trident

Prerequisites

Project Structure After Init

Writing Your First Fuzz Test

The Target Program

The Fuzz Test

The Invariant Check

Running the Fuzzer

Manually Guided Fuzzing: Simulating Attacker Behavior

Example: Oracle Manipulation + Borrow Drain

Real Bugs Found by Fuzzing Solana Programs

Trident vs Other Solana Security Tools

Practical Tips for Effective Fuzzing

1. Start With Your Protocol's Economic Invariants

2. Seed Your Corpus

3. Fuzz in CI

4. Fuzz After Every Audit

The Bottom Line Your Anchor program has 100% branch coverage. Every instruction handler has a matching unit test. Clippy is clean. anchor test passes. Then someone calls withdraw() after deposit() after update_oracle() in the same transaction, and 40,000 SOL vanishes into an attacker's wallet. Unit tests verify the paths you imagined. Fuzzers find the paths you didn't. This guide walks through Trident — Ackee Blockchain Security's open-source Rust fuzzer for Solana Anchor programs — and shows you how to catch real vulnerability classes before auditors (or attackers) do. Solana programs are stateful, multi-account, and composable. A single instruction can read from 8+ accounts, each with their own owner, data layout, and lifecycle. The attack surface isn't just "bad input" — it's bad sequences of valid inputs. Consider three vulnerability classes that unit tests routinely miss: A unit test that always passes the correct admin signer will never catch this. A fuzzer that randomizes account inputs will find it in seconds. You wrote checked_mul, so you think you're safe. But a fuzzer feeding amount = u64::MAX / 2 and fee_bps = 3 will show you that unwrap() panics — and on Solana, a panic during CPI means the outer transaction continues with no fee deducted. No individual instruction is buggy. The sequence is the exploit. This is exactly what fuzzers are built to find. This creates a trident-tests/ directory with: Let's fuzz a simplified lending pool that has a hidden vulnerability. The bug: borrow() checks if the current request fits under the collateral limit, but doesn't check the cumulative borrowed amount. A user can call borrow() repeatedly, each time borrowing up to max_borrow, draining the pool. This is where fuzzing becomes powerful. Define properties that must always hold: Within seconds, Trident will generate a sequence like: Trident's Manually Guided Fuzzing (MGF) feature lets you constrain the fuzzer to explore specific attack scenarios. Instead of pure random sequences, you model what a sophisticated attacker would do. This approach finds bugs that random fuzzing takes hours to discover, because you're encoding economic attack logic into the sequence. Here's a non-exhaustive list of vulnerability classes that Trident and similar fuzzers have caught in production Solana programs: The ideal pipeline: Semgrep (fast pattern scan) → Trident (stateful fuzzing) → Manual audit (economic logic review). Before writing any fuzz code, write down 5-10 properties that must always hold: These become your assertion functions. Give the fuzzer realistic starting inputs: This helps the fuzzer find deep bugs faster instead of spending time on trivially invalid inputs. Run 10 minutes per PR. If it crashes, the crash input is saved as an artifact for reproduction. Auditors miss things. Fuzzers explore differently. The combination is stronger than either alone. After receiving your audit report, write Trident scenarios that test every finding's class of bug — not just the specific instance. Solana's account model makes programs fundamentally harder to test than EVM contracts. Multi-account state, CPI composability, and PDA derivation create an exponential input space that unit tests can't cover. Trident won't replace auditors. But it will: The DeFi programs that survive 2026 won't be the ones with the most tests. They'll be the ones whose tests explored the most unexpected states. Start fuzzing. The attackers already are. This is part of the DeFi Security Research series. Previously: Formal Verification with Halmos/Certora/HEVM, Custom Semgrep Rules for Solana, Custom Slither Detectors. Templates let you quickly answer FAQs or store snippets for re-use. Hide child comments as well For further actions, you may consider blocking this person and/or reporting abuse

Command

Copy

$ // Vulnerable: admin_update doesn't verify current_admin signed pub fn admin_update(ctx: Context<AdminUpdate>, new_fee: u64) -> Result<()> { ctx.accounts.config.fee_bps = new_fee; Ok(()) } // Vulnerable: admin_update doesn't verify current_admin signed pub fn admin_update(ctx: Context<AdminUpdate>, new_fee: u64) -> Result<()> { ctx.accounts.config.fee_bps = new_fee; Ok(()) } // Vulnerable: admin_update doesn't verify current_admin signed pub fn admin_update(ctx: Context<AdminUpdate>, new_fee: u64) -> Result<()> { ctx.accounts.config.fee_bps = new_fee; Ok(()) } // Vulnerable: u64 overflow when amount * fee_bps > u64::MAX let fee = amount.checked_mul(fee_bps) .unwrap() .checked_div(10_000) .unwrap(); // Vulnerable: u64 overflow when amount * fee_bps > u64::MAX let fee = amount.checked_mul(fee_bps) .unwrap() .checked_div(10_000) .unwrap(); // Vulnerable: u64 overflow when amount * fee_bps > u64::MAX let fee = amount.checked_mul(fee_bps) .unwrap() .checked_div(10_000) .unwrap(); // Instruction A: deposit collateral // Instruction B: -weight: 500;">update oracle price // Instruction C: borrow against inflated collateral // All three in one transaction // Instruction A: deposit collateral // Instruction B: -weight: 500;">update oracle price // Instruction C: borrow against inflated collateral // All three in one transaction // Instruction A: deposit collateral // Instruction B: -weight: 500;">update oracle price // Instruction C: borrow against inflated collateral // All three in one transaction # Install Trident CLI cargo -weight: 500;">install trident-cli # Verify installation trident --version # Navigate to your Anchor project cd my-anchor-project/ # Initialize Trident in your project trident init # Install Trident CLI cargo -weight: 500;">install trident-cli # Verify installation trident --version # Navigate to your Anchor project cd my-anchor-project/ # Initialize Trident in your project trident init # Install Trident CLI cargo -weight: 500;">install trident-cli # Verify installation trident --version # Navigate to your Anchor project cd my-anchor-project/ # Initialize Trident in your project trident init my-anchor-project/ ├── programs/ │ └── my_program/ │ └── src/lib.rs ├── trident-tests/ │ └── fuzz_tests/ │ ├── fuzz_0/ │ │ ├── accounts_snapshots.rs │ │ ├── fuzz_instructions.rs │ │ └── test_fuzz.rs │ └── Cargo.toml └── Trident.toml my-anchor-project/ ├── programs/ │ └── my_program/ │ └── src/lib.rs ├── trident-tests/ │ └── fuzz_tests/ │ ├── fuzz_0/ │ │ ├── accounts_snapshots.rs │ │ ├── fuzz_instructions.rs │ │ └── test_fuzz.rs │ └── Cargo.toml └── Trident.toml my-anchor-project/ ├── programs/ │ └── my_program/ │ └── src/lib.rs ├── trident-tests/ │ └── fuzz_tests/ │ ├── fuzz_0/ │ │ ├── accounts_snapshots.rs │ │ ├── fuzz_instructions.rs │ │ └── test_fuzz.rs │ └── Cargo.toml └── Trident.toml #[program] pub mod lending_pool { use super::*; pub fn initialize(ctx: Context<Initialize>, oracle: Pubkey) -> Result<()> { let pool = &mut ctx.accounts.pool; pool.authority = ctx.accounts.authority.key(); pool.oracle = oracle; pool.total_deposits = 0; pool.total_borrows = 0; Ok(()) } pub fn deposit(ctx: Context<Deposit>, amount: u64) -> Result<()> { let pool = &mut ctx.accounts.pool; // Transfer SOL from user to pool vault // ... transfer logic ... pool.total_deposits = pool.total_deposits.checked_add(amount) .ok_or(ErrorCode::MathOverflow)?; let user = &mut ctx.accounts.user_account; user.deposited = user.deposited.checked_add(amount) .ok_or(ErrorCode::MathOverflow)?; Ok(()) } pub fn borrow(ctx: Context<Borrow>, amount: u64) -> Result<()> { let pool = &mut ctx.accounts.pool; let user = &mut ctx.accounts.user_account; let oracle = &ctx.accounts.oracle_account; let collateral_value = user.deposited .checked_mul(oracle.price) .ok_or(ErrorCode::MathOverflow)?; let max_borrow = collateral_value / 150; // 150% collateralization // BUG: doesn't check user.borrowed, only checks against max_borrow require!(amount <= max_borrow, ErrorCode::InsufficientCollateral); pool.total_borrows = pool.total_borrows.checked_add(amount) .ok_or(ErrorCode::MathOverflow)?; user.borrowed = user.borrowed.checked_add(amount) .ok_or(ErrorCode::MathOverflow)?; Ok(()) } } #[program] pub mod lending_pool { use super::*; pub fn initialize(ctx: Context<Initialize>, oracle: Pubkey) -> Result<()> { let pool = &mut ctx.accounts.pool; pool.authority = ctx.accounts.authority.key(); pool.oracle = oracle; pool.total_deposits = 0; pool.total_borrows = 0; Ok(()) } pub fn deposit(ctx: Context<Deposit>, amount: u64) -> Result<()> { let pool = &mut ctx.accounts.pool; // Transfer SOL from user to pool vault // ... transfer logic ... pool.total_deposits = pool.total_deposits.checked_add(amount) .ok_or(ErrorCode::MathOverflow)?; let user = &mut ctx.accounts.user_account; user.deposited = user.deposited.checked_add(amount) .ok_or(ErrorCode::MathOverflow)?; Ok(()) } pub fn borrow(ctx: Context<Borrow>, amount: u64) -> Result<()> { let pool = &mut ctx.accounts.pool; let user = &mut ctx.accounts.user_account; let oracle = &ctx.accounts.oracle_account; let collateral_value = user.deposited .checked_mul(oracle.price) .ok_or(ErrorCode::MathOverflow)?; let max_borrow = collateral_value / 150; // 150% collateralization // BUG: doesn't check user.borrowed, only checks against max_borrow require!(amount <= max_borrow, ErrorCode::InsufficientCollateral); pool.total_borrows = pool.total_borrows.checked_add(amount) .ok_or(ErrorCode::MathOverflow)?; user.borrowed = user.borrowed.checked_add(amount) .ok_or(ErrorCode::MathOverflow)?; Ok(()) } } #[program] pub mod lending_pool { use super::*; pub fn initialize(ctx: Context<Initialize>, oracle: Pubkey) -> Result<()> { let pool = &mut ctx.accounts.pool; pool.authority = ctx.accounts.authority.key(); pool.oracle = oracle; pool.total_deposits = 0; pool.total_borrows = 0; Ok(()) } pub fn deposit(ctx: Context<Deposit>, amount: u64) -> Result<()> { let pool = &mut ctx.accounts.pool; // Transfer SOL from user to pool vault // ... transfer logic ... pool.total_deposits = pool.total_deposits.checked_add(amount) .ok_or(ErrorCode::MathOverflow)?; let user = &mut ctx.accounts.user_account; user.deposited = user.deposited.checked_add(amount) .ok_or(ErrorCode::MathOverflow)?; Ok(()) } pub fn borrow(ctx: Context<Borrow>, amount: u64) -> Result<()> { let pool = &mut ctx.accounts.pool; let user = &mut ctx.accounts.user_account; let oracle = &ctx.accounts.oracle_account; let collateral_value = user.deposited .checked_mul(oracle.price) .ok_or(ErrorCode::MathOverflow)?; let max_borrow = collateral_value / 150; // 150% collateralization // BUG: doesn't check user.borrowed, only checks against max_borrow require!(amount <= max_borrow, ErrorCode::InsufficientCollateral); pool.total_borrows = pool.total_borrows.checked_add(amount) .ok_or(ErrorCode::MathOverflow)?; user.borrowed = user.borrowed.checked_add(amount) .ok_or(ErrorCode::MathOverflow)?; Ok(()) } } // trident-tests/fuzz_tests/fuzz_0/fuzz_instructions.rs use trident_client::fuzzing::*; #[derive(Arbitrary, Debug)] pub struct DepositData { pub amount: u64, } #[derive(Arbitrary, Debug)] pub struct BorrowData { pub amount: u64, } impl FuzzInstruction for DepositData { fn build( &self, _client: &mut impl FuzzClient, _fuzz_accounts: &mut FuzzAccounts, ) -> Result<Instruction, FuzzingError> { // Build deposit instruction with fuzzed amount let deposit_ix = lending_pool::instruction::Deposit { amount: self.amount, }; // ... account setup ... Ok(ix) } } impl FuzzInstruction for BorrowData { fn build( &self, _client: &mut impl FuzzClient, _fuzz_accounts: &mut FuzzAccounts, ) -> Result<Instruction, FuzzingError> { let borrow_ix = lending_pool::instruction::Borrow { amount: self.amount, }; // ... account setup ... Ok(ix) } } // trident-tests/fuzz_tests/fuzz_0/fuzz_instructions.rs use trident_client::fuzzing::*; #[derive(Arbitrary, Debug)] pub struct DepositData { pub amount: u64, } #[derive(Arbitrary, Debug)] pub struct BorrowData { pub amount: u64, } impl FuzzInstruction for DepositData { fn build( &self, _client: &mut impl FuzzClient, _fuzz_accounts: &mut FuzzAccounts, ) -> Result<Instruction, FuzzingError> { // Build deposit instruction with fuzzed amount let deposit_ix = lending_pool::instruction::Deposit { amount: self.amount, }; // ... account setup ... Ok(ix) } } impl FuzzInstruction for BorrowData { fn build( &self, _client: &mut impl FuzzClient, _fuzz_accounts: &mut FuzzAccounts, ) -> Result<Instruction, FuzzingError> { let borrow_ix = lending_pool::instruction::Borrow { amount: self.amount, }; // ... account setup ... Ok(ix) } } // trident-tests/fuzz_tests/fuzz_0/fuzz_instructions.rs use trident_client::fuzzing::*; #[derive(Arbitrary, Debug)] pub struct DepositData { pub amount: u64, } #[derive(Arbitrary, Debug)] pub struct BorrowData { pub amount: u64, } impl FuzzInstruction for DepositData { fn build( &self, _client: &mut impl FuzzClient, _fuzz_accounts: &mut FuzzAccounts, ) -> Result<Instruction, FuzzingError> { // Build deposit instruction with fuzzed amount let deposit_ix = lending_pool::instruction::Deposit { amount: self.amount, }; // ... account setup ... Ok(ix) } } impl FuzzInstruction for BorrowData { fn build( &self, _client: &mut impl FuzzClient, _fuzz_accounts: &mut FuzzAccounts, ) -> Result<Instruction, FuzzingError> { let borrow_ix = lending_pool::instruction::Borrow { amount: self.amount, }; // ... account setup ... Ok(ix) } } // In test_fuzz.rs fn check_invariants( pre_state: &PoolState, post_state: &PoolState, ix: &FuzzInstruction, ) -> Result<(), FuzzingError> { // INVARIANT 1: Total borrows must never exceed total deposits assert!( post_state.total_borrows <= post_state.total_deposits, "Protocol insolvency: borrows ({}) > deposits ({})", post_state.total_borrows, post_state.total_deposits ); // INVARIANT 2: Each user's borrowed amount must respect collateral ratio for user in &post_state.users { let max_allowed = user.deposited .saturating_mul(post_state.oracle_price) / 150; assert!( user.borrowed <= max_allowed, "User {} borrowed {} but collateral only supports {}", user.address, user.borrowed, max_allowed ); } // INVARIANT 3: Pool vault balance == total_deposits - total_borrows assert_eq!( post_state.vault_balance, post_state.total_deposits.saturating_sub(post_state.total_borrows), "Accounting mismatch: vault doesn't match pool state" ); Ok(()) } // In test_fuzz.rs fn check_invariants( pre_state: &PoolState, post_state: &PoolState, ix: &FuzzInstruction, ) -> Result<(), FuzzingError> { // INVARIANT 1: Total borrows must never exceed total deposits assert!( post_state.total_borrows <= post_state.total_deposits, "Protocol insolvency: borrows ({}) > deposits ({})", post_state.total_borrows, post_state.total_deposits ); // INVARIANT 2: Each user's borrowed amount must respect collateral ratio for user in &post_state.users { let max_allowed = user.deposited .saturating_mul(post_state.oracle_price) / 150; assert!( user.borrowed <= max_allowed, "User {} borrowed {} but collateral only supports {}", user.address, user.borrowed, max_allowed ); } // INVARIANT 3: Pool vault balance == total_deposits - total_borrows assert_eq!( post_state.vault_balance, post_state.total_deposits.saturating_sub(post_state.total_borrows), "Accounting mismatch: vault doesn't match pool state" ); Ok(()) } // In test_fuzz.rs fn check_invariants( pre_state: &PoolState, post_state: &PoolState, ix: &FuzzInstruction, ) -> Result<(), FuzzingError> { // INVARIANT 1: Total borrows must never exceed total deposits assert!( post_state.total_borrows <= post_state.total_deposits, "Protocol insolvency: borrows ({}) > deposits ({})", post_state.total_borrows, post_state.total_deposits ); // INVARIANT 2: Each user's borrowed amount must respect collateral ratio for user in &post_state.users { let max_allowed = user.deposited .saturating_mul(post_state.oracle_price) / 150; assert!( user.borrowed <= max_allowed, "User {} borrowed {} but collateral only supports {}", user.address, user.borrowed, max_allowed ); } // INVARIANT 3: Pool vault balance == total_deposits - total_borrows assert_eq!( post_state.vault_balance, post_state.total_deposits.saturating_sub(post_state.total_borrows), "Accounting mismatch: vault doesn't match pool state" ); Ok(()) } # Run with Honggfuzz backend (default) trident fuzz run fuzz_0 # Run for a specific duration trident fuzz run fuzz_0 -- --run_time 300 # 5 minutes # Run with specific thread count trident fuzz run fuzz_0 -- --threads 8 # Run with Honggfuzz backend (default) trident fuzz run fuzz_0 # Run for a specific duration trident fuzz run fuzz_0 -- --run_time 300 # 5 minutes # Run with specific thread count trident fuzz run fuzz_0 -- --threads 8 # Run with Honggfuzz backend (default) trident fuzz run fuzz_0 # Run for a specific duration trident fuzz run fuzz_0 -- --run_time 300 # 5 minutes # Run with specific thread count trident fuzz run fuzz_0 -- --threads 8 [CRASH] Invariant violated after 847 iterations Sequence: 1. deposit(amount: 1000000000) ← Deposit 1 SOL 2. borrow(amount: 666666666) ← Borrow ~0.67 SOL (valid) 3. borrow(amount: 666666666) ← Borrow again (still passes check!) 4. borrow(amount: 666666666) ← Total borrowed: 2 SOL > 1 SOL deposited Invariant 2 failed: borrowed 1999999998 but collateral only supports 666666666 [CRASH] Invariant violated after 847 iterations Sequence: 1. deposit(amount: 1000000000) ← Deposit 1 SOL 2. borrow(amount: 666666666) ← Borrow ~0.67 SOL (valid) 3. borrow(amount: 666666666) ← Borrow again (still passes check!) 4. borrow(amount: 666666666) ← Total borrowed: 2 SOL > 1 SOL deposited Invariant 2 failed: borrowed 1999999998 but collateral only supports 666666666 [CRASH] Invariant violated after 847 iterations Sequence: 1. deposit(amount: 1000000000) ← Deposit 1 SOL 2. borrow(amount: 666666666) ← Borrow ~0.67 SOL (valid) 3. borrow(amount: 666666666) ← Borrow again (still passes check!) 4. borrow(amount: 666666666) ← Total borrowed: 2 SOL > 1 SOL deposited Invariant 2 failed: borrowed 1999999998 but collateral only supports 666666666 pub fn borrow(ctx: Context<Borrow>, amount: u64) -> Result<()> { // ... let total_after_borrow = user.borrowed.checked_add(amount) .ok_or(ErrorCode::MathOverflow)?; require!(total_after_borrow <= max_borrow, ErrorCode::InsufficientCollateral); // ... } pub fn borrow(ctx: Context<Borrow>, amount: u64) -> Result<()> { // ... let total_after_borrow = user.borrowed.checked_add(amount) .ok_or(ErrorCode::MathOverflow)?; require!(total_after_borrow <= max_borrow, ErrorCode::InsufficientCollateral); // ... } pub fn borrow(ctx: Context<Borrow>, amount: u64) -> Result<()> { // ... let total_after_borrow = user.borrowed.checked_add(amount) .ok_or(ErrorCode::MathOverflow)?; require!(total_after_borrow <= max_borrow, ErrorCode::InsufficientCollateral); // ... } #[derive(Debug)] pub struct OracleManipulationScenario; impl FuzzScenario for OracleManipulationScenario { fn sequence(&self) -> Vec<FuzzStep> { vec![ // Step 1: Normal setup FuzzStep::instruction(InitializeData::arbitrary()), FuzzStep::instruction(DepositData { amount: 10_000_000_000 }), // Step 2: Simulate oracle price spike (attacker-controlled) FuzzStep::instruction(UpdateOracleData { price: Range(1..u64::MAX), // Fuzz the price }), // Step 3: Borrow maximum against inflated collateral FuzzStep::instruction(BorrowData { amount: Range(1..u64::MAX), }), // Step 4: Oracle returns to normal FuzzStep::instruction(UpdateOracleData { price: Range(1..1000), // Realistic price }), // Invariant: pool should still be solvent ] } } #[derive(Debug)] pub struct OracleManipulationScenario; impl FuzzScenario for OracleManipulationScenario { fn sequence(&self) -> Vec<FuzzStep> { vec![ // Step 1: Normal setup FuzzStep::instruction(InitializeData::arbitrary()), FuzzStep::instruction(DepositData { amount: 10_000_000_000 }), // Step 2: Simulate oracle price spike (attacker-controlled) FuzzStep::instruction(UpdateOracleData { price: Range(1..u64::MAX), // Fuzz the price }), // Step 3: Borrow maximum against inflated collateral FuzzStep::instruction(BorrowData { amount: Range(1..u64::MAX), }), // Step 4: Oracle returns to normal FuzzStep::instruction(UpdateOracleData { price: Range(1..1000), // Realistic price }), // Invariant: pool should still be solvent ] } } #[derive(Debug)] pub struct OracleManipulationScenario; impl FuzzScenario for OracleManipulationScenario { fn sequence(&self) -> Vec<FuzzStep> { vec![ // Step 1: Normal setup FuzzStep::instruction(InitializeData::arbitrary()), FuzzStep::instruction(DepositData { amount: 10_000_000_000 }), // Step 2: Simulate oracle price spike (attacker-controlled) FuzzStep::instruction(UpdateOracleData { price: Range(1..u64::MAX), // Fuzz the price }), // Step 3: Borrow maximum against inflated collateral FuzzStep::instruction(BorrowData { amount: Range(1..u64::MAX), }), // Step 4: Oracle returns to normal FuzzStep::instruction(UpdateOracleData { price: Range(1..1000), // Realistic price }), // Invariant: pool should still be solvent ] } } mkdir -p trident-tests/fuzz_tests/fuzz_0/corpus/ # Add known valid transaction sequences as seed inputs mkdir -p trident-tests/fuzz_tests/fuzz_0/corpus/ # Add known valid transaction sequences as seed inputs mkdir -p trident-tests/fuzz_tests/fuzz_0/corpus/ # Add known valid transaction sequences as seed inputs # .github/workflows/fuzz.yml name: Fuzz Tests on: pull_request: paths: ['programs/**'] jobs: fuzz: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: nicetry/setup-solana@v1 - run: cargo -weight: 500;">install trident-cli - run: trident fuzz run fuzz_0 -- --run_time 600 --exit_upon_crash - uses: actions/upload-artifact@v4 if: failure() with: name: crash-inputs path: trident-tests/fuzz_tests/fuzz_0/cr*/ # .github/workflows/fuzz.yml name: Fuzz Tests on: pull_request: paths: ['programs/**'] jobs: fuzz: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: nicetry/setup-solana@v1 - run: cargo -weight: 500;">install trident-cli - run: trident fuzz run fuzz_0 -- --run_time 600 --exit_upon_crash - uses: actions/upload-artifact@v4 if: failure() with: name: crash-inputs path: trident-tests/fuzz_tests/fuzz_0/cr*/ # .github/workflows/fuzz.yml name: Fuzz Tests on: pull_request: paths: ['programs/**'] jobs: fuzz: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: nicetry/setup-solana@v1 - run: cargo -weight: 500;">install trident-cli - run: trident fuzz run fuzz_0 -- --run_time 600 --exit_upon_crash - uses: actions/upload-artifact@v4 if: failure() with: name: crash-inputs path: trident-tests/fuzz_tests/fuzz_0/cr*/ - fuzz_tests/ — your fuzz test files - Cargo.toml — fuzzer dependencies - Configuration for Honggfuzz integration - Total borrows ≤ total deposits × collateralization_ratio - User balances sum to pool total - Admin authority can only be transferred by current admin - Fees are always non-zero for non-zero transactions - Closed accounts have zero lamports - Find the "stupid" bugs that waste auditor time (and your money) - Catch regression bugs when you refactor - Validate your invariants across millions of random sequences - Build confidence that your program behaves correctly under adversarial conditions