-
Notifications
You must be signed in to change notification settings - Fork 2.2k
token-swap: Add fuzzer for swap / withdraw / deposit #875
Conversation
/// else that may be required | ||
swap_curve: SwapCurve, | ||
}, | ||
Initialize(Initialize), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Kinda sad to see this. Arbitrary doesn't work over enums?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I totally agree. This is actually long-standing rust issue that enum variants don't count as types. I didn't follow the whole discussion over here, but I know there were efforts to make that happen: rust-lang/rust#40666
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the other option is to have the fuzz instructions live separately, but I preferred having them re-use the existing instructions. I can be overruled on that though
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Separately sounds a lot worse. Maybe there'll be other options when using Borsh encoding since there we have the ability to programmatically figure the types, even from an enum, and could use that information to construct the fuzzer inputs
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh yeah that's a great point. I'll look into possibilities for how to integrate borsh with the libfuzzer inputs and ditch Arbitrary
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sweet, feature-proposal
is already borshed up and its instruction interface is really small. It'd be a good guinea pig for Borsh-based fuzzing. (cc: https://github.com/solana-labs/solana-program-library/blob/master/feature-proposal/program/src/borsh_utils.rs). Might be slightly quicker to iterate on than token-swap for experiments.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
good point, I'll take a look at it next then :-)
I'll have this run somewhere over the weekend, but so far, results are good. Here's 4 threads running for nearly 3 hours without any problems:
|
const INITIAL_SWAP_TOKEN_A_AMOUNT: u64 = 100_000_000_000; | ||
const INITIAL_SWAP_TOKEN_B_AMOUNT: u64 = 300_000_000_000; | ||
|
||
const INITIAL_USER_TOKEN_A_AMOUNT: u64 = 1_000_000_000; | ||
const INITIAL_USER_TOKEN_B_AMOUNT: u64 = 3_000_000_000; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure if these numbers should be higher to allow for more extensive testing -- when doing random numbers for swapping and depositing, they're much higher than these
let trade_fee_numerator = 25; | ||
let trade_fee_denominator = 10000; | ||
let owner_trade_fee_numerator = 5; | ||
let owner_trade_fee_denominator = 10000; | ||
let owner_withdraw_fee_numerator = 30; | ||
let owner_withdraw_fee_denominator = 10000; | ||
let host_fee_numerator = 1; | ||
let host_fee_denominator = 5; | ||
let swap_curve = SwapCurve { | ||
curve_type: CurveType::ConstantProduct, | ||
calculator: Box::new(ConstantProductCurve { | ||
trade_fee_numerator, | ||
trade_fee_denominator, | ||
owner_trade_fee_numerator, | ||
owner_trade_fee_denominator, | ||
owner_withdraw_fee_numerator, | ||
owner_withdraw_fee_denominator, | ||
host_fee_numerator, | ||
host_fee_denominator, | ||
}), | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can eventually add more curves or even fuzz the curves, but that would be an initialize test as well
if e == SwapError::CalculationFailure.into() { | ||
} else if e == SwapError::ConversionFailure.into() { | ||
} else if e == SwapError::FeeCalculationFailure.into() { | ||
} else if e == SwapError::ExceededSlippage.into() { | ||
} else if e == SwapError::ZeroTradingTokens.into() { | ||
} else if e == TokenError::InsufficientFunds.into() { | ||
} else { | ||
Err(e).unwrap() | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This allows certain errors to slide, since they don't mean anything truly wrong has happened. Note that these checks do run the risk of overlap, since ProgramError::Custom(u32)
can re-use the same value for TokenError
and SwapError
. We should consider changing it to ProgramError::Custom(Pubkey, u32)
to separate better
token-swap/program/fuzz/src/lib.rs
Outdated
impl program_stubs::SyscallStubs for TestSyscallStubs { | ||
fn sol_invoke_signed( | ||
&self, | ||
instruction: &Instruction, | ||
account_infos: &[AccountInfo], | ||
signers_seeds: &[&[&[u8]]], | ||
) -> ProgramResult { | ||
let mut new_account_infos = vec![]; | ||
|
||
// mimic check for token program in accounts | ||
if !account_infos.iter().any(|x| *x.key == spl_token::id()) { | ||
return Err(ProgramError::InvalidAccountData); | ||
} | ||
|
||
for meta in instruction.accounts.iter() { | ||
for account_info in account_infos.iter() { | ||
if meta.pubkey == *account_info.key { | ||
let mut new_account_info = account_info.clone(); | ||
for seeds in signers_seeds.iter() { | ||
let signer = | ||
Pubkey::create_program_address(&seeds, &spl_token_swap::id()).unwrap(); | ||
if *account_info.key == signer { | ||
new_account_info.is_signer = true; | ||
} | ||
} | ||
new_account_infos.push(new_account_info); | ||
} | ||
} | ||
} | ||
|
||
spl_token::processor::Processor::process( | ||
&instruction.program_id, | ||
&new_account_infos, | ||
&instruction.data, | ||
) | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is lifted directly from the processor test code, not sure how we can share this better.
token-swap/program/fuzz/src/lib.rs
Outdated
#[derive(Clone)] | ||
pub struct AccountData { | ||
pub key: Pubkey, | ||
pub lamports: u64, | ||
pub data: Vec<u8>, | ||
pub program_id: Pubkey, | ||
pub is_signer: bool, | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a bit hacky, but encompasses account data in an easy-to-use way. Is there a better solution for this?
The concept of these fuzzing tests is to run fuzzed withdraw / swap / deposit instructions on a hard-coded curve, allow for certain errors (like Insufficient funds and calculation errors), and at the end check that we haven't created or deleted any tokens, and that we can also withdraw everything from the token swap. also tagging @aeyakovenko if he has any additional ideas for conditions to check. |
I'd like to leave the CI for a follow-up PR, and I'll keep running the fuzzing over the weekend |
After running this over the whole weekend, it only caught an error in the fuzzer logic! |
I've tried getting this to run in bpf, but we'll need nightly for that, specifically for |
I've tried using honggfuzz in 2997203. Specifically, a version that doesn't use |
Converting back to draft as I try honggfuzz some more |
sudo apt-get install -y binutils-dev | ||
sudo apt-get install -y libunwind-dev |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
these can also get added to fuzz.sh
if preferred
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Na, this is fine IMO
@@ -9,6 +9,7 @@ set -x | |||
|
|||
cargo --version | |||
cargo install rustfilt || true | |||
cargo install honggfuzz || true |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this can get added to fuzz.sh
as well
A nice note for
Meaning people who have forked the repo won't run the nightly fuzzer |
@@ -26,7 +25,7 @@ fn run_program( | |||
program_id: &Pubkey, | |||
parameter_accounts: &[KeyedAccount], | |||
instruction_data: &[u8], | |||
) -> Result<u64, InstructionError> { | |||
) -> u64 { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This came from a new clippy error
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good overall! I have some concerns about the amount of boilerplate that seems to be required to fuzz but as a v1 there's no need for this PR to continue to dangle. This'll be a good example for other programs at least, and hopefully inspiration for future improvements to streamline fuzzing.
Errr, so did we find any actual bugs in token-swap though?
sudo apt-get install -y binutils-dev | ||
sudo apt-get install -y libunwind-dev |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Na, this is fine IMO
native_account_data::NativeAccountData, native_token::get_token_balance, | ||
native_token_swap::NativeTokenSwap, | ||
}; | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: try remove whitespace between successive use
statements
} | ||
} | ||
|
||
fn run_fuzz_instructions(fuzz_instructions: Vec<FuzzInstruction>) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: avoid requiring a Vec
when a slice will do
fn run_fuzz_instructions(fuzz_instructions: Vec<FuzzInstruction>) { | |
fn run_fuzz_instructions(fuzz_instructions: &[FuzzInstruction]) { |
@@ -550,7 +553,8 @@ impl Processor { | |||
to_u128(token_a.amount)?, | |||
) | |||
.ok_or(SwapError::ZeroTradingTokens)?; | |||
if a_amount < to_u128(minimum_token_a_amount)? { | |||
let a_amount = to_u64(a_amount)?; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Was this a bug fix as a result of fuzzing?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It actually wasn't, just an inconsistency in the code from when we transitioned to u128
@mvines thanks for the review. I ended up starting on the banks version, and we can definitely refactor some logic that's also used in the stake pool end-to-end testing in a |
Using libfuzzer / cargo-fuzz (https://github.com/rust-fuzz/cargo-fuzz), this adds fuzz instruction testing in token-swap, following the example over at serum (https://github.com/project-serum/serum-dex/tree/master/dex/fuzz).
Still left to do:
-max_total_time
flag