Skip to content

Commit 0b1471c

Browse files
committed
Unify fuzz transaction generation
1 parent 7ea163d commit 0b1471c

11 files changed

Lines changed: 664 additions & 335 deletions

File tree

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

Lines changed: 42 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,12 @@
3636
3737
use super::corpus_io::{CorpusDirEntry, read_corpus_dir};
3838
use crate::{
39-
executors::{Executor, RawCallResult, invariant::execute_tx},
39+
executors::{Executor, RawCallResult, sequence::execute_tx},
4040
inspectors::{CmpOperands, EdgeIndexMap, MAX_EDGE_COUNT},
4141
};
4242
use alloy_dyn_abi::JsonAbiExt;
4343
use alloy_json_abi::Function;
44-
use alloy_primitives::{Address, Bytes, I256};
44+
use alloy_primitives::{Address, I256};
4545
use eyre::{Result, eyre};
4646
use foundry_common::{ContractsByAddress, ContractsByArtifact, sh_warn};
4747
use foundry_config::FuzzCorpusConfig;
@@ -843,10 +843,13 @@ impl WorkerCorpus {
843843
test_runner: &mut TestRunner,
844844
fuzz_state: &EvmFuzzState,
845845
function: &Function,
846-
) -> Result<Bytes> {
846+
sender: Address,
847+
target: Address,
848+
) -> Result<BasicTxDetails> {
847849
// Early return if not running with coverage guided fuzzing.
848850
if !self.config.is_coverage_guided() {
849-
return Ok(self.new_tx(test_runner)?.call_details.calldata);
851+
let tx = self.new_tx(test_runner)?;
852+
return Ok(Self::normalize_stateless_tx(tx, sender, target));
850853
}
851854

852855
self.evict_oldest_corpus()?;
@@ -867,7 +870,20 @@ impl WorkerCorpus {
867870
tx
868871
};
869872

870-
Ok(tx.call_details.calldata)
873+
Ok(Self::normalize_stateless_tx(tx, sender, target))
874+
}
875+
876+
fn normalize_stateless_tx(
877+
mut tx: BasicTxDetails,
878+
sender: Address,
879+
target: Address,
880+
) -> BasicTxDetails {
881+
tx.warp = None;
882+
tx.roll = None;
883+
tx.sender = sender;
884+
tx.call_details.target = target;
885+
tx.call_details.value = None;
886+
tx
871887
}
872888

873889
/// Generates single call from corpus strategy.
@@ -1406,7 +1422,7 @@ fn has_legacy_invariant_corpus_dirs(path: &Path) -> bool {
14061422
mod tests {
14071423
use super::*;
14081424
use alloy_dyn_abi::DynSolValue;
1409-
use alloy_primitives::U256;
1425+
use alloy_primitives::{Bytes, U256};
14101426
use std::fs;
14111427

14121428
fn basic_tx() -> BasicTxDetails {
@@ -1428,6 +1444,26 @@ mod tests {
14281444
dir
14291445
}
14301446

1447+
#[test]
1448+
fn stateless_normalization_discards_persisted_execution_context() {
1449+
let mut tx = basic_tx();
1450+
tx.warp = Some(U256::from(1));
1451+
tx.roll = Some(U256::from(2));
1452+
tx.sender = Address::with_last_byte(0x11);
1453+
tx.call_details.target = Address::with_last_byte(0x22);
1454+
tx.call_details.value = Some(U256::from(3));
1455+
1456+
let sender = Address::with_last_byte(0xaa);
1457+
let target = Address::with_last_byte(0xbb);
1458+
let normalized = WorkerCorpus::normalize_stateless_tx(tx, sender, target);
1459+
1460+
assert_eq!(normalized.warp, None);
1461+
assert_eq!(normalized.roll, None);
1462+
assert_eq!(normalized.sender, sender);
1463+
assert_eq!(normalized.call_details.target, target);
1464+
assert_eq!(normalized.call_details.value, None);
1465+
}
1466+
14311467
#[test]
14321468
fn cmp_mutate_replaces_matching_calldata_operand() {
14331469
let function = Function::parse("testCmp(uint256)").unwrap();

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

Lines changed: 100 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use crate::executors::{
22
DURATION_BETWEEN_METRICS_REPORT, EarlyExit, Executor, FuzzTestTimer, RawCallResult,
33
corpus::{GlobalCorpusMetrics, WorkerCorpus},
4+
sequence::replay_tx,
45
};
56
use alloy_dyn_abi::JsonAbiExt;
67
use alloy_json_abi::Function;
@@ -18,17 +19,15 @@ use foundry_evm_coverage::HitMaps;
1819
use foundry_evm_fuzz::{
1920
BaseCounterExample, BasicTxDetails, CallDetails, CounterExample, FuzzCase, FuzzError,
2021
FuzzFixtures, FuzzRunMetadata, FuzzTestResult,
21-
strategies::{EvmFuzzState, fuzz_calldata, fuzz_calldata_from_state},
22+
strategies::{EvmFuzzState, TxGenerator},
2223
};
2324
use foundry_evm_traces::SparsedTraceArena;
2425
use indicatif::ProgressBar;
25-
use proptest::{
26-
strategy::Strategy,
27-
test_runner::{RngAlgorithm, TestCaseError, TestRng, TestRunner},
28-
};
26+
use proptest::test_runner::{RngAlgorithm, TestCaseError, TestRng, TestRunner};
2927
use rayon::iter::{IntoParallelIterator, ParallelIterator};
3028
use serde_json::json;
3129
use std::{
30+
ops::ControlFlow,
3231
sync::{
3332
Arc, OnceLock,
3433
atomic::{AtomicU32, Ordering},
@@ -260,69 +259,70 @@ impl<FEN: FoundryEvmNetwork> FuzzedExecutor<FEN> {
260259
fn single_fuzz(
261260
&self,
262261
executor: &Executor<FEN>,
263-
address: Address,
264-
calldata: Bytes,
262+
tx: BasicTxDetails,
265263
coverage_metrics: &mut WorkerCorpus,
266264
) -> Result<FuzzOutcome<FEN>, TestCaseError> {
267-
let mut call = executor
268-
.call_raw(self.sender, address, calldata.clone(), U256::ZERO)
269-
.map_err(|e| TestCaseError::fail(e.to_string()))?;
270-
let cmp_values = call.evm_cmp_values.take().unwrap_or_default();
271-
let new_coverage = coverage_metrics.merge_edge_coverage(&mut call);
272-
coverage_metrics.process_inputs(
273-
&[BasicTxDetails {
274-
warp: None,
275-
roll: None,
276-
sender: self.sender,
277-
call_details: CallDetails {
278-
target: address,
279-
calldata: calldata.clone(),
280-
value: None,
281-
},
282-
}],
283-
&[cmp_values],
284-
new_coverage,
285-
None,
286-
);
287-
288-
// Handle `vm.assume`.
289-
if call.result.as_ref() == MAGIC_ASSUME {
290-
return Err(TestCaseError::reject(FuzzError::AssumeReject));
291-
}
265+
let target = tx.call_details.target;
266+
let calldata = tx.call_details.calldata.clone();
267+
let tx_for_corpus = tx.clone();
268+
let mut executor = executor.clone();
269+
let outcome =
270+
replay_tx(&mut executor, &tx, /* commit_state */ false, |executor, mut call| {
271+
let cmp_values = call.evm_cmp_values.take().unwrap_or_default();
272+
let new_coverage = coverage_metrics.merge_edge_coverage(&mut call);
273+
coverage_metrics.process_inputs(
274+
std::slice::from_ref(&tx_for_corpus),
275+
&[cmp_values],
276+
new_coverage,
277+
None,
278+
);
292279

293-
let (breakpoints, deprecated_cheatcodes) =
294-
call.cheatcodes.as_ref().map_or_else(Default::default, |cheats| {
295-
(cheats.breakpoints.clone(), cheats.deprecated.clone())
296-
});
297-
298-
// Consider call success if test should not fail on reverts and reverter is not the
299-
// cheatcode or test address.
300-
let success = if !self.config.fail_on_revert
301-
&& call
302-
.reverter
303-
.is_some_and(|reverter| reverter != address && reverter != CHEATCODE_ADDRESS)
304-
{
305-
true
306-
} else {
307-
executor.is_raw_call_mut_success(address, &mut call, false)
308-
};
280+
// Handle `vm.assume`.
281+
if call.result.as_ref() == MAGIC_ASSUME {
282+
return Ok(ControlFlow::Break(Err(TestCaseError::reject(
283+
FuzzError::AssumeReject,
284+
))));
285+
}
309286

310-
if success {
311-
Ok(FuzzOutcome::Case(CaseOutcome {
312-
case: FuzzCase { gas: call.gas_used, stipend: call.stipend },
313-
traces: call.traces,
314-
coverage: call.line_coverage,
315-
breakpoints,
316-
logs: call.logs,
317-
deprecated_cheatcodes,
318-
}))
319-
} else {
320-
Ok(FuzzOutcome::CounterExample(CounterExampleOutcome {
321-
exit_reason: call.exit_reason,
322-
counterexample: (calldata, call),
323-
breakpoints,
324-
}))
325-
}
287+
let (breakpoints, deprecated_cheatcodes) =
288+
call.cheatcodes.as_ref().map_or_else(Default::default, |cheats| {
289+
(cheats.breakpoints.clone(), cheats.deprecated.clone())
290+
});
291+
292+
// Consider call success if test should not fail on reverts and reverter is not the
293+
// cheatcode or test address.
294+
let success = if !self.config.fail_on_revert
295+
&& call
296+
.reverter
297+
.is_some_and(|reverter| reverter != target && reverter != CHEATCODE_ADDRESS)
298+
{
299+
true
300+
} else {
301+
executor
302+
.is_raw_call_mut_success(target, &mut call, /* should_fail */ false)
303+
};
304+
305+
let outcome = if success {
306+
FuzzOutcome::Case(CaseOutcome {
307+
case: FuzzCase { gas: call.gas_used, stipend: call.stipend },
308+
traces: call.traces,
309+
coverage: call.line_coverage,
310+
breakpoints,
311+
logs: call.logs,
312+
deprecated_cheatcodes,
313+
})
314+
} else {
315+
FuzzOutcome::CounterExample(CounterExampleOutcome {
316+
exit_reason: call.exit_reason,
317+
counterexample: (calldata.clone(), call),
318+
breakpoints,
319+
})
320+
};
321+
Ok(ControlFlow::Break(Ok(outcome)))
322+
})
323+
.map_err(|e| TestCaseError::fail(e.to_string()))?;
324+
325+
outcome.expect("depth-1 stateless replay always stops after the first tx")
326326
}
327327

328328
/// Aggregates the results from all workers
@@ -441,22 +441,20 @@ impl<FEN: FoundryEvmNetwork> FuzzedExecutor<FEN> {
441441
) -> Result<WorkerState<FEN>> {
442442
// Prepare
443443
let fuzz_state = shared_state.state.fork();
444-
let dictionary_weight = self.config.dictionary.dictionary_weight.min(100);
445-
let strategy = proptest::prop_oneof![
446-
100 - dictionary_weight => fuzz_calldata(func.clone(), fuzz_fixtures),
447-
dictionary_weight => fuzz_calldata_from_state(func.clone(), &fuzz_state),
448-
]
449-
.prop_map(move |calldata| BasicTxDetails {
450-
warp: None,
451-
roll: None,
452-
sender: Default::default(),
453-
call_details: CallDetails { target: Default::default(), calldata, value: None },
454-
});
444+
let strategy = TxGenerator::stateless(
445+
fuzz_state.clone(),
446+
self.sender,
447+
address,
448+
func.clone(),
449+
fuzz_fixtures.clone(),
450+
self.config.dictionary.dictionary_weight,
451+
)
452+
.strategy();
455453

456454
let mut corpus = WorkerCorpus::new(
457455
worker_id,
458456
self.config.corpus.clone(),
459-
strategy.boxed(),
457+
strategy,
460458
// Master worker replays the persisted corpus using the executor
461459
(worker_id == 0).then_some(&self.executor_f),
462460
Some(func),
@@ -487,7 +485,9 @@ impl<FEN: FoundryEvmNetwork> FuzzedExecutor<FEN> {
487485

488486
if let Some(target_run) = self.config.run {
489487
for _ in 1..target_run {
490-
if let Err(err) = corpus.new_input(&mut runner, &fuzz_state, func) {
488+
if let Err(err) =
489+
corpus.new_input(&mut runner, &fuzz_state, func, self.sender, address)
490+
{
491491
worker.failure = Some(TestCaseError::fail(format!(
492492
"failed to generate fuzzed input in worker {}: {err}",
493493
worker.id
@@ -526,7 +526,16 @@ impl<FEN: FoundryEvmNetwork> FuzzedExecutor<FEN> {
526526
}
527527

528528
(
529-
failure.calldata.clone(),
529+
BasicTxDetails {
530+
warp: None,
531+
roll: None,
532+
sender: self.sender,
533+
call_details: CallDetails {
534+
target: address,
535+
calldata: failure.calldata.clone(),
536+
value: None,
537+
},
538+
},
530539
Some(FuzzRunMetadata::new(
531540
seed,
532541
failure.fuzz.run,
@@ -556,17 +565,18 @@ impl<FEN: FoundryEvmNetwork> FuzzedExecutor<FEN> {
556565
cheats.set_seed(Self::fuzz_run_seed(seed, worker_id, fuzz_run));
557566
}
558567

559-
let input = match corpus.new_input(&mut runner, &fuzz_state, func) {
560-
Ok(input) => input,
561-
Err(err) => {
562-
worker.failure = Some(TestCaseError::fail(format!(
563-
"failed to generate fuzzed input in worker {}: {err}",
564-
worker.id
565-
)));
566-
shared_state.try_claim_failure(worker_id);
567-
break 'stop;
568-
}
569-
};
568+
let input =
569+
match corpus.new_input(&mut runner, &fuzz_state, func, self.sender, address) {
570+
Ok(input) => input,
571+
Err(err) => {
572+
worker.failure = Some(TestCaseError::fail(format!(
573+
"failed to generate fuzzed input in worker {}: {err}",
574+
worker.id
575+
)));
576+
shared_state.try_claim_failure(worker_id);
577+
break 'stop;
578+
}
579+
};
570580

571581
(
572582
input,
@@ -594,7 +604,7 @@ impl<FEN: FoundryEvmNetwork> FuzzedExecutor<FEN> {
594604
};
595605

596606
worker.last_run_timestamp = SystemTime::now().duration_since(UNIX_EPOCH)?.as_millis();
597-
match self.single_fuzz(&executor, address, input, &mut corpus) {
607+
match self.single_fuzz(&executor, input, &mut corpus) {
598608
Ok(fuzz_outcome) => match fuzz_outcome {
599609
FuzzOutcome::Case(case) => {
600610
let total_runs = inc_runs();

0 commit comments

Comments
 (0)