Introducing Trident: The First Open-Source Fuzzer for Solana Programs
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
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
▫️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
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
andSolana
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
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.
Discover More about Trident Fuzz Test here.
FUZZING A SOLANA PROGRAMS
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
- 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.
INTEGRATION AND TESTING OF TRIDENT
INTEGRATION :
Trident supports writing Integration Tests in Rust.
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
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.
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