-
Notifications
You must be signed in to change notification settings - Fork 2.5k
feat(fuzz): enhance corpus mutation with all-call strategy and msg.value support #13177
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
1465ae0
23d39bc
9ebe3d8
679fe6d
d452197
40fd045
6ce3a1e
cda8d11
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -42,8 +42,10 @@ use eyre::{Result, eyre}; | |
| use foundry_config::FuzzCorpusConfig; | ||
| use foundry_evm_fuzz::{ | ||
| BasicTxDetails, | ||
| invariant::FuzzRunIdentifiedContracts, | ||
| strategies::{EvmFuzzState, mutate_param_value}, | ||
| invariant::{FuzzRunIdentifiedContracts, SenderFilters}, | ||
| strategies::{ | ||
| EvmFuzzState, generate_msg_value, mutate_param_value, select_random_sender_for_mutation, | ||
| }, | ||
| }; | ||
| use proptest::{ | ||
| prelude::{Just, Rng, Strategy}, | ||
|
|
@@ -83,12 +85,12 @@ enum MutationType { | |
| Repeat, | ||
| /// Interleave calls from two random call sequences. | ||
| Interleave, | ||
| /// Replace prefix of the original call sequence with new calls. | ||
| Prefix, | ||
| /// Replace suffix of the original call sequence with new calls. | ||
| Suffix, | ||
| /// ABI mutate random args of selected call in sequence. | ||
| Abi, | ||
| /// Generate new calls for a random prefix of the sequence. | ||
| GenPrefix, | ||
| /// Generate new calls for a random suffix of the sequence. | ||
| GenSuffix, | ||
| /// ABI mutate a random number of calls (1 to all) in the sequence. | ||
| GenMutate, | ||
| } | ||
|
|
||
| /// Holds Corpus information. | ||
|
|
@@ -284,9 +286,9 @@ impl WorkerCorpus { | |
| Just(MutationType::Splice), | ||
| Just(MutationType::Repeat), | ||
| Just(MutationType::Interleave), | ||
| Just(MutationType::Prefix), | ||
| Just(MutationType::Suffix), | ||
| Just(MutationType::Abi), | ||
| Just(MutationType::GenPrefix), | ||
| Just(MutationType::GenSuffix), | ||
| Just(MutationType::GenMutate), | ||
| ] | ||
| .boxed(); | ||
|
|
||
|
|
@@ -461,6 +463,7 @@ impl WorkerCorpus { | |
| test_runner: &mut TestRunner, | ||
| fuzz_state: &EvmFuzzState, | ||
| targeted_contracts: &FuzzRunIdentifiedContracts, | ||
| senders: Option<&SenderFilters>, | ||
| ) -> Result<Vec<BasicTxDetails>> { | ||
| let mut new_seq = vec![]; | ||
|
|
||
|
|
@@ -528,30 +531,33 @@ impl WorkerCorpus { | |
| new_seq.push(tx); | ||
| } | ||
| } | ||
| MutationType::Prefix => { | ||
| MutationType::GenPrefix => { | ||
| let corpus = if rng.random::<bool>() { primary } else { secondary }; | ||
| trace!(target: "corpus", "overwrite prefix of {}", corpus.uuid); | ||
| trace!(target: "corpus", "generate prefix of {}", corpus.uuid); | ||
|
|
||
| self.current_mutated = Some(corpus.uuid); | ||
|
|
||
| new_seq = corpus.tx_seq.clone(); | ||
| // Generate new calls for a random prefix (0 to all elements). | ||
| for i in 0..rng.random_range(0..=new_seq.len()) { | ||
| new_seq[i] = self.new_tx(test_runner)?; | ||
| } | ||
| } | ||
| MutationType::Suffix => { | ||
| MutationType::GenSuffix => { | ||
| let corpus = if rng.random::<bool>() { primary } else { secondary }; | ||
| trace!(target: "corpus", "overwrite suffix of {}", corpus.uuid); | ||
| trace!(target: "corpus", "generate suffix of {}", corpus.uuid); | ||
|
|
||
| self.current_mutated = Some(corpus.uuid); | ||
|
|
||
| new_seq = corpus.tx_seq.clone(); | ||
| for i in new_seq.len() - rng.random_range(0..new_seq.len())..corpus.tx_seq.len() | ||
| { | ||
| new_seq[i] = self.new_tx(test_runner)?; | ||
| // Generate new calls for a random suffix (0 to all elements). | ||
| let len = new_seq.len(); | ||
| let start = len - rng.random_range(0..len); | ||
| for tx in new_seq.iter_mut().skip(start) { | ||
| *tx = self.new_tx(test_runner)?; | ||
| } | ||
| } | ||
| MutationType::Abi => { | ||
| MutationType::GenMutate => { | ||
| let targets = targeted_contracts.targets.lock(); | ||
| let corpus = if rng.random::<bool>() { primary } else { secondary }; | ||
| trace!(target: "corpus", "ABI mutate args of {}", corpus.uuid); | ||
|
|
@@ -560,13 +566,24 @@ impl WorkerCorpus { | |
|
|
||
| new_seq = corpus.tx_seq.clone(); | ||
|
|
||
| let idx = rng.random_range(0..new_seq.len()); | ||
| let tx = new_seq.get_mut(idx).unwrap(); | ||
| if let (_, Some(function)) = targets.fuzzed_artifacts(tx) { | ||
| // TODO: add call_value to call details and mutate it as well as sender some | ||
| // of the time. | ||
| if !function.inputs.is_empty() { | ||
| self.abi_mutate(tx, function, test_runner, fuzz_state)?; | ||
| let len = new_seq.len(); | ||
| // Mutate a random number of calls (1 to all), similar to how GenPrefix | ||
| // generates a random number of new calls. | ||
| let n_to_mutate = rng.random_range(1..=len); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It would be nice if multiple args of the same tx can be mutated. I think right now, only one per tx can mutated even if |
||
|
|
||
| // Shuffle indices to select which calls to mutate. | ||
| let mut indices: Vec<usize> = (0..len).collect(); | ||
| for i in (1..len).rev() { | ||
| let j = rng.random_range(0..=i); | ||
| indices.swap(i, j); | ||
| } | ||
|
|
||
| for i in indices.into_iter().take(n_to_mutate) { | ||
| let tx = &mut new_seq[i]; | ||
| if let (_, Some(function)) = targets.fuzzed_artifacts(tx) | ||
| && !function.inputs.is_empty() | ||
| { | ||
| self.abi_mutate(tx, function, test_runner, fuzz_state, senders)?; | ||
| } | ||
| } | ||
| } | ||
|
|
@@ -603,7 +620,7 @@ impl WorkerCorpus { | |
| [test_runner.rng().random_range(0..self.in_memory_corpus.len())]; | ||
| self.current_mutated = Some(corpus.uuid); | ||
| let mut tx = corpus.tx_seq.first().unwrap().clone(); | ||
| self.abi_mutate(&mut tx, function, test_runner, fuzz_state)?; | ||
| self.abi_mutate(&mut tx, function, test_runner, fuzz_state, None)?; | ||
| tx | ||
| } else { | ||
| self.new_tx(test_runner)? | ||
|
|
@@ -686,8 +703,36 @@ impl WorkerCorpus { | |
| function: &Function, | ||
| test_runner: &mut TestRunner, | ||
| fuzz_state: &EvmFuzzState, | ||
| senders: Option<&SenderFilters>, | ||
| ) -> Result<()> { | ||
| // let rng = test_runner.rng(); | ||
| // Mutate sender with 15% probability, respecting targeted/excluded senders if provided. | ||
| if test_runner.rng().random_ratio(15, 100) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should this also check targeted/excluded senders as was done here #13090?
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. added in d452197 |
||
| if let Some(senders) = senders { | ||
| if let Some(addr) = | ||
| select_random_sender_for_mutation(test_runner, fuzz_state, senders) | ||
| { | ||
| tx.sender = addr; | ||
| } | ||
| } else { | ||
| let dict = fuzz_state.dictionary_read(); | ||
| let addresses = dict.addresses(); | ||
| if !addresses.is_empty() { | ||
| let idx = test_runner.rng().random_range(0..addresses.len()); | ||
| if let Some(&addr) = addresses.get_index(idx) { | ||
| tx.sender = addr; | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // Mutate value with 15% probability for payable functions. | ||
| if function.state_mutability == alloy_json_abi::StateMutability::Payable | ||
| && test_runner.rng().random_ratio(15, 100) | ||
| { | ||
| tx.call_details.value = Some(generate_msg_value(test_runner)); | ||
| } | ||
|
|
||
| // Mutate calldata. | ||
| let mut arg_mutation_rounds = | ||
| test_runner.rng().random_range(0..=function.inputs.len()).max(1); | ||
| let round_arg_idx: Vec<usize> = if function.inputs.len() <= 1 { | ||
|
|
@@ -1100,10 +1145,12 @@ mod tests { | |
| BasicTxDetails { | ||
| warp: None, | ||
| roll: None, | ||
| deal: None, | ||
| sender: Address::ZERO, | ||
| call_details: foundry_evm_fuzz::CallDetails { | ||
| target: Address::ZERO, | ||
| calldata: Bytes::new(), | ||
| value: None, | ||
| }, | ||
| } | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -158,6 +158,8 @@ struct InvariantTest { | |
| fuzz_state: EvmFuzzState, | ||
| // Contracts fuzzed by the invariant test. | ||
| targeted_contracts: FuzzRunIdentifiedContracts, | ||
| // Sender filters (targeted/excluded senders). | ||
| sender_filters: SenderFilters, | ||
| // Data collected during invariant runs. | ||
| test_data: InvariantTestData, | ||
| } | ||
|
|
@@ -167,6 +169,7 @@ impl InvariantTest { | |
| fn new( | ||
| fuzz_state: EvmFuzzState, | ||
| targeted_contracts: FuzzRunIdentifiedContracts, | ||
| sender_filters: SenderFilters, | ||
| failures: InvariantFailures, | ||
| last_call_results: Option<RawCallResult>, | ||
| branch_runner: TestRunner, | ||
|
|
@@ -187,7 +190,7 @@ impl InvariantTest { | |
| optimization_best_value: None, | ||
| optimization_best_sequence: vec![], | ||
| }; | ||
| Self { fuzz_state, targeted_contracts, test_data } | ||
| Self { fuzz_state, targeted_contracts, sender_filters, test_data } | ||
| } | ||
|
|
||
| /// Returns number of invariant test reverts. | ||
|
|
@@ -381,6 +384,7 @@ impl<'a> InvariantExecutor<'a> { | |
| &mut invariant_test.test_data.branch_runner, | ||
| &invariant_test.fuzz_state, | ||
| &invariant_test.targeted_contracts, | ||
| Some(&invariant_test.sender_filters), | ||
| )?; | ||
|
|
||
| // Create current invariant run data. | ||
|
|
@@ -621,13 +625,13 @@ impl<'a> InvariantExecutor<'a> { | |
| ) -> Result<(InvariantTest, WorkerCorpus)> { | ||
| // Finds out the chosen deployed contracts and/or senders. | ||
| self.select_contract_artifacts(invariant_contract.address)?; | ||
| let (targeted_senders, targeted_contracts) = | ||
| let (sender_filters, targeted_contracts) = | ||
| self.select_contracts_and_senders(invariant_contract.address)?; | ||
|
|
||
| // Creates the invariant strategy. | ||
| let strategy = invariant_strat( | ||
| fuzz_state.clone(), | ||
| targeted_senders, | ||
| sender_filters.clone(), | ||
| targeted_contracts.clone(), | ||
| self.config.clone(), | ||
| fuzz_fixtures.clone(), | ||
|
|
@@ -710,6 +714,7 @@ impl<'a> InvariantExecutor<'a> { | |
| let invariant_test = InvariantTest::new( | ||
| fuzz_state, | ||
| targeted_contracts, | ||
| sender_filters, | ||
| failures, | ||
| last_call_results, | ||
| self.runner.clone(), | ||
|
|
@@ -1100,7 +1105,8 @@ pub(crate) fn call_invariant_function( | |
| } | ||
|
|
||
| /// Executes a fuzz call and returns the result. | ||
| /// Applies any block timestamp (warp) and block number (roll) adjustments before the call. | ||
| /// Applies any block timestamp (warp), block number (roll), and balance (deal) adjustments before | ||
| /// the call. | ||
| pub(crate) fn execute_tx(executor: &mut Executor, tx: &BasicTxDetails) -> Result<RawCallResult> { | ||
| let warp = tx.warp.unwrap_or_default(); | ||
| let roll = tx.roll.unwrap_or_default(); | ||
|
|
@@ -1124,7 +1130,24 @@ pub(crate) fn execute_tx(executor: &mut Executor, tx: &BasicTxDetails) -> Result | |
| } | ||
| } | ||
|
|
||
| let requested_value = tx.call_details.value.unwrap_or(U256::ZERO); | ||
|
|
||
| // If no value requested, skip balance checks and deal logic. | ||
| let value = if requested_value.is_zero() { | ||
| U256::ZERO | ||
| } else { | ||
| // Apply deal (increase sender balance) if specified. | ||
| if let Some(deal) = tx.deal { | ||
| let current_balance = executor.get_balance(tx.sender)?; | ||
| executor.set_balance(tx.sender, current_balance + deal)?; | ||
| } | ||
|
|
||
| // Only use value if sender has sufficient balance (after deal), otherwise fall back to 0. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe just do |
||
| let sender_balance = executor.get_balance(tx.sender)?; | ||
| if sender_balance >= requested_value { requested_value } else { U256::ZERO } | ||
| }; | ||
|
|
||
| executor | ||
| .call_raw(tx.sender, tx.call_details.target, tx.call_details.calldata.clone(), U256::ZERO) | ||
| .call_raw(tx.sender, tx.call_details.target, tx.call_details.calldata.clone(), value) | ||
| .map_err(|e| eyre!(format!("Could not make raw evm call: {e}"))) | ||
| } | ||
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.
Not necessarily in this PR, but maybe we should provides
Some(..)defaults formax_time_delay,max_block_delay, andmax_deal.