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