1#![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#[derive(Clone, Debug, Serialize, Deserialize)]
38pub struct BasicTxDetails {
39 #[serde(default, skip_serializing_if = "Option::is_none")]
41 pub warp: Option<U256>,
42 #[serde(default, skip_serializing_if = "Option::is_none")]
44 pub roll: Option<U256>,
45 pub sender: Address,
47 #[serde(flatten)]
49 pub call_details: CallDetails,
50}
51
52#[derive(Clone, Debug, Serialize, Deserialize)]
54pub struct CallDetails {
55 pub target: Address,
57 pub calldata: Bytes,
59}
60
61impl BasicTxDetails {
62 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 Single(BaseCounterExample),
73 Sequence(usize, Vec<BaseCounterExample>),
75}
76
77#[derive(Clone, Debug, Serialize, Deserialize)]
78pub struct BaseCounterExample {
79 pub warp: Option<U256>,
81 pub roll: Option<U256>,
83 pub sender: Option<Address>,
85 pub addr: Option<Address>,
87 pub calldata: Bytes,
89 pub contract_name: Option<String>,
91 pub func_name: Option<String>,
93 pub signature: Option<String>,
95 pub args: Option<String>,
97 pub raw_args: Option<String>,
99 #[serde(skip)]
101 pub traces: Option<SparsedTraceArena>,
102 #[serde(skip)]
104 pub show_solidity: bool,
105}
106
107impl BaseCounterExample {
108 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 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 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 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 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#[derive(Debug, Default)]
245pub struct FuzzTestResult {
246 pub first_case: FuzzCase,
248 pub gas_by_case: Vec<(u64, u64)>,
250 pub success: bool,
254 pub skipped: bool,
256
257 pub reason: Option<String>,
260
261 pub counterexample: Option<CounterExample>,
263
264 pub logs: Vec<Log>,
267
268 pub labels: AddressHashMap<String>,
270
271 pub traces: Option<SparsedTraceArena>,
276
277 pub gas_report_traces: Vec<CallTraceArena>,
280
281 pub line_coverage: Option<HitMaps>,
283
284 pub breakpoints: Option<Breakpoints>,
286
287 pub deprecated_cheatcodes: HashMap<&'static str, Option<&'static str>>,
289
290 pub failed_corpus_replays: usize,
292}
293
294impl FuzzTestResult {
295 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 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#[derive(Clone, Debug, Default, Serialize, Deserialize)]
319pub struct FuzzCase {
320 pub gas: u64,
322 pub stipend: u64,
324}
325
326#[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 pub fn last(&self) -> Option<&FuzzCase> {
349 self.cases.last()
350 }
351
352 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 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 pub fn highest(&self) -> Option<&FuzzCase> {
375 self.cases.last()
376 }
377
378 pub fn lowest(&self) -> Option<&FuzzCase> {
380 self.cases.first()
381 }
382
383 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 pub fn lowest_gas(&self) -> u64 {
392 self.lowest().map(|c| c.gas).unwrap_or_default()
393 }
394}
395
396#[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 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
422pub fn fixture_name(function_name: String) -> String {
425 normalize_fixture(function_name.strip_prefix("fixture").unwrap())
426}
427
428fn normalize_fixture(param_name: &str) -> String {
430 param_name.trim_matches('_').to_ascii_lowercase()
431}