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