Important
For more details about the Trident, check the Trident documentation Tridetn docs
The Program specified within this Lesson has the following structure
Program creates within its initialize instruction an Asset Account that has the following fields
authority
: responsible for Asset creation and authority over the Asset.mint
: tied to the Asset Account and is also initialized within the initialize instruction.counter
: calculated during initialization.
#[account]
pub struct Asset {
pub authority: Pubkey,
pub mint: Pubkey,
pub counter: u64,
}
impl Asset {
pub const LEN: usize = 32 + 32 + 8;
}
The initialize instruction expects the following accounts
signer
: the new authority over Asset.asset
: the new asset account that is going to be initialized.mint
: the mint account tied to asset, also going to be initialized.metadata_account
: storing additional data for the mint account, going to be initialized using CPI to the Metaplex Metadata Program.- rest are programs required for initialization of accounts and the CPI.
Tip
For more details about the Metaplex Metadata Program, check the Token Metadata documentation Token Metadata
#[derive(Accounts)]
pub struct InitializeContext<'info> {
// 1.
#[account(mut)]
pub signer: Signer<'info>,
// 2.
#[account(
init,
payer = signer,
space = 8 + Asset::LEN,
seeds = [b"asset",signer.key().as_ref(),mint.key().as_ref()],
bump
)]
pub asset: Account<'info, Asset>,
// 3.
#[account(
init,
payer = signer,
mint::decimals = 9,
mint::authority = signer,
)]
pub mint: Account<'info, Mint>,
// 4.
/// CHECK: Will be initialized
#[account(mut)]
pub metadata_account: UncheckedAccount<'info>,
// 5.
pub mpl_token_metadata: Program<'info, Metadata>,
pub system_program: Program<'info, System>,
pub token_program: Interface<'info, TokenInterface>,
}
- Create Metadata Account with specified Name, Symbol and URI
- Call
buggy_math_function
which result is going to be assigned into the counter field.
pub fn _initialize_ix(
ctx: Context<InitializeContext>,
input1: u8,
input2: u8,
name: String,
symbol: String,
uri: String,
) -> Result<()> {
// 1.
ctx.accounts.create_metadata(name, symbol, uri)?;
let asset = &mut ctx.accounts.asset;
asset.authority = ctx.accounts.signer.key();
asset.mint = ctx.accounts.mint.key();
// 2.
asset.counter = buggy_math_function(input1, input2).into();
Ok(())
}
Caution
The buggy_math_function
is not correct and contains the following issue.
pub fn buggy_math_function(input1: u8, input2: u8) -> u8 {
let divisor = 254 - input2;
input1 / divisor
}
- In case of
input2 == 254
-> panic for division by zero occurs. - In case of
input2 > 254
-> panic for subtraction overflow occurs.
Within the Anchor warkspace, call
trident init
This will initialize new Trident warkspace and prepare all required files and fuzz templates.
Tip
In case you want to add new fuzz test template, call
trident fuzz add
Within the trident-tests/fuzz_tests/Cargo.toml
, add required dependencies for writing fuzz tests.
Tip
Most of the time, these are the same dependencies you have specified within the program Cargo.toml.
In our case of provided example we need to add the following dependencies
[[bin]]
name = "fuzz_0"
path = "fuzz_0/test_fuzz.rs"
[package]
name = "fuzz_tests"
version = "0.1.0"
description = "Created with Trident"
edition = "2021"
[dependencies]
honggfuzz = "0.5.56"
arbitrary = "1.3.0"
assert_matches = "1.4.0"
# --- ADDED ---
anchor-spl = { version = "0.30.1", features = ["metadata"] }
[dependencies.trident-client]
version = "0.7.0"
[dependencies.trident-lesson-part-i]
path = "../../programs/trident-lesson-part-i"
Within the trident-tests/fuzz_tests/fuzz_0/accounts_snapshots.rs
add the required use statements.
Tip
In our case, we work with Metadata
, Mint
and TokenInterface
so we have to add
use anchor_spl::{metadata::Metadata, token::Mint, token_interface::TokenInterface};
Within the trident-tests/fuzz_tests/fuzz_0/test_fuzz.rs
, specify all of the programs that should be included in the Testing Environment.
Tip
In our case, we use CPI to the Metaplex Token Metadata, so we need to also include this program in genesis
- Create new FuzzingProgram instance for your program.
- Create new FuzzingProgram instance for the Metaplex Token Metadata program.
- Initialize ProgramTest with both FuzzingPrograms specified.
Note
Notice how the Metaplex Token Metadata
has the entrypoint fn specified as None. This is because we are including the SBF binary file, which is automatically read from the trident-genesis
folder.
Important
Do not forget do specify what types of instructions you want the fuzzer to generate, in this case we want to generate the FuzzInstruction_trident_lesson_part_i
. If you have multiple programs within your Anchor Environment you can decide which programs instructions should the fuzzer generate.
// ...
pub type FuzzInstruction = FuzzInstruction_trident_lesson_part_i;
// ...
fn main() {
loop {
fuzz_trident!(fuzz_ix: FuzzInstruction, |fuzz_data: MyFuzzData| {
// 1.
let fuzzing_program1 = FuzzingProgram::new(
PROGRAM_NAME_TRIDENT_LESSON_PART_I,
&PROGRAM_ID_TRIDENT_LESSON_PART_I,
processor!(convert_entry!(entry_trident_lesson_part_i))
);
// 2.
let fuzzing_program2 = FuzzingProgram::new(
"metaplex-token-metadata",
&anchor_spl::metadata::ID,
None
);
// 3.
let mut client =
ProgramTestClientBlocking::new(&[fuzzing_program1,fuzzing_program2])
.unwrap();
let _ = fuzz_data.run_with_runtime(PROGRAM_ID_TRIDENT_LESSON_PART_I, &mut client);
});
}
}
Important
Writting Fuzz Tests comes down to 3 basic steps
- Specifying
FuzzAccounts
which is Storage for Accounts. - Specifying
get_data()
functions. - Specifying
get_accounts()
functions. - Optionaly you can specify Invariants Checks.
- Invariants checks are great to compare accounts before instruction and after instruction. So if the instruction passed you can compare if Accounts were updated as expected.
Tip
It is not necessary to specify the type for all of the Accounts. For example for the well known programs such as System Program, Token Program or Metaplex program we do not need storage. This means within the get_accounts()
function we do not need to take the programs from the storage.
Note
In case of metadata_account it can be stored within the storage of type PdaStore. In our example, however, we have only one Instruction so we do not need to store and reuse the metadata_account, moreover instead of specifying seeds by hand we can use the find_pda
function from the Metaplex.
#[doc = r" Use AccountsStorage<T> where T can be one of:"]
#[doc = r" Keypair, PdaStore, TokenStore, MintStore, ProgramStore"]
#[derive(Default)]
pub struct FuzzAccounts {
asset: AccountsStorage<PdaStore>,
// metadata_account: AccountsStorage<PdaStore>,
mint: AccountsStorage<Keypair>,
// mpl_token_metadata: AccountsStorage<todo!()>,
signer: AccountsStorage<Keypair>,
// system_program: AccountsStorage<todo!()>,
// token_program: AccountsStorage<todo!()>,
}
Note
In case of Instruction input data we do not need to do anything apart from asigning the correct data field within the InitializeIx struct. This is because we can send the automatically generated random data to the Instruction.
impl<'info> IxOps<'info> for InitializeIx {
// ...
fn get_data(
&self,
_client: &mut impl FuzzClient,
_fuzz_accounts: &mut FuzzAccounts,
) -> Result<Self::IxData, FuzzingError> {
let data = trident_lesson_part_i::instruction::InitializeIx {
input1: self.data.input1,
input2: self.data.input2,
name: self.data.name.clone(),
symbol: self.data.symbol.clone(),
uri: self.data.uri.clone(),
};
Ok(data)
}
// ...
}
Note
In the get_accounts()
we specify which accounts from the Account Storage to create or reuse, This can be particularly helpful when your program contains multiple Instructions. In that case you can mix the accounts sent to the Instructions and see if there is unauthorized access.
impl<'info> IxOps<'info> for InitializeIx {
// ...
fn get_accounts(
&self,
client: &mut impl FuzzClient,
fuzz_accounts: &mut FuzzAccounts,
) -> Result<(Vec<Keypair>, Vec<AccountMeta>), FuzzingError> {
let signer = fuzz_accounts.signer.get_or_create_account(
self.accounts.signer,
client,
10 * LAMPORTS_PER_SOL,
);
let mint = fuzz_accounts.mint.get_or_create_account(
self.accounts.mint,
client,
10 * LAMPORTS_PER_SOL,
);
let metadata_account =
anchor_spl::metadata::mpl_token_metadata::accounts::Metadata::find_pda(
&mint.pubkey(),
);
let asset = fuzz_accounts
.asset
.get_or_create_account(
self.accounts.asset,
&[b"asset", signer.pubkey().as_ref(), mint.pubkey().as_ref()],
&trident_lesson_part_i::ID,
)
.unwrap();
let signers = vec![signer.clone(), mint.clone()];
let acc_meta = trident_lesson_part_i::accounts::InitializeContext {
signer: signer.pubkey(),
asset: asset.pubkey,
mint: mint.pubkey(),
metadata_account: metadata_account.0,
mpl_token_metadata: anchor_spl::metadata::ID,
system_program: solana_sdk::system_program::ID,
token_program: anchor_spl::token::ID,
}.to_account_metas(None);
Ok((signers, acc_meta))
// ...
}
}
To run the particular Fuzz Test:
# trident fuzz run fuzz_0
trident fuzz run <FUZZ_TARGET>
Important
The output provided by Honggfuzz is as follows
- Number of Fuzzing Iterations.
- Feedback Driven Mode = Honggfuzz generates data based on the feedback (i.e. feedback based on Coverage progress).
- Average Iterations per second
- Number of crashes it found (panics or failed invariant checks)
------------------------[ 0 days 00 hrs 00 mins 01 secs ]----------------------
Iterations : 688 (out of: 1000 [68%]) # -- 1. --
Mode [3/3] : Feedback Driven Mode # -- 2. --
Target : trident-tests/fuzz_tests/fuzzing.....wn-linux-gnu/release/fuzz_0
Threads : 16, CPUs: 32, CPU%: 1262% [39%/CPU]
Speed : 680/sec [avg: 688] # -- 3. --
Crashes : 1 [unique: 1, blocklist: 0, verified: 0] # -- 4. --
Timeouts : 0 [10 sec]
Corpus Size : 98, max: 1048576 bytes, init: 0 files
Cov Update : 0 days 00 hrs 00 mins 00 secs ago
Coverage : edge: 10345/882951 [1%] pc: 163 cmp: 622547
---------------------------------- [ LOGS ] ------------------/ honggfuzz 2.6 /-
To see the results on the found Crashfile.
Tip
CrashFiles are by default stored within the trident-tests/fuzz_tests/fuzzing/hfuzz_workspace/<FUZZ_TARGET>
trident fuzz run-debug <FUZZ_TARGET> <PATH_TO_CRASHFILE>
Important
In Case of the following error message during debug:
:personality set failed: Operation not permitted
Run the following:
echo 'settings set target.disable-aslr false' >~/.lldbinit
Important
In Case of the following error message:
mportError: cannot import name 'SBData' from 'lldb' (unknown location)
Check the solution here llvm-project-issue
Important
The debug output is at current development stage really verbose and contains lldb parts. We are working on improving this experience. In the picture below you can see an example of provided debug output.
- Series of Transaction Logs
- Structures of data send within the Instructions
- Panic or Crash, based on if the Fuzzing panicked within the Solana Program or Invariant Check failed.