foundry_evm/executors/fuzz/
mod.rs1use crate::executors::{
2 DURATION_BETWEEN_METRICS_REPORT, EarlyExit, Executor, FuzzTestTimer, RawCallResult,
3};
4use alloy_dyn_abi::JsonAbiExt;
5use alloy_json_abi::Function;
6use alloy_primitives::{Address, Bytes, Log, U256, map::HashMap};
7use eyre::Result;
8use foundry_common::sh_println;
9use foundry_config::FuzzConfig;
10use foundry_evm_core::{
11 Breakpoints,
12 constants::{CHEATCODE_ADDRESS, MAGIC_ASSUME},
13 decode::{RevertDecoder, SkipReason},
14};
15use foundry_evm_coverage::HitMaps;
16use foundry_evm_fuzz::{
17 BaseCounterExample, BasicTxDetails, CallDetails, CounterExample, FuzzCase, FuzzError,
18 FuzzFixtures, FuzzTestResult,
19 strategies::{EvmFuzzState, fuzz_calldata, fuzz_calldata_from_state},
20};
21use foundry_evm_traces::SparsedTraceArena;
22use indicatif::ProgressBar;
23use proptest::{
24 strategy::Strategy,
25 test_runner::{TestCaseError, TestRunner},
26};
27use serde_json::json;
28use std::time::{Instant, SystemTime, UNIX_EPOCH};
29
30mod types;
31use crate::executors::corpus::CorpusManager;
32pub use types::{CaseOutcome, CounterExampleOutcome, FuzzOutcome};
33
34#[derive(Default)]
36struct FuzzTestData {
37 first_case: Option<FuzzCase>,
39 gas_by_case: Vec<(u64, u64)>,
41 counterexample: (Bytes, RawCallResult),
43 traces: Vec<SparsedTraceArena>,
45 breakpoints: Option<Breakpoints>,
47 coverage: Option<HitMaps>,
49 logs: Vec<Log>,
52 deprecated_cheatcodes: HashMap<&'static str, Option<&'static str>>,
54 runs: u32,
56 rejects: u32,
58 failure: Option<TestCaseError>,
60}
61
62pub struct FuzzedExecutor {
68 executor: Executor,
70 runner: TestRunner,
72 sender: Address,
74 config: FuzzConfig,
76 persisted_failure: Option<BaseCounterExample>,
78}
79
80impl FuzzedExecutor {
81 pub fn new(
83 executor: Executor,
84 runner: TestRunner,
85 sender: Address,
86 config: FuzzConfig,
87 persisted_failure: Option<BaseCounterExample>,
88 ) -> Self {
89 Self { executor, runner, sender, config, persisted_failure }
90 }
91
92 #[allow(clippy::too_many_arguments)]
98 pub fn fuzz(
99 &mut self,
100 func: &Function,
101 fuzz_fixtures: &FuzzFixtures,
102 state: EvmFuzzState,
103 address: Address,
104 rd: &RevertDecoder,
105 progress: Option<&ProgressBar>,
106 early_exit: &EarlyExit,
107 ) -> Result<FuzzTestResult> {
108 let state = &state;
109 let mut test_data = FuzzTestData::default();
111 let dictionary_weight = self.config.dictionary.dictionary_weight.min(100);
112 let strategy = proptest::prop_oneof![
113 100 - dictionary_weight => fuzz_calldata(func.clone(), fuzz_fixtures),
114 dictionary_weight => fuzz_calldata_from_state(func.clone(), state),
115 ]
116 .prop_map(move |calldata| BasicTxDetails {
117 warp: None,
118 roll: None,
119 sender: Default::default(),
120 call_details: CallDetails { target: Default::default(), calldata },
121 });
122 let max_traces_to_collect = std::cmp::max(1, self.config.gas_report_samples) as usize;
124
125 let mut corpus_manager = CorpusManager::new(
126 self.config.corpus.clone(),
127 strategy.boxed(),
128 &self.executor,
129 Some(func),
130 None,
131 )?;
132
133 let timer = FuzzTestTimer::new(self.config.timeout);
135 let mut last_metrics_report = Instant::now();
136 let max_runs = self.config.runs;
137 let continue_campaign = |runs: u32| {
138 if early_exit.should_stop() {
139 return false;
140 }
141
142 if timer.is_enabled() { !timer.is_timed_out() } else { runs < max_runs }
143 };
144
145 'stop: while continue_campaign(test_data.runs) {
146 let input = if let Some(failure) = self.persisted_failure.take()
148 && failure.calldata.get(..4).is_some_and(|selector| func.selector() == selector)
149 {
150 failure.calldata.clone()
151 } else {
152 if let Some(progress) = progress {
154 progress.inc(1);
155 if self.config.corpus.collect_edge_coverage() {
157 progress.set_message(format!("{}", &corpus_manager.metrics));
158 }
159 } else if self.config.corpus.collect_edge_coverage()
160 && last_metrics_report.elapsed() > DURATION_BETWEEN_METRICS_REPORT
161 {
162 let metrics = json!({
164 "timestamp": SystemTime::now()
165 .duration_since(UNIX_EPOCH)?
166 .as_secs(),
167 "test": func.name,
168 "metrics": &corpus_manager.metrics,
169 });
170 let _ = sh_println!("{}", serde_json::to_string(&metrics)?);
171 last_metrics_report = Instant::now();
172 };
173
174 test_data.runs += 1;
175
176 match corpus_manager.new_input(&mut self.runner, state, func) {
177 Ok(input) => input,
178 Err(err) => {
179 test_data.failure = Some(TestCaseError::fail(format!(
180 "failed to generate fuzzed input: {err}"
181 )));
182 break 'stop;
183 }
184 }
185 };
186
187 match self.single_fuzz(address, input, &mut corpus_manager) {
188 Ok(fuzz_outcome) => match fuzz_outcome {
189 FuzzOutcome::Case(case) => {
190 test_data.gas_by_case.push((case.case.gas, case.case.stipend));
191
192 if test_data.first_case.is_none() {
193 test_data.first_case.replace(case.case);
194 }
195
196 if let Some(call_traces) = case.traces {
197 if test_data.traces.len() == max_traces_to_collect {
198 test_data.traces.pop();
199 }
200 test_data.traces.push(call_traces);
201 test_data.breakpoints.replace(case.breakpoints);
202 }
203
204 if self.config.show_logs {
208 test_data.logs.extend(case.logs);
209 } else {
210 test_data.logs = case.logs;
211 }
212
213 HitMaps::merge_opt(&mut test_data.coverage, case.coverage);
214 test_data.deprecated_cheatcodes = case.deprecated_cheatcodes;
215 }
216 FuzzOutcome::CounterExample(CounterExampleOutcome {
217 exit_reason: status,
218 counterexample: outcome,
219 ..
220 }) => {
221 let reason = rd.maybe_decode(&outcome.1.result, status);
222 test_data.logs.extend(outcome.1.logs.clone());
223 test_data.counterexample = outcome;
224 test_data.failure = Some(TestCaseError::fail(reason.unwrap_or_default()));
225 break 'stop;
226 }
227 },
228 Err(err) => {
229 match err {
230 TestCaseError::Fail(_) => {
231 test_data.failure = Some(err);
232 break 'stop;
233 }
234 TestCaseError::Reject(_) => {
235 test_data.runs = test_data.runs.saturating_sub(1);
238 test_data.rejects += 1;
239
240 if let Some(progress) = progress {
242 progress.set_message(format!("([{}] rejected)", test_data.rejects));
243 progress.dec(1);
244 }
245
246 if self.config.max_test_rejects > 0
247 && test_data.rejects >= self.config.max_test_rejects
248 {
249 test_data.failure = Some(TestCaseError::reject(
250 FuzzError::TooManyRejects(self.config.max_test_rejects),
251 ));
252 break 'stop;
253 }
254 }
255 }
256 }
257 }
258 }
259
260 let (calldata, call) = test_data.counterexample;
261 let mut traces = test_data.traces;
262 let (last_run_traces, last_run_breakpoints) = if test_data.failure.is_none() {
263 (traces.pop(), test_data.breakpoints)
264 } else {
265 (call.traces.clone(), call.cheatcodes.map(|c| c.breakpoints))
266 };
267
268 let result_logs = test_data.logs;
273
274 let mut result = FuzzTestResult {
275 first_case: test_data.first_case.unwrap_or_default(),
276 gas_by_case: test_data.gas_by_case,
277 success: test_data.failure.is_none(),
278 skipped: false,
279 reason: None,
280 counterexample: None,
281 logs: result_logs,
282 labels: call.labels,
283 traces: last_run_traces,
284 breakpoints: last_run_breakpoints,
285 gas_report_traces: traces.into_iter().map(|a| a.arena).collect(),
286 line_coverage: test_data.coverage,
287 deprecated_cheatcodes: test_data.deprecated_cheatcodes,
288 failed_corpus_replays: corpus_manager.failed_replays(),
289 };
290
291 match test_data.failure {
292 Some(TestCaseError::Fail(reason)) => {
293 let reason = reason.to_string();
294 result.reason = (!reason.is_empty()).then_some(reason);
295 let args = if let Some(data) = calldata.get(4..) {
296 func.abi_decode_input(data).unwrap_or_default()
297 } else {
298 vec![]
299 };
300 result.counterexample = Some(CounterExample::Single(
301 BaseCounterExample::from_fuzz_call(calldata, args, call.traces),
302 ));
303 }
304 Some(TestCaseError::Reject(reason)) => {
305 let reason = reason.to_string();
306 result.reason = (!reason.is_empty()).then_some(reason);
307 }
308 None => {}
309 }
310
311 if let Some(reason) = &result.reason
312 && let Some(reason) = SkipReason::decode_self(reason)
313 {
314 result.skipped = true;
315 result.reason = reason.0;
316 }
317
318 state.log_stats();
319
320 Ok(result)
321 }
322
323 fn single_fuzz(
326 &mut self,
327 address: Address,
328 calldata: Bytes,
329 coverage_metrics: &mut CorpusManager,
330 ) -> Result<FuzzOutcome, TestCaseError> {
331 let mut call = self
332 .executor
333 .call_raw(self.sender, address, calldata.clone(), U256::ZERO)
334 .map_err(|e| TestCaseError::fail(e.to_string()))?;
335 let new_coverage = coverage_metrics.merge_edge_coverage(&mut call);
336 coverage_metrics.process_inputs(
337 &[BasicTxDetails {
338 warp: None,
339 roll: None,
340 sender: self.sender,
341 call_details: CallDetails { target: address, calldata: calldata.clone() },
342 }],
343 new_coverage,
344 );
345
346 if call.result.as_ref() == MAGIC_ASSUME {
348 return Err(TestCaseError::reject(FuzzError::AssumeReject));
349 }
350
351 let (breakpoints, deprecated_cheatcodes) =
352 call.cheatcodes.as_ref().map_or_else(Default::default, |cheats| {
353 (cheats.breakpoints.clone(), cheats.deprecated.clone())
354 });
355
356 let success = if !self.config.fail_on_revert
359 && call
360 .reverter
361 .is_some_and(|reverter| reverter != address && reverter != CHEATCODE_ADDRESS)
362 {
363 true
364 } else {
365 self.executor.is_raw_call_mut_success(address, &mut call, false)
366 };
367
368 if success {
369 Ok(FuzzOutcome::Case(CaseOutcome {
370 case: FuzzCase { calldata, gas: call.gas_used, stipend: call.stipend },
371 traces: call.traces,
372 coverage: call.line_coverage,
373 breakpoints,
374 logs: call.logs,
375 deprecated_cheatcodes,
376 }))
377 } else {
378 Ok(FuzzOutcome::CounterExample(CounterExampleOutcome {
379 exit_reason: call.exit_reason,
380 counterexample: (calldata, call),
381 breakpoints,
382 }))
383 }
384 }
385}