Trident is a Rust-based framework designed to help developers fuzz test Solana programs written in Anchor. Developed by Ackee Blockchain and supported by the Solana Foundation, Trident simplifies the testing process and helps developers ship secure code by uncovering edge-case vulnerabilities.
Trident Fuzzing Video Tutorial
For School of Solana we prepared a bonus lecture for developers to learn how to fuzz test Solana programs with Trident.
Trident is powerful
Trident’s development began over 2.5 years ago in 2021, originally under the name Trdelník. Trident has won the Marinade Finance community prize during the Solana Riptide Hackathon in 2022 and has received a development grant from the Solana Foundation in 2023.
Features
- Automated Fuzz Test Generation: Simplifies the creation of test templates for Anchor programs, reducing setup time and effort.
- Adaptive Inputs: Generates dynamic and adaptive inputs to improve test coverage and uncover hidden vulnerabilities.
- Guided Instruction Sequences: Uses customizable sequences of instructions for faster, more effective testing results.
- Invariant Checks: Allows developers to implement custom checks to spot vulnerabilities and unwanted behavior.
Getting Started
Installation
Trident is distributed via Rust’s cargo package manager. To install Trident and its dependencies, follow these steps:
- Install Trident CLI and honggfuzz:
cargo install trident-cli cargo install honggfuzz
- Initialize Trident in Your Project:
trident init
This command sets up Trident in your project, generating necessary files and configurations.
Fuzz Testing Solana Programs
Fuzz testing is an automated technique that provides generated random, invalid, or unexpected input data to your program. This helps discover unknown bugs and vulnerabilities, potentially preventing zero-day exploits. Trident integrates the well-known fuzzer honggfuzz, developed by Google, to facilitate fuzz testing for Solana programs.
Example: Setting Up a New Anchor Project
-
- Initialize the Project:
anchor init my-trident-fuzz-test cd my-trident-fuzz-test anchor build
- Create a Buggy Program:
Create a program namedunchecked_arithmetic_0
with intentional bugs for testing.
use anchor_lang::prelude::*; const MAGIC_NUMBER: u8 = 254; declare_id!("...."); // paste your program ID here #[program] pub mod unchecked_arithmetic_0 { use super::*; pub fn initialize(ctx: Context<Initialize>) -> Result<()> { let counter = &mut ctx.accounts.counter; counter.count = 0; counter.authority = ctx.accounts.user.key(); Ok(()) } pub fn update(ctx: Context<Update>, input1: u8, input2: u8) -> Result<()> { let counter = &mut ctx.accounts.counter; msg!("input1 = {}, input2 = {}", input1, input2); counter.count = buggy_math_function(input1, input2).into(); Ok(()) } } pub fn buggy_math_function(input1: u8, input2: u8) -> u8 { // INFO uncommenting the if statement can prevent // div-by-zero and subtract with overflow panic // if input2 >= MAGIC_NUMBER { // return 0; // } let divisor = MAGIC_NUMBER - input2; input1 / divisor } #[derive(Accounts)] pub struct Initialize<'info> { #[account(init, payer = user, space = 8 + 40)] pub counter: Account<'info, Counter>, #[account(mut)] pub user: Signer<'info>, pub system_program: Program<'info, System>, } #[derive(Accounts)] pub struct Update<'info> { #[account(mut, has_one = authority)] pub counter: Account<'info, Counter>, pub authority: Signer<'info>, } #[account] pub struct Counter { pub authority: Pubkey, pub count: u64, }
- Initialize Trident:
trident init
- Write a Fuzz Test:
Modify the fuzz test template located at ‘trident-tests/fuzz_tests/fuzz_0/fuzz_instructions.rs’ and finish the implementation ofget_data
andget_accounts
methods andFuzzAccounts
struct:
pub mod unchecked_arithmetic_0_fuzz_instructions { use crate::accounts_snapshots::*; use trident_client::{fuzzing::*, solana_sdk::native_token::LAMPORTS_PER_SOL}; #[derive(Arbitrary, DisplayIx, FuzzTestExecutor, FuzzDeserialize)] pub enum FuzzInstruction { Initialize(Initialize), Update(Update), } #[derive(Arbitrary, Debug)] pub struct Initialize { pub accounts: InitializeAccounts, pub data: InitializeData, } #[derive(Arbitrary, Debug)] pub struct InitializeAccounts { pub counter: AccountId, pub user: AccountId, pub system_program: AccountId, } #[derive(Arbitrary, Debug)] pub struct InitializeData {} #[derive(Arbitrary, Debug)] pub struct Update { pub accounts: UpdateAccounts, pub data: UpdateData, } #[derive(Arbitrary, Debug)] pub struct UpdateAccounts { pub counter: AccountId, pub authority: AccountId, } #[derive(Arbitrary, Debug)] pub struct UpdateData { pub input1: u8, pub input2: u8, } impl<'info> IxOps<'info> for Initialize { type IxData = unchecked_arithmetic_0::instruction::Initialize; type IxAccounts = FuzzAccounts; type IxSnapshot = InitializeSnapshot<'info>; fn get_data( &self, _client: &mut impl FuzzClient, _fuzz_accounts: &mut FuzzAccounts, ) -> Result<Self::IxData, FuzzingError> { let data = unchecked_arithmetic_0::instruction::Initialize {}; Ok(data) } fn get_accounts( &self, client: &mut impl FuzzClient, fuzz_accounts: &mut FuzzAccounts, ) -> Result<(Vec<Keypair>, Vec<AccountMeta>), FuzzingError> { let user = fuzz_accounts.user.get_or_create_account( self.accounts.user, client, 5 * LAMPORTS_PER_SOL, ); let counter = fuzz_accounts.counter.get_or_create_account( self.accounts.counter, client, 5 * LAMPORTS_PER_SOL, ); let acc_meta = unchecked_arithmetic_0::accounts::Initialize { counter: counter.pubkey(), user: user.pubkey(), system_program: SYSTEM_PROGRAM_ID, } .to_account_metas(None); Ok((vec![user, counter], acc_meta)) } } impl<'info> IxOps<'info> for Update { type IxData = unchecked_arithmetic_0::instruction::Update; type IxAccounts = FuzzAccounts; type IxSnapshot = UpdateSnapshot<'info>; fn get_data( &self, _client: &mut impl FuzzClient, _fuzz_accounts: &mut FuzzAccounts, ) -> Result<Self::IxData, FuzzingError> { let data = unchecked_arithmetic_0::instruction::Update { input1: self.data.input1, input2: self.data.input2, }; Ok(data) } fn get_accounts( &self, client: &mut impl FuzzClient, fuzz_accounts: &mut FuzzAccounts, ) -> Result<(Vec<Keypair>, Vec<AccountMeta>), FuzzingError> { let user = fuzz_accounts.user.get_or_create_account( self.accounts.authority, client, 15 * LAMPORTS_PER_SOL, ); let counter = fuzz_accounts.counter.get_or_create_account( self.accounts.counter, client, 5 * LAMPORTS_PER_SOL, ); let acc_meta = unchecked_arithmetic_0::accounts::Update { counter: counter.pubkey(), authority: user.pubkey(), } .to_account_metas(None); Ok((vec![user], acc_meta)) } } #[doc = r" Use AccountsStorage<T> where T can be one of:"] #[doc = r" Keypair, PdaStore, TokenStore, MintStore, ProgramStore"] #[derive(Default)] pub struct FuzzAccounts { // The 'authority' and 'system_program' were automatically // generated in the FuzzAccounts struct, as they are both // used in the program. However, the 'authority' is in fact // the user account, just named differently. Therefore, we will use only // the generated user accounts for both 'user' and 'authority account' fields // in this fuzz test. Additionally, there is no need to fuzz the 'system_program' account. user: AccountsStorage<Keypair>, counter: AccountsStorage<Keypair>, } }
Modify the fuzz test template located at ‘trident-tests/fuzz_tests/fuzz_0/test_fuzz.rs’:
use fuzz_instructions::unchecked_arithmetic_0_fuzz_instructions::FuzzInstruction; use fuzz_instructions::unchecked_arithmetic_0_fuzz_instructions::Initialize; use trident_client::{convert_entry, fuzz_trident, fuzzing::*}; use unchecked_arithmetic_0::entry; use unchecked_arithmetic_0::ID as PROGRAM_ID; mod accounts_snapshots; mod fuzz_instructions; const PROGRAM_NAME: &str = "unchecked_arithmetic_0"; struct MyFuzzData; impl FuzzDataBuilder<FuzzInstruction> for MyFuzzData { fn pre_ixs(u: &mut arbitrary::Unstructured) -> arbitrary::Result<Vec<FuzzInstruction>> { let init = FuzzInstruction::Initialize(Initialize::arbitrary(u)?); Ok(vec![init]) } } fn main() { loop { fuzz_trident!(fuzz_ix: FuzzInstruction, |fuzz_data: MyFuzzData| { let mut client = ProgramTestClientBlocking::new(PROGRAM_NAME, PROGRAM_ID, processor!(convert_entry!(entry))) .unwrap(); let _ = fuzz_data.run_with_runtime(PROGRAM_ID, &mut client); }); } }
- Run the Fuzz Test:
trident fuzz run fuzz_0
- Debugging with Crash Files:Use crash files to debug and inspect issues:
trident fuzz run-debug fuzz_0 trident-tests/fuzz_tests/fuzzing/hfuzz_workspace/fuzz_0/<CRASH_FILE>.fuzz
- Initialize the Project:
Integration Testing
Trident also supports integration testing, allowing developers to test their Solana programs in a simulated environment that closely mirrors the actual Solana blockchain. This ensures that programs interact correctly with the blockchain and other programs.
Trident Developer Support
For developer support find us in our Discord #trident-chat and on warpcast /trident channel. Please read the documentation, star our GitHub repo and follow Trident on Twitter/X @TridentSolana for updates.