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 Address, Bytes, Log,
14 map::{AddressHashMap, HashMap},
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)]
36pub struct BasicTxDetails {
37 pub sender: Address,
39 pub call_details: CallDetails,
41}
42
43#[derive(Clone, Debug, Serialize, Deserialize)]
45pub struct CallDetails {
46 pub target: Address,
48 pub calldata: Bytes,
50}
51
52#[derive(Clone, Debug, Serialize, Deserialize)]
53#[expect(clippy::large_enum_variant)]
54pub enum CounterExample {
55 Single(BaseCounterExample),
57 Sequence(usize, Vec<BaseCounterExample>),
59}
60
61#[derive(Clone, Debug, Serialize, Deserialize)]
62pub struct BaseCounterExample {
63 pub sender: Option<Address>,
65 pub addr: Option<Address>,
67 pub calldata: Bytes,
69 pub contract_name: Option<String>,
71 pub func_name: Option<String>,
73 pub signature: Option<String>,
75 pub args: Option<String>,
77 pub raw_args: Option<String>,
79 #[serde(skip)]
81 pub traces: Option<SparsedTraceArena>,
82 #[serde(skip)]
84 pub show_solidity: bool,
85}
86
87impl BaseCounterExample {
88 pub fn from_invariant_call(
90 sender: Address,
91 addr: Address,
92 bytes: &Bytes,
93 contracts: &ContractsByAddress,
94 traces: Option<SparsedTraceArena>,
95 show_solidity: bool,
96 ) -> Self {
97 if let Some((name, abi)) = &contracts.get(&addr)
98 && let Some(func) = abi.functions().find(|f| f.selector() == bytes[..4])
99 {
100 if let Ok(args) = func.abi_decode_input(&bytes[4..]) {
102 return Self {
103 sender: Some(sender),
104 addr: Some(addr),
105 calldata: bytes.clone(),
106 contract_name: Some(name.clone()),
107 func_name: Some(func.name.clone()),
108 signature: Some(func.signature()),
109 args: Some(foundry_common::fmt::format_tokens(&args).format(", ").to_string()),
110 raw_args: Some(
111 foundry_common::fmt::format_tokens_raw(&args).format(", ").to_string(),
112 ),
113 traces,
114 show_solidity,
115 };
116 }
117 }
118
119 Self {
120 sender: Some(sender),
121 addr: Some(addr),
122 calldata: bytes.clone(),
123 contract_name: None,
124 func_name: None,
125 signature: None,
126 args: None,
127 raw_args: None,
128 traces,
129 show_solidity: false,
130 }
131 }
132
133 pub fn from_fuzz_call(
135 bytes: Bytes,
136 args: Vec<DynSolValue>,
137 traces: Option<SparsedTraceArena>,
138 ) -> Self {
139 Self {
140 sender: None,
141 addr: None,
142 calldata: bytes,
143 contract_name: None,
144 func_name: None,
145 signature: None,
146 args: Some(foundry_common::fmt::format_tokens(&args).format(", ").to_string()),
147 raw_args: Some(foundry_common::fmt::format_tokens_raw(&args).format(", ").to_string()),
148 traces,
149 show_solidity: false,
150 }
151 }
152}
153
154impl fmt::Display for BaseCounterExample {
155 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
156 if self.show_solidity
158 && let (Some(sender), Some(contract), Some(address), Some(func_name), Some(args)) =
159 (&self.sender, &self.contract_name, &self.addr, &self.func_name, &self.raw_args)
160 {
161 writeln!(f, "\t\tvm.prank({sender});")?;
162 write!(
163 f,
164 "\t\t{}({}).{}({});",
165 contract.split_once(':').map_or(contract.as_str(), |(_, contract)| contract),
166 address,
167 func_name,
168 args
169 )?;
170
171 return Ok(());
172 }
173
174 if let Some(sender) = self.sender {
176 write!(f, "\t\tsender={sender} addr=")?
177 }
178
179 if let Some(name) = &self.contract_name {
180 write!(f, "[{name}]")?
181 }
182
183 if let Some(addr) = &self.addr {
184 write!(f, "{addr} ")?
185 }
186
187 if let Some(sig) = &self.signature {
188 write!(f, "calldata={sig}")?
189 } else {
190 write!(f, "calldata={}", &self.calldata)?
191 }
192
193 if let Some(args) = &self.args {
194 write!(f, " args=[{args}]")
195 } else {
196 write!(f, " args=[]")
197 }
198 }
199}
200
201#[derive(Debug)]
203pub struct FuzzTestResult {
204 pub first_case: FuzzCase,
206 pub gas_by_case: Vec<(u64, u64)>,
208 pub success: bool,
212 pub skipped: bool,
214
215 pub reason: Option<String>,
218
219 pub counterexample: Option<CounterExample>,
221
222 pub logs: Vec<Log>,
225
226 pub labels: AddressHashMap<String>,
228
229 pub traces: Option<SparsedTraceArena>,
234
235 pub gas_report_traces: Vec<CallTraceArena>,
238
239 pub line_coverage: Option<HitMaps>,
241
242 pub breakpoints: Option<Breakpoints>,
244
245 pub deprecated_cheatcodes: HashMap<&'static str, Option<&'static str>>,
247
248 pub failed_corpus_replays: usize,
250}
251
252impl FuzzTestResult {
253 pub fn median_gas(&self, with_stipend: bool) -> u64 {
255 let mut values = self.gas_values(with_stipend);
256 values.sort_unstable();
257 calc::median_sorted(&values)
258 }
259
260 pub fn mean_gas(&self, with_stipend: bool) -> u64 {
262 let mut values = self.gas_values(with_stipend);
263 values.sort_unstable();
264 calc::mean(&values)
265 }
266
267 fn gas_values(&self, with_stipend: bool) -> Vec<u64> {
268 self.gas_by_case
269 .iter()
270 .map(|gas| if with_stipend { gas.0 } else { gas.0.saturating_sub(gas.1) })
271 .collect()
272 }
273}
274
275#[derive(Clone, Debug, Default, Serialize, Deserialize)]
277pub struct FuzzCase {
278 pub calldata: Bytes,
280 pub gas: u64,
282 pub stipend: u64,
284}
285
286#[derive(Clone, Debug, Serialize, Deserialize)]
288#[serde(transparent)]
289pub struct FuzzedCases {
290 cases: Vec<FuzzCase>,
291}
292
293impl FuzzedCases {
294 pub fn new(mut cases: Vec<FuzzCase>) -> Self {
295 cases.sort_by_key(|c| c.gas);
296 Self { cases }
297 }
298
299 pub fn cases(&self) -> &[FuzzCase] {
300 &self.cases
301 }
302
303 pub fn into_cases(self) -> Vec<FuzzCase> {
304 self.cases
305 }
306
307 pub fn last(&self) -> Option<&FuzzCase> {
309 self.cases.last()
310 }
311
312 pub fn median_gas(&self, with_stipend: bool) -> u64 {
314 let mut values = self.gas_values(with_stipend);
315 values.sort_unstable();
316 calc::median_sorted(&values)
317 }
318
319 pub fn mean_gas(&self, with_stipend: bool) -> u64 {
321 let mut values = self.gas_values(with_stipend);
322 values.sort_unstable();
323 calc::mean(&values)
324 }
325
326 fn gas_values(&self, with_stipend: bool) -> Vec<u64> {
327 self.cases
328 .iter()
329 .map(|c| if with_stipend { c.gas } else { c.gas.saturating_sub(c.stipend) })
330 .collect()
331 }
332
333 pub fn highest(&self) -> Option<&FuzzCase> {
335 self.cases.last()
336 }
337
338 pub fn lowest(&self) -> Option<&FuzzCase> {
340 self.cases.first()
341 }
342
343 pub fn highest_gas(&self, with_stipend: bool) -> u64 {
345 self.highest()
346 .map(|c| if with_stipend { c.gas } else { c.gas - c.stipend })
347 .unwrap_or_default()
348 }
349
350 pub fn lowest_gas(&self) -> u64 {
352 self.lowest().map(|c| c.gas).unwrap_or_default()
353 }
354}
355
356#[derive(Clone, Default, Debug)]
363pub struct FuzzFixtures {
364 inner: Arc<HashMap<String, DynSolValue>>,
365}
366
367impl FuzzFixtures {
368 pub fn new(fixtures: HashMap<String, DynSolValue>) -> Self {
369 Self { inner: Arc::new(fixtures) }
370 }
371
372 pub fn param_fixtures(&self, param_name: &str) -> Option<&[DynSolValue]> {
374 if let Some(param_fixtures) = self.inner.get(&normalize_fixture(param_name)) {
375 param_fixtures.as_fixed_array().or_else(|| param_fixtures.as_array())
376 } else {
377 None
378 }
379 }
380}
381
382pub fn fixture_name(function_name: String) -> String {
385 normalize_fixture(function_name.strip_prefix("fixture").unwrap())
386}
387
388fn normalize_fixture(param_name: &str) -> String {
390 param_name.trim_matches('_').to_ascii_lowercase()
391}