foundry_evm_fuzz/
lib.rs

1//! # foundry-evm-fuzz
2//!
3//! EVM fuzzing implementation using [`proptest`].
4
5#![cfg_attr(not(test), warn(unused_crate_dependencies))]
6#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))]
7
8#[macro_use]
9extern crate tracing;
10
11use alloy_dyn_abi::{DynSolValue, JsonAbiExt};
12use alloy_primitives::{
13    Address, Bytes, Log,
14    map::{AddressHashMap, HashMap},
15};
16use foundry_common::{calc, contracts::ContractsByAddress, evm::Breakpoints};
17use foundry_evm_coverage::HitMaps;
18use foundry_evm_traces::{CallTraceArena, SparsedTraceArena};
19use itertools::Itertools;
20use serde::{Deserialize, Serialize};
21use std::{fmt, sync::Arc};
22
23pub use proptest::test_runner::{Config as FuzzConfig, Reason};
24
25mod error;
26pub use error::FuzzError;
27
28pub mod invariant;
29pub mod strategies;
30
31mod inspector;
32pub use inspector::Fuzzer;
33
34/// Details of a transaction generated by fuzz strategy for fuzzing a target.
35#[derive(Clone, Debug, Serialize, Deserialize)]
36pub struct BasicTxDetails {
37    // Transaction sender address.
38    pub sender: Address,
39    // Transaction call details.
40    pub call_details: CallDetails,
41}
42
43/// Call details of a transaction generated to fuzz.
44#[derive(Clone, Debug, Serialize, Deserialize)]
45pub struct CallDetails {
46    // Address of target contract.
47    pub target: Address,
48    // The data of the transaction.
49    pub calldata: Bytes,
50}
51
52#[derive(Clone, Debug, Serialize, Deserialize)]
53#[expect(clippy::large_enum_variant)]
54pub enum CounterExample {
55    /// Call used as a counter example for fuzz tests.
56    Single(BaseCounterExample),
57    /// Original sequence size and sequence of calls used as a counter example for invariant tests.
58    Sequence(usize, Vec<BaseCounterExample>),
59}
60
61#[derive(Clone, Debug, Serialize, Deserialize)]
62pub struct BaseCounterExample {
63    /// Address which makes the call.
64    pub sender: Option<Address>,
65    /// Address to which to call to.
66    pub addr: Option<Address>,
67    /// The data to provide.
68    pub calldata: Bytes,
69    /// Contract name if it exists.
70    pub contract_name: Option<String>,
71    /// Function name if it exists.
72    pub func_name: Option<String>,
73    /// Function signature if it exists.
74    pub signature: Option<String>,
75    /// Pretty formatted args used to call the function.
76    pub args: Option<String>,
77    /// Unformatted args used to call the function.
78    pub raw_args: Option<String>,
79    /// Counter example traces.
80    #[serde(skip)]
81    pub traces: Option<SparsedTraceArena>,
82    /// Whether to display sequence as solidity.
83    #[serde(skip)]
84    pub show_solidity: bool,
85}
86
87impl BaseCounterExample {
88    /// Creates counter example representing a step from invariant call sequence.
89    pub fn from_invariant_call(
90        sender: Address,
91        addr: Address,
92        bytes: &Bytes,
93        contracts: &ContractsByAddress,
94        traces: Option<SparsedTraceArena>,
95        show_solidity: bool,
96    ) -> Self {
97        if let Some((name, abi)) = &contracts.get(&addr)
98            && let Some(func) = abi.functions().find(|f| f.selector() == bytes[..4])
99        {
100            // skip the function selector when decoding
101            if let Ok(args) = func.abi_decode_input(&bytes[4..]) {
102                return Self {
103                    sender: Some(sender),
104                    addr: Some(addr),
105                    calldata: bytes.clone(),
106                    contract_name: Some(name.clone()),
107                    func_name: Some(func.name.clone()),
108                    signature: Some(func.signature()),
109                    args: Some(foundry_common::fmt::format_tokens(&args).format(", ").to_string()),
110                    raw_args: Some(
111                        foundry_common::fmt::format_tokens_raw(&args).format(", ").to_string(),
112                    ),
113                    traces,
114                    show_solidity,
115                };
116            }
117        }
118
119        Self {
120            sender: Some(sender),
121            addr: Some(addr),
122            calldata: bytes.clone(),
123            contract_name: None,
124            func_name: None,
125            signature: None,
126            args: None,
127            raw_args: None,
128            traces,
129            show_solidity: false,
130        }
131    }
132
133    /// Creates counter example for a fuzz test failure.
134    pub fn from_fuzz_call(
135        bytes: Bytes,
136        args: Vec<DynSolValue>,
137        traces: Option<SparsedTraceArena>,
138    ) -> Self {
139        Self {
140            sender: None,
141            addr: None,
142            calldata: bytes,
143            contract_name: None,
144            func_name: None,
145            signature: None,
146            args: Some(foundry_common::fmt::format_tokens(&args).format(", ").to_string()),
147            raw_args: Some(foundry_common::fmt::format_tokens_raw(&args).format(", ").to_string()),
148            traces,
149            show_solidity: false,
150        }
151    }
152}
153
154impl fmt::Display for BaseCounterExample {
155    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
156        // Display counterexample as solidity.
157        if self.show_solidity
158            && let (Some(sender), Some(contract), Some(address), Some(func_name), Some(args)) =
159                (&self.sender, &self.contract_name, &self.addr, &self.func_name, &self.raw_args)
160        {
161            writeln!(f, "\t\tvm.prank({sender});")?;
162            write!(
163                f,
164                "\t\t{}({}).{}({});",
165                contract.split_once(':').map_or(contract.as_str(), |(_, contract)| contract),
166                address,
167                func_name,
168                args
169            )?;
170
171            return Ok(());
172        }
173
174        // Regular counterexample display.
175        if let Some(sender) = self.sender {
176            write!(f, "\t\tsender={sender} addr=")?
177        }
178
179        if let Some(name) = &self.contract_name {
180            write!(f, "[{name}]")?
181        }
182
183        if let Some(addr) = &self.addr {
184            write!(f, "{addr} ")?
185        }
186
187        if let Some(sig) = &self.signature {
188            write!(f, "calldata={sig}")?
189        } else {
190            write!(f, "calldata={}", &self.calldata)?
191        }
192
193        if let Some(args) = &self.args {
194            write!(f, " args=[{args}]")
195        } else {
196            write!(f, " args=[]")
197        }
198    }
199}
200
201/// The outcome of a fuzz test
202#[derive(Debug)]
203pub struct FuzzTestResult {
204    /// we keep this for the debugger
205    pub first_case: FuzzCase,
206    /// Gas usage (gas_used, call_stipend) per cases
207    pub gas_by_case: Vec<(u64, u64)>,
208    /// Whether the test case was successful. This means that the transaction executed
209    /// properly, or that there was a revert and that the test was expected to fail
210    /// (prefixed with `testFail`)
211    pub success: bool,
212    /// Whether the test case was skipped. `reason` will contain the skip reason, if any.
213    pub skipped: bool,
214
215    /// If there was a revert, this field will be populated. Note that the test can
216    /// still be successful (i.e self.success == true) when it's expected to fail.
217    pub reason: Option<String>,
218
219    /// Minimal reproduction test case for failing fuzz tests
220    pub counterexample: Option<CounterExample>,
221
222    /// Any captured & parsed as strings logs along the test's execution which should
223    /// be printed to the user.
224    pub logs: Vec<Log>,
225
226    /// Labeled addresses
227    pub labels: AddressHashMap<String>,
228
229    /// Exemplary traces for a fuzz run of the test function
230    ///
231    /// **Note** We only store a single trace of a successful fuzz call, otherwise we would get
232    /// `num(fuzz_cases)` traces, one for each run, which is neither helpful nor performant.
233    pub traces: Option<SparsedTraceArena>,
234
235    /// Additional traces used for gas report construction.
236    /// Those traces should not be displayed.
237    pub gas_report_traces: Vec<CallTraceArena>,
238
239    /// Raw line coverage info
240    pub line_coverage: Option<HitMaps>,
241
242    /// Breakpoints for debugger. Correspond to the same fuzz case as `traces`.
243    pub breakpoints: Option<Breakpoints>,
244
245    // Deprecated cheatcodes mapped to their replacements.
246    pub deprecated_cheatcodes: HashMap<&'static str, Option<&'static str>>,
247
248    /// NUmber of failed replays from persisted corpus.
249    pub failed_corpus_replays: usize,
250}
251
252impl FuzzTestResult {
253    /// Returns the median gas of all test cases
254    pub fn median_gas(&self, with_stipend: bool) -> u64 {
255        let mut values = self.gas_values(with_stipend);
256        values.sort_unstable();
257        calc::median_sorted(&values)
258    }
259
260    /// Returns the average gas use of all test cases
261    pub fn mean_gas(&self, with_stipend: bool) -> u64 {
262        let mut values = self.gas_values(with_stipend);
263        values.sort_unstable();
264        calc::mean(&values)
265    }
266
267    fn gas_values(&self, with_stipend: bool) -> Vec<u64> {
268        self.gas_by_case
269            .iter()
270            .map(|gas| if with_stipend { gas.0 } else { gas.0.saturating_sub(gas.1) })
271            .collect()
272    }
273}
274
275/// Data of a single fuzz test case
276#[derive(Clone, Debug, Default, Serialize, Deserialize)]
277pub struct FuzzCase {
278    /// The calldata used for this fuzz test
279    pub calldata: Bytes,
280    /// Consumed gas
281    pub gas: u64,
282    /// The initial gas stipend for the transaction
283    pub stipend: u64,
284}
285
286/// Container type for all successful test cases
287#[derive(Clone, Debug, Serialize, Deserialize)]
288#[serde(transparent)]
289pub struct FuzzedCases {
290    cases: Vec<FuzzCase>,
291}
292
293impl FuzzedCases {
294    pub fn new(mut cases: Vec<FuzzCase>) -> Self {
295        cases.sort_by_key(|c| c.gas);
296        Self { cases }
297    }
298
299    pub fn cases(&self) -> &[FuzzCase] {
300        &self.cases
301    }
302
303    pub fn into_cases(self) -> Vec<FuzzCase> {
304        self.cases
305    }
306
307    /// Get the last [FuzzCase]
308    pub fn last(&self) -> Option<&FuzzCase> {
309        self.cases.last()
310    }
311
312    /// Returns the median gas of all test cases
313    pub fn median_gas(&self, with_stipend: bool) -> u64 {
314        let mut values = self.gas_values(with_stipend);
315        values.sort_unstable();
316        calc::median_sorted(&values)
317    }
318
319    /// Returns the average gas use of all test cases
320    pub fn mean_gas(&self, with_stipend: bool) -> u64 {
321        let mut values = self.gas_values(with_stipend);
322        values.sort_unstable();
323        calc::mean(&values)
324    }
325
326    fn gas_values(&self, with_stipend: bool) -> Vec<u64> {
327        self.cases
328            .iter()
329            .map(|c| if with_stipend { c.gas } else { c.gas.saturating_sub(c.stipend) })
330            .collect()
331    }
332
333    /// Returns the case with the highest gas usage
334    pub fn highest(&self) -> Option<&FuzzCase> {
335        self.cases.last()
336    }
337
338    /// Returns the case with the lowest gas usage
339    pub fn lowest(&self) -> Option<&FuzzCase> {
340        self.cases.first()
341    }
342
343    /// Returns the highest amount of gas spent on a fuzz case
344    pub fn highest_gas(&self, with_stipend: bool) -> u64 {
345        self.highest()
346            .map(|c| if with_stipend { c.gas } else { c.gas - c.stipend })
347            .unwrap_or_default()
348    }
349
350    /// Returns the lowest amount of gas spent on a fuzz case
351    pub fn lowest_gas(&self) -> u64 {
352        self.lowest().map(|c| c.gas).unwrap_or_default()
353    }
354}
355
356/// Fixtures to be used for fuzz tests.
357///
358/// The key represents name of the fuzzed parameter, value holds possible fuzzed values.
359/// For example, for a fixture function declared as
360/// `function fixture_sender() external returns (address[] memory senders)`
361/// the fuzz fixtures will contain `sender` key with `senders` array as value
362#[derive(Clone, Default, Debug)]
363pub struct FuzzFixtures {
364    inner: Arc<HashMap<String, DynSolValue>>,
365}
366
367impl FuzzFixtures {
368    pub fn new(fixtures: HashMap<String, DynSolValue>) -> Self {
369        Self { inner: Arc::new(fixtures) }
370    }
371
372    /// Returns configured fixtures for `param_name` fuzzed parameter.
373    pub fn param_fixtures(&self, param_name: &str) -> Option<&[DynSolValue]> {
374        if let Some(param_fixtures) = self.inner.get(&normalize_fixture(param_name)) {
375            param_fixtures.as_fixed_array().or_else(|| param_fixtures.as_array())
376        } else {
377            None
378        }
379    }
380}
381
382/// Extracts fixture name from a function name.
383/// For example: fixtures defined in `fixture_Owner` function will be applied for `owner` parameter.
384pub fn fixture_name(function_name: String) -> String {
385    normalize_fixture(function_name.strip_prefix("fixture").unwrap())
386}
387
388/// Normalize fixture parameter name, for example `_Owner` to `owner`.
389fn normalize_fixture(param_name: &str) -> String {
390    param_name.trim_matches('_').to_ascii_lowercase()
391}