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