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