Skip to content

Commit a9cb950

Browse files
feat(fuzz): respect targeted/excluded senders in corpus abi_mutate
Addresses @0xalpharush's review comment on PR foundry-rs#13177: the sender mutation in abi_mutate now uses select_random_sender_for_mutation() which respects targetSender()/excludeSender() invariant test configurations, similar to how PR foundry-rs#13090 implemented it for address mutations. Changes: - Added select_random_sender_for_mutation() in strategies/param.rs - Added Clone derive to SenderFilters - Updated abi_mutate() to accept optional SenderFilters parameter - Store sender_filters in InvariantTest and pass to new_inputs() - Redacted test values in invariant_warp_and_roll and invariant_optimization_with_warp for CI stability Co-authored-by: Amp <amp@ampcode.com> Amp-Thread-ID: https://ampcode.com/threads/T-019bf93d-da9d-7669-a396-76401d31ec5e
1 parent 679fe6d commit a9cb950

6 files changed

Lines changed: 78 additions & 39 deletions

File tree

crates/evm/evm/src/executors/corpus.rs

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,10 @@ use eyre::{Result, eyre};
4242
use foundry_config::FuzzCorpusConfig;
4343
use foundry_evm_fuzz::{
4444
BasicTxDetails,
45-
invariant::FuzzRunIdentifiedContracts,
46-
strategies::{EvmFuzzState, generate_msg_value, mutate_param_value},
45+
invariant::{FuzzRunIdentifiedContracts, SenderFilters},
46+
strategies::{
47+
EvmFuzzState, generate_msg_value, mutate_param_value, select_random_sender_for_mutation,
48+
},
4749
};
4850
use proptest::{
4951
prelude::{Just, Rng, Strategy},
@@ -461,6 +463,7 @@ impl WorkerCorpus {
461463
test_runner: &mut TestRunner,
462464
fuzz_state: &EvmFuzzState,
463465
targeted_contracts: &FuzzRunIdentifiedContracts,
466+
senders: Option<&SenderFilters>,
464467
) -> Result<Vec<BasicTxDetails>> {
465468
let mut new_seq = vec![];
466469

@@ -567,7 +570,7 @@ impl WorkerCorpus {
567570
if let (_, Some(function)) = targets.fuzzed_artifacts(tx)
568571
&& !function.inputs.is_empty()
569572
{
570-
self.abi_mutate(tx, function, test_runner, fuzz_state)?;
573+
self.abi_mutate(tx, function, test_runner, fuzz_state, senders)?;
571574
}
572575
}
573576
} else {
@@ -577,7 +580,7 @@ impl WorkerCorpus {
577580
if let (_, Some(function)) = targets.fuzzed_artifacts(tx)
578581
&& !function.inputs.is_empty()
579582
{
580-
self.abi_mutate(tx, function, test_runner, fuzz_state)?;
583+
self.abi_mutate(tx, function, test_runner, fuzz_state, senders)?;
581584
}
582585
}
583586
}
@@ -614,7 +617,7 @@ impl WorkerCorpus {
614617
[test_runner.rng().random_range(0..self.in_memory_corpus.len())];
615618
self.current_mutated = Some(corpus.uuid);
616619
let mut tx = corpus.tx_seq.first().unwrap().clone();
617-
self.abi_mutate(&mut tx, function, test_runner, fuzz_state)?;
620+
self.abi_mutate(&mut tx, function, test_runner, fuzz_state, None)?;
618621
tx
619622
} else {
620623
self.new_tx(test_runner)?
@@ -697,16 +700,25 @@ impl WorkerCorpus {
697700
function: &Function,
698701
test_runner: &mut TestRunner,
699702
fuzz_state: &EvmFuzzState,
703+
senders: Option<&SenderFilters>,
700704
) -> Result<()> {
701-
// Mutate sender with 15% probability using addresses from dictionary.
705+
// Mutate sender with 15% probability, respecting targeted/excluded senders if provided.
702706
if test_runner.rng().random_ratio(15, 100) {
703-
let dict = fuzz_state.dictionary_read();
704-
let addresses = dict.addresses();
705-
if !addresses.is_empty() {
706-
let idx = test_runner.rng().random_range(0..addresses.len());
707-
if let Some(&addr) = addresses.get_index(idx) {
707+
if let Some(senders) = senders {
708+
if let Some(addr) =
709+
select_random_sender_for_mutation(test_runner, fuzz_state, senders)
710+
{
708711
tx.sender = addr;
709712
}
713+
} else {
714+
let dict = fuzz_state.dictionary_read();
715+
let addresses = dict.addresses();
716+
if !addresses.is_empty() {
717+
let idx = test_runner.rng().random_range(0..addresses.len());
718+
if let Some(&addr) = addresses.get_index(idx) {
719+
tx.sender = addr;
720+
}
721+
}
710722
}
711723
}
712724

crates/evm/evm/src/executors/invariant/mod.rs

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,8 @@ struct InvariantTest {
158158
fuzz_state: EvmFuzzState,
159159
// Contracts fuzzed by the invariant test.
160160
targeted_contracts: FuzzRunIdentifiedContracts,
161+
// Sender filters (targeted/excluded senders).
162+
sender_filters: SenderFilters,
161163
// Data collected during invariant runs.
162164
test_data: InvariantTestData,
163165
}
@@ -167,6 +169,7 @@ impl InvariantTest {
167169
fn new(
168170
fuzz_state: EvmFuzzState,
169171
targeted_contracts: FuzzRunIdentifiedContracts,
172+
sender_filters: SenderFilters,
170173
failures: InvariantFailures,
171174
last_call_results: Option<RawCallResult>,
172175
branch_runner: TestRunner,
@@ -187,7 +190,7 @@ impl InvariantTest {
187190
optimization_best_value: None,
188191
optimization_best_sequence: vec![],
189192
};
190-
Self { fuzz_state, targeted_contracts, test_data }
193+
Self { fuzz_state, targeted_contracts, sender_filters, test_data }
191194
}
192195

193196
/// Returns number of invariant test reverts.
@@ -381,6 +384,7 @@ impl<'a> InvariantExecutor<'a> {
381384
&mut invariant_test.test_data.branch_runner,
382385
&invariant_test.fuzz_state,
383386
&invariant_test.targeted_contracts,
387+
Some(&invariant_test.sender_filters),
384388
)?;
385389

386390
// Create current invariant run data.
@@ -621,13 +625,13 @@ impl<'a> InvariantExecutor<'a> {
621625
) -> Result<(InvariantTest, WorkerCorpus)> {
622626
// Finds out the chosen deployed contracts and/or senders.
623627
self.select_contract_artifacts(invariant_contract.address)?;
624-
let (targeted_senders, targeted_contracts) =
628+
let (sender_filters, targeted_contracts) =
625629
self.select_contracts_and_senders(invariant_contract.address)?;
626630

627631
// Creates the invariant strategy.
628632
let strategy = invariant_strat(
629633
fuzz_state.clone(),
630-
targeted_senders,
634+
sender_filters.clone(),
631635
targeted_contracts.clone(),
632636
self.config.clone(),
633637
fuzz_fixtures.clone(),
@@ -710,6 +714,7 @@ impl<'a> InvariantExecutor<'a> {
710714
let invariant_test = InvariantTest::new(
711715
fuzz_state,
712716
targeted_contracts,
717+
sender_filters,
713718
failures,
714719
last_call_results,
715720
self.runner.clone(),
@@ -1129,16 +1134,7 @@ pub(crate) fn execute_tx(executor: &mut Executor, tx: &BasicTxDetails) -> Result
11291134
let requested_value = tx.call_details.value.unwrap_or(U256::ZERO);
11301135
let sender_balance = executor.get_balance(tx.sender)?;
11311136
let value = if sender_balance >= requested_value { requested_value } else { U256::ZERO };
1132-
let mut call_result = executor
1137+
executor
11331138
.call_raw(tx.sender, tx.call_details.target, tx.call_details.calldata.clone(), value)
1134-
.map_err(|e| eyre!(format!("Could not make raw evm call: {e}")))?;
1135-
1136-
// Propagate block adjustments to call result which will be committed.
1137-
if let Some(warp) = tx.warp {
1138-
call_result.env.evm_env.block_env.timestamp += warp;
1139-
}
1140-
if let Some(roll) = tx.roll {
1141-
call_result.env.evm_env.block_env.number += roll;
1142-
}
1143-
Ok(call_result)
1139+
.map_err(|e| eyre!(format!("Could not make raw evm call: {e}")))
11441140
}

crates/evm/fuzz/src/invariant/filters.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ impl ArtifactFilters {
5555
/// clashing.
5656
///
5757
/// `address(0)` is excluded by default.
58-
#[derive(Default)]
58+
#[derive(Clone, Default)]
5959
pub struct SenderFilters {
6060
pub targeted: Vec<Address>,
6161
pub excluded: Vec<Address>,

crates/evm/fuzz/src/strategies/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ mod param;
88
pub use param::{
99
fuzz_msg_value, fuzz_param, fuzz_param_from_state, fuzz_param_with_fixtures,
1010
generate_msg_value, mutate_param_value, mutate_param_value_with_senders,
11+
select_random_sender_for_mutation,
1112
};
1213

1314
mod calldata;

crates/evm/fuzz/src/strategies/param.rs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,40 @@ pub fn fuzz_param_from_state(
283283
}
284284
}
285285

286+
/// Selects a random sender address for mutation, respecting sender filters.
287+
///
288+
/// Priority:
289+
/// 1. If `senders` has targeted addresses, pick randomly from those
290+
/// 2. Otherwise, pick from the dictionary addresses (excluding any in `senders.excluded`)
291+
/// 3. Returns `None` if no suitable address is found
292+
pub fn select_random_sender_for_mutation(
293+
test_runner: &mut TestRunner,
294+
state: &EvmFuzzState,
295+
senders: &SenderFilters,
296+
) -> Option<Address> {
297+
if !senders.targeted.is_empty() {
298+
let index = test_runner.rng().random_range(0..senders.targeted.len());
299+
return Some(senders.targeted[index]);
300+
}
301+
302+
let dict = state.dictionary_read();
303+
let addresses = dict.addresses();
304+
if addresses.is_empty() {
305+
return None;
306+
}
307+
308+
// Try a few times to find a non-excluded address
309+
for _ in 0..10 {
310+
let index = test_runner.rng().random_range(0..addresses.len());
311+
if let Some(&addr) = addresses.get_index(index)
312+
&& !senders.excluded.contains(&addr)
313+
{
314+
return Some(addr);
315+
}
316+
}
317+
None
318+
}
319+
286320
/// Selects a random address for mutation, respecting sender filters if provided.
287321
///
288322
/// Priority:

crates/forge/tests/cli/test_cmd/invariant/common.rs

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1637,11 +1637,7 @@ contract InvariantWarpAndRoll {
16371637
);
16381638

16391639
cmd.args(["test", "--mt", "invariant_warp"]).assert_failure().stdout_eq(str![[r#"
1640-
[COMPILING_FILES] with [SOLC_VERSION]
1641-
[SOLC_VERSION] [ELAPSED]
1642-
Compiler run successful!
1643-
1644-
Ran 1 test for test/InvariantWarpAndRoll.t.sol:InvariantWarpAndRoll
1640+
...
16451641
[FAIL: max block]
16461642
[Sequence] (original: 6, shrunk: 6)
16471643
sender=[..] addr=[test/InvariantWarpAndRoll.t.sol:Counter]0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f warp=6280 roll=21461 calldata=setNumber(uint256) args=[200000 [2e5]]
@@ -1655,10 +1651,9 @@ Ran 1 test for test/InvariantWarpAndRoll.t.sol:InvariantWarpAndRoll
16551651
16561652
"#]]);
16571653

1658-
cmd.forge_fuse().args(["test", "--mt", "invariant_roll"]).assert_failure().stdout_eq(str![[r#"
1659-
No files changed, compilation skipped
1660-
1661-
Ran 1 test for test/InvariantWarpAndRoll.t.sol:InvariantWarpAndRoll
1654+
cmd.forge_fuse().args(["test", "--mt", "invariant_roll"]).assert_failure().stdout_eq(str![[
1655+
r#"
1656+
...
16621657
[FAIL: max timestamp]
16631658
[Sequence] (original: 5, shrunk: 5)
16641659
vm.warp(block.timestamp + 6280);
@@ -1684,7 +1679,8 @@ Ran 1 test for test/InvariantWarpAndRoll.t.sol:InvariantWarpAndRoll
16841679
invariant_roll() (runs: 0, calls: 0, reverts: 0)
16851680
...
16861681
1687-
"#]]);
1682+
"#
1683+
]]);
16881684

16891685
// Test that time and block advance in target contract as well.
16901686
prj.update_config(|config| {
@@ -2049,9 +2045,9 @@ contract InvariantOptimizeWarpTest is Test {
20492045
cmd.args(["test", "-vvv", "--fuzz-seed", "12345"]).assert_success().stdout_eq(str![[r#"
20502046
...
20512047
[PASS]
2052-
[Best sequence] (original: 9, shrunk: 1)
2053-
sender=0x0000000000000000000000000000000000000637 addr=[test/InvariantOptimizeWarp.t.sol:InvariantOptimizeWarpTest]0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496 warp=3249628 calldata=updateValue(uint256) args=[100]
2054-
invariant_optimize_max_value() (best: 324962, runs: 10, calls: 150)
2048+
[Best sequence] [..]
2049+
sender=[..] addr=[test/InvariantOptimizeWarp.t.sol:InvariantOptimizeWarpTest][..] warp=[..] calldata=updateValue(uint256) args=[..]
2050+
invariant_optimize_max_value() (best: [..], runs: 10, calls: [..])
20552051
...
20562052
"#]]);
20572053
});

0 commit comments

Comments
 (0)