1#![cfg_attr(not(test), warn(unused_crate_dependencies))]
6#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))]
7
8#[macro_use]
9extern crate tracing;
10
11use alloy_dyn_abi::{DynSolValue, JsonAbiExt};
12use alloy_primitives::{
13 map::{AddressHashMap, HashMap},
14 Address, Bytes, Log,
15};
16use foundry_common::{calc, contracts::ContractsByAddress, evm::Breakpoints};
17use foundry_evm_coverage::HitMaps;
18use foundry_evm_traces::{CallTraceArena, SparsedTraceArena};
19use itertools::Itertools;
20use serde::{Deserialize, Serialize};
21use std::{fmt, sync::Arc};
22
23pub use proptest::test_runner::{Config as FuzzConfig, Reason};
24
25mod error;
26pub use error::FuzzError;
27
28pub mod invariant;
29pub mod strategies;
30
31mod inspector;
32pub use inspector::Fuzzer;
33
34#[derive(Clone, Debug, Serialize, Deserialize)]
35#[expect(clippy::large_enum_variant)]
36pub enum CounterExample {
37 Single(BaseCounterExample),
39 Sequence(usize, Vec<BaseCounterExample>),
41}
42
43#[derive(Clone, Debug, Serialize, Deserialize)]
44pub struct BaseCounterExample {
45 pub sender: Option<Address>,
47 pub addr: Option<Address>,
49 pub calldata: Bytes,
51 pub contract_name: Option<String>,
53 pub func_name: Option<String>,
55 pub signature: Option<String>,
57 pub args: Option<String>,
59 pub raw_args: Option<String>,
61 #[serde(skip)]
63 pub traces: Option<SparsedTraceArena>,
64 #[serde(skip)]
66 pub show_solidity: bool,
67}
68
69impl BaseCounterExample {
70 pub fn from_invariant_call(
72 sender: Address,
73 addr: Address,
74 bytes: &Bytes,
75 contracts: &ContractsByAddress,
76 traces: Option<SparsedTraceArena>,
77 show_solidity: bool,
78 ) -> Self {
79 if let Some((name, abi)) = &contracts.get(&addr) {
80 if let Some(func) = abi.functions().find(|f| f.selector() == bytes[..4]) {
81 if let Ok(args) = func.abi_decode_input(&bytes[4..], false) {
83 return Self {
84 sender: Some(sender),
85 addr: Some(addr),
86 calldata: bytes.clone(),
87 contract_name: Some(name.clone()),
88 func_name: Some(func.name.clone()),
89 signature: Some(func.signature()),
90 args: Some(
91 foundry_common::fmt::format_tokens(&args).format(", ").to_string(),
92 ),
93 raw_args: Some(
94 foundry_common::fmt::format_tokens_raw(&args).format(", ").to_string(),
95 ),
96 traces,
97 show_solidity,
98 };
99 }
100 }
101 }
102
103 Self {
104 sender: Some(sender),
105 addr: Some(addr),
106 calldata: bytes.clone(),
107 contract_name: None,
108 func_name: None,
109 signature: None,
110 args: None,
111 raw_args: None,
112 traces,
113 show_solidity: false,
114 }
115 }
116
117 pub fn from_fuzz_call(
119 bytes: Bytes,
120 args: Vec<DynSolValue>,
121 traces: Option<SparsedTraceArena>,
122 ) -> Self {
123 Self {
124 sender: None,
125 addr: None,
126 calldata: bytes,
127 contract_name: None,
128 func_name: None,
129 signature: None,
130 args: Some(foundry_common::fmt::format_tokens(&args).format(", ").to_string()),
131 raw_args: Some(foundry_common::fmt::format_tokens_raw(&args).format(", ").to_string()),
132 traces,
133 show_solidity: false,
134 }
135 }
136}
137
138impl fmt::Display for BaseCounterExample {
139 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
140 if self.show_solidity {
142 if let (Some(sender), Some(contract), Some(address), Some(func_name), Some(args)) =
143 (&self.sender, &self.contract_name, &self.addr, &self.func_name, &self.raw_args)
144 {
145 writeln!(f, "\t\tvm.prank({sender});")?;
146 write!(
147 f,
148 "\t\t{}({}).{}({});",
149 contract.split_once(':').map_or(contract.as_str(), |(_, contract)| contract),
150 address,
151 func_name,
152 args
153 )?;
154
155 return Ok(())
156 }
157 }
158
159 if let Some(sender) = self.sender {
161 write!(f, "\t\tsender={sender} addr=")?
162 }
163
164 if let Some(name) = &self.contract_name {
165 write!(f, "[{name}]")?
166 }
167
168 if let Some(addr) = &self.addr {
169 write!(f, "{addr} ")?
170 }
171
172 if let Some(sig) = &self.signature {
173 write!(f, "calldata={sig}")?
174 } else {
175 write!(f, "calldata={}", &self.calldata)?
176 }
177
178 if let Some(args) = &self.args {
179 write!(f, " args=[{args}]")
180 } else {
181 write!(f, " args=[]")
182 }
183 }
184}
185
186#[derive(Debug)]
188pub struct FuzzTestResult {
189 pub first_case: FuzzCase,
191 pub gas_by_case: Vec<(u64, u64)>,
193 pub success: bool,
197 pub skipped: bool,
199
200 pub reason: Option<String>,
203
204 pub counterexample: Option<CounterExample>,
206
207 pub logs: Vec<Log>,
210
211 pub labeled_addresses: AddressHashMap<String>,
213
214 pub traces: Option<SparsedTraceArena>,
219
220 pub gas_report_traces: Vec<CallTraceArena>,
223
224 pub coverage: Option<HitMaps>,
226
227 pub breakpoints: Option<Breakpoints>,
229
230 pub deprecated_cheatcodes: HashMap<&'static str, Option<&'static str>>,
232}
233
234impl FuzzTestResult {
235 pub fn median_gas(&self, with_stipend: bool) -> u64 {
237 let mut values = self.gas_values(with_stipend);
238 values.sort_unstable();
239 calc::median_sorted(&values)
240 }
241
242 pub fn mean_gas(&self, with_stipend: bool) -> u64 {
244 let mut values = self.gas_values(with_stipend);
245 values.sort_unstable();
246 calc::mean(&values)
247 }
248
249 fn gas_values(&self, with_stipend: bool) -> Vec<u64> {
250 self.gas_by_case
251 .iter()
252 .map(|gas| if with_stipend { gas.0 } else { gas.0.saturating_sub(gas.1) })
253 .collect()
254 }
255}
256
257#[derive(Clone, Debug, Default, Serialize, Deserialize)]
259pub struct FuzzCase {
260 pub calldata: Bytes,
262 pub gas: u64,
264 pub stipend: u64,
266}
267
268#[derive(Clone, Debug, Serialize, Deserialize)]
270#[serde(transparent)]
271pub struct FuzzedCases {
272 cases: Vec<FuzzCase>,
273}
274
275impl FuzzedCases {
276 #[inline]
277 pub fn new(mut cases: Vec<FuzzCase>) -> Self {
278 cases.sort_by_key(|c| c.gas);
279 Self { cases }
280 }
281
282 #[inline]
283 pub fn cases(&self) -> &[FuzzCase] {
284 &self.cases
285 }
286
287 #[inline]
288 pub fn into_cases(self) -> Vec<FuzzCase> {
289 self.cases
290 }
291
292 #[inline]
294 pub fn last(&self) -> Option<&FuzzCase> {
295 self.cases.last()
296 }
297
298 #[inline]
300 pub fn median_gas(&self, with_stipend: bool) -> u64 {
301 let mut values = self.gas_values(with_stipend);
302 values.sort_unstable();
303 calc::median_sorted(&values)
304 }
305
306 #[inline]
308 pub fn mean_gas(&self, with_stipend: bool) -> u64 {
309 let mut values = self.gas_values(with_stipend);
310 values.sort_unstable();
311 calc::mean(&values)
312 }
313
314 #[inline]
315 fn gas_values(&self, with_stipend: bool) -> Vec<u64> {
316 self.cases
317 .iter()
318 .map(|c| if with_stipend { c.gas } else { c.gas.saturating_sub(c.stipend) })
319 .collect()
320 }
321
322 #[inline]
324 pub fn highest(&self) -> Option<&FuzzCase> {
325 self.cases.last()
326 }
327
328 #[inline]
330 pub fn lowest(&self) -> Option<&FuzzCase> {
331 self.cases.first()
332 }
333
334 #[inline]
336 pub fn highest_gas(&self, with_stipend: bool) -> u64 {
337 self.highest()
338 .map(|c| if with_stipend { c.gas } else { c.gas - c.stipend })
339 .unwrap_or_default()
340 }
341
342 #[inline]
344 pub fn lowest_gas(&self) -> u64 {
345 self.lowest().map(|c| c.gas).unwrap_or_default()
346 }
347}
348
349#[derive(Clone, Default, Debug)]
356pub struct FuzzFixtures {
357 inner: Arc<HashMap<String, DynSolValue>>,
358}
359
360impl FuzzFixtures {
361 pub fn new(fixtures: HashMap<String, DynSolValue>) -> Self {
362 Self { inner: Arc::new(fixtures) }
363 }
364
365 pub fn param_fixtures(&self, param_name: &str) -> Option<&[DynSolValue]> {
367 if let Some(param_fixtures) = self.inner.get(&normalize_fixture(param_name)) {
368 param_fixtures.as_fixed_array().or_else(|| param_fixtures.as_array())
369 } else {
370 None
371 }
372 }
373}
374
375pub fn fixture_name(function_name: String) -> String {
378 normalize_fixture(function_name.strip_prefix("fixture").unwrap())
379}
380
381fn normalize_fixture(param_name: &str) -> String {
383 param_name.trim_matches('_').to_ascii_lowercase()
384}