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, Debug, Serialize, Deserialize)]
38pub struct BasicTxDetails {
39 pub warp: Option<U256>,
41 pub roll: Option<U256>,
43 pub sender: Address,
45 pub call_details: CallDetails,
47}
48
49#[derive(Clone, Debug, Serialize, Deserialize)]
51pub struct CallDetails {
52 pub target: Address,
54 pub calldata: Bytes,
56}
57
58#[derive(Clone, Debug, Serialize, Deserialize)]
59#[expect(clippy::large_enum_variant)]
60pub enum CounterExample {
61 Single(BaseCounterExample),
63 Sequence(usize, Vec<BaseCounterExample>),
65}
66
67#[derive(Clone, Debug, Serialize, Deserialize)]
68pub struct BaseCounterExample {
69 pub warp: Option<U256>,
71 pub roll: Option<U256>,
73 pub sender: Option<Address>,
75 pub addr: Option<Address>,
77 pub calldata: Bytes,
79 pub contract_name: Option<String>,
81 pub func_name: Option<String>,
83 pub signature: Option<String>,
85 pub args: Option<String>,
87 pub raw_args: Option<String>,
89 #[serde(skip)]
91 pub traces: Option<SparsedTraceArena>,
92 #[serde(skip)]
94 pub show_solidity: bool,
95}
96
97impl BaseCounterExample {
98 pub fn from_invariant_call(
100 tx: &BasicTxDetails,
101 contracts: &ContractsByAddress,
102 traces: Option<SparsedTraceArena>,
103 show_solidity: bool,
104 ) -> Self {
105 let sender = tx.sender;
106 let target = tx.call_details.target;
107 let bytes = &tx.call_details.calldata;
108 let warp = tx.warp;
109 let roll = tx.roll;
110 if let Some((name, abi)) = &contracts.get(&target)
111 && let Some(func) = abi.functions().find(|f| f.selector() == bytes[..4])
112 {
113 if let Ok(args) = func.abi_decode_input(&bytes[4..]) {
115 return Self {
116 warp,
117 roll,
118 sender: Some(sender),
119 addr: Some(target),
120 calldata: bytes.clone(),
121 contract_name: Some(name.clone()),
122 func_name: Some(func.name.clone()),
123 signature: Some(func.signature()),
124 args: Some(foundry_common::fmt::format_tokens(&args).format(", ").to_string()),
125 raw_args: Some(
126 foundry_common::fmt::format_tokens_raw(&args).format(", ").to_string(),
127 ),
128 traces,
129 show_solidity,
130 };
131 }
132 }
133
134 Self {
135 warp,
136 roll,
137 sender: Some(sender),
138 addr: Some(target),
139 calldata: bytes.clone(),
140 contract_name: None,
141 func_name: None,
142 signature: None,
143 args: None,
144 raw_args: None,
145 traces,
146 show_solidity: false,
147 }
148 }
149
150 pub fn from_fuzz_call(
152 bytes: Bytes,
153 args: Vec<DynSolValue>,
154 traces: Option<SparsedTraceArena>,
155 ) -> Self {
156 Self {
157 warp: None,
158 roll: None,
159 sender: None,
160 addr: None,
161 calldata: bytes,
162 contract_name: None,
163 func_name: None,
164 signature: None,
165 args: Some(foundry_common::fmt::format_tokens(&args).format(", ").to_string()),
166 raw_args: Some(foundry_common::fmt::format_tokens_raw(&args).format(", ").to_string()),
167 traces,
168 show_solidity: false,
169 }
170 }
171}
172
173impl fmt::Display for BaseCounterExample {
174 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
175 if self.show_solidity
177 && let (Some(sender), Some(contract), Some(address), Some(func_name), Some(args)) =
178 (&self.sender, &self.contract_name, &self.addr, &self.func_name, &self.raw_args)
179 {
180 if let Some(warp) = &self.warp {
181 writeln!(f, "\t\tvm.warp(block.timestamp + {warp});")?;
182 }
183 if let Some(roll) = &self.roll {
184 writeln!(f, "\t\tvm.roll(block.number + {roll});")?;
185 }
186 writeln!(f, "\t\tvm.prank({sender});")?;
187 write!(
188 f,
189 "\t\t{}({}).{}({});",
190 contract.split_once(':').map_or(contract.as_str(), |(_, contract)| contract),
191 address,
192 func_name,
193 args
194 )?;
195
196 return Ok(());
197 }
198
199 if let Some(sender) = self.sender {
201 write!(f, "\t\tsender={sender} addr=")?
202 }
203
204 if let Some(name) = &self.contract_name {
205 write!(f, "[{name}]")?
206 }
207
208 if let Some(addr) = &self.addr {
209 write!(f, "{addr} ")?
210 }
211
212 if let Some(warp) = &self.warp {
213 write!(f, "warp={warp} ")?;
214 }
215 if let Some(roll) = &self.roll {
216 write!(f, "roll={roll} ")?;
217 }
218
219 if let Some(sig) = &self.signature {
220 write!(f, "calldata={sig}")?
221 } else {
222 write!(f, "calldata={}", &self.calldata)?
223 }
224
225 if let Some(args) = &self.args {
226 write!(f, " args=[{args}]")
227 } else {
228 write!(f, " args=[]")
229 }
230 }
231}
232
233#[derive(Debug, Default)]
235pub struct FuzzTestResult {
236 pub first_case: FuzzCase,
238 pub gas_by_case: Vec<(u64, u64)>,
240 pub success: bool,
244 pub skipped: bool,
246
247 pub reason: Option<String>,
250
251 pub counterexample: Option<CounterExample>,
253
254 pub logs: Vec<Log>,
257
258 pub labels: AddressHashMap<String>,
260
261 pub traces: Option<SparsedTraceArena>,
266
267 pub gas_report_traces: Vec<CallTraceArena>,
270
271 pub line_coverage: Option<HitMaps>,
273
274 pub breakpoints: Option<Breakpoints>,
276
277 pub deprecated_cheatcodes: HashMap<&'static str, Option<&'static str>>,
279
280 pub failed_corpus_replays: usize,
282}
283
284impl FuzzTestResult {
285 pub fn median_gas(&self, with_stipend: bool) -> u64 {
287 let mut values = self.gas_values(with_stipend);
288 values.sort_unstable();
289 calc::median_sorted(&values)
290 }
291
292 pub fn mean_gas(&self, with_stipend: bool) -> u64 {
294 let mut values = self.gas_values(with_stipend);
295 values.sort_unstable();
296 calc::mean(&values)
297 }
298
299 fn gas_values(&self, with_stipend: bool) -> Vec<u64> {
300 self.gas_by_case
301 .iter()
302 .map(|gas| if with_stipend { gas.0 } else { gas.0.saturating_sub(gas.1) })
303 .collect()
304 }
305}
306
307#[derive(Clone, Debug, Default, Serialize, Deserialize)]
309pub struct FuzzCase {
310 pub calldata: Bytes,
312 pub gas: u64,
314 pub stipend: u64,
316}
317
318#[derive(Clone, Debug, Serialize, Deserialize)]
320#[serde(transparent)]
321pub struct FuzzedCases {
322 cases: Vec<FuzzCase>,
323}
324
325impl FuzzedCases {
326 pub fn new(mut cases: Vec<FuzzCase>) -> Self {
327 cases.sort_by_key(|c| c.gas);
328 Self { cases }
329 }
330
331 pub fn cases(&self) -> &[FuzzCase] {
332 &self.cases
333 }
334
335 pub fn into_cases(self) -> Vec<FuzzCase> {
336 self.cases
337 }
338
339 pub fn last(&self) -> Option<&FuzzCase> {
341 self.cases.last()
342 }
343
344 pub fn median_gas(&self, with_stipend: bool) -> u64 {
346 let mut values = self.gas_values(with_stipend);
347 values.sort_unstable();
348 calc::median_sorted(&values)
349 }
350
351 pub fn mean_gas(&self, with_stipend: bool) -> u64 {
353 let mut values = self.gas_values(with_stipend);
354 values.sort_unstable();
355 calc::mean(&values)
356 }
357
358 fn gas_values(&self, with_stipend: bool) -> Vec<u64> {
359 self.cases
360 .iter()
361 .map(|c| if with_stipend { c.gas } else { c.gas.saturating_sub(c.stipend) })
362 .collect()
363 }
364
365 pub fn highest(&self) -> Option<&FuzzCase> {
367 self.cases.last()
368 }
369
370 pub fn lowest(&self) -> Option<&FuzzCase> {
372 self.cases.first()
373 }
374
375 pub fn highest_gas(&self, with_stipend: bool) -> u64 {
377 self.highest()
378 .map(|c| if with_stipend { c.gas } else { c.gas - c.stipend })
379 .unwrap_or_default()
380 }
381
382 pub fn lowest_gas(&self) -> u64 {
384 self.lowest().map(|c| c.gas).unwrap_or_default()
385 }
386}
387
388#[derive(Clone, Default, Debug)]
395pub struct FuzzFixtures {
396 inner: Arc<HashMap<String, DynSolValue>>,
397}
398
399impl FuzzFixtures {
400 pub fn new(fixtures: HashMap<String, DynSolValue>) -> Self {
401 Self { inner: Arc::new(fixtures) }
402 }
403
404 pub fn param_fixtures(&self, param_name: &str) -> Option<&[DynSolValue]> {
406 if let Some(param_fixtures) = self.inner.get(&normalize_fixture(param_name)) {
407 param_fixtures.as_fixed_array().or_else(|| param_fixtures.as_array())
408 } else {
409 None
410 }
411 }
412}
413
414pub fn fixture_name(function_name: String) -> String {
417 normalize_fixture(function_name.strip_prefix("fixture").unwrap())
418}
419
420fn normalize_fixture(param_name: &str) -> String {
422 param_name.trim_matches('_').to_ascii_lowercase()
423}