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,
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;
31
32mod inspector;
33pub use inspector::Fuzzer;
34
35#[derive(Clone, Debug, Serialize, Deserialize)]
37pub struct BasicTxDetails {
38 pub sender: Address,
40 pub call_details: CallDetails,
42}
43
44#[derive(Clone, Debug, Serialize, Deserialize)]
46pub struct CallDetails {
47 pub target: Address,
49 pub calldata: Bytes,
51}
52
53#[derive(Clone, Debug, Serialize, Deserialize)]
54#[expect(clippy::large_enum_variant)]
55pub enum CounterExample {
56 Single(BaseCounterExample),
58 Sequence(usize, Vec<BaseCounterExample>),
60}
61
62#[derive(Clone, Debug, Serialize, Deserialize)]
63pub struct BaseCounterExample {
64 pub sender: Option<Address>,
66 pub addr: Option<Address>,
68 pub calldata: Bytes,
70 pub contract_name: Option<String>,
72 pub func_name: Option<String>,
74 pub signature: Option<String>,
76 pub args: Option<String>,
78 pub raw_args: Option<String>,
80 #[serde(skip)]
82 pub traces: Option<SparsedTraceArena>,
83 #[serde(skip)]
85 pub show_solidity: bool,
86}
87
88impl BaseCounterExample {
89 pub fn from_invariant_call(
91 sender: Address,
92 addr: Address,
93 bytes: &Bytes,
94 contracts: &ContractsByAddress,
95 traces: Option<SparsedTraceArena>,
96 show_solidity: bool,
97 ) -> Self {
98 if let Some((name, abi)) = &contracts.get(&addr)
99 && let Some(func) = abi.functions().find(|f| f.selector() == bytes[..4])
100 {
101 if let Ok(args) = func.abi_decode_input(&bytes[4..]) {
103 return Self {
104 sender: Some(sender),
105 addr: Some(addr),
106 calldata: bytes.clone(),
107 contract_name: Some(name.clone()),
108 func_name: Some(func.name.clone()),
109 signature: Some(func.signature()),
110 args: Some(foundry_common::fmt::format_tokens(&args).format(", ").to_string()),
111 raw_args: Some(
112 foundry_common::fmt::format_tokens_raw(&args).format(", ").to_string(),
113 ),
114 traces,
115 show_solidity,
116 };
117 }
118 }
119
120 Self {
121 sender: Some(sender),
122 addr: Some(addr),
123 calldata: bytes.clone(),
124 contract_name: None,
125 func_name: None,
126 signature: None,
127 args: None,
128 raw_args: None,
129 traces,
130 show_solidity: false,
131 }
132 }
133
134 pub fn from_fuzz_call(
136 bytes: Bytes,
137 args: Vec<DynSolValue>,
138 traces: Option<SparsedTraceArena>,
139 ) -> Self {
140 Self {
141 sender: None,
142 addr: None,
143 calldata: bytes,
144 contract_name: None,
145 func_name: None,
146 signature: None,
147 args: Some(foundry_common::fmt::format_tokens(&args).format(", ").to_string()),
148 raw_args: Some(foundry_common::fmt::format_tokens_raw(&args).format(", ").to_string()),
149 traces,
150 show_solidity: false,
151 }
152 }
153}
154
155impl fmt::Display for BaseCounterExample {
156 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
157 if self.show_solidity
159 && let (Some(sender), Some(contract), Some(address), Some(func_name), Some(args)) =
160 (&self.sender, &self.contract_name, &self.addr, &self.func_name, &self.raw_args)
161 {
162 writeln!(f, "\t\tvm.prank({sender});")?;
163 write!(
164 f,
165 "\t\t{}({}).{}({});",
166 contract.split_once(':').map_or(contract.as_str(), |(_, contract)| contract),
167 address,
168 func_name,
169 args
170 )?;
171
172 return Ok(());
173 }
174
175 if let Some(sender) = self.sender {
177 write!(f, "\t\tsender={sender} addr=")?
178 }
179
180 if let Some(name) = &self.contract_name {
181 write!(f, "[{name}]")?
182 }
183
184 if let Some(addr) = &self.addr {
185 write!(f, "{addr} ")?
186 }
187
188 if let Some(sig) = &self.signature {
189 write!(f, "calldata={sig}")?
190 } else {
191 write!(f, "calldata={}", &self.calldata)?
192 }
193
194 if let Some(args) = &self.args {
195 write!(f, " args=[{args}]")
196 } else {
197 write!(f, " args=[]")
198 }
199 }
200}
201
202#[derive(Debug, Default)]
204pub struct FuzzTestResult {
205 pub first_case: FuzzCase,
207 pub gas_by_case: Vec<(u64, u64)>,
209 pub success: bool,
213 pub skipped: bool,
215
216 pub reason: Option<String>,
219
220 pub counterexample: Option<CounterExample>,
222
223 pub logs: Vec<Log>,
226
227 pub labels: AddressHashMap<String>,
229
230 pub traces: Option<SparsedTraceArena>,
235
236 pub gas_report_traces: Vec<CallTraceArena>,
239
240 pub line_coverage: Option<HitMaps>,
242
243 pub breakpoints: Option<Breakpoints>,
245
246 pub deprecated_cheatcodes: HashMap<&'static str, Option<&'static str>>,
248
249 pub failed_corpus_replays: usize,
251}
252
253impl FuzzTestResult {
254 pub fn median_gas(&self, with_stipend: bool) -> u64 {
256 let mut values = self.gas_values(with_stipend);
257 values.sort_unstable();
258 calc::median_sorted(&values)
259 }
260
261 pub fn mean_gas(&self, with_stipend: bool) -> u64 {
263 let mut values = self.gas_values(with_stipend);
264 values.sort_unstable();
265 calc::mean(&values)
266 }
267
268 fn gas_values(&self, with_stipend: bool) -> Vec<u64> {
269 self.gas_by_case
270 .iter()
271 .map(|gas| if with_stipend { gas.0 } else { gas.0.saturating_sub(gas.1) })
272 .collect()
273 }
274}
275
276#[derive(Clone, Debug, Default, Serialize, Deserialize)]
278pub struct FuzzCase {
279 pub calldata: Bytes,
281 pub gas: u64,
283 pub stipend: u64,
285}
286
287#[derive(Clone, Debug, Serialize, Deserialize)]
289#[serde(transparent)]
290pub struct FuzzedCases {
291 cases: Vec<FuzzCase>,
292}
293
294impl FuzzedCases {
295 pub fn new(mut cases: Vec<FuzzCase>) -> Self {
296 cases.sort_by_key(|c| c.gas);
297 Self { cases }
298 }
299
300 pub fn cases(&self) -> &[FuzzCase] {
301 &self.cases
302 }
303
304 pub fn into_cases(self) -> Vec<FuzzCase> {
305 self.cases
306 }
307
308 pub fn last(&self) -> Option<&FuzzCase> {
310 self.cases.last()
311 }
312
313 pub fn median_gas(&self, with_stipend: bool) -> u64 {
315 let mut values = self.gas_values(with_stipend);
316 values.sort_unstable();
317 calc::median_sorted(&values)
318 }
319
320 pub fn mean_gas(&self, with_stipend: bool) -> u64 {
322 let mut values = self.gas_values(with_stipend);
323 values.sort_unstable();
324 calc::mean(&values)
325 }
326
327 fn gas_values(&self, with_stipend: bool) -> Vec<u64> {
328 self.cases
329 .iter()
330 .map(|c| if with_stipend { c.gas } else { c.gas.saturating_sub(c.stipend) })
331 .collect()
332 }
333
334 pub fn highest(&self) -> Option<&FuzzCase> {
336 self.cases.last()
337 }
338
339 pub fn lowest(&self) -> Option<&FuzzCase> {
341 self.cases.first()
342 }
343
344 pub fn highest_gas(&self, with_stipend: bool) -> u64 {
346 self.highest()
347 .map(|c| if with_stipend { c.gas } else { c.gas - c.stipend })
348 .unwrap_or_default()
349 }
350
351 pub fn lowest_gas(&self) -> u64 {
353 self.lowest().map(|c| c.gas).unwrap_or_default()
354 }
355}
356
357#[derive(Clone, Default, Debug)]
364pub struct FuzzFixtures {
365 inner: Arc<HashMap<String, DynSolValue>>,
366}
367
368impl FuzzFixtures {
369 pub fn new(fixtures: HashMap<String, DynSolValue>) -> Self {
370 Self { inner: Arc::new(fixtures) }
371 }
372
373 pub fn param_fixtures(&self, param_name: &str) -> Option<&[DynSolValue]> {
375 if let Some(param_fixtures) = self.inner.get(&normalize_fixture(param_name)) {
376 param_fixtures.as_fixed_array().or_else(|| param_fixtures.as_array())
377 } else {
378 None
379 }
380 }
381}
382
383pub fn fixture_name(function_name: String) -> String {
386 normalize_fixture(function_name.strip_prefix("fixture").unwrap())
387}
388
389fn normalize_fixture(param_name: &str) -> String {
391 param_name.trim_matches('_').to_ascii_lowercase()
392}