foundry_evm/executors/fuzz/
mod.rs

1use crate::executors::{Executor, FuzzTestTimer, RawCallResult};
2use alloy_dyn_abi::JsonAbiExt;
3use alloy_json_abi::Function;
4use alloy_primitives::{map::HashMap, Address, Bytes, Log, U256};
5use eyre::Result;
6use foundry_common::evm::Breakpoints;
7use foundry_config::FuzzConfig;
8use foundry_evm_core::{
9    constants::{MAGIC_ASSUME, TEST_TIMEOUT},
10    decode::{RevertDecoder, SkipReason},
11};
12use foundry_evm_coverage::HitMaps;
13use foundry_evm_fuzz::{
14    strategies::{fuzz_calldata, fuzz_calldata_from_state, EvmFuzzState},
15    BaseCounterExample, CounterExample, FuzzCase, FuzzError, FuzzFixtures, FuzzTestResult,
16};
17use foundry_evm_traces::SparsedTraceArena;
18use indicatif::ProgressBar;
19use proptest::test_runner::{TestCaseError, TestError, TestRunner};
20use std::{cell::RefCell, collections::BTreeMap};
21
22mod types;
23pub use types::{CaseOutcome, CounterExampleOutcome, FuzzOutcome};
24
25/// Contains data collected during fuzz test runs.
26#[derive(Default)]
27pub struct FuzzTestData {
28    // Stores the first fuzz case.
29    pub first_case: Option<FuzzCase>,
30    // Stored gas usage per fuzz case.
31    pub gas_by_case: Vec<(u64, u64)>,
32    // Stores the result and calldata of the last failed call, if any.
33    pub counterexample: (Bytes, RawCallResult),
34    // Stores up to `max_traces_to_collect` traces.
35    pub traces: Vec<SparsedTraceArena>,
36    // Stores breakpoints for the last fuzz case.
37    pub breakpoints: Option<Breakpoints>,
38    // Stores coverage information for all fuzz cases.
39    pub coverage: Option<HitMaps>,
40    // Stores logs for all fuzz cases
41    pub logs: Vec<Log>,
42    // Stores gas snapshots for all fuzz cases
43    pub gas_snapshots: BTreeMap<String, BTreeMap<String, String>>,
44    // Deprecated cheatcodes mapped to their replacements.
45    pub deprecated_cheatcodes: HashMap<&'static str, Option<&'static str>>,
46}
47
48/// Wrapper around an [`Executor`] which provides fuzzing support using [`proptest`].
49///
50/// After instantiation, calling `fuzz` will proceed to hammer the deployed smart contract with
51/// inputs, until it finds a counterexample. The provided [`TestRunner`] contains all the
52/// configuration which can be overridden via [environment variables](proptest::test_runner::Config)
53pub struct FuzzedExecutor {
54    /// The EVM executor
55    executor: Executor,
56    /// The fuzzer
57    runner: TestRunner,
58    /// The account that calls tests
59    sender: Address,
60    /// The fuzz configuration
61    config: FuzzConfig,
62}
63
64impl FuzzedExecutor {
65    /// Instantiates a fuzzed executor given a testrunner
66    pub fn new(
67        executor: Executor,
68        runner: TestRunner,
69        sender: Address,
70        config: FuzzConfig,
71    ) -> Self {
72        Self { executor, runner, sender, config }
73    }
74
75    /// Fuzzes the provided function, assuming it is available at the contract at `address`
76    /// If `should_fail` is set to `true`, then it will stop only when there's a success
77    /// test case.
78    ///
79    /// Returns a list of all the consumed gas and calldata of every fuzz case
80    pub fn fuzz(
81        &self,
82        func: &Function,
83        fuzz_fixtures: &FuzzFixtures,
84        deployed_libs: &[Address],
85        address: Address,
86        rd: &RevertDecoder,
87        progress: Option<&ProgressBar>,
88    ) -> FuzzTestResult {
89        // Stores the fuzz test execution data.
90        let execution_data = RefCell::new(FuzzTestData::default());
91        let state = self.build_fuzz_state(deployed_libs);
92        let dictionary_weight = self.config.dictionary.dictionary_weight.min(100);
93        let strategy = proptest::prop_oneof![
94            100 - dictionary_weight => fuzz_calldata(func.clone(), fuzz_fixtures),
95            dictionary_weight => fuzz_calldata_from_state(func.clone(), &state),
96        ];
97        // We want to collect at least one trace which will be displayed to user.
98        let max_traces_to_collect = std::cmp::max(1, self.config.gas_report_samples) as usize;
99        let show_logs = self.config.show_logs;
100
101        // Start timer for this fuzz test.
102        let timer = FuzzTestTimer::new(self.config.timeout);
103
104        let run_result = self.runner.clone().run(&strategy, |calldata| {
105            // Check if the timeout has been reached.
106            if timer.is_timed_out() {
107                return Err(TestCaseError::fail(TEST_TIMEOUT));
108            }
109
110            let fuzz_res = self.single_fuzz(address, calldata)?;
111
112            // If running with progress then increment current run.
113            if let Some(progress) = progress {
114                progress.inc(1);
115            };
116
117            match fuzz_res {
118                FuzzOutcome::Case(case) => {
119                    let mut data = execution_data.borrow_mut();
120                    data.gas_by_case.push((case.case.gas, case.case.stipend));
121
122                    if data.first_case.is_none() {
123                        data.first_case.replace(case.case);
124                    }
125
126                    if let Some(call_traces) = case.traces {
127                        if data.traces.len() == max_traces_to_collect {
128                            data.traces.pop();
129                        }
130                        data.traces.push(call_traces);
131                        data.breakpoints.replace(case.breakpoints);
132                    }
133
134                    if show_logs {
135                        data.logs.extend(case.logs);
136                    }
137
138                    HitMaps::merge_opt(&mut data.coverage, case.coverage);
139
140                    data.deprecated_cheatcodes = case.deprecated_cheatcodes;
141
142                    Ok(())
143                }
144                FuzzOutcome::CounterExample(CounterExampleOutcome {
145                    exit_reason: status,
146                    counterexample: outcome,
147                    ..
148                }) => {
149                    // We cannot use the calldata returned by the test runner in `TestError::Fail`,
150                    // since that input represents the last run case, which may not correspond with
151                    // our failure - when a fuzz case fails, proptest will try to run at least one
152                    // more case to find a minimal failure case.
153                    let reason = rd.maybe_decode(&outcome.1.result, Some(status));
154                    execution_data.borrow_mut().logs.extend(outcome.1.logs.clone());
155                    execution_data.borrow_mut().counterexample = outcome;
156                    // HACK: we have to use an empty string here to denote `None`.
157                    Err(TestCaseError::fail(reason.unwrap_or_default()))
158                }
159            }
160        });
161
162        let fuzz_result = execution_data.into_inner();
163        let (calldata, call) = fuzz_result.counterexample;
164
165        let mut traces = fuzz_result.traces;
166        let (last_run_traces, last_run_breakpoints) = if run_result.is_ok() {
167            (traces.pop(), fuzz_result.breakpoints)
168        } else {
169            (call.traces.clone(), call.cheatcodes.map(|c| c.breakpoints))
170        };
171
172        let mut result = FuzzTestResult {
173            first_case: fuzz_result.first_case.unwrap_or_default(),
174            gas_by_case: fuzz_result.gas_by_case,
175            success: run_result.is_ok(),
176            skipped: false,
177            reason: None,
178            counterexample: None,
179            logs: fuzz_result.logs,
180            labeled_addresses: call.labels,
181            traces: last_run_traces,
182            breakpoints: last_run_breakpoints,
183            gas_report_traces: traces.into_iter().map(|a| a.arena).collect(),
184            coverage: fuzz_result.coverage,
185            deprecated_cheatcodes: fuzz_result.deprecated_cheatcodes,
186        };
187
188        match run_result {
189            Ok(()) => {}
190            Err(TestError::Abort(reason)) => {
191                let msg = reason.message();
192                // Currently the only operation that can trigger proptest global rejects is the
193                // `vm.assume` cheatcode, thus we surface this info to the user when the fuzz test
194                // aborts due to too many global rejects, making the error message more actionable.
195                result.reason = if msg == "Too many global rejects" {
196                    let error = FuzzError::TooManyRejects(self.runner.config().max_global_rejects);
197                    Some(error.to_string())
198                } else {
199                    Some(msg.to_string())
200                };
201            }
202            Err(TestError::Fail(reason, _)) => {
203                let reason = reason.to_string();
204                if reason == TEST_TIMEOUT {
205                    // If the reason is a timeout, we consider the fuzz test successful.
206                    result.success = true;
207                } else {
208                    result.reason = (!reason.is_empty()).then_some(reason);
209                    let args = if let Some(data) = calldata.get(4..) {
210                        func.abi_decode_input(data, false).unwrap_or_default()
211                    } else {
212                        vec![]
213                    };
214
215                    result.counterexample = Some(CounterExample::Single(
216                        BaseCounterExample::from_fuzz_call(calldata, args, call.traces),
217                    ));
218                }
219            }
220        }
221
222        if let Some(reason) = &result.reason {
223            if let Some(reason) = SkipReason::decode_self(reason) {
224                result.skipped = true;
225                result.reason = reason.0;
226            }
227        }
228
229        state.log_stats();
230
231        result
232    }
233
234    /// Granular and single-step function that runs only one fuzz and returns either a `CaseOutcome`
235    /// or a `CounterExampleOutcome`
236    pub fn single_fuzz(
237        &self,
238        address: Address,
239        calldata: alloy_primitives::Bytes,
240    ) -> Result<FuzzOutcome, TestCaseError> {
241        let mut call = self
242            .executor
243            .call_raw(self.sender, address, calldata.clone(), U256::ZERO)
244            .map_err(|e| TestCaseError::fail(e.to_string()))?;
245
246        // Handle `vm.assume`.
247        if call.result.as_ref() == MAGIC_ASSUME {
248            return Err(TestCaseError::reject(FuzzError::AssumeReject))
249        }
250
251        let (breakpoints, deprecated_cheatcodes) =
252            call.cheatcodes.as_ref().map_or_else(Default::default, |cheats| {
253                (cheats.breakpoints.clone(), cheats.deprecated.clone())
254            });
255
256        let success = self.executor.is_raw_call_mut_success(address, &mut call, false);
257        if success {
258            Ok(FuzzOutcome::Case(CaseOutcome {
259                case: FuzzCase { calldata, gas: call.gas_used, stipend: call.stipend },
260                traces: call.traces,
261                coverage: call.coverage,
262                breakpoints,
263                logs: call.logs,
264                deprecated_cheatcodes,
265            }))
266        } else {
267            Ok(FuzzOutcome::CounterExample(CounterExampleOutcome {
268                exit_reason: call.exit_reason,
269                counterexample: (calldata, call),
270                breakpoints,
271            }))
272        }
273    }
274
275    /// Stores fuzz state for use with [fuzz_calldata_from_state]
276    pub fn build_fuzz_state(&self, deployed_libs: &[Address]) -> EvmFuzzState {
277        if let Some(fork_db) = self.executor.backend().active_fork_db() {
278            EvmFuzzState::new(fork_db, self.config.dictionary, deployed_libs)
279        } else {
280            EvmFuzzState::new(
281                self.executor.backend().mem_db(),
282                self.config.dictionary,
283                deployed_libs,
284            )
285        }
286    }
287}