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)]
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 && let Some(func) = abi.functions().find(|f| f.selector() == bytes[..4])
81 {
82 if let Ok(args) = func.abi_decode_input(&bytes[4..]) {
84 return Self {
85 sender: Some(sender),
86 addr: Some(addr),
87 calldata: bytes.clone(),
88 contract_name: Some(name.clone()),
89 func_name: Some(func.name.clone()),
90 signature: Some(func.signature()),
91 args: Some(foundry_common::fmt::format_tokens(&args).format(", ").to_string()),
92 raw_args: Some(
93 foundry_common::fmt::format_tokens_raw(&args).format(", ").to_string(),
94 ),
95 traces,
96 show_solidity,
97 };
98 }
99 }
100
101 Self {
102 sender: Some(sender),
103 addr: Some(addr),
104 calldata: bytes.clone(),
105 contract_name: None,
106 func_name: None,
107 signature: None,
108 args: None,
109 raw_args: None,
110 traces,
111 show_solidity: false,
112 }
113 }
114
115 pub fn from_fuzz_call(
117 bytes: Bytes,
118 args: Vec<DynSolValue>,
119 traces: Option<SparsedTraceArena>,
120 ) -> Self {
121 Self {
122 sender: None,
123 addr: None,
124 calldata: bytes,
125 contract_name: None,
126 func_name: None,
127 signature: None,
128 args: Some(foundry_common::fmt::format_tokens(&args).format(", ").to_string()),
129 raw_args: Some(foundry_common::fmt::format_tokens_raw(&args).format(", ").to_string()),
130 traces,
131 show_solidity: false,
132 }
133 }
134}
135
136impl fmt::Display for BaseCounterExample {
137 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
138 if self.show_solidity
140 && let (Some(sender), Some(contract), Some(address), Some(func_name), Some(args)) =
141 (&self.sender, &self.contract_name, &self.addr, &self.func_name, &self.raw_args)
142 {
143 writeln!(f, "\t\tvm.prank({sender});")?;
144 write!(
145 f,
146 "\t\t{}({}).{}({});",
147 contract.split_once(':').map_or(contract.as_str(), |(_, contract)| contract),
148 address,
149 func_name,
150 args
151 )?;
152
153 return Ok(());
154 }
155
156 if let Some(sender) = self.sender {
158 write!(f, "\t\tsender={sender} addr=")?
159 }
160
161 if let Some(name) = &self.contract_name {
162 write!(f, "[{name}]")?
163 }
164
165 if let Some(addr) = &self.addr {
166 write!(f, "{addr} ")?
167 }
168
169 if let Some(sig) = &self.signature {
170 write!(f, "calldata={sig}")?
171 } else {
172 write!(f, "calldata={}", &self.calldata)?
173 }
174
175 if let Some(args) = &self.args {
176 write!(f, " args=[{args}]")
177 } else {
178 write!(f, " args=[]")
179 }
180 }
181}
182
183#[derive(Debug)]
185pub struct FuzzTestResult {
186 pub first_case: FuzzCase,
188 pub gas_by_case: Vec<(u64, u64)>,
190 pub success: bool,
194 pub skipped: bool,
196
197 pub reason: Option<String>,
200
201 pub counterexample: Option<CounterExample>,
203
204 pub logs: Vec<Log>,
207
208 pub labeled_addresses: AddressHashMap<String>,
210
211 pub traces: Option<SparsedTraceArena>,
216
217 pub gas_report_traces: Vec<CallTraceArena>,
220
221 pub line_coverage: Option<HitMaps>,
223
224 pub breakpoints: Option<Breakpoints>,
226
227 pub deprecated_cheatcodes: HashMap<&'static str, Option<&'static str>>,
229}
230
231impl FuzzTestResult {
232 pub fn median_gas(&self, with_stipend: bool) -> u64 {
234 let mut values = self.gas_values(with_stipend);
235 values.sort_unstable();
236 calc::median_sorted(&values)
237 }
238
239 pub fn mean_gas(&self, with_stipend: bool) -> u64 {
241 let mut values = self.gas_values(with_stipend);
242 values.sort_unstable();
243 calc::mean(&values)
244 }
245
246 fn gas_values(&self, with_stipend: bool) -> Vec<u64> {
247 self.gas_by_case
248 .iter()
249 .map(|gas| if with_stipend { gas.0 } else { gas.0.saturating_sub(gas.1) })
250 .collect()
251 }
252}
253
254#[derive(Clone, Debug, Default, Serialize, Deserialize)]
256pub struct FuzzCase {
257 pub calldata: Bytes,
259 pub gas: u64,
261 pub stipend: u64,
263}
264
265#[derive(Clone, Debug, Serialize, Deserialize)]
267#[serde(transparent)]
268pub struct FuzzedCases {
269 cases: Vec<FuzzCase>,
270}
271
272impl FuzzedCases {
273 #[inline]
274 pub fn new(mut cases: Vec<FuzzCase>) -> Self {
275 cases.sort_by_key(|c| c.gas);
276 Self { cases }
277 }
278
279 #[inline]
280 pub fn cases(&self) -> &[FuzzCase] {
281 &self.cases
282 }
283
284 #[inline]
285 pub fn into_cases(self) -> Vec<FuzzCase> {
286 self.cases
287 }
288
289 #[inline]
291 pub fn last(&self) -> Option<&FuzzCase> {
292 self.cases.last()
293 }
294
295 #[inline]
297 pub fn median_gas(&self, with_stipend: bool) -> u64 {
298 let mut values = self.gas_values(with_stipend);
299 values.sort_unstable();
300 calc::median_sorted(&values)
301 }
302
303 #[inline]
305 pub fn mean_gas(&self, with_stipend: bool) -> u64 {
306 let mut values = self.gas_values(with_stipend);
307 values.sort_unstable();
308 calc::mean(&values)
309 }
310
311 #[inline]
312 fn gas_values(&self, with_stipend: bool) -> Vec<u64> {
313 self.cases
314 .iter()
315 .map(|c| if with_stipend { c.gas } else { c.gas.saturating_sub(c.stipend) })
316 .collect()
317 }
318
319 #[inline]
321 pub fn highest(&self) -> Option<&FuzzCase> {
322 self.cases.last()
323 }
324
325 #[inline]
327 pub fn lowest(&self) -> Option<&FuzzCase> {
328 self.cases.first()
329 }
330
331 #[inline]
333 pub fn highest_gas(&self, with_stipend: bool) -> u64 {
334 self.highest()
335 .map(|c| if with_stipend { c.gas } else { c.gas - c.stipend })
336 .unwrap_or_default()
337 }
338
339 #[inline]
341 pub fn lowest_gas(&self) -> u64 {
342 self.lowest().map(|c| c.gas).unwrap_or_default()
343 }
344}
345
346#[derive(Clone, Default, Debug)]
353pub struct FuzzFixtures {
354 inner: Arc<HashMap<String, DynSolValue>>,
355}
356
357impl FuzzFixtures {
358 pub fn new(fixtures: HashMap<String, DynSolValue>) -> Self {
359 Self { inner: Arc::new(fixtures) }
360 }
361
362 pub fn param_fixtures(&self, param_name: &str) -> Option<&[DynSolValue]> {
364 if let Some(param_fixtures) = self.inner.get(&normalize_fixture(param_name)) {
365 param_fixtures.as_fixed_array().or_else(|| param_fixtures.as_array())
366 } else {
367 None
368 }
369 }
370}
371
372pub fn fixture_name(function_name: String) -> String {
375 normalize_fixture(function_name.strip_prefix("fixture").unwrap())
376}
377
378fn normalize_fixture(param_name: &str) -> String {
380 param_name.trim_matches('_').to_ascii_lowercase()
381}