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 sequence;
31pub mod strategies;
32pub use strategies::LiteralMaps;
33
34mod inspector;
35pub use inspector::{Fuzzer, ObservedCall};
36
37#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize)]
39pub struct FuzzRunMetadata {
40 #[serde(default, rename = "fuzz_seed", skip_serializing_if = "Option::is_none")]
42 pub seed: Option<U256>,
43 #[serde(default, rename = "fuzz_run", skip_serializing_if = "Option::is_none")]
45 pub run: Option<u32>,
46 #[serde(default, rename = "fuzz_worker", skip_serializing_if = "Option::is_none")]
48 pub worker: Option<u32>,
49}
50
51impl FuzzRunMetadata {
52 pub const fn new(seed: Option<U256>, run: Option<u32>, worker: Option<u32>) -> Self {
54 Self { seed, run, worker }
55 }
56}
57
58#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
60pub struct BasicTxDetails {
61 #[serde(default, skip_serializing_if = "Option::is_none")]
63 pub warp: Option<U256>,
64 #[serde(default, skip_serializing_if = "Option::is_none")]
66 pub roll: Option<U256>,
67 pub sender: Address,
69 #[serde(flatten)]
71 pub call_details: CallDetails,
72}
73
74#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
76pub struct CallDetails {
77 pub target: Address,
79 pub calldata: Bytes,
81 #[serde(default, skip_serializing_if = "Option::is_none")]
84 pub value: Option<U256>,
85}
86
87impl BasicTxDetails {
88 pub fn estimate_serialized_size(&self) -> usize {
90 size_of::<Self>() + self.call_details.calldata.len() * 2
91 }
92}
93
94#[derive(Clone, Debug, Serialize, Deserialize)]
95#[expect(clippy::large_enum_variant)]
96pub enum CounterExample {
97 Single(BaseCounterExample),
99 Sequence(usize, Vec<BaseCounterExample>),
101}
102
103#[derive(Clone, Debug, Serialize, Deserialize)]
104pub struct BaseCounterExample {
105 pub warp: Option<U256>,
107 pub roll: Option<U256>,
109 pub sender: Option<Address>,
111 pub addr: Option<Address>,
113 pub calldata: Bytes,
115 #[serde(default, skip_serializing_if = "Option::is_none")]
117 pub value: Option<U256>,
118 pub contract_name: Option<String>,
120 pub func_name: Option<String>,
122 pub signature: Option<String>,
124 pub args: Option<String>,
126 pub raw_args: Option<String>,
128 #[serde(skip)]
130 pub traces: Option<SparsedTraceArena>,
131 #[serde(skip)]
133 pub show_solidity: bool,
134 #[serde(flatten)]
136 pub fuzz: FuzzRunMetadata,
137 #[serde(default, skip_serializing_if = "Option::is_none")]
139 pub fork_block_number: Option<u64>,
140}
141
142impl BaseCounterExample {
143 pub fn from_invariant_call(
145 tx: &BasicTxDetails,
146 contracts: &ContractsByAddress,
147 traces: Option<SparsedTraceArena>,
148 show_solidity: bool,
149 ) -> Self {
150 let sender = tx.sender;
151 let target = tx.call_details.target;
152 let bytes = &tx.call_details.calldata;
153 let value = tx.call_details.value;
154 let warp = tx.warp;
155 let roll = tx.roll;
156 if let Some((name, abi)) = &contracts.get(&target)
157 && let Some(func) = abi.functions().find(|f| f.selector() == bytes[..4])
158 {
159 if let Ok(args) = func.abi_decode_input(&bytes[4..]) {
161 return Self {
162 warp,
163 roll,
164 sender: Some(sender),
165 addr: Some(target),
166 calldata: bytes.clone(),
167 value,
168 contract_name: Some(name.clone()),
169 func_name: Some(func.name.clone()),
170 signature: Some(func.signature()),
171 args: Some(foundry_common::fmt::format_tokens(&args).format(", ").to_string()),
172 raw_args: Some(
173 foundry_common::fmt::format_tokens_raw(&args).format(", ").to_string(),
174 ),
175 traces,
176 show_solidity,
177 fuzz: FuzzRunMetadata::default(),
178 fork_block_number: None,
179 };
180 }
181 }
182
183 Self {
184 warp,
185 roll,
186 sender: Some(sender),
187 addr: Some(target),
188 calldata: bytes.clone(),
189 value,
190 contract_name: None,
191 func_name: None,
192 signature: None,
193 args: None,
194 raw_args: None,
195 traces,
196 show_solidity: false,
197 fuzz: FuzzRunMetadata::default(),
198 fork_block_number: None,
199 }
200 }
201
202 pub fn from_fuzz_call(
204 bytes: Bytes,
205 args: Vec<DynSolValue>,
206 traces: Option<SparsedTraceArena>,
207 ) -> Self {
208 Self {
209 warp: None,
210 roll: None,
211 sender: None,
212 addr: None,
213 calldata: bytes,
214 value: None,
215 contract_name: None,
216 func_name: None,
217 signature: None,
218 args: Some(foundry_common::fmt::format_tokens(&args).format(", ").to_string()),
219 raw_args: Some(foundry_common::fmt::format_tokens_raw(&args).format(", ").to_string()),
220 traces,
221 show_solidity: false,
222 fuzz: FuzzRunMetadata::default(),
223 fork_block_number: None,
224 }
225 }
226
227 pub fn from_fuzz_tx(
229 tx: &BasicTxDetails,
230 args: Vec<DynSolValue>,
231 traces: Option<SparsedTraceArena>,
232 ) -> Self {
233 Self {
234 warp: tx.warp,
235 roll: tx.roll,
236 sender: Some(tx.sender),
237 addr: Some(tx.call_details.target),
238 calldata: tx.call_details.calldata.clone(),
239 value: tx.call_details.value,
240 contract_name: None,
241 func_name: None,
242 signature: None,
243 args: Some(foundry_common::fmt::format_tokens(&args).format(", ").to_string()),
244 raw_args: Some(foundry_common::fmt::format_tokens_raw(&args).format(", ").to_string()),
245 traces,
246 show_solidity: false,
247 fuzz: FuzzRunMetadata::default(),
248 fork_block_number: None,
249 }
250 }
251
252 pub const fn with_fuzz_metadata(mut self, fuzz: FuzzRunMetadata) -> Self {
254 self.fuzz = fuzz;
255 self
256 }
257}
258
259impl fmt::Display for BaseCounterExample {
260 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
261 if self.show_solidity
263 && let (Some(sender), Some(contract), Some(address), Some(func_name), Some(args)) =
264 (&self.sender, &self.contract_name, &self.addr, &self.func_name, &self.raw_args)
265 {
266 if let Some(warp) = &self.warp {
267 writeln!(f, "\t\tvm.warp(block.timestamp + {warp});")?;
268 }
269 if let Some(roll) = &self.roll {
270 writeln!(f, "\t\tvm.roll(block.number + {roll});")?;
271 }
272 writeln!(f, "\t\tvm.prank({sender});")?;
273 if let Some(value) = &self.value
275 && !value.is_zero()
276 {
277 write!(
278 f,
279 "\t\t{}({}).{}{{value: {value}}}({});",
280 contract.split_once(':').map_or(contract.as_str(), |(_, contract)| contract),
281 address,
282 func_name,
283 args
284 )?;
285 return Ok(());
286 }
287 write!(
288 f,
289 "\t\t{}({}).{}({});",
290 contract.split_once(':').map_or(contract.as_str(), |(_, contract)| contract),
291 address,
292 func_name,
293 args
294 )?;
295
296 return Ok(());
297 }
298
299 let show_tx_context =
303 self.fuzz.worker.is_none() || self.value.as_ref().is_some_and(|value| !value.is_zero());
304 if show_tx_context {
305 if let Some(sender) = self.sender {
306 write!(f, "\t\tsender={sender} addr=")?
307 }
308
309 if let Some(name) = &self.contract_name {
310 write!(f, "[{name}]")?
311 }
312
313 if let Some(addr) = &self.addr {
314 write!(f, "{addr} ")?
315 }
316 }
317
318 if let Some(warp) = &self.warp {
319 write!(f, "warp={warp} ")?;
320 }
321 if let Some(roll) = &self.roll {
322 write!(f, "roll={roll} ")?;
323 }
324
325 if let Some(value) = &self.value
327 && !value.is_zero()
328 {
329 write!(f, "value={value} ")?;
330 }
331
332 if let Some(sig) = &self.signature {
333 write!(f, "calldata={sig}")?
334 } else {
335 write!(f, "calldata={}", self.calldata)?
336 }
337
338 if let Some(args) = &self.args {
339 write!(f, " args=[{args}]")
340 } else {
341 write!(f, " args=[]")
342 }
343 }
344}
345
346#[derive(Debug, Default)]
348pub struct FuzzTestResult {
349 pub first_case: FuzzCase,
351 pub gas_by_case: Vec<(u64, u64)>,
353 pub success: bool,
356 pub skipped: bool,
358
359 pub reason: Option<String>,
361
362 pub counterexample: Option<CounterExample>,
364
365 pub logs: Vec<Log>,
368
369 pub labels: AddressHashMap<String>,
371
372 pub traces: Option<SparsedTraceArena>,
377
378 pub gas_report_traces: Vec<CallTraceArena>,
381
382 pub line_coverage: Option<HitMaps>,
384
385 pub breakpoints: Option<Breakpoints>,
387
388 pub debug_bytecodes: AddressHashMap<Bytes>,
390
391 pub deprecated_cheatcodes: HashMap<&'static str, Option<&'static str>>,
393
394 pub failed_corpus_replays: usize,
396
397 pub fork_block_number: Option<u64>,
399}
400
401impl FuzzTestResult {
402 pub fn median_gas(&self, with_stipend: bool) -> u64 {
404 let mut values = self.gas_values(with_stipend);
405 values.sort_unstable();
406 calc::median_sorted(&values)
407 }
408
409 pub fn mean_gas(&self, with_stipend: bool) -> u64 {
411 let mut values = self.gas_values(with_stipend);
412 values.sort_unstable();
413 calc::mean(&values)
414 }
415
416 fn gas_values(&self, with_stipend: bool) -> Vec<u64> {
417 self.gas_by_case
418 .iter()
419 .map(|gas| if with_stipend { gas.0 } else { gas.0.saturating_sub(gas.1) })
420 .collect()
421 }
422}
423
424#[derive(Clone, Debug, Default, Serialize, Deserialize)]
426pub struct FuzzCase {
427 pub gas: u64,
429 pub stipend: u64,
431}
432
433#[derive(Clone, Debug, Serialize, Deserialize)]
435#[serde(transparent)]
436pub struct FuzzedCases {
437 cases: Vec<FuzzCase>,
438}
439
440impl FuzzedCases {
441 pub fn new(mut cases: Vec<FuzzCase>) -> Self {
442 cases.sort_by_key(|c| c.gas);
443 Self { cases }
444 }
445
446 pub fn cases(&self) -> &[FuzzCase] {
447 &self.cases
448 }
449
450 pub fn into_cases(self) -> Vec<FuzzCase> {
451 self.cases
452 }
453
454 pub fn last(&self) -> Option<&FuzzCase> {
456 self.cases.last()
457 }
458
459 pub fn median_gas(&self, with_stipend: bool) -> u64 {
461 let mut values = self.gas_values(with_stipend);
462 values.sort_unstable();
463 calc::median_sorted(&values)
464 }
465
466 pub fn mean_gas(&self, with_stipend: bool) -> u64 {
468 let mut values = self.gas_values(with_stipend);
469 values.sort_unstable();
470 calc::mean(&values)
471 }
472
473 fn gas_values(&self, with_stipend: bool) -> Vec<u64> {
474 self.cases
475 .iter()
476 .map(|c| if with_stipend { c.gas } else { c.gas.saturating_sub(c.stipend) })
477 .collect()
478 }
479
480 pub fn highest(&self) -> Option<&FuzzCase> {
482 self.cases.last()
483 }
484
485 pub fn lowest(&self) -> Option<&FuzzCase> {
487 self.cases.first()
488 }
489
490 pub fn highest_gas(&self, with_stipend: bool) -> u64 {
492 self.highest()
493 .map(|c| if with_stipend { c.gas } else { c.gas - c.stipend })
494 .unwrap_or_default()
495 }
496
497 pub fn lowest_gas(&self) -> u64 {
499 self.lowest().map(|c| c.gas).unwrap_or_default()
500 }
501}
502
503#[derive(Clone, Default, Debug)]
510pub struct FuzzFixtures {
511 inner: Arc<HashMap<String, DynSolValue>>,
512 enum_bounds: strategies::EnumBounds,
514}
515
516impl FuzzFixtures {
517 pub fn new(fixtures: HashMap<String, DynSolValue>) -> Self {
518 Self { inner: Arc::new(fixtures), enum_bounds: strategies::EnumBounds::default() }
519 }
520
521 pub fn with_enum_bounds(mut self, enum_bounds: strategies::EnumBounds) -> Self {
523 self.enum_bounds = enum_bounds;
524 self
525 }
526
527 pub fn param_fixtures(&self, param_name: &str) -> Option<&[DynSolValue]> {
529 if let Some(param_fixtures) = self.inner.get(&normalize_fixture(param_name)) {
530 param_fixtures.as_fixed_array().or_else(|| param_fixtures.as_array())
531 } else {
532 None
533 }
534 }
535
536 pub fn enum_variant_count(&self, contract: Option<&str>, name: &str) -> Option<usize> {
539 self.enum_bounds.variant_count(contract, name)
540 }
541}
542
543pub fn fixture_name(function_name: String) -> String {
546 normalize_fixture(function_name.strip_prefix("fixture").unwrap())
547}
548
549fn normalize_fixture(param_name: &str) -> String {
551 param_name.trim_matches('_').to_ascii_lowercase()
552}