Skip to main content

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