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