$ // 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