foundry_evm/executors/fuzz/
mod.rs

1use crate::executors::{
2    DURATION_BETWEEN_METRICS_REPORT, Executor, FailFast, FuzzTestTimer, RawCallResult,
3};
4use alloy_dyn_abi::JsonAbiExt;
5use alloy_json_abi::Function;
6use alloy_primitives::{Address, Bytes, Log, U256, map::HashMap};
7use eyre::Result;
8use foundry_common::{evm::Breakpoints, sh_println};
9use foundry_config::FuzzConfig;
10use foundry_evm_core::{
11    constants::{CHEATCODE_ADDRESS, MAGIC_ASSUME},
12    decode::{RevertDecoder, SkipReason},
13};
14use foundry_evm_coverage::HitMaps;
15use foundry_evm_fuzz::{
16    BaseCounterExample, BasicTxDetails, CallDetails, CounterExample, FuzzCase, FuzzError,
17    FuzzFixtures, FuzzTestResult,
18    strategies::{EvmFuzzState, fuzz_calldata, fuzz_calldata_from_state},
19};
20use foundry_evm_traces::SparsedTraceArena;
21use indicatif::ProgressBar;
22use proptest::{
23    strategy::Strategy,
24    test_runner::{TestCaseError, TestRunner},
25};
26use serde_json::json;
27use std::time::{Instant, SystemTime, UNIX_EPOCH};
28
29mod types;
30use crate::executors::corpus::CorpusManager;
31pub use types::{CaseOutcome, CounterExampleOutcome, FuzzOutcome};
32
33/// Contains data collected during fuzz test runs.
34#[derive(Default)]
35struct FuzzTestData {
36    // Stores the first fuzz case.
37    first_case: Option<FuzzCase>,
38    // Stored gas usage per fuzz case.
39    gas_by_case: Vec<(u64, u64)>,
40    // Stores the result and calldata of the last failed call, if any.
41    counterexample: (Bytes, RawCallResult),
42    // Stores up to `max_traces_to_collect` traces.
43    traces: Vec<SparsedTraceArena>,
44    // Stores breakpoints for the last fuzz case.
45    breakpoints: Option<Breakpoints>,
46    // Stores coverage information for all fuzz cases.
47    coverage: Option<HitMaps>,
48    // Stores logs for all fuzz cases
49    logs: Vec<Log>,
50    // Deprecated cheatcodes mapped to their replacements.
51    deprecated_cheatcodes: HashMap<&'static str, Option<&'static str>>,
52    // Runs performed in fuzz test.
53    runs: u32,
54    // Current assume rejects of the fuzz run.
55    rejects: u32,
56    // Test failure.
57    failure: Option<TestCaseError>,
58}
59
60/// Wrapper around an [`Executor`] which provides fuzzing support using [`proptest`].
61///
62/// After instantiation, calling `fuzz` will proceed to hammer the deployed smart contract with
63/// inputs, until it finds a counterexample. The provided [`TestRunner`] contains all the
64/// configuration which can be overridden via [environment variables](proptest::test_runner::Config)
65pub struct FuzzedExecutor {
66    /// The EVM executor.
67    executor: Executor,
68    /// The fuzzer
69    runner: TestRunner,
70    /// The account that calls tests.
71    sender: Address,
72    /// The fuzz configuration.
73    config: FuzzConfig,
74    /// The persisted counterexample to be replayed, if any.
75    persisted_failure: Option<BaseCounterExample>,
76}
77
78impl FuzzedExecutor {
79    /// Instantiates a fuzzed executor given a testrunner
80    pub fn new(
81        executor: Executor,
82        runner: TestRunner,
83        sender: Address,
84        config: FuzzConfig,
85        persisted_failure: Option<BaseCounterExample>,
86    ) -> Self {
87        Self { executor, runner, sender, config, persisted_failure }
88    }
89
90    /// Fuzzes the provided function, assuming it is available at the contract at `address`
91    /// If `should_fail` is set to `true`, then it will stop only when there's a success
92    /// test case.
93    ///
94    /// Returns a list of all the consumed gas and calldata of every fuzz case.
95    #[allow(clippy::too_many_arguments)]
96    pub fn fuzz(
97        &mut self,
98        func: &Function,
99        fuzz_fixtures: &FuzzFixtures,
100        deployed_libs: &[Address],
101        address: Address,
102        rd: &RevertDecoder,
103        progress: Option<&ProgressBar>,
104        fail_fast: &FailFast,
105    ) -> Result<FuzzTestResult> {
106        // Stores the fuzz test execution data.
107        let mut test_data = FuzzTestData::default();
108        let state = self.build_fuzz_state(deployed_libs);
109        let dictionary_weight = self.config.dictionary.dictionary_weight.min(100);
110        let strategy = proptest::prop_oneof![
111            100 - dictionary_weight => fuzz_calldata(func.clone(), fuzz_fixtures),
112            dictionary_weight => fuzz_calldata_from_state(func.clone(), &state),
113        ]
114        .prop_map(move |calldata| BasicTxDetails {
115            sender: Default::default(),
116            call_details: CallDetails { target: Default::default(), calldata },
117        });
118        // We want to collect at least one trace which will be displayed to user.
119        let max_traces_to_collect = std::cmp::max(1, self.config.gas_report_samples) as usize;
120
121        let mut corpus_manager = CorpusManager::new(
122            self.config.corpus.clone(),
123            strategy.boxed(),
124            &self.executor,
125            Some(func),
126            None,
127        )?;
128
129        // Start timer for this fuzz test.
130        let timer = FuzzTestTimer::new(self.config.timeout);
131        let mut last_metrics_report = Instant::now();
132        let max_runs = self.config.runs;
133        let continue_campaign = |runs: u32| {
134            if fail_fast.should_stop() {
135                return false;
136            }
137
138            if timer.is_enabled() { !timer.is_timed_out() } else { runs < max_runs }
139        };
140
141        'stop: while continue_campaign(test_data.runs) {
142            // If counterexample recorded, replay it first, without incrementing runs.
143            let input = if let Some(failure) = self.persisted_failure.take() {
144                failure.calldata
145            } else {
146                // If running with progress, then increment current run.
147                if let Some(progress) = progress {
148                    progress.inc(1);
149                    // Display metrics in progress bar.
150                    if self.config.corpus.collect_edge_coverage() {
151                        progress.set_message(format!("{}", &corpus_manager.metrics));
152                    }
153                } else if self.config.corpus.collect_edge_coverage()
154                    && last_metrics_report.elapsed() > DURATION_BETWEEN_METRICS_REPORT
155                {
156                    // Display metrics inline.
157                    let metrics = json!({
158                        "timestamp": SystemTime::now()
159                            .duration_since(UNIX_EPOCH)?
160                            .as_secs(),
161                        "test": func.name,
162                        "metrics": &corpus_manager.metrics,
163                    });
164                    let _ = sh_println!("{}", serde_json::to_string(&metrics)?);
165                    last_metrics_report = Instant::now();
166                };
167
168                test_data.runs += 1;
169
170                match corpus_manager.new_input(&mut self.runner, &state, func) {
171                    Ok(input) => input,
172                    Err(err) => {
173                        test_data.failure = Some(TestCaseError::fail(format!(
174                            "failed to generate fuzzed input: {err}"
175                        )));
176                        break 'stop;
177                    }
178                }
179            };
180
181            match self.single_fuzz(address, input, &mut corpus_manager) {
182                Ok(fuzz_outcome) => match fuzz_outcome {
183                    FuzzOutcome::Case(case) => {
184                        test_data.gas_by_case.push((case.case.gas, case.case.stipend));
185
186                        if test_data.first_case.is_none() {
187                            test_data.first_case.replace(case.case);
188                        }
189
190                        if let Some(call_traces) = case.traces {
191                            if test_data.traces.len() == max_traces_to_collect {
192                                test_data.traces.pop();
193                            }
194                            test_data.traces.push(call_traces);
195                            test_data.breakpoints.replace(case.breakpoints);
196                        }
197
198                        if self.config.show_logs {
199                            test_data.logs.extend(case.logs);
200                        }
201
202                        HitMaps::merge_opt(&mut test_data.coverage, case.coverage);
203                        test_data.deprecated_cheatcodes = case.deprecated_cheatcodes;
204                    }
205                    FuzzOutcome::CounterExample(CounterExampleOutcome {
206                        exit_reason: status,
207                        counterexample: outcome,
208                        ..
209                    }) => {
210                        let reason = rd.maybe_decode(&outcome.1.result, status);
211                        test_data.logs.extend(outcome.1.logs.clone());
212                        test_data.counterexample = outcome;
213                        test_data.failure = Some(TestCaseError::fail(reason.unwrap_or_default()));
214                        break 'stop;
215                    }
216                },
217                Err(err) => {
218                    match err {
219                        TestCaseError::Fail(_) => {
220                            test_data.failure = Some(err);
221                            break 'stop;
222                        }
223                        TestCaseError::Reject(_) => {
224                            // Apply max rejects only if configured, otherwise silently discard run.
225                            if self.config.max_test_rejects > 0 {
226                                test_data.rejects += 1;
227                                if test_data.rejects >= self.config.max_test_rejects {
228                                    test_data.failure = Some(err);
229                                    break 'stop;
230                                }
231                            }
232                        }
233                    }
234                }
235            }
236        }
237
238        let (calldata, call) = test_data.counterexample;
239        let mut traces = test_data.traces;
240        let (last_run_traces, last_run_breakpoints) = if test_data.failure.is_none() {
241            (traces.pop(), test_data.breakpoints)
242        } else {
243            (call.traces.clone(), call.cheatcodes.map(|c| c.breakpoints))
244        };
245
246        let mut result = FuzzTestResult {
247            first_case: test_data.first_case.unwrap_or_default(),
248            gas_by_case: test_data.gas_by_case,
249            success: test_data.failure.is_none(),
250            skipped: false,
251            reason: None,
252            counterexample: None,
253            logs: test_data.logs,
254            labels: call.labels,
255            traces: last_run_traces,
256            breakpoints: last_run_breakpoints,
257            gas_report_traces: traces.into_iter().map(|a| a.arena).collect(),
258            line_coverage: test_data.coverage,
259            deprecated_cheatcodes: test_data.deprecated_cheatcodes,
260            failed_corpus_replays: corpus_manager.failed_replays(),
261        };
262
263        match test_data.failure {
264            Some(TestCaseError::Fail(reason)) => {
265                let reason = reason.to_string();
266                result.reason = (!reason.is_empty()).then_some(reason);
267                let args = if let Some(data) = calldata.get(4..) {
268                    func.abi_decode_input(data).unwrap_or_default()
269                } else {
270                    vec![]
271                };
272                result.counterexample = Some(CounterExample::Single(
273                    BaseCounterExample::from_fuzz_call(calldata, args, call.traces),
274                ));
275            }
276            Some(TestCaseError::Reject(reason)) => {
277                let reason = reason.to_string();
278                result.reason = (!reason.is_empty()).then_some(reason);
279            }
280            None => {}
281        }
282
283        if let Some(reason) = &result.reason
284            && let Some(reason) = SkipReason::decode_self(reason)
285        {
286            result.skipped = true;
287            result.reason = reason.0;
288        }
289
290        state.log_stats();
291
292        Ok(result)
293    }
294
295    /// Granular and single-step function that runs only one fuzz and returns either a `CaseOutcome`
296    /// or a `CounterExampleOutcome`
297    fn single_fuzz(
298        &mut self,
299        address: Address,
300        calldata: Bytes,
301        coverage_metrics: &mut CorpusManager,
302    ) -> Result<FuzzOutcome, TestCaseError> {
303        let mut call = self
304            .executor
305            .call_raw(self.sender, address, calldata.clone(), U256::ZERO)
306            .map_err(|e| TestCaseError::fail(e.to_string()))?;
307        let new_coverage = coverage_metrics.merge_edge_coverage(&mut call);
308        coverage_metrics.process_inputs(
309            &[BasicTxDetails {
310                sender: self.sender,
311                call_details: CallDetails { target: address, calldata: calldata.clone() },
312            }],
313            new_coverage,
314        );
315
316        // Handle `vm.assume`.
317        if call.result.as_ref() == MAGIC_ASSUME {
318            return Err(TestCaseError::reject(FuzzError::TooManyRejects(
319                self.config.max_test_rejects,
320            )));
321        }
322
323        let (breakpoints, deprecated_cheatcodes) =
324            call.cheatcodes.as_ref().map_or_else(Default::default, |cheats| {
325                (cheats.breakpoints.clone(), cheats.deprecated.clone())
326            });
327
328        // Consider call success if test should not fail on reverts and reverter is not the
329        // cheatcode or test address.
330        let success = if !self.config.fail_on_revert
331            && call
332                .reverter
333                .is_some_and(|reverter| reverter != address && reverter != CHEATCODE_ADDRESS)
334        {
335            true
336        } else {
337            self.executor.is_raw_call_mut_success(address, &mut call, false)
338        };
339
340        if success {
341            Ok(FuzzOutcome::Case(CaseOutcome {
342                case: FuzzCase { calldata, gas: call.gas_used, stipend: call.stipend },
343                traces: call.traces,
344                coverage: call.line_coverage,
345                breakpoints,
346                logs: call.logs,
347                deprecated_cheatcodes,
348            }))
349        } else {
350            Ok(FuzzOutcome::CounterExample(CounterExampleOutcome {
351                exit_reason: call.exit_reason,
352                counterexample: (calldata, call),
353                breakpoints,
354            }))
355        }
356    }
357
358    /// Stores fuzz state for use with [fuzz_calldata_from_state]
359    pub fn build_fuzz_state(&self, deployed_libs: &[Address]) -> EvmFuzzState {
360        if let Some(fork_db) = self.executor.backend().active_fork_db() {
361            EvmFuzzState::new(fork_db, self.config.dictionary, deployed_libs)
362        } else {
363            EvmFuzzState::new(
364                self.executor.backend().mem_db(),
365                self.config.dictionary,
366                deployed_libs,
367            )
368        }
369    }
370}