Skip to content
3 changes: 3 additions & 0 deletions crates/config/src/invariant.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ pub struct InvariantConfig {
pub max_time_delay: Option<u32>,
/// Maximum number of blocks elapsed between generated txs.
pub max_block_delay: Option<u32>,
/// Maximum amount (in wei) to deal to sender before each tx for payable functions.
pub max_deal: Option<u64>,
/// Number of calls to execute between invariant assertions.
///
/// - `0`: Only assert on the last call of each run (fastest, but may miss exact breaking call)
Expand Down Expand Up @@ -69,6 +71,7 @@ impl Default for InvariantConfig {
show_solidity: false,
max_time_delay: None,
max_block_delay: None,
max_deal: None,
Copy link
Copy Markdown
Contributor

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 for max_time_delay, max_block_delay, and max_deal.

check_interval: 1,
}
}
Expand Down
103 changes: 75 additions & 28 deletions crates/evm/evm/src/executors/corpus.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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();

Expand Down Expand Up @@ -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![];

Expand Down Expand Up @@ -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);
Expand All @@ -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);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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 n_to_mutate > 1


// 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)?;
}
}
}
Expand Down Expand Up @@ -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)?
Expand Down Expand Up @@ -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) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this also check targeted/excluded senders as was done here #13090?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The 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 {
Expand Down Expand Up @@ -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,
},
}
}
Expand Down
10 changes: 8 additions & 2 deletions crates/evm/evm/src/executors/fuzz/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -250,8 +250,13 @@ impl FuzzedExecutor {
&[BasicTxDetails {
warp: None,
roll: None,
deal: None,
sender: self.sender,
call_details: CallDetails { target: address, calldata: calldata.clone() },
call_details: CallDetails {
target: address,
calldata: calldata.clone(),
value: None,
},
}],
new_coverage,
);
Expand Down Expand Up @@ -413,8 +418,9 @@ impl FuzzedExecutor {
.prop_map(move |calldata| BasicTxDetails {
warp: None,
roll: None,
deal: None,
sender: Default::default(),
call_details: CallDetails { target: Default::default(), calldata },
call_details: CallDetails { target: Default::default(), calldata, value: None },
});

let mut corpus = WorkerCorpus::new(
Expand Down
33 changes: 28 additions & 5 deletions crates/evm/evm/src/executors/invariant/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
Expand All @@ -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,
Expand All @@ -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.
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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(),
Expand Down Expand Up @@ -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(),
Expand Down Expand Up @@ -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();
Expand All @@ -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.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe just do sender_balance.min(requested_value) instead of sending zero?

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}")))
}
8 changes: 7 additions & 1 deletion crates/evm/fuzz/src/invariant/call_override.rs
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,13 @@ impl RandomCallGenerator {

// `original_caller` has a 80% chance of being the `new_target`.
let choice = self.strategy.new_tree(&mut self.runner.lock()).unwrap().current().map(
|call_details| BasicTxDetails { warp: None, roll: None, sender, call_details },
|call_details| BasicTxDetails {
warp: None,
roll: None,
deal: None,
sender,
call_details,
},
);

self.last_sequence.write().push(choice.clone());
Expand Down
2 changes: 1 addition & 1 deletion crates/evm/fuzz/src/invariant/filters.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ impl ArtifactFilters {
/// clashing.
///
/// `address(0)` is excluded by default.
#[derive(Default)]
#[derive(Clone, Default)]
pub struct SenderFilters {
pub targeted: Vec<Address>,
pub excluded: Vec<Address>,
Expand Down
Loading
Loading