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, Copy, Debug, Default, Serialize, Deserialize)]
38pub struct FuzzRunMetadata {
39 #[serde(default, rename = "fuzz_seed", skip_serializing_if = "Option::is_none")]
41 pub seed: Option<U256>,
42 #[serde(default, rename = "fuzz_run", skip_serializing_if = "Option::is_none")]
44 pub run: Option<u32>,
45 #[serde(default, rename = "fuzz_worker", skip_serializing_if = "Option::is_none")]
47 pub worker: Option<u32>,
48}
49
50impl FuzzRunMetadata {
51 pub const fn new(seed: Option<U256>, run: Option<u32>, worker: Option<u32>) -> Self {
53 Self { seed, run, worker }
54 }
55}
56
57#[derive(Clone, Debug, Serialize, Deserialize)]
59pub struct BasicTxDetails {
60 #[serde(default, skip_serializing_if = "Option::is_none")]
62 pub warp: Option<U256>,
63 #[serde(default, skip_serializing_if = "Option::is_none")]
65 pub roll: Option<U256>,
66 pub sender: Address,
68 #[serde(flatten)]
70 pub call_details: CallDetails,
71}
72
73#[derive(Clone, Debug, Serialize, Deserialize)]
75pub struct CallDetails {
76 pub target: Address,
78 pub calldata: Bytes,
80 #[serde(default, skip_serializing_if = "Option::is_none")]
83 pub value: Option<U256>,
84}
85
86impl BasicTxDetails {
87 pub fn estimate_serialized_size(&self) -> usize {
89 size_of::<Self>() + self.call_details.calldata.len() * 2
90 }
91}
92
93#[derive(Clone, Debug, Serialize, Deserialize)]
94#[expect(clippy::large_enum_variant)]
95pub enum CounterExample {
96 Single(BaseCounterExample),
98 Sequence(usize, Vec<BaseCounterExample>),
100}
101
102#[derive(Clone, Debug, Serialize, Deserialize)]
103pub struct BaseCounterExample {
104 pub warp: Option<U256>,
106 pub roll: Option<U256>,
108 pub sender: Option<Address>,
110 pub addr: Option<Address>,
112 pub calldata: Bytes,
114 #[serde(default, skip_serializing_if = "Option::is_none")]
116 pub value: Option<U256>,
117 pub contract_name: Option<String>,
119 pub func_name: Option<String>,
121 pub signature: Option<String>,
123 pub args: Option<String>,
125 pub raw_args: Option<String>,
127 #[serde(skip)]
129 pub traces: Option<SparsedTraceArena>,
130 #[serde(skip)]
132 pub show_solidity: bool,
133 #[serde(flatten)]
135 pub fuzz: FuzzRunMetadata,
136}
137
138impl BaseCounterExample {
139 pub fn from_invariant_call(
141 tx: &BasicTxDetails,
142 contracts: &ContractsByAddress,
143 traces: Option<SparsedTraceArena>,
144 show_solidity: bool,
145 ) -> Self {
146 let sender = tx.sender;
147 let target = tx.call_details.target;
148 let bytes = &tx.call_details.calldata;
149 let value = tx.call_details.value;
150 let warp = tx.warp;
151 let roll = tx.roll;
152 if let Some((name, abi)) = &contracts.get(&target)
153 && let Some(func) = abi.functions().find(|f| f.selector() == bytes[..4])
154 {
155 if let Ok(args) = func.abi_decode_input(&bytes[4..]) {
157 return Self {
158 warp,
159 roll,
160 sender: Some(sender),
161 addr: Some(target),
162 calldata: bytes.clone(),
163 value,
164 contract_name: Some(name.clone()),
165 func_name: Some(func.name.clone()),
166 signature: Some(func.signature()),
167 args: Some(foundry_common::fmt::format_tokens(&args).format(", ").to_string()),
168 raw_args: Some(
169 foundry_common::fmt::format_tokens_raw(&args).format(", ").to_string(),
170 ),
171 traces,
172 show_solidity,
173 fuzz: FuzzRunMetadata::default(),
174 };
175 }
176 }
177
178 Self {
179 warp,
180 roll,
181 sender: Some(sender),
182 addr: Some(target),
183 calldata: bytes.clone(),
184 value,
185 contract_name: None,
186 func_name: None,
187 signature: None,
188 args: None,
189 raw_args: None,
190 traces,
191 show_solidity: false,
192 fuzz: FuzzRunMetadata::default(),
193 }
194 }
195
196 pub fn from_fuzz_call(
198 bytes: Bytes,
199 args: Vec<DynSolValue>,
200 traces: Option<SparsedTraceArena>,
201 ) -> Self {
202 Self {
203 warp: None,
204 roll: None,
205 sender: None,
206 addr: None,
207 calldata: bytes,
208 value: None,
209 contract_name: None,
210 func_name: None,
211 signature: None,
212 args: Some(foundry_common::fmt::format_tokens(&args).format(", ").to_string()),
213 raw_args: Some(foundry_common::fmt::format_tokens_raw(&args).format(", ").to_string()),
214 traces,
215 show_solidity: false,
216 fuzz: FuzzRunMetadata::default(),
217 }
218 }
219
220 pub const fn with_fuzz_metadata(mut self, fuzz: FuzzRunMetadata) -> Self {
222 self.fuzz = fuzz;
223 self
224 }
225}
226
227impl fmt::Display for BaseCounterExample {
228 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
229 if self.show_solidity
231 && let (Some(sender), Some(contract), Some(address), Some(func_name), Some(args)) =
232 (&self.sender, &self.contract_name, &self.addr, &self.func_name, &self.raw_args)
233 {
234 if let Some(warp) = &self.warp {
235 writeln!(f, "\t\tvm.warp(block.timestamp + {warp});")?;
236 }
237 if let Some(roll) = &self.roll {
238 writeln!(f, "\t\tvm.roll(block.number + {roll});")?;
239 }
240 writeln!(f, "\t\tvm.prank({sender});")?;
241 if let Some(value) = &self.value
243 && !value.is_zero()
244 {
245 write!(
246 f,
247 "\t\t{}({}).{}{{value: {value}}}({});",
248 contract.split_once(':').map_or(contract.as_str(), |(_, contract)| contract),
249 address,
250 func_name,
251 args
252 )?;
253 return Ok(());
254 }
255 write!(
256 f,
257 "\t\t{}({}).{}({});",
258 contract.split_once(':').map_or(contract.as_str(), |(_, contract)| contract),
259 address,
260 func_name,
261 args
262 )?;
263
264 return Ok(());
265 }
266
267 if let Some(sender) = self.sender {
269 write!(f, "\t\tsender={sender} addr=")?
270 }
271
272 if let Some(name) = &self.contract_name {
273 write!(f, "[{name}]")?
274 }
275
276 if let Some(addr) = &self.addr {
277 write!(f, "{addr} ")?
278 }
279
280 if let Some(warp) = &self.warp {
281 write!(f, "warp={warp} ")?;
282 }
283 if let Some(roll) = &self.roll {
284 write!(f, "roll={roll} ")?;
285 }
286
287 if let Some(value) = &self.value
289 && !value.is_zero()
290 {
291 write!(f, "value={value} ")?;
292 }
293
294 if let Some(sig) = &self.signature {
295 write!(f, "calldata={sig}")?
296 } else {
297 write!(f, "calldata={}", self.calldata)?
298 }
299
300 if let Some(args) = &self.args {
301 write!(f, " args=[{args}]")
302 } else {
303 write!(f, " args=[]")
304 }
305 }
306}
307
308#[derive(Debug, Default)]
310pub struct FuzzTestResult {
311 pub first_case: FuzzCase,
313 pub gas_by_case: Vec<(u64, u64)>,
315 pub success: bool,
319 pub skipped: bool,
321
322 pub reason: Option<String>,
325
326 pub counterexample: Option<CounterExample>,
328
329 pub logs: Vec<Log>,
332
333 pub labels: AddressHashMap<String>,
335
336 pub traces: Option<SparsedTraceArena>,
341
342 pub gas_report_traces: Vec<CallTraceArena>,
345
346 pub line_coverage: Option<HitMaps>,
348
349 pub breakpoints: Option<Breakpoints>,
351
352 pub deprecated_cheatcodes: HashMap<&'static str, Option<&'static str>>,
354
355 pub failed_corpus_replays: usize,
357}
358
359impl FuzzTestResult {
360 pub fn median_gas(&self, with_stipend: bool) -> u64 {
362 let mut values = self.gas_values(with_stipend);
363 values.sort_unstable();
364 calc::median_sorted(&values)
365 }
366
367 pub fn mean_gas(&self, with_stipend: bool) -> u64 {
369 let mut values = self.gas_values(with_stipend);
370 values.sort_unstable();
371 calc::mean(&values)
372 }
373
374 fn gas_values(&self, with_stipend: bool) -> Vec<u64> {
375 self.gas_by_case
376 .iter()
377 .map(|gas| if with_stipend { gas.0 } else { gas.0.saturating_sub(gas.1) })
378 .collect()
379 }
380}
381
382#[derive(Clone, Debug, Default, Serialize, Deserialize)]
384pub struct FuzzCase {
385 pub gas: u64,
387 pub stipend: u64,
389}
390
391#[derive(Clone, Debug, Serialize, Deserialize)]
393#[serde(transparent)]
394pub struct FuzzedCases {
395 cases: Vec<FuzzCase>,
396}
397
398impl FuzzedCases {
399 pub fn new(mut cases: Vec<FuzzCase>) -> Self {
400 cases.sort_by_key(|c| c.gas);
401 Self { cases }
402 }
403
404 pub fn cases(&self) -> &[FuzzCase] {
405 &self.cases
406 }
407
408 pub fn into_cases(self) -> Vec<FuzzCase> {
409 self.cases
410 }
411
412 pub fn last(&self) -> Option<&FuzzCase> {
414 self.cases.last()
415 }
416
417 pub fn median_gas(&self, with_stipend: bool) -> u64 {
419 let mut values = self.gas_values(with_stipend);
420 values.sort_unstable();
421 calc::median_sorted(&values)
422 }
423
424 pub fn mean_gas(&self, with_stipend: bool) -> u64 {
426 let mut values = self.gas_values(with_stipend);
427 values.sort_unstable();
428 calc::mean(&values)
429 }
430
431 fn gas_values(&self, with_stipend: bool) -> Vec<u64> {
432 self.cases
433 .iter()
434 .map(|c| if with_stipend { c.gas } else { c.gas.saturating_sub(c.stipend) })
435 .collect()
436 }
437
438 pub fn highest(&self) -> Option<&FuzzCase> {
440 self.cases.last()
441 }
442
443 pub fn lowest(&self) -> Option<&FuzzCase> {
445 self.cases.first()
446 }
447
448 pub fn highest_gas(&self, with_stipend: bool) -> u64 {
450 self.highest()
451 .map(|c| if with_stipend { c.gas } else { c.gas - c.stipend })
452 .unwrap_or_default()
453 }
454
455 pub fn lowest_gas(&self) -> u64 {
457 self.lowest().map(|c| c.gas).unwrap_or_default()
458 }
459}
460
461#[derive(Clone, Default, Debug)]
468pub struct FuzzFixtures {
469 inner: Arc<HashMap<String, DynSolValue>>,
470}
471
472impl FuzzFixtures {
473 pub fn new(fixtures: HashMap<String, DynSolValue>) -> Self {
474 Self { inner: Arc::new(fixtures) }
475 }
476
477 pub fn param_fixtures(&self, param_name: &str) -> Option<&[DynSolValue]> {
479 if let Some(param_fixtures) = self.inner.get(&normalize_fixture(param_name)) {
480 param_fixtures.as_fixed_array().or_else(|| param_fixtures.as_array())
481 } else {
482 None
483 }
484 }
485}
486
487pub fn fixture_name(function_name: String) -> String {
490 normalize_fixture(function_name.strip_prefix("fixture").unwrap())
491}
492
493fn normalize_fixture(param_name: &str) -> String {
495 param_name.trim_matches('_').to_ascii_lowercase()
496}