1use crate::executors::{
2 DURATION_BETWEEN_METRICS_REPORT, EarlyExit, Executor, FuzzTestTimer, RawCallResult,
3 corpus::{GlobalCorpusMetrics, WorkerCorpus},
4};
5use alloy_dyn_abi::JsonAbiExt;
6use alloy_json_abi::Function;
7use alloy_primitives::{
8 Address, Bytes, Log, U256, keccak256,
9 map::{AddressHashMap, HashMap},
10};
11use eyre::Result;
12use foundry_common::sh_println;
13use foundry_config::FuzzConfig;
14use foundry_evm_core::{
15 Breakpoints,
16 constants::{CHEATCODE_ADDRESS, MAGIC_ASSUME},
17 decode::{RevertDecoder, SkipReason},
18 evm::FoundryEvmNetwork,
19};
20use foundry_evm_coverage::HitMaps;
21use foundry_evm_fuzz::{
22 BaseCounterExample, BasicTxDetails, CallDetails, CounterExample, FuzzCase, FuzzError,
23 FuzzFixtures, FuzzRunMetadata, FuzzTestResult,
24 strategies::{EvmFuzzState, fuzz_calldata, fuzz_calldata_from_state},
25};
26use foundry_evm_traces::SparsedTraceArena;
27use indicatif::ProgressBar;
28use proptest::{
29 strategy::Strategy,
30 test_runner::{RngAlgorithm, TestCaseError, TestRng, TestRunner},
31};
32use rayon::iter::{IntoParallelIterator, ParallelIterator};
33use serde_json::json;
34use std::{
35 sync::{
36 Arc, OnceLock,
37 atomic::{AtomicU32, Ordering},
38 },
39 time::{Instant, SystemTime, UNIX_EPOCH},
40};
41
42mod types;
43pub use types::{CaseOutcome, CounterExampleOutcome, FuzzOutcome};
44
45const SYNC_INTERVAL: u32 = 1000;
47
48const MIN_RUNS_PER_WORKER: u32 = 64;
51
52struct WorkerState<FEN: FoundryEvmNetwork> {
53 id: usize,
55 first_case: Option<(u32, FuzzCase)>,
57 gas_by_case: Vec<(u64, u64)>,
59 counterexample: (Bytes, RawCallResult<FEN>),
61 traces: Vec<SparsedTraceArena>,
65 debug_bytecodes: AddressHashMap<Bytes>,
67 breakpoints: Option<Breakpoints>,
69 coverage: Option<HitMaps>,
71 logs: Vec<Log>,
73 deprecated_cheatcodes: HashMap<&'static str, Option<&'static str>>,
75 runs: u32,
77 failure: Option<TestCaseError>,
79 failure_run: Option<FuzzRunMetadata>,
81 last_run_timestamp: u128,
85 failed_corpus_replays: usize,
87}
88
89impl<FEN: FoundryEvmNetwork> WorkerState<FEN> {
90 fn new(worker_id: usize) -> Self {
91 Self {
92 id: worker_id,
93 first_case: None,
94 gas_by_case: Vec::new(),
95 counterexample: (Bytes::new(), RawCallResult::default()),
96 traces: Vec::new(),
97 debug_bytecodes: HashMap::default(),
98 breakpoints: None,
99 coverage: None,
100 logs: Vec::new(),
101 deprecated_cheatcodes: HashMap::default(),
102 runs: 0,
103 failure: None,
104 failure_run: None,
105 last_run_timestamp: 0,
106 failed_corpus_replays: 0,
107 }
108 }
109}
110
111struct SharedFuzzState {
113 state: EvmFuzzState,
114 total_runs: Arc<AtomicU32>,
116 failed_worker_id: OnceLock<usize>,
122 total_rejects: Arc<AtomicU32>,
124 timer: FuzzTestTimer,
126 global_corpus_metrics: GlobalCorpusMetrics,
128
129 global_early_exit: EarlyExit,
131 local_early_exit: EarlyExit,
133}
134
135impl SharedFuzzState {
136 fn new(state: EvmFuzzState, timeout: Option<u32>, early_exit: EarlyExit) -> Self {
137 Self {
138 state,
139 total_runs: Arc::new(AtomicU32::new(0)),
140 failed_worker_id: OnceLock::new(),
141 total_rejects: Arc::new(AtomicU32::new(0)),
142 timer: FuzzTestTimer::new(timeout),
143 global_corpus_metrics: GlobalCorpusMetrics::default(),
144 global_early_exit: early_exit,
145 local_early_exit: EarlyExit::new(true),
146 }
147 }
148
149 fn increment_runs(&self) -> u32 {
151 self.total_runs.fetch_add(1, Ordering::Relaxed) + 1
152 }
153
154 fn increment_rejects(&self) -> u32 {
156 self.total_rejects.fetch_add(1, Ordering::Relaxed) + 1
157 }
158
159 fn should_continue(&self) -> bool {
161 !(self.global_early_exit.should_stop()
162 || self.local_early_exit.should_stop()
163 || self.timer.is_timed_out())
164 }
165
166 fn try_claim_failure(&self, worker_id: usize) -> bool {
169 let mut claimed = false;
170 let _ = self.failed_worker_id.get_or_init(|| {
171 claimed = true;
172 self.local_early_exit.record_failure();
173 worker_id
174 });
175 claimed
176 }
177}
178
179pub struct FuzzedExecutor<FEN: FoundryEvmNetwork> {
185 executor_f: Executor<FEN>,
187 runner: TestRunner,
189 sender: Address,
191 config: FuzzConfig,
193 persisted_failure: Option<BaseCounterExample>,
195 num_workers: usize,
197}
198
199impl<FEN: FoundryEvmNetwork> FuzzedExecutor<FEN> {
200 pub fn new(
202 executor: Executor<FEN>,
203 runner: TestRunner,
204 sender: Address,
205 config: FuzzConfig,
206 persisted_failure: Option<BaseCounterExample>,
207 ) -> Self {
208 let run_limit = if config.run.is_some() { 1 } else { config.runs };
209 let max_workers = if run_limit == 0 {
210 0
211 } else if config.run.is_some() {
212 1
213 } else {
214 Ord::max(1, run_limit / MIN_RUNS_PER_WORKER)
215 };
216 let num_workers = Ord::min(rayon::current_num_threads(), max_workers as usize);
217 Self { executor_f: executor, runner, sender, config, persisted_failure, num_workers }
218 }
219
220 #[allow(clippy::too_many_arguments)]
226 pub fn fuzz(
227 &mut self,
228 func: &Function,
229 fuzz_fixtures: &FuzzFixtures,
230 state: EvmFuzzState,
231 address: Address,
232 rd: &RevertDecoder,
233 progress: Option<&ProgressBar>,
234 early_exit: &EarlyExit,
235 tokio_handle: &tokio::runtime::Handle,
236 ) -> Result<FuzzTestResult> {
237 let shared_state = SharedFuzzState::new(state, self.config.timeout, early_exit.clone());
238
239 let worker_ids = self.worker_ids();
240 debug!(n = worker_ids.len(), "spawning workers");
241 let workers = worker_ids
242 .into_par_iter()
243 .map(|worker_id| {
244 let _guard = tokio_handle.enter();
245 let _guard = info_span!("fuzz_worker", id = worker_id).entered();
246 let timer = Instant::now();
247 let r = self.run_worker(
248 worker_id,
249 func,
250 fuzz_fixtures,
251 address,
252 rd,
253 &shared_state,
254 progress,
255 );
256 debug!("finished in {:?}", timer.elapsed());
257 r
258 })
259 .collect::<Result<Vec<_>>>()?;
260
261 Ok(self.aggregate_results(workers, func, &shared_state))
262 }
263
264 fn single_fuzz(
267 &self,
268 executor: &Executor<FEN>,
269 address: Address,
270 calldata: Bytes,
271 coverage_metrics: &mut WorkerCorpus,
272 ) -> Result<FuzzOutcome<FEN>, TestCaseError> {
273 let mut call = executor
274 .call_raw(self.sender, address, calldata.clone(), U256::ZERO)
275 .map_err(|e| TestCaseError::fail(e.to_string()))?;
276 let cmp_values = call.evm_cmp_values.take().unwrap_or_default();
277 let new_coverage = coverage_metrics.merge_edge_coverage(&mut call);
278 coverage_metrics.process_inputs(
279 &[BasicTxDetails {
280 warp: None,
281 roll: None,
282 sender: self.sender,
283 call_details: CallDetails {
284 target: address,
285 calldata: calldata.clone(),
286 value: None,
287 },
288 }],
289 &[cmp_values],
290 new_coverage,
291 None,
292 );
293
294 if call.result.as_ref() == MAGIC_ASSUME {
296 return Err(TestCaseError::reject(FuzzError::AssumeReject));
297 }
298
299 let (breakpoints, deprecated_cheatcodes) =
300 call.cheatcodes.as_ref().map_or_else(Default::default, |cheats| {
301 (cheats.breakpoints.clone(), cheats.deprecated.clone())
302 });
303
304 let success = if !self.config.fail_on_revert
307 && call
308 .reverter
309 .is_some_and(|reverter| reverter != address && reverter != CHEATCODE_ADDRESS)
310 {
311 true
312 } else {
313 executor.is_raw_call_mut_success(address, &mut call, false)
314 };
315
316 if success {
317 Ok(FuzzOutcome::Case(CaseOutcome {
318 case: FuzzCase { gas: call.gas_used, stipend: call.stipend },
319 traces: call.traces,
320 debug_bytecodes: call.debug_bytecodes,
321 coverage: call.line_coverage,
322 breakpoints,
323 logs: call.logs,
324 deprecated_cheatcodes,
325 }))
326 } else {
327 Ok(FuzzOutcome::CounterExample(CounterExampleOutcome {
328 exit_reason: call.exit_reason,
329 counterexample: (calldata, call),
330 breakpoints,
331 }))
332 }
333 }
334
335 fn aggregate_results(
337 &self,
338 mut workers: Vec<WorkerState<FEN>>,
339 func: &Function,
340 shared_state: &SharedFuzzState,
341 ) -> FuzzTestResult {
342 let mut result = FuzzTestResult::default();
343 if workers.is_empty() {
344 result.success = true;
345 return result;
346 }
347
348 let mut first_case_candidate = None;
350 let mut last_run_worker = None;
351 for (i, worker) in workers.iter().enumerate() {
352 if let Some((run, ref case)) = worker.first_case
353 && first_case_candidate.as_ref().is_none_or(|&(r, _)| run < r)
354 {
355 first_case_candidate = Some((run, case.clone()));
356 }
357
358 if last_run_worker.is_none_or(|(t, _)| worker.last_run_timestamp > t) {
359 last_run_worker = Some((worker.last_run_timestamp, i));
360 }
361
362 if worker.id == 0 {
364 result.failed_corpus_replays = worker.failed_corpus_replays;
365 }
366 }
367 result.first_case = first_case_candidate.map(|(_, case)| case).unwrap_or_default();
368 let (_, last_run_worker_idx) = last_run_worker.expect("at least one worker");
369
370 if let Some(&failed_worker_id) = shared_state.failed_worker_id.get() {
371 result.success = false;
372
373 let failed_worker_idx = workers.iter().position(|w| w.id == failed_worker_id).unwrap();
374 let failed_worker = &mut workers[failed_worker_idx];
375
376 let (calldata, call) = std::mem::take(&mut failed_worker.counterexample);
377 result.labels = call.labels;
378 result.traces = call.traces.clone();
379 result.debug_bytecodes = call.debug_bytecodes.clone();
380 result.breakpoints = call.cheatcodes.map(|c| c.breakpoints);
381
382 match &failed_worker.failure {
383 Some(TestCaseError::Fail(reason)) => {
384 let reason = reason.to_string();
385 result.reason = (!reason.is_empty()).then_some(reason);
386 let args = if let Some(data) = calldata.get(4..) {
387 func.abi_decode_input(data).unwrap_or_default()
388 } else {
389 vec![]
390 };
391 let fuzz = failed_worker.failure_run.unwrap_or_default();
392 result.counterexample = Some(CounterExample::Single(
393 BaseCounterExample::from_fuzz_call(calldata, args, call.traces)
394 .with_fuzz_metadata(FuzzRunMetadata::new(
395 fuzz.seed.or(self.config.seed),
396 fuzz.run,
397 fuzz.worker,
398 )),
399 ));
400 }
401 Some(TestCaseError::Reject(reason)) => {
402 let reason = reason.to_string();
403 result.reason = (!reason.is_empty()).then_some(reason);
404 }
405 None => {}
406 }
407 } else {
408 let last_run_worker = &workers[last_run_worker_idx];
409 result.success = true;
410 result.traces = last_run_worker.traces.last().cloned();
411 result.debug_bytecodes.clone_from(&last_run_worker.debug_bytecodes);
412 result.breakpoints = last_run_worker.breakpoints.clone();
413 }
414
415 if !self.config.show_logs {
416 result.logs = workers[last_run_worker_idx].logs.clone();
417 }
418
419 for mut worker in workers {
420 result.gas_by_case.append(&mut worker.gas_by_case);
421 if self.config.show_logs {
422 result.logs.append(&mut worker.logs);
423 }
424 result.gas_report_traces.extend(worker.traces.into_iter().map(|t| t.arena));
425 HitMaps::merge_opt(&mut result.line_coverage, worker.coverage);
426 result.deprecated_cheatcodes.extend(worker.deprecated_cheatcodes);
427 }
428
429 if let Some(reason) = &result.reason
430 && let Some(reason) = SkipReason::decode_self(reason)
431 {
432 result.skipped = true;
433 result.reason = reason.0;
434 }
435
436 result
437 }
438
439 #[allow(clippy::too_many_arguments)]
441 fn run_worker(
442 &self,
443 worker_id: usize,
444 func: &Function,
445 fuzz_fixtures: &FuzzFixtures,
446 address: Address,
447 rd: &RevertDecoder,
448 shared_state: &SharedFuzzState,
449 progress: Option<&ProgressBar>,
450 ) -> Result<WorkerState<FEN>> {
451 let fuzz_state = shared_state.state.fork();
453 let dictionary_weight = self.config.dictionary.dictionary_weight.min(100);
454 let strategy = proptest::prop_oneof![
455 100 - dictionary_weight => fuzz_calldata(func.clone(), fuzz_fixtures),
456 dictionary_weight => fuzz_calldata_from_state(func.clone(), &fuzz_state),
457 ]
458 .prop_map(move |calldata| BasicTxDetails {
459 warp: None,
460 roll: None,
461 sender: Default::default(),
462 call_details: CallDetails { target: Default::default(), calldata, value: None },
463 });
464
465 let mut corpus = WorkerCorpus::new(
466 worker_id,
467 self.config.corpus.clone(),
468 strategy.boxed(),
469 (worker_id == 0).then_some(&self.executor_f),
471 Some(func),
472 None, None, )?;
475 let mut executor = self.executor_f.clone();
476
477 let mut worker = WorkerState::new(worker_id);
478 let max_traces_to_collect =
480 std::cmp::max(1, self.config.gas_report_samples / self.num_workers as u32);
481
482 let worker_runs = self.runs_per_worker(worker_id);
483 debug!(worker_runs);
484
485 let mut runner_config = self.runner.config().clone();
486 runner_config.cases = worker_runs;
487
488 let mut runner = if let Some(seed) = self.config.seed {
489 let worker_seed = Self::fuzz_worker_seed(seed, worker_id);
490 trace!(target: "forge::test", ?worker_seed, "deterministic seed for worker {worker_id}");
491 let rng = TestRng::from_seed(RngAlgorithm::ChaCha, &worker_seed.to_be_bytes::<32>());
492 TestRunner::new_with_rng(runner_config, rng)
493 } else {
494 TestRunner::new(runner_config)
495 };
496
497 if let Some(target_run) = self.config.run {
498 for _ in 1..target_run {
499 if let Err(err) = corpus.new_input(&mut runner, &fuzz_state, func) {
500 worker.failure = Some(TestCaseError::fail(format!(
501 "failed to generate fuzzed input in worker {}: {err}",
502 worker.id
503 )));
504 shared_state.try_claim_failure(worker_id);
505 return Ok(worker);
506 }
507 }
508 }
509
510 let mut persisted_failure =
511 self.persisted_failure.as_ref().filter(|_| worker_id == 0 && self.config.run.is_none());
512
513 let sync_offset = (worker_id as u32).saturating_mul(100);
516 let sync_threshold = SYNC_INTERVAL + sync_offset;
517 let mut runs_since_sync = sync_threshold; let mut last_metrics_report = Instant::now();
519 'stop: while shared_state.should_continue() && worker.runs < worker_runs {
523 let (input, fuzz_run) = if worker_id == 0
525 && let Some(failure) = persisted_failure.take()
526 && failure.calldata.get(..4).is_some_and(|selector| func.selector() == selector)
527 {
528 let seed = failure.fuzz.seed.or(self.config.seed);
529 if let Some(cheats) = executor.inspector_mut().cheatcodes.as_mut()
530 && let Some(seed) = seed
531 {
532 let run = failure.fuzz.run.unwrap_or(1);
533 let worker = failure.fuzz.worker.unwrap_or(worker_id as u32) as usize;
534 cheats.set_seed(Self::fuzz_run_seed(seed, worker, run));
535 }
536
537 (
538 failure.calldata.clone(),
539 Some(FuzzRunMetadata::new(
540 seed,
541 failure.fuzz.run,
542 Some(failure.fuzz.worker.unwrap_or(worker_id as u32)),
543 )),
544 )
545 } else {
546 runs_since_sync += 1;
547 if runs_since_sync >= sync_threshold {
548 let timer = Instant::now();
549 corpus.sync(
550 self.num_workers,
551 &executor,
552 Some(func),
553 None,
554 None,
555 &shared_state.global_corpus_metrics,
556 )?;
557 trace!("finished corpus sync in {:?}", timer.elapsed());
558 runs_since_sync = 0;
559 }
560
561 let fuzz_run = self.config.run.unwrap_or(worker.runs + 1);
562 if let Some(cheats) = executor.inspector_mut().cheatcodes.as_mut()
563 && let Some(seed) = self.config.seed
564 {
565 cheats.set_seed(Self::fuzz_run_seed(seed, worker_id, fuzz_run));
566 }
567
568 let input = match corpus.new_input(&mut runner, &fuzz_state, func) {
569 Ok(input) => input,
570 Err(err) => {
571 worker.failure = Some(TestCaseError::fail(format!(
572 "failed to generate fuzzed input in worker {}: {err}",
573 worker.id
574 )));
575 shared_state.try_claim_failure(worker_id);
576 break 'stop;
577 }
578 };
579
580 (
581 input,
582 Some(FuzzRunMetadata::new(
583 self.config.seed,
584 Some(fuzz_run),
585 Some(worker_id as u32),
586 )),
587 )
588 };
589
590 let mut inc_runs = || {
591 let total_runs = shared_state.increment_runs();
592 debug_assert!(
593 shared_state.timer.is_enabled()
594 || total_runs
595 <= if self.config.run.is_some() { 1 } else { self.config.runs },
596 "worker runs were not distributed correctly"
597 );
598 worker.runs += 1;
599 if let Some(progress) = progress {
600 progress.inc(1);
601 }
602 total_runs
603 };
604
605 worker.last_run_timestamp = SystemTime::now().duration_since(UNIX_EPOCH)?.as_millis();
606 match self.single_fuzz(&executor, address, input, &mut corpus) {
607 Ok(fuzz_outcome) => match fuzz_outcome {
608 FuzzOutcome::Case(case) => {
609 let total_runs = inc_runs();
610
611 if worker_id == 0 && self.config.corpus.collect_edge_coverage() {
612 if let Some(progress) = progress {
613 corpus.sync_metrics(&shared_state.global_corpus_metrics);
614 progress
615 .set_message(format!("{}", shared_state.global_corpus_metrics));
616 } else if last_metrics_report.elapsed()
617 > DURATION_BETWEEN_METRICS_REPORT
618 {
619 corpus.sync_metrics(&shared_state.global_corpus_metrics);
620 let metrics = json!({
622 "timestamp": SystemTime::now()
623 .duration_since(UNIX_EPOCH)?
624 .as_secs(),
625 "test": func.name,
626 "metrics": shared_state.global_corpus_metrics.load(),
627 });
628 let _ = sh_println!("{metrics}");
629 last_metrics_report = Instant::now();
630 }
631 }
632
633 worker.gas_by_case.push((case.case.gas, case.case.stipend));
634
635 if worker.first_case.is_none() {
636 worker.first_case = Some((total_runs, case.case));
637 }
638
639 if let Some(call_traces) = case.traces {
640 if worker.traces.len() == max_traces_to_collect as usize {
641 worker.traces.pop();
642 }
643 worker.traces.push(call_traces);
644 worker.debug_bytecodes = case.debug_bytecodes;
645 worker.breakpoints = Some(case.breakpoints);
646 }
647
648 if self.config.show_logs {
652 worker.logs.extend(case.logs);
653 } else {
654 worker.logs = case.logs;
655 }
656
657 HitMaps::merge_opt(&mut worker.coverage, case.coverage);
658 worker.deprecated_cheatcodes = case.deprecated_cheatcodes;
659 }
660 FuzzOutcome::CounterExample(CounterExampleOutcome {
661 exit_reason: status,
662 counterexample: outcome,
663 ..
664 }) => {
665 inc_runs();
666 worker.failure_run = fuzz_run;
667
668 let reason = if outcome.1.reverter == Some(CHEATCODE_ADDRESS) {
671 SkipReason::decode(&outcome.1.result)
672 .map(|reason| reason.to_string())
673 .or_else(|| rd.maybe_decode(&outcome.1.result, status))
674 } else {
675 rd.maybe_decode(&outcome.1.result, status)
676 };
677 worker.logs.extend(outcome.1.logs.clone());
678 worker.counterexample = outcome;
679 worker.failure = Some(TestCaseError::fail(reason.unwrap_or_default()));
680 shared_state.try_claim_failure(worker_id);
681 break 'stop;
682 }
683 },
684 Err(err) => match err {
685 TestCaseError::Fail(_) => {
686 worker.failure = Some(err);
687 shared_state.try_claim_failure(worker_id);
688 break 'stop;
689 }
690 TestCaseError::Reject(_) => {
691 let max = self.config.max_test_rejects;
692
693 let total = shared_state.increment_rejects();
694
695 if !self.config.corpus.collect_edge_coverage()
698 && let Some(progress) = progress
699 {
700 progress.set_message(format!("([{total}] rejected)"));
701 }
702
703 if max > 0 && total > max {
704 worker.failure =
705 Some(TestCaseError::reject(FuzzError::TooManyRejects(max)));
706 shared_state.try_claim_failure(worker_id);
707 break 'stop;
708 }
709 }
710 },
711 }
712 }
713
714 if worker_id == 0 {
715 worker.failed_corpus_replays = corpus.failed_replays;
716 }
717
718 trace!("worker {worker_id} fuzz stats");
720 fuzz_state.log_stats();
721
722 Ok(worker)
723 }
724
725 const fn runs_per_worker(&self, worker_id: usize) -> u32 {
727 let worker_id = worker_id as u32;
728 let total_runs = if self.config.run.is_some() { 1 } else { self.config.runs };
729 let n = self.num_workers as u32;
730 let runs = total_runs / n;
731 let remainder = total_runs % n;
732 if worker_id < remainder { runs + 1 } else { runs }
735 }
736
737 fn worker_ids(&self) -> Vec<usize> {
739 if self.config.run.is_some() {
740 vec![self.config.worker.unwrap_or(0) as usize]
741 } else {
742 (0..self.num_workers).collect()
743 }
744 }
745
746 fn fuzz_worker_seed(seed: U256, worker_id: usize) -> U256 {
748 if worker_id == 0 {
749 seed
750 } else {
751 let worker_id = worker_id as u32;
752 let seed_data = [&seed.to_be_bytes::<32>()[..], &worker_id.to_be_bytes()[..]].concat();
753 U256::from_be_bytes(keccak256(seed_data).0)
754 }
755 }
756
757 fn fuzz_run_seed(seed: U256, worker_id: usize, run: u32) -> U256 {
759 Self::fuzz_worker_seed(seed, worker_id).wrapping_add(U256::from(run.saturating_sub(1)))
760 }
761}