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, 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, 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}
138
139impl BaseCounterExample {
140 pub fn from_invariant_call(
142 tx: &BasicTxDetails,
143 contracts: &ContractsByAddress,
144 traces: Option<SparsedTraceArena>,
145 show_solidity: bool,
146 ) -> Self {
147 let sender = tx.sender;
148 let target = tx.call_details.target;
149 let bytes = &tx.call_details.calldata;
150 let value = tx.call_details.value;
151 let warp = tx.warp;
152 let roll = tx.roll;
153 if let Some((name, abi)) = &contracts.get(&target)
154 && let Some(func) = abi.functions().find(|f| f.selector() == bytes[..4])
155 {
156 if let Ok(args) = func.abi_decode_input(&bytes[4..]) {
158 return Self {
159 warp,
160 roll,
161 sender: Some(sender),
162 addr: Some(target),
163 calldata: bytes.clone(),
164 value,
165 contract_name: Some(name.clone()),
166 func_name: Some(func.name.clone()),
167 signature: Some(func.signature()),
168 args: Some(foundry_common::fmt::format_tokens(&args).format(", ").to_string()),
169 raw_args: Some(
170 foundry_common::fmt::format_tokens_raw(&args).format(", ").to_string(),
171 ),
172 traces,
173 show_solidity,
174 fuzz: FuzzRunMetadata::default(),
175 };
176 }
177 }
178
179 Self {
180 warp,
181 roll,
182 sender: Some(sender),
183 addr: Some(target),
184 calldata: bytes.clone(),
185 value,
186 contract_name: None,
187 func_name: None,
188 signature: None,
189 args: None,
190 raw_args: None,
191 traces,
192 show_solidity: false,
193 fuzz: FuzzRunMetadata::default(),
194 }
195 }
196
197 pub fn from_fuzz_call(
199 bytes: Bytes,
200 args: Vec<DynSolValue>,
201 traces: Option<SparsedTraceArena>,
202 ) -> Self {
203 Self {
204 warp: None,
205 roll: None,
206 sender: None,
207 addr: None,
208 calldata: bytes,
209 value: None,
210 contract_name: None,
211 func_name: None,
212 signature: None,
213 args: Some(foundry_common::fmt::format_tokens(&args).format(", ").to_string()),
214 raw_args: Some(foundry_common::fmt::format_tokens_raw(&args).format(", ").to_string()),
215 traces,
216 show_solidity: false,
217 fuzz: FuzzRunMetadata::default(),
218 }
219 }
220
221 pub fn from_fuzz_tx(
223 tx: &BasicTxDetails,
224 args: Vec<DynSolValue>,
225 traces: Option<SparsedTraceArena>,
226 ) -> Self {
227 Self {
228 warp: tx.warp,
229 roll: tx.roll,
230 sender: Some(tx.sender),
231 addr: Some(tx.call_details.target),
232 calldata: tx.call_details.calldata.clone(),
233 value: tx.call_details.value,
234 contract_name: None,
235 func_name: None,
236 signature: None,
237 args: Some(foundry_common::fmt::format_tokens(&args).format(", ").to_string()),
238 raw_args: Some(foundry_common::fmt::format_tokens_raw(&args).format(", ").to_string()),
239 traces,
240 show_solidity: false,
241 fuzz: FuzzRunMetadata::default(),
242 }
243 }
244
245 pub const fn with_fuzz_metadata(mut self, fuzz: FuzzRunMetadata) -> Self {
247 self.fuzz = fuzz;
248 self
249 }
250}
251
252impl fmt::Display for BaseCounterExample {
253 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
254 if self.show_solidity
256 && let (Some(sender), Some(contract), Some(address), Some(func_name), Some(args)) =
257 (&self.sender, &self.contract_name, &self.addr, &self.func_name, &self.raw_args)
258 {
259 if let Some(warp) = &self.warp {
260 writeln!(f, "\t\tvm.warp(block.timestamp + {warp});")?;
261 }
262 if let Some(roll) = &self.roll {
263 writeln!(f, "\t\tvm.roll(block.number + {roll});")?;
264 }
265 writeln!(f, "\t\tvm.prank({sender});")?;
266 if let Some(value) = &self.value
268 && !value.is_zero()
269 {
270 write!(
271 f,
272 "\t\t{}({}).{}{{value: {value}}}({});",
273 contract.split_once(':').map_or(contract.as_str(), |(_, contract)| contract),
274 address,
275 func_name,
276 args
277 )?;
278 return Ok(());
279 }
280 write!(
281 f,
282 "\t\t{}({}).{}({});",
283 contract.split_once(':').map_or(contract.as_str(), |(_, contract)| contract),
284 address,
285 func_name,
286 args
287 )?;
288
289 return Ok(());
290 }
291
292 let show_tx_context =
296 self.fuzz.worker.is_none() || self.value.as_ref().is_some_and(|value| !value.is_zero());
297 if show_tx_context {
298 if let Some(sender) = self.sender {
299 write!(f, "\t\tsender={sender} addr=")?
300 }
301
302 if let Some(name) = &self.contract_name {
303 write!(f, "[{name}]")?
304 }
305
306 if let Some(addr) = &self.addr {
307 write!(f, "{addr} ")?
308 }
309 }
310
311 if let Some(warp) = &self.warp {
312 write!(f, "warp={warp} ")?;
313 }
314 if let Some(roll) = &self.roll {
315 write!(f, "roll={roll} ")?;
316 }
317
318 if let Some(value) = &self.value
320 && !value.is_zero()
321 {
322 write!(f, "value={value} ")?;
323 }
324
325 if let Some(sig) = &self.signature {
326 write!(f, "calldata={sig}")?
327 } else {
328 write!(f, "calldata={}", self.calldata)?
329 }
330
331 if let Some(args) = &self.args {
332 write!(f, " args=[{args}]")
333 } else {
334 write!(f, " args=[]")
335 }
336 }
337}
338
339#[derive(Debug, Default)]
341pub struct FuzzTestResult {
342 pub first_case: FuzzCase,
344 pub gas_by_case: Vec<(u64, u64)>,
346 pub success: bool,
350 pub skipped: bool,
352
353 pub reason: Option<String>,
356
357 pub counterexample: Option<CounterExample>,
359
360 pub logs: Vec<Log>,
363
364 pub labels: AddressHashMap<String>,
366
367 pub traces: Option<SparsedTraceArena>,
372
373 pub gas_report_traces: Vec<CallTraceArena>,
376
377 pub line_coverage: Option<HitMaps>,
379
380 pub breakpoints: Option<Breakpoints>,
382
383 pub debug_bytecodes: AddressHashMap<Bytes>,
385
386 pub deprecated_cheatcodes: HashMap<&'static str, Option<&'static str>>,
388
389 pub failed_corpus_replays: usize,
391}
392
393impl FuzzTestResult {
394 pub fn median_gas(&self, with_stipend: bool) -> u64 {
396 let mut values = self.gas_values(with_stipend);
397 values.sort_unstable();
398 calc::median_sorted(&values)
399 }
400
401 pub fn mean_gas(&self, with_stipend: bool) -> u64 {
403 let mut values = self.gas_values(with_stipend);
404 values.sort_unstable();
405 calc::mean(&values)
406 }
407
408 fn gas_values(&self, with_stipend: bool) -> Vec<u64> {
409 self.gas_by_case
410 .iter()
411 .map(|gas| if with_stipend { gas.0 } else { gas.0.saturating_sub(gas.1) })
412 .collect()
413 }
414}
415
416#[derive(Clone, Debug, Default, Serialize, Deserialize)]
418pub struct FuzzCase {
419 pub gas: u64,
421 pub stipend: u64,
423}
424
425#[derive(Clone, Debug, Serialize, Deserialize)]
427#[serde(transparent)]
428pub struct FuzzedCases {
429 cases: Vec<FuzzCase>,
430}
431
432impl FuzzedCases {
433 pub fn new(mut cases: Vec<FuzzCase>) -> Self {
434 cases.sort_by_key(|c| c.gas);
435 Self { cases }
436 }
437
438 pub fn cases(&self) -> &[FuzzCase] {
439 &self.cases
440 }
441
442 pub fn into_cases(self) -> Vec<FuzzCase> {
443 self.cases
444 }
445
446 pub fn last(&self) -> Option<&FuzzCase> {
448 self.cases.last()
449 }
450
451 pub fn median_gas(&self, with_stipend: bool) -> u64 {
453 let mut values = self.gas_values(with_stipend);
454 values.sort_unstable();
455 calc::median_sorted(&values)
456 }
457
458 pub fn mean_gas(&self, with_stipend: bool) -> u64 {
460 let mut values = self.gas_values(with_stipend);
461 values.sort_unstable();
462 calc::mean(&values)
463 }
464
465 fn gas_values(&self, with_stipend: bool) -> Vec<u64> {
466 self.cases
467 .iter()
468 .map(|c| if with_stipend { c.gas } else { c.gas.saturating_sub(c.stipend) })
469 .collect()
470 }
471
472 pub fn highest(&self) -> Option<&FuzzCase> {
474 self.cases.last()
475 }
476
477 pub fn lowest(&self) -> Option<&FuzzCase> {
479 self.cases.first()
480 }
481
482 pub fn highest_gas(&self, with_stipend: bool) -> u64 {
484 self.highest()
485 .map(|c| if with_stipend { c.gas } else { c.gas - c.stipend })
486 .unwrap_or_default()
487 }
488
489 pub fn lowest_gas(&self) -> u64 {
491 self.lowest().map(|c| c.gas).unwrap_or_default()
492 }
493}
494
495#[derive(Clone, Default, Debug)]
502pub struct FuzzFixtures {
503 inner: Arc<HashMap<String, DynSolValue>>,
504 enum_bounds: strategies::EnumBounds,
506}
507
508impl FuzzFixtures {
509 pub fn new(fixtures: HashMap<String, DynSolValue>) -> Self {
510 Self { inner: Arc::new(fixtures), enum_bounds: strategies::EnumBounds::default() }
511 }
512
513 pub fn with_enum_bounds(mut self, enum_bounds: strategies::EnumBounds) -> Self {
515 self.enum_bounds = enum_bounds;
516 self
517 }
518
519 pub fn param_fixtures(&self, param_name: &str) -> Option<&[DynSolValue]> {
521 if let Some(param_fixtures) = self.inner.get(&normalize_fixture(param_name)) {
522 param_fixtures.as_fixed_array().or_else(|| param_fixtures.as_array())
523 } else {
524 None
525 }
526 }
527
528 pub fn enum_variant_count(&self, contract: Option<&str>, name: &str) -> Option<usize> {
531 self.enum_bounds.variant_count(contract, name)
532 }
533}
534
535pub fn fixture_name(function_name: String) -> String {
538 normalize_fixture(function_name.strip_prefix("fixture").unwrap())
539}
540
541fn normalize_fixture(param_name: &str) -> String {
543 param_name.trim_matches('_').to_ascii_lowercase()
544}