Introducing Trident: The First Open-Source Fuzzer for Solana Programs

God_Did_Vel🌟!
12 min readAug 3, 2024

--

Designed by Vel

INTRODUCTION TO TRIDENT:

Trident is a Rust-based framework created to aid developers in fuzz testing Solana programs written in Anchor. Developed by Ackee Blockchain and supported by the Solana Foundation, Trident streamlines the testing process, enabling developers to ship secure code by identifying edge-case vulnerabilities.

Trident’s development began over 2.5 years ago in 2021, initially under the name Trdelník. In 2022, Trident won the Marinade Finance Community Prize during the Solana Riptide Hackathon. It later received a development grant from the Solana Foundation in 2023.

HOW ACKEEBLOCKCHAIN SECURITY AND SOLANA FOUNDATION SUPPORT TRIDENT

Designed by Vel

Ackee Blockchain Security is an organization specializing in blockchain security, trusted by top-tier web3 organizations. Their mission is to strengthen the blockchain ecosystem by offering security services, developing security tools, and providing education in the field. Ackee Blockchain Security developed Trident, an open-source fuzzer for Solana programs written in Anchor, as part of their mission to enhance blockchain security. Supported by the Solana Foundation, Trident helps identify and fix vulnerabilities in Solana smart contracts by automatically generating and testing inputs to uncover potential issues. This tool contributes to a more secure Solana ecosystem by ensuring that smart contracts are robust and free from common security flaws.

The Solana Foundation supported Trident, by awarding it a development grant in 2023. This grant followed Trident’s success in winning the Marinade Finance Community Prize during the Solana Riptide Hackathon, highlighting the foundation’s commitment to fostering innovative security tools within the Solana ecosystem.

KEY FEATURES OF TRIDENT

Designed by Vel

▫️Automated Test Generation:

Streamlines the testing process by automatically generating templates for fuzz and integration tests for programs developed with the Anchor Framework.

▫️Dynamic Data Generation:

Enhances test coverage by using random instruction data and pseudo-random accounts to create diverse and unpredictable scenarios for fuzz testing.

▫️Custom Instruction Sequences:

Offers the ability to design tailored sequences of instructions to address specific testing requirements or to target particular aspects of program behavior during fuzz testing.

▫️Invariant Checks:

Enables the implementation of custom invariant checks to detect vulnerabilities and undesirable behaviors.

GETTING STARTED WITH TRIDENT

Designed by Vel

Dependencies:

Please Check supported versions section for further details.

▫️Install Rust
▫️Install Solana tool suite
▫️Install Anchor
▫️Install Honggfuzz-rs for fuzz testing

Installation:

cargo install trident-cli
# or the specific version
cargo install --version <version> trident-cli

Supported versions

  • Trident support Anchor and Solana versions specified in the table below.

Trident CLI Anchor Solana Rust
v0.6.0 >=0.29.*1 ^1.17 nightly
v0.5.0 ~0.28.* =1.16.6
v0.4.0 ~0.27.* >=1.15
v0.3.0 ~0.25.* >=1.10
v0.2.0 ~0.24.* >=1.9

To use Trident with Anchor 0.29.0, run the following commands from your project's root directory after Trident initialization:

cargo update anchor-client@0.30.0 --precise 0.29.0
cargo update anchor-spl@0.30.0 --precise 0.29.0

THE FUZZ TEST

Designed by Vel

Fuzzing is a software testing technique that involves supplying invalid, unexpected, or random data as inputs to a computer program. The goal is to uncover bugs and vulnerabilities that might not be detected through conventional testing strategies.

TRIDENT FUZZ TESTING

The Trident testing framework provides developers with tools to efficiently create fuzz tests for Anchor-based programs. It streamlines the fuzz testing process through automation and extensive support.

▫️Automatically parses Anchor-based programs to generate the required implementations for deserializing instruction accounts.

▫️Generates templates for developers to customize based on the specific needs of their fuzz test scenarios.

▫️Provides derive macros to easily implement necessary traits, reducing manual coding efforts.

▫️Offers derive macros to implement required traits effortlessly, reducing manual coding efforts.

▫️Includes a bank client and helper functions to simplify account management during testing.

▫️Provides a Command-Line Interface (CLI) for easily executing and debugging fuzz tests.

Trident is designed for customization, allowing developers to tailor their fuzz tests by adjusting:

▫️Execution Order of Instructions:

Test various sequences and their impacts on the program to uncover sequence-related vulnerabilities.

▫️Instruction Parameters:

Identify how variations in inputs influence program behavior, testing for robustness against a wide range of data.

▫️Instruction Accounts:

Explore the impact of different account states on the software’s functionality, ensuring comprehensive account testing.

▫️Comprehensive Testing:

Conduct thorough and effective fuzz testing by combining any of the above aspects.

This framework supports a detailed and methodical approach to fuzz testing, facilitating the identification and remediation of potential vulnerabilities in software applications.

Fuzz test-only:

If you are interested only in generating templates for fuzz tests run.

trident init fuzz

The command will generate the following folder structure:

project-root
├── trident-tests
│ ├── fuzz_tests # fuzz tests folder
│ │ ├── fuzz_0 # particular fuzz test
│ │ │ ├── accounts_snapshots.rs # generated accounts deserialization methods
│ │ │ ├── test_fuzz.rs # the binary target of your fuzz test
│ │ │ └── fuzz_instructions.rs # the definition of your fuzz test
│ │ ├── fuzz_1
│ │ ├── fuzz_X # possible multiple fuzz tests
│ │ ├── fuzzing # compilations and crashes folder
│ │ └── Cargo.toml
├── Trident.toml
└── ...

Add new Fuzz Test:

If you have already initialized Trident within your project, and you are interested in initializing a new fuzz test run.

trident fuzz add

The command will generate a new fuzz test as follows:

project-root
├── trident-tests
│ ├── fuzz_tests # fuzz tests folder
│ │ ├── fuzz_X # new fuzz test folder
│ │ │ ├── accounts_snapshots.rs
│ │ │ ├── test_fuzz.rs
│ │ │ └── fuzz_instructions.rs
│ │ ├── fuzzing
│ │ └── Cargo.toml # already present
├── Trident.toml # already present
└── ...

All-Suite

To initialize Trident and generate all-suite test templates, navigate to your project’s root directory and run

trident init

The command will generate the following folder structure:

project-root
├── .program_client
├── trident-tests
│ ├── fuzz_tests # fuzz tests folder
│ │ ├── fuzz_0 # particular fuzz test
│ │ │ ├── accounts_snapshots.rs # generated accounts deserialization methods
│ │ │ ├── test_fuzz.rs # the binary target of your fuzz test
│ │ │ └── fuzz_instructions.rs # the definition of your fuzz test
│ │ ├── fuzz_1
│ │ ├── fuzz_X # possible multiple fuzz tests
│ │ ├── fuzzing # compilations and crashes folder
│ │ └── Cargo.toml
│ └── poc_tests # integration tests folder
├── Trident.toml
└── ...

Fuzz test lifecycle

In the sequence diagram below you can see a simplified fuzz test lifecycle.

Some diagram states are labeled with emojis:

⚡ Mandatory methods that must be implemented by the user.
👤 Optional methods that can be implemented by the user.

Trident’s fuzz test lifecycle involves generating random instructions for a program until a maximum number of iterations is reached, a crash occurs, or the test is manually stopped. Each iteration includes generating instructions, executing them, and capturing account snapshots before and after execution. Users must define methods for collecting account data and instruction details, and optionally check invariants.

Source: Trident

Discover More about Trident Fuzz Test here.

FUZZING A SOLANA PROGRAMS

Designed by Vel

Fuzz testing is an automated technique that provides randomly generated, invalid, or unexpected input data to a program, helping to uncover unknown bugs and vulnerabilities that could lead to zero-day exploits. Trident integrates honggfuzz, a widely recognized fuzzer developed by Google, to enable effective fuzz testing for Solana programs.

Example: Setting Up a New Anchor Project

  1. Initialize the project.
anchor init my-trident-fuzz-test
cd my-trident-fuzz-test
anchor build

2. Create a Buggy Program:
Create a program named unchecked_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,
}

3. Initialize Trident.

trident init

4. Write a Fuzz Test:
Modify the fuzz test template located at ‘trident-tests/fuzz_tests/fuzz_0/fuzz_instructions.rs’ and finish the implementation of get_data and get_accounts methods and FuzzAccounts 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);
});
}
}
Rust

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

Trident Fuzzing Video Tutorial

For School of Solana trident prepared a bonus lecture for developers to learn how to fuzz test Solana programs with Trident.

Trident YouTube

INTEGRATION AND TESTING OF TRIDENT

Designed by Vel

INTEGRATION :

Trident supports writing Integration Tests in Rust.

Credit trident

INITIALIZATION

Integration test-only:

If you are interested only in generating templates for Integration Tests run

trident init poc

The command will generate the following folder structure:

project-root
├── .program_client
├── trident-tests
│ ├── poc_tests # integration tests folder
│ │ ├── tests
│ │ │ └── test.rs
│ │ └── Cargo.toml
├── Trident.toml
└── ...

Program Client:

By default, Integration Tests initialization generates also a .program_client crate with all necessary implementation for Instructions.

If you are interested in updating the .program_client implementation due to an update inside your program, run

trident build

This command will also initialize .program_client if the crate does not exist yet.

All suite:

To initialize Trident and generate all-suite test templates, navigate to your project’s root directory and run

trident init

The command will generate the following folder structure:

project-root
├── .program_client
├── trident-tests
│ ├── fuzz_tests # fuzz tests folder
│ ├── poc_tests # integration tests folder
│ │ ├── tests
│ │ │ └── test.rs # Integration Tests implementation
│ │ └── Cargo.toml
├── Trident.toml
└── ...

HOW TO WRITE INTEGRATION

Init Fixture

// <my_project>/trident-tests/poc_tests/tests/test.rs
// TODO: do not forget to add all necessary dependencies to the generated `trident-tests/poc_tests/Cargo.toml`
use program_client::my_instruction;
use trident_client::*;
use my_program;

#[throws]
#[fixture]
async fn init_fixture() -> Fixture {
// create a test fixture
let mut fixture = Fixture {
client: Client::new(system_keypair(0)),
// make sure to pass the correct name of your program
program: anchor_keypair("my_program_name").unwrap(),
state: keypair(42),
};
// deploy the program to test
fixture.deploy().await?;
// call instruction init
my_instruction::initialize(
&fixture.client,
fixture.state.pubkey(),
fixture.client.payer().pubkey(),
System::id(),
Some(fixture.state.clone()),
).await?;
fixture
}

#[trident_test]
async fn test_happy_path(#[future] init_fixture: Result<Fixture>) {
let fixture = init_fixture.await?;
// call the instruction
my_instruction::do_something(
&fixture.client,
"dummy_string".to_owned(),
fixture.state.pubkey(),
None,
).await?;
// check the test result
let state = fixture.get_state().await?;
assert_eq!(state.something_changed, "yes");
}

Testing program with associated Token account.

Trident does not export anchor-spl and spl-associated-token-account, so you have to add it manually.

# <my-project>/trident-tests/poc_tests/Cargo.toml
# import the correct versions manually
anchor-spl = "0.29.0"
spl-associated-token-account = "2.0.0"
// <my-project>/trident-tests/poc_tests/tests/test.rs
use anchor_spl::token::Token;
use spl_associated_token_account;

async fn init_fixture() -> Fixture {
// ...
let account = keypair(1);
let mint = keypair(2);
// constructs a token mint
client
.create_token_mint(&mint, mint.pubkey(), None, 0)
.await?;
// constructs associated token account
let token_account = client
.create_associated_token_account(&account, mint.pubkey())
.await?;
let associated_token_program = spl_associated_token_account::id();
// derives the associated token account address for the given wallet and mint
let associated_token_address = spl_associated_token_account::get_associated_token_address(&account.pubkey(), mint);
Fixture {
// ...
token_program: Token::id(),
}
}

Run

Once you have finished the implementation of the Integration Test, you can run the Test as follows:

trident test

Skipping tests

You can add the #[ignore] macro to skip the test.

#[trident_test]
#[ignore]
async fn test() {}

THE IMPORTANT OF FUZZING IN BLOCKCHAIN DEVELOPMENTS

Designed by Vel

Fuzzing is a vital automated testing technique used to ensure the security of blockchain protocols and smart contracts. It works by generating a wide range of random inputs to test the system’s response to unexpected scenarios. This randomness helps uncover potential security vulnerabilities that might not be detected through conventional testing methods. By stressing the system with unpredictable inputs, fuzzing exposes weaknesses and flaws, allowing developers to address these issues before they can be exploited. This process is particularly important in blockchain development, where it enhances the security and reliability of smart contracts and protocols. be exploited.

TRIDENT COMMUNITY AND DEVELOPERS SUPPORT.

Designed by Vel

For developer support find exquisite instructions and community Engagement here.

REFERENCES

https://ackee.xyz/trident/docs/latest/integration-tests/poc-introduction/

https://ackee.xyz/blog/introducing-trident-the-first-open-source-fuzzer-for-solana-programs/

https://ackee.xyz/

https://warpcast.com/ackee

--

--

God_Did_Vel🌟!

SOFTWARE ENGINEER, web3 writer, and enthusiast of Web3. Passionate about exploring the decentralized web world.