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