Skip to main content

foundry_evm/executors/fuzz/
mod.rs

1use crate::executors::{
2    DURATION_BETWEEN_METRICS_REPORT, EarlyExit, Executor, FuzzTestTimer, RawCallResult,
3    campaign::{CampaignCallKind, CampaignControl, CampaignEvent, FuzzCampaign, FuzzCampaignMode},
4    corpus::{
5        CorpusSyncCoordinator, GlobalCorpusMetrics, ReplayTarget, StatelessReplayTarget,
6        WorkerCorpus,
7    },
8    should_ignore_revert, worker_seed,
9};
10use alloy_dyn_abi::JsonAbiExt;
11use alloy_json_abi::Function;
12use alloy_primitives::{
13    Address, Bytes, Log, U256,
14    map::{AddressHashMap, HashMap},
15};
16use eyre::Result;
17use foundry_common::{sh_println, sh_warn};
18use foundry_config::FuzzConfig;
19use foundry_evm_core::{
20    Breakpoints,
21    constants::MAGIC_ASSUME,
22    decode::{RevertDecoder, SkipReason},
23    evm::FoundryEvmNetwork,
24};
25use foundry_evm_coverage::HitMaps;
26use foundry_evm_fuzz::{
27    BaseCounterExample, BasicTxDetails, CallDetails, CounterExample, FuzzCase, FuzzError,
28    FuzzFixtures, FuzzRunMetadata, FuzzTestResult,
29    strategies::{EvmFuzzState, TxGenerator},
30};
31use foundry_evm_traces::SparsedTraceArena;
32use indicatif::ProgressBar;
33use proptest::test_runner::{RngAlgorithm, TestCaseError, TestRng, TestRunner};
34use rayon::iter::{IntoParallelIterator, ParallelIterator};
35use serde_json::json;
36use std::{
37    path::PathBuf,
38    sync::{
39        Arc, OnceLock,
40        atomic::{AtomicBool, AtomicU32, Ordering},
41    },
42    time::{Instant, SystemTime, UNIX_EPOCH},
43};
44
45mod frontier;
46mod types;
47use frontier::FuzzBranchFrontierArtifact;
48pub(super) use frontier::{
49    FuzzBranchFrontier, FuzzFrontierRecorder, StatefulFuzzBranchFrontierArtifact, merge_frontiers,
50    write_frontier_artifact,
51};
52pub use types::{CaseOutcome, CounterExampleOutcome, FuzzOutcome};
53
54/// Corpus syncs across workers every `SYNC_INTERVAL` runs.
55const SYNC_INTERVAL: u32 = 1000;
56
57/// Minimum number of runs per worker.
58/// This is mainly to reduce the overall number of rayon jobs.
59const MIN_RUNS_PER_WORKER: u32 = 64;
60
61struct WorkerState<FEN: FoundryEvmNetwork> {
62    /// Worker identifier
63    id: usize,
64    /// First fuzz case this worker encountered (with global run number)
65    first_case: Option<(u32, FuzzCase)>,
66    /// Gas usage for all cases this worker ran
67    gas_by_case: Vec<(u64, u64)>,
68    /// Counterexample if this worker found one
69    counterexample: Option<(BasicTxDetails, RawCallResult<FEN>)>,
70    /// Traces collected by this worker
71    ///
72    /// Stores up to `max_traces_to_collect` which is `config.gas_report_samples / num_workers`
73    traces: Vec<SparsedTraceArena>,
74    /// Runtime bytecodes for the last collected trace.
75    debug_bytecodes: AddressHashMap<Bytes>,
76    /// Last breakpoints from this worker
77    breakpoints: Option<Breakpoints>,
78    /// The input of the last collected trace and the run that generated it.
79    last_case: Option<(BasicTxDetails, Option<FuzzRunMetadata>)>,
80    /// Coverage collected by this worker
81    coverage: Option<HitMaps>,
82    /// Logs from all cases this worker ran
83    logs: Vec<Log>,
84    /// Deprecated cheatcodes seen by this worker
85    deprecated_cheatcodes: HashMap<&'static str, Option<&'static str>>,
86    /// Number of runs this worker completed
87    runs: u32,
88    /// Failure reason if this worker failed
89    failure: Option<TestCaseError>,
90    /// Fuzz run metadata that produced the failure.
91    failure_run: Option<FuzzRunMetadata>,
92    /// Last run timestamp in milliseconds
93    ///
94    /// Used to identify which worker ran last and collect its traces and call breakpoints
95    last_run_timestamp: u128,
96    /// Failed corpus replays
97    failed_corpus_replays: usize,
98    /// Branch frontiers captured for symbolic follow-up.
99    frontiers: Vec<FuzzBranchFrontier>,
100}
101
102impl<FEN: FoundryEvmNetwork> WorkerState<FEN> {
103    fn new(worker_id: usize) -> Self {
104        Self {
105            id: worker_id,
106            first_case: None,
107            gas_by_case: Vec::new(),
108            counterexample: None,
109            traces: Vec::new(),
110            debug_bytecodes: HashMap::default(),
111            breakpoints: None,
112            last_case: None,
113            coverage: None,
114            logs: Vec::new(),
115            deprecated_cheatcodes: HashMap::default(),
116            runs: 0,
117            failure: None,
118            failure_run: None,
119            last_run_timestamp: 0,
120            failed_corpus_replays: 0,
121            frontiers: Vec::new(),
122        }
123    }
124}
125
126/// Shared state for coordinating parallel fuzz workers
127struct SharedFuzzState {
128    state: EvmFuzzState,
129    /// Total runs across workers
130    total_runs: Arc<AtomicU32>,
131    /// Found failure
132    ///
133    /// The worker that found the failure sets it's ID.
134    ///
135    /// This ID is then used to correctly extract the failure reason and counterexample.
136    failed_worker_id: OnceLock<usize>,
137    /// Total rejects across workers
138    total_rejects: Arc<AtomicU32>,
139    /// Fuzz timer
140    timer: FuzzTestTimer,
141    /// Whether a worker stopped because the timer expired.
142    timed_out: AtomicBool,
143    /// Global corpus metrics
144    global_corpus_metrics: GlobalCorpusMetrics,
145
146    /// Global test suite early exit.
147    global_early_exit: EarlyExit,
148    /// Local fuzz early exit.
149    local_early_exit: EarlyExit,
150}
151
152impl SharedFuzzState {
153    fn new(state: EvmFuzzState, timeout: Option<u32>, early_exit: EarlyExit) -> Self {
154        Self {
155            state,
156            total_runs: Arc::new(AtomicU32::new(0)),
157            failed_worker_id: OnceLock::new(),
158            total_rejects: Arc::new(AtomicU32::new(0)),
159            timer: FuzzTestTimer::new(timeout),
160            timed_out: AtomicBool::new(false),
161            global_corpus_metrics: GlobalCorpusMetrics::default(),
162            global_early_exit: early_exit,
163            local_early_exit: EarlyExit::new(true),
164        }
165    }
166
167    /// Increments the number of runs and returns the new value.
168    fn increment_runs(&self) -> u32 {
169        self.total_runs.fetch_add(1, Ordering::Relaxed) + 1
170    }
171
172    /// Increments and returns the new value of the number of rejected tests.
173    fn increment_rejects(&self) -> u32 {
174        self.total_rejects.fetch_add(1, Ordering::Relaxed) + 1
175    }
176
177    /// Returns `true` if the worker should continue running.
178    fn should_continue(&self) -> bool {
179        if self.global_early_exit.should_stop() || self.local_early_exit.should_stop() {
180            return false;
181        }
182        if self.timer.is_timed_out() {
183            self.timed_out.store(true, Ordering::Relaxed);
184            return false;
185        }
186        true
187    }
188
189    /// Returns `true` if fail-fast or Ctrl-C stopped the campaign before its timeout or
190    /// `planned_runs`.
191    fn interrupted(&self, planned_runs: u32) -> bool {
192        self.global_early_exit.should_stop()
193            && !self.timed_out.load(Ordering::Relaxed)
194            && self.total_runs.load(Ordering::Relaxed) < planned_runs
195    }
196
197    /// Returns true if the worker was able to claim the failure, false if failure was set by
198    /// another worker
199    fn try_claim_failure(&self, worker_id: usize) -> bool {
200        let mut claimed = false;
201        let _ = self.failed_worker_id.get_or_init(|| {
202            claimed = true;
203            self.local_early_exit.record_failure();
204            worker_id
205        });
206        claimed
207    }
208}
209
210/// Wrapper around an [`Executor`] which provides fuzzing support using [`proptest`].
211///
212/// After instantiation, calling `fuzz` will proceed to hammer the deployed smart contract with
213/// inputs, until it finds a counterexample. The provided [`TestRunner`] contains all the
214/// configuration which can be overridden via [environment variables](proptest::test_runner::Config)
215pub struct FuzzedExecutor<FEN: FoundryEvmNetwork> {
216    /// The EVM executor.
217    executor_f: Executor<FEN>,
218    /// The fuzzer
219    runner: TestRunner,
220    /// The account that calls tests.
221    sender: Address,
222    /// The fuzz configuration.
223    config: FuzzConfig,
224    /// The persisted counterexample to be replayed, if any.
225    persisted_failure: Option<BaseCounterExample>,
226    /// An existing corpus to replay before persisting into the configured corpus directory.
227    corpus_replay_dir: Option<PathBuf>,
228    /// The number of parallel workers.
229    num_workers: usize,
230}
231
232impl<FEN: FoundryEvmNetwork> FuzzedExecutor<FEN> {
233    /// Instantiates a fuzzed executor given a testrunner
234    pub fn new(
235        executor: Executor<FEN>,
236        runner: TestRunner,
237        sender: Address,
238        config: FuzzConfig,
239        persisted_failure: Option<BaseCounterExample>,
240        corpus_replay_dir: Option<PathBuf>,
241    ) -> Self {
242        let run_limit = if config.run.is_some() { 1 } else { config.runs };
243        let max_workers = if run_limit == 0 {
244            0
245        } else if config.run.is_some() {
246            1
247        } else {
248            Ord::max(1, run_limit / MIN_RUNS_PER_WORKER)
249        };
250        let num_workers = Ord::min(rayon::current_num_threads(), max_workers as usize);
251        Self {
252            executor_f: executor,
253            runner,
254            sender,
255            config,
256            persisted_failure,
257            corpus_replay_dir,
258            num_workers,
259        }
260    }
261
262    /// Fuzzes the provided function, assuming it is available at the contract at `address`
263    ///
264    /// Returns a list of all the consumed gas and calldata of every fuzz case.
265    #[allow(clippy::too_many_arguments)]
266    pub fn fuzz(
267        &mut self,
268        func: &Function,
269        fuzz_fixtures: &FuzzFixtures,
270        state: EvmFuzzState,
271        address: Address,
272        rd: &RevertDecoder,
273        progress: Option<&ProgressBar>,
274        early_exit: &EarlyExit,
275        tokio_handle: &tokio::runtime::Handle,
276    ) -> Result<FuzzTestResult> {
277        let shared_state = SharedFuzzState::new(state, self.config.timeout, early_exit.clone());
278        // Every run would append its steps to the shared step table, so the campaign runs without
279        // the debug recorder and the shown case runs again with it.
280        let recorder = self.executor_f.inspector_mut().debug_recorder.take();
281
282        let worker_ids = self.worker_ids();
283        debug!(n = worker_ids.len(), "spawning workers");
284        let final_sync = (worker_ids.len() > 1 && self.config.corpus.corpus_dir.is_some())
285            .then(|| Arc::new(CorpusSyncCoordinator::new(worker_ids.len())));
286        let workers = worker_ids
287            .into_par_iter()
288            .map(|worker_id| {
289                let _guard = tokio_handle.enter();
290                let _guard = info_span!("fuzz_worker", id = worker_id).entered();
291                let timer = Instant::now();
292                let r = self.run_worker(
293                    worker_id,
294                    func,
295                    fuzz_fixtures,
296                    address,
297                    rd,
298                    &shared_state,
299                    progress,
300                    final_sync.as_deref(),
301                );
302                if r.is_err()
303                    && let Some(final_sync) = &final_sync
304                {
305                    final_sync.abort();
306                }
307                debug!("finished in {:?}", timer.elapsed());
308                r
309            })
310            .collect::<Result<Vec<_>>>();
311        self.executor_f.inspector_mut().debug_recorder = recorder;
312
313        let (mut result, shown_case) = self.aggregate_results(workers?, func, &shared_state);
314        if self.executor_f.inspector().debug_recorder.is_some()
315            && let Some(case) = shown_case
316        {
317            self.record_case(&mut result, address, case)?;
318        }
319        Ok(result)
320    }
321
322    /// Runs `case` again and replaces the shown trace of `result` with the one it records, along
323    /// with its bytecodes and breakpoints.
324    fn record_case(
325        &mut self,
326        result: &mut FuzzTestResult,
327        address: Address,
328        (mut tx, run): (BasicTxDetails, Option<FuzzRunMetadata>),
329    ) -> Result<()> {
330        if let Some(cheats) = self.executor_f.inspector_mut().cheatcodes.as_mut()
331            && let Some(run) = run
332            && let Some(seed) = run.seed.or(self.config.seed)
333        {
334            let worker = run.worker.unwrap_or(0) as usize;
335            cheats.core.set_seed(Self::fuzz_run_seed(seed, worker, run.run.unwrap_or(1)));
336        }
337        tx.sender = self.sender;
338        tx.call_details.target = address;
339        tx.warp = None;
340        tx.roll = None;
341        self.resolve_stateless_tx_with_executor(&self.executor_f, &mut tx)?;
342        let mut call = self.executor_f.call_raw(
343            tx.sender,
344            tx.call_details.target,
345            tx.call_details.calldata,
346            tx.call_details.value.unwrap_or_default(),
347        )?;
348        // State outside the EVM, such as files or environment variables, can change the outcome.
349        let success = should_ignore_revert(
350            self.config.fail_on_revert,
351            address,
352            call.reverter,
353            self.executor_f.inspector().extra_cheatcode_addresses(),
354        ) || self.executor_f.is_raw_call_mut_success(address, &mut call);
355        if success != result.success {
356            let _ = sh_warn!(
357                "the debugged fuzz case {} when run again, unlike in the campaign, so the debugger \
358                 may show a different path",
359                if success { "passed" } else { "failed" }
360            );
361        }
362        result.traces = call.traces;
363        result.debug_bytecodes = call.debug_bytecodes;
364        result.breakpoints = call.cheatcodes.map(|cheats| cheats.breakpoints.clone());
365        Ok(())
366    }
367
368    /// Replays the persisted single-call counterexample exactly once.
369    ///
370    /// Unlike [`Self::fuzz`], this never falls through to generated inputs if the persisted
371    /// calldata now succeeds or is rejected by `vm.assume`.
372    pub fn replay_persisted_failure(
373        &mut self,
374        func: &Function,
375        address: Address,
376        rd: &RevertDecoder,
377    ) -> Result<FuzzTestResult> {
378        let Some(failure) = self.persisted_failure.as_ref() else {
379            return Ok(FuzzTestResult {
380                skipped: true,
381                reason: Some("no persisted fuzz failure to replay".to_string()),
382                ..Default::default()
383            });
384        };
385
386        let seed = failure.fuzz.seed.or(self.config.seed);
387        if let Some(cheats) = self.executor_f.inspector_mut().cheatcodes.as_mut()
388            && let Some(seed) = seed
389        {
390            let run = failure.fuzz.run.unwrap_or(1);
391            let worker = failure.fuzz.worker.unwrap_or(0) as usize;
392            cheats.core.set_seed(Self::fuzz_run_seed(seed, worker, run));
393        }
394
395        let mut tx = BasicTxDetails {
396            warp: None,
397            roll: None,
398            sender: self.sender,
399            call_details: CallDetails {
400                target: address,
401                calldata: failure.calldata.clone(),
402                value: failure.value,
403            },
404        };
405        self.resolve_stateless_tx_with_executor(&self.executor_f, &mut tx)?;
406        let mut call = self.executor_f.call_raw(
407            tx.sender,
408            tx.call_details.target,
409            tx.call_details.calldata.clone(),
410            tx.call_details.value.unwrap_or_default(),
411        )?;
412        if call.result.as_ref() == MAGIC_ASSUME {
413            return Ok(FuzzTestResult {
414                skipped: true,
415                reason: Some("persisted fuzz failure rejected by `vm.assume`".to_string()),
416                ..Default::default()
417            });
418        }
419        if let Some(reason) = call.skip_reason() {
420            return Ok(FuzzTestResult { skipped: true, reason: reason.0, ..Default::default() });
421        }
422
423        let (breakpoints, deprecated_cheatcodes) =
424            call.cheatcodes.as_ref().map_or_else(Default::default, |cheats| {
425                (cheats.breakpoints.clone(), cheats.deprecated.clone())
426            });
427        let success = should_ignore_revert(
428            self.config.fail_on_revert,
429            address,
430            call.reverter,
431            self.executor_f.inspector().extra_cheatcode_addresses(),
432        ) || self.executor_f.is_raw_call_mut_success(address, &mut call);
433
434        let mut result = FuzzTestResult {
435            success,
436            labels: call.labels.clone(),
437            traces: call.traces.clone(),
438            debug_bytecodes: call.debug_bytecodes.clone(),
439            breakpoints: Some(breakpoints),
440            deprecated_cheatcodes,
441            fork_block_number: call.fork_block_number,
442            ..Default::default()
443        };
444
445        if success {
446            result.first_case = FuzzCase { gas: call.gas_used, stipend: call.stipend };
447            result.gas_by_case.push((call.gas_used, call.stipend));
448            result.line_coverage = call.line_coverage;
449            result.logs = call.logs;
450            result.gas_report_traces.extend(call.traces.into_iter().map(|trace| trace.arena));
451        } else {
452            let reason = call
453                .skip_reason()
454                .map(|reason| reason.to_string())
455                .or_else(|| rd.maybe_decode(&call.result, call.exit_reason));
456            result.reason = reason;
457            let args = tx
458                .call_details
459                .calldata
460                .get(4..)
461                .map_or_else(Vec::new, |data| func.abi_decode_input(data).unwrap_or_default());
462            result.counterexample = Some(CounterExample::Single(
463                BaseCounterExample::from_fuzz_tx(&tx, args, call.traces).with_fuzz_metadata(
464                    FuzzRunMetadata::new(
465                        seed,
466                        failure.fuzz.run,
467                        Some(failure.fuzz.worker.unwrap_or(0)),
468                    ),
469                ),
470            ));
471            result.logs = call.logs;
472        }
473
474        Ok(result)
475    }
476
477    /// Granular and single-step function that runs only one fuzz and returns either a `CaseOutcome`
478    /// or a `CounterExampleOutcome`
479    fn single_fuzz(
480        &self,
481        executor: &mut Executor<FEN>,
482        address: Address,
483        mut tx: BasicTxDetails,
484        coverage_metrics: &mut WorkerCorpus,
485        frontier_recorder: &mut FuzzFrontierRecorder,
486        fuzz_run: Option<&FuzzRunMetadata>,
487    ) -> Result<FuzzOutcome<FEN>, TestCaseError> {
488        tx.sender = self.sender;
489        tx.call_details.target = address;
490        tx.warp = None;
491        tx.roll = None;
492        self.resolve_stateless_tx_with_executor(executor, &mut tx)
493            .map_err(|e| TestCaseError::fail(e.to_string()))?;
494        let campaign = FuzzCampaign::new(FuzzCampaignMode::Stateless);
495        let mut state = (executor, tx);
496        let mut cmp_values = Vec::new();
497        let mut checked = None;
498        campaign
499            .run_sequence(
500                &mut state,
501                1,
502                |state| (state.0, &mut state.1),
503                |_| 0,
504                |_| false,
505                |_, event| {
506                    match event {
507                        CampaignEvent::Feedback(call) => {
508                            cmp_values = call.evm_cmp_values.take().unwrap_or_default();
509                        }
510                        CampaignEvent::Check { result, kind, .. } => {
511                            checked = Some((
512                                result.take().expect("campaign check result is available"),
513                                kind,
514                            ));
515                            return Ok(CampaignControl::Stop);
516                        }
517                        CampaignEvent::Advance | CampaignEvent::Next { .. } => {
518                            unreachable!("depth-one campaigns cannot advance")
519                        }
520                        CampaignEvent::PostCheck => {}
521                    }
522                    Ok(CampaignControl::Continue)
523                },
524            )
525            .map_err(|e| TestCaseError::fail(e.to_string()))?;
526        let (mut call, kind) = checked.expect("depth-one campaign emits a check event");
527        let tx = state.1.clone();
528
529        // Handle `vm.assume` before recording coverage or persisting the input.
530        if kind == CampaignCallKind::AssumptionRejected {
531            // Account for the attempted corpus mutation without retaining or crediting the input.
532            coverage_metrics.process_inputs(&[], &[], false, None);
533            return Err(TestCaseError::reject(FuzzError::AssumeReject));
534        }
535
536        if call.skip_reason().is_some() {
537            // Account for the attempted corpus mutation without retaining or crediting the input.
538            coverage_metrics.process_inputs(&[], &[], false, None);
539        } else {
540            let new_coverage = coverage_metrics.merge_edge_coverage(&mut call);
541            // `new_coverage` is only meaningful when edge coverage is collected; otherwise
542            // `merge_edge_coverage` always returns `false`, so record it as unknown for frontiers.
543            let frontier_new_coverage =
544                self.config.corpus.collect_edge_coverage().then_some(new_coverage);
545            frontier_recorder.capture_call(
546                fuzz_run,
547                std::slice::from_ref(&tx),
548                0,
549                &cmp_values,
550                frontier_new_coverage,
551            );
552            coverage_metrics.process_inputs(
553                std::slice::from_ref(&tx),
554                &[cmp_values],
555                new_coverage,
556                None,
557            );
558        }
559
560        let (breakpoints, deprecated_cheatcodes) =
561            call.cheatcodes.as_ref().map_or_else(Default::default, |cheats| {
562                (cheats.breakpoints.clone(), cheats.deprecated.clone())
563            });
564
565        // Consider call success if test should not fail on reverts and reverter is not the test
566        // address or one of the network's cheatcode contracts.
567        let success = should_ignore_revert(
568            self.config.fail_on_revert,
569            address,
570            call.reverter,
571            state.0.inspector().extra_cheatcode_addresses(),
572        ) || state.0.is_raw_call_mut_success(address, &mut call);
573
574        if success {
575            Ok(FuzzOutcome::Case(CaseOutcome {
576                case: FuzzCase { gas: call.gas_used, stipend: call.stipend },
577                traces: call.traces,
578                debug_bytecodes: call.debug_bytecodes,
579                coverage: call.line_coverage,
580                breakpoints,
581                logs: call.logs,
582                deprecated_cheatcodes,
583            }))
584        } else {
585            Ok(FuzzOutcome::CounterExample(CounterExampleOutcome {
586                exit_reason: call.exit_reason,
587                counterexample: (tx, call),
588                breakpoints,
589            }))
590        }
591    }
592
593    fn resolve_stateless_tx_with_executor(
594        &self,
595        executor: &Executor<FEN>,
596        tx: &mut BasicTxDetails,
597    ) -> Result<()> {
598        tx.call_details.value = match tx.call_details.value {
599            Some(requested) if !requested.is_zero() => {
600                let value = requested.min(executor.get_balance(tx.sender)?);
601                (!value.is_zero()).then_some(value)
602            }
603            _ => None,
604        };
605        Ok(())
606    }
607
608    /// Aggregates the results from all workers, and returns the input of the shown trace with the
609    /// run that generated it.
610    fn aggregate_results(
611        &self,
612        mut workers: Vec<WorkerState<FEN>>,
613        func: &Function,
614        shared_state: &SharedFuzzState,
615    ) -> (FuzzTestResult, Option<(BasicTxDetails, Option<FuzzRunMetadata>)>) {
616        self.write_branch_frontiers(&mut workers, func);
617
618        let mut result = FuzzTestResult::default();
619        if workers.is_empty() {
620            result.success = true;
621            return (result, None);
622        }
623
624        // Find first case and last run worker. Set `failed_corpus_replays`.
625        let mut first_case_candidate = None;
626        let mut last_run_worker = None;
627        for (i, worker) in workers.iter().enumerate() {
628            if let Some((run, ref case)) = worker.first_case
629                && first_case_candidate.as_ref().is_none_or(|&(r, _)| run < r)
630            {
631                first_case_candidate = Some((run, case.clone()));
632            }
633
634            if last_run_worker.is_none_or(|(t, _)| worker.last_run_timestamp > t) {
635                last_run_worker = Some((worker.last_run_timestamp, i));
636            }
637
638            // Only set replays from master which is responsible for replaying persisted corpus.
639            if worker.id == 0 {
640                result.failed_corpus_replays = worker.failed_corpus_replays;
641            }
642        }
643        result.first_case = first_case_candidate.map(|(_, case)| case).unwrap_or_default();
644        let (_, last_run_worker_idx) = last_run_worker.expect("at least one worker");
645        let mut output_worker_idx = last_run_worker_idx;
646        let shown_case;
647
648        if let Some(&failed_worker_id) = shared_state.failed_worker_id.get() {
649            result.success = false;
650
651            let failed_worker_idx = workers.iter().position(|w| w.id == failed_worker_id).unwrap();
652            output_worker_idx = failed_worker_idx;
653            let failed_worker = &mut workers[failed_worker_idx];
654
655            let counterexample = failed_worker.counterexample.take();
656            shown_case =
657                counterexample.as_ref().map(|(tx, _)| (tx.clone(), failed_worker.failure_run));
658            if let Some((_, call)) = &counterexample {
659                result.labels.clone_from(&call.labels);
660                result.traces.clone_from(&call.traces);
661                result.debug_bytecodes.clone_from(&call.debug_bytecodes);
662                result.breakpoints = call.cheatcodes.as_ref().map(|c| c.breakpoints.clone());
663                result.fork_block_number = call.fork_block_number;
664            }
665
666            match &failed_worker.failure {
667                Some(TestCaseError::Fail(reason)) => {
668                    let reason = reason.to_string();
669                    result.reason = (!reason.is_empty()).then_some(reason);
670                    if let Some((tx, call)) = counterexample {
671                        let args =
672                            tx.call_details.calldata.get(4..).map_or_else(Vec::new, |data| {
673                                func.abi_decode_input(data).unwrap_or_default()
674                            });
675                        let fuzz = failed_worker.failure_run.unwrap_or_default();
676                        result.counterexample = Some(CounterExample::Single(
677                            BaseCounterExample::from_fuzz_tx(&tx, args, call.traces)
678                                .with_fuzz_metadata(FuzzRunMetadata::new(
679                                    fuzz.seed.or(self.config.seed),
680                                    fuzz.run,
681                                    fuzz.worker,
682                                )),
683                        ));
684                    }
685                }
686                Some(TestCaseError::Reject(reason)) => {
687                    let reason = reason.to_string();
688                    result.reason = (!reason.is_empty()).then_some(reason);
689                }
690                None => {}
691            }
692        } else {
693            let last_run_worker = &workers[last_run_worker_idx];
694            result.success = true;
695            let planned_runs = if self.config.run.is_some() { 1 } else { self.config.runs };
696            if shared_state.interrupted(planned_runs) {
697                result.skipped = true;
698                result.reason = Some("interrupted".to_string());
699            }
700            result.traces = last_run_worker.traces.last().cloned();
701            result.debug_bytecodes.clone_from(&last_run_worker.debug_bytecodes);
702            result.breakpoints = last_run_worker.breakpoints.clone();
703            shown_case = last_run_worker.last_case.clone();
704        }
705
706        if !self.config.show_logs {
707            result.logs = workers[output_worker_idx].logs.clone();
708        }
709
710        for mut worker in workers {
711            result.gas_by_case.append(&mut worker.gas_by_case);
712            if self.config.show_logs {
713                result.logs.append(&mut worker.logs);
714            }
715            result.gas_report_traces.extend(worker.traces.into_iter().map(|t| t.arena));
716            HitMaps::merge_opt(&mut result.line_coverage, worker.coverage);
717            result.deprecated_cheatcodes.extend(worker.deprecated_cheatcodes);
718        }
719
720        if let Some(reason) = &result.reason
721            && let Some(reason) = SkipReason::decode_self(reason)
722        {
723            result.skipped = true;
724            result.reason = reason.0;
725        }
726
727        (result, shown_case)
728    }
729
730    fn write_branch_frontiers(&self, workers: &mut [WorkerState<FEN>], func: &Function) {
731        let Some(frontier_dir) = &self.config.corpus.frontier_dir else {
732            return;
733        };
734        let limit = self.config.corpus.frontier_limit;
735        if limit == 0 {
736            return;
737        }
738
739        let frontiers = frontier::merge_frontiers(
740            limit,
741            workers.iter_mut().flat_map(|worker| worker.frontiers.drain(..)),
742        );
743        if frontiers.is_empty() {
744            return;
745        }
746
747        let artifact = FuzzBranchFrontierArtifact::new(func, limit, frontiers);
748        if let Err(err) = frontier::write_frontier_artifact(frontier_dir, &artifact) {
749            warn!(%err, path = ?frontier_dir, "failed to write fuzz branch frontier artifact");
750        }
751    }
752
753    /// Runs a single fuzz worker
754    #[allow(clippy::too_many_arguments)]
755    fn run_worker(
756        &self,
757        worker_id: usize,
758        func: &Function,
759        fuzz_fixtures: &FuzzFixtures,
760        address: Address,
761        rd: &RevertDecoder,
762        shared_state: &SharedFuzzState,
763        progress: Option<&ProgressBar>,
764        final_sync: Option<&CorpusSyncCoordinator>,
765    ) -> Result<WorkerState<FEN>> {
766        // Prepare
767        let fuzz_seed = shared_state.state.fork();
768        let generator = TxGenerator::stateless(
769            fuzz_seed.clone(),
770            fuzz_fixtures.clone(),
771            address,
772            self.sender,
773            func.clone(),
774            self.config.dictionary.dictionary_weight,
775            self.config.corpus.payable_value_weight,
776        );
777        let fuzz_state = fuzz_seed.stateless_worker();
778        let generator = foundry_evm_fuzz::sequence::SequenceGenerator::stateless_with_fixtures(
779            generator,
780            fuzz_state.clone(),
781            fuzz_fixtures.clone(),
782            func.clone(),
783            &self.config.corpus,
784        )?;
785
786        let replay_target = ReplayTarget {
787            stateless: Some(StatelessReplayTarget { function: func, address }),
788            fuzzed_contracts: None,
789            dynamic: None,
790            senders: None,
791        };
792        let mut corpus = WorkerCorpus::new(
793            worker_id,
794            self.config.corpus.clone(),
795            generator,
796            self.corpus_replay_dir.as_deref(),
797            // Master worker replays the persisted corpus using the executor
798            (worker_id == 0).then_some(&self.executor_f),
799            replay_target,
800        )?;
801        let mut executor = self.executor_f.clone();
802        let frontier_limit = if self.config.corpus.capture_branch_frontiers() {
803            self.config.corpus.frontier_limit
804        } else {
805            0
806        };
807        let mut frontier_recorder = FuzzFrontierRecorder::new(frontier_limit);
808
809        let mut worker = WorkerState::new(worker_id);
810        // We want to collect at least one trace which will be displayed to user.
811        let max_traces_to_collect =
812            std::cmp::max(1, self.config.gas_report_samples / self.num_workers as u32);
813
814        let worker_runs = self.runs_per_worker(worker_id);
815        debug!(worker_runs);
816
817        let mut runner_config = self.runner.config().clone();
818        runner_config.cases = worker_runs;
819
820        let mut runner = if let Some(seed) = self.config.seed {
821            let worker_seed = worker_seed(seed, worker_id as u32);
822            trace!(target: "forge::test", ?worker_seed, "deterministic seed for worker {worker_id}");
823            let rng = TestRng::from_seed(RngAlgorithm::ChaCha, &worker_seed.to_be_bytes::<32>());
824            TestRunner::new_with_rng(runner_config, rng)
825        } else {
826            TestRunner::new(runner_config)
827        };
828
829        if let Some(target_run) = self.config.run {
830            for _ in 1..target_run {
831                if let Err(err) = corpus.new_sequence(&mut runner) {
832                    worker.failure = Some(TestCaseError::fail(format!(
833                        "failed to generate fuzzed input in worker {}: {err}",
834                        worker.id
835                    )));
836                    shared_state.try_claim_failure(worker_id);
837                    return Ok(worker);
838                }
839            }
840        }
841        let mut generated_inputs = 0;
842
843        let mut persisted_failure =
844            self.persisted_failure.as_ref().filter(|_| worker_id == 0 && self.config.run.is_none());
845
846        // Offset to stagger corpus syncs across workers; so that workers don't sync at the same
847        // time.
848        let sync_offset = (worker_id as u32).saturating_mul(100);
849        let sync_threshold = SYNC_INTERVAL + sync_offset;
850        let mut runs_since_sync = sync_threshold; // Always sync at the start.
851        let mut last_metrics_report = Instant::now();
852        // Continue while:
853        // 1. Global state allows (not timed out, not at global limit, no failure found)
854        // 2. Worker hasn't reached its specific run limit
855        'stop: while shared_state.should_continue() && worker.runs < worker_runs {
856            // If counterexample recorded, replay it first, without incrementing runs.
857            let (input, fuzz_run, is_persisted_replay) = if worker_id == 0
858                && let Some(failure) = persisted_failure.take()
859                && failure.calldata.get(..4).is_some_and(|selector| func.selector() == selector)
860            {
861                let seed = failure.fuzz.seed.or(self.config.seed);
862                if let Some(cheats) = executor.inspector_mut().cheatcodes.as_mut()
863                    && let Some(seed) = seed
864                {
865                    let run = failure.fuzz.run.unwrap_or(1);
866                    let worker = failure.fuzz.worker.unwrap_or(worker_id as u32) as usize;
867                    cheats.core.set_seed(Self::fuzz_run_seed(seed, worker, run));
868                }
869
870                (
871                    BasicTxDetails {
872                        warp: None,
873                        roll: None,
874                        sender: self.sender,
875                        call_details: CallDetails {
876                            target: address,
877                            calldata: failure.calldata.clone(),
878                            value: failure.value,
879                        },
880                    },
881                    Some(FuzzRunMetadata::new(
882                        seed,
883                        failure.fuzz.run,
884                        Some(failure.fuzz.worker.unwrap_or(worker_id as u32)),
885                    )),
886                    true,
887                )
888            } else {
889                runs_since_sync += 1;
890                if runs_since_sync >= sync_threshold {
891                    let timer = Instant::now();
892                    corpus.sync(
893                        self.num_workers,
894                        &executor,
895                        replay_target,
896                        &shared_state.global_corpus_metrics,
897                    )?;
898                    trace!("finished corpus sync in {:?}", timer.elapsed());
899                    runs_since_sync = 0;
900                }
901
902                let fuzz_run = self.config.run.unwrap_or_else(|| {
903                    generated_inputs += 1;
904                    generated_inputs
905                });
906                if let Some(cheats) = executor.inspector_mut().cheatcodes.as_mut()
907                    && let Some(seed) = self.config.seed
908                {
909                    cheats.core.set_seed(Self::fuzz_run_seed(seed, worker_id, fuzz_run));
910                }
911
912                let input = match corpus.new_sequence(&mut runner) {
913                    Ok(plan) => plan.into_first(),
914                    Err(err) => {
915                        worker.failure = Some(TestCaseError::fail(format!(
916                            "failed to generate fuzzed input in worker {}: {err}",
917                            worker.id
918                        )));
919                        shared_state.try_claim_failure(worker_id);
920                        break 'stop;
921                    }
922                };
923
924                (
925                    input,
926                    Some(FuzzRunMetadata::new(
927                        self.config.seed,
928                        Some(fuzz_run),
929                        Some(worker_id as u32),
930                    )),
931                    false,
932                )
933            };
934
935            let mut inc_runs = || {
936                let total_runs = shared_state.increment_runs();
937                debug_assert!(
938                    shared_state.timer.is_enabled()
939                        || total_runs
940                            <= if self.config.run.is_some() { 1 } else { self.config.runs },
941                    "worker runs were not distributed correctly"
942                );
943                worker.runs += 1;
944                if let Some(progress) = progress {
945                    progress.inc(1);
946                }
947                total_runs
948            };
949
950            worker.last_run_timestamp = SystemTime::now().duration_since(UNIX_EPOCH)?.as_millis();
951            let case_input = input.clone();
952            match self.single_fuzz(
953                &mut executor,
954                address,
955                input,
956                &mut corpus,
957                &mut frontier_recorder,
958                fuzz_run.as_ref(),
959            ) {
960                Ok(fuzz_outcome) => match fuzz_outcome {
961                    FuzzOutcome::Case(case) => {
962                        if is_persisted_replay {
963                            continue 'stop;
964                        }
965                        let total_runs = inc_runs();
966
967                        if worker_id == 0 && self.config.corpus.collect_edge_coverage() {
968                            if let Some(progress) = progress {
969                                corpus.sync_metrics(&shared_state.global_corpus_metrics);
970                                progress
971                                    .set_message(format!("{}", shared_state.global_corpus_metrics));
972                            } else if last_metrics_report.elapsed()
973                                > DURATION_BETWEEN_METRICS_REPORT
974                            {
975                                corpus.sync_metrics(&shared_state.global_corpus_metrics);
976                                // Display metrics inline.
977                                let metrics = json!({
978                                    "timestamp": SystemTime::now()
979                                        .duration_since(UNIX_EPOCH)?
980                                        .as_secs(),
981                                    "test": func.name,
982                                    "metrics": shared_state.global_corpus_metrics.load(),
983                                });
984                                let _ = sh_println!("{metrics}");
985                                last_metrics_report = Instant::now();
986                            }
987                        }
988
989                        worker.gas_by_case.push((case.case.gas, case.case.stipend));
990
991                        if worker.first_case.is_none() {
992                            worker.first_case = Some((total_runs, case.case));
993                        }
994
995                        if let Some(call_traces) = case.traces {
996                            if worker.traces.len() == max_traces_to_collect as usize {
997                                worker.traces.pop();
998                            }
999                            worker.traces.push(call_traces);
1000                            worker.debug_bytecodes = case.debug_bytecodes;
1001                            worker.breakpoints = Some(case.breakpoints);
1002                            worker.last_case = Some((case_input, fuzz_run));
1003                        }
1004
1005                        // Always store logs from the last run in test_data.logs for display at
1006                        // verbosity >= 2. When show_logs is true,
1007                        // accumulate all logs. When false, only keep the last run's logs.
1008                        if self.config.show_logs {
1009                            worker.logs.extend(case.logs);
1010                        } else {
1011                            worker.logs = case.logs;
1012                        }
1013
1014                        HitMaps::merge_opt(&mut worker.coverage, case.coverage);
1015                        worker.deprecated_cheatcodes = case.deprecated_cheatcodes;
1016                    }
1017                    FuzzOutcome::CounterExample(CounterExampleOutcome {
1018                        exit_reason: status,
1019                        counterexample: outcome,
1020                        ..
1021                    }) => {
1022                        if !is_persisted_replay {
1023                            inc_runs();
1024                        }
1025                        worker.failure_run = fuzz_run;
1026
1027                        // Only classify magic skip payloads minted by the skip cheatcode.
1028                        let reason = outcome
1029                            .1
1030                            .skip_reason()
1031                            .map(|reason| reason.to_string())
1032                            .or_else(|| rd.maybe_decode(&outcome.1.result, status));
1033                        if self.config.show_logs {
1034                            worker.logs.extend(outcome.1.logs.clone());
1035                        } else {
1036                            worker.logs.clone_from(&outcome.1.logs);
1037                        }
1038                        worker.counterexample = Some(outcome);
1039                        worker.failure = Some(TestCaseError::fail(reason.unwrap_or_default()));
1040                        shared_state.try_claim_failure(worker_id);
1041                        break 'stop;
1042                    }
1043                },
1044                Err(err) => match err {
1045                    TestCaseError::Fail(_) => {
1046                        worker.failure = Some(err);
1047                        shared_state.try_claim_failure(worker_id);
1048                        break 'stop;
1049                    }
1050                    TestCaseError::Reject(_) => {
1051                        let max = self.config.max_test_rejects;
1052
1053                        let total = shared_state.increment_rejects();
1054
1055                        // Update progress bar to reflect rejected runs.
1056                        // TODO(dani): (pre-existing) conflicts with corpus metrics `set_message`
1057                        if !self.config.corpus.collect_edge_coverage()
1058                            && let Some(progress) = progress
1059                        {
1060                            progress.set_message(format!("([{total}] rejected)"));
1061                        }
1062
1063                        if max > 0 && total > max {
1064                            worker.failure =
1065                                Some(TestCaseError::reject(FuzzError::TooManyRejects(max)));
1066                            shared_state.try_claim_failure(worker_id);
1067                            break 'stop;
1068                        }
1069                    }
1070                },
1071            }
1072        }
1073
1074        if let Some(final_sync) = final_sync {
1075            corpus.finalize_sync(&executor, replay_target, final_sync)?;
1076        }
1077
1078        if worker_id == 0 {
1079            worker.failed_corpus_replays = corpus.failed_replays;
1080        }
1081        worker.frontiers = frontier_recorder.into_frontiers();
1082
1083        // Logs stats
1084        trace!("worker {worker_id} fuzz stats");
1085        fuzz_state.log_stats();
1086
1087        Ok(worker)
1088    }
1089
1090    /// Determines the number of runs per worker.
1091    const fn runs_per_worker(&self, worker_id: usize) -> u32 {
1092        let worker_id = worker_id as u32;
1093        let total_runs = if self.config.run.is_some() { 1 } else { self.config.runs };
1094        let n = self.num_workers as u32;
1095        let runs = total_runs / n;
1096        let remainder = total_runs % n;
1097        // Distribute the remainder evenly among the first `remainder` workers,
1098        // assuming `worker_id` is in `0..n`.
1099        if worker_id < remainder { runs + 1 } else { runs }
1100    }
1101
1102    /// Returns the worker IDs to execute.
1103    fn worker_ids(&self) -> Vec<usize> {
1104        if self.config.run.is_some() {
1105            vec![self.config.worker.unwrap_or(0) as usize]
1106        } else {
1107            (0..self.num_workers).collect()
1108        }
1109    }
1110
1111    /// Derives the deterministic RNG seed for cheatcode randomness in a worker-local run.
1112    fn fuzz_run_seed(seed: U256, worker_id: usize, run: u32) -> U256 {
1113        worker_seed(seed, worker_id as u32).wrapping_add(U256::from(run.saturating_sub(1)))
1114    }
1115}