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