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 if let Some(cheats) = self.executor.inspector_mut().cheatcodes.as_mut()
175 && let Some(seed) = self.config.seed
176 {
177 cheats.set_seed(seed.wrapping_add(U256::from(test_data.runs)));
178 }
179 test_data.runs += 1;
180
181 match corpus_manager.new_input(&mut self.runner, state, func) {
182 Ok(input) => input,
183 Err(err) => {
184 test_data.failure = Some(TestCaseError::fail(format!(
185 "failed to generate fuzzed input: {err}"
186 )));
187 break 'stop;
188 }
189 }
190 };
191
192 match self.single_fuzz(address, input, &mut corpus_manager) {
193 Ok(fuzz_outcome) => match fuzz_outcome {
194 FuzzOutcome::Case(case) => {
195 test_data.gas_by_case.push((case.case.gas, case.case.stipend));
196
197 if test_data.first_case.is_none() {
198 test_data.first_case.replace(case.case);
199 }
200
201 if let Some(call_traces) = case.traces {
202 if test_data.traces.len() == max_traces_to_collect {
203 test_data.traces.pop();
204 }
205 test_data.traces.push(call_traces);
206 test_data.breakpoints.replace(case.breakpoints);
207 }
208
209 if self.config.show_logs {
213 test_data.logs.extend(case.logs);
214 } else {
215 test_data.logs = case.logs;
216 }
217
218 HitMaps::merge_opt(&mut test_data.coverage, case.coverage);
219 test_data.deprecated_cheatcodes = case.deprecated_cheatcodes;
220 }
221 FuzzOutcome::CounterExample(CounterExampleOutcome {
222 exit_reason: status,
223 counterexample: outcome,
224 ..
225 }) => {
226 let reason = rd.maybe_decode(&outcome.1.result, status);
227 test_data.logs.extend(outcome.1.logs.clone());
228 test_data.counterexample = outcome;
229 test_data.failure = Some(TestCaseError::fail(reason.unwrap_or_default()));
230 break 'stop;
231 }
232 },
233 Err(err) => {
234 match err {
235 TestCaseError::Fail(_) => {
236 test_data.failure = Some(err);
237 break 'stop;
238 }
239 TestCaseError::Reject(_) => {
240 test_data.runs = test_data.runs.saturating_sub(1);
243 test_data.rejects += 1;
244
245 if let Some(progress) = progress {
247 progress.set_message(format!("([{}] rejected)", test_data.rejects));
248 progress.dec(1);
249 }
250
251 if self.config.max_test_rejects > 0
252 && test_data.rejects >= self.config.max_test_rejects
253 {
254 test_data.failure = Some(TestCaseError::reject(
255 FuzzError::TooManyRejects(self.config.max_test_rejects),
256 ));
257 break 'stop;
258 }
259 }
260 }
261 }
262 }
263 }
264
265 let (calldata, call) = test_data.counterexample;
266 let mut traces = test_data.traces;
267 let (last_run_traces, last_run_breakpoints) = if test_data.failure.is_none() {
268 (traces.pop(), test_data.breakpoints)
269 } else {
270 (call.traces.clone(), call.cheatcodes.map(|c| c.breakpoints))
271 };
272
273 let result_logs = test_data.logs;
278
279 let mut result = FuzzTestResult {
280 first_case: test_data.first_case.unwrap_or_default(),
281 gas_by_case: test_data.gas_by_case,
282 success: test_data.failure.is_none(),
283 skipped: false,
284 reason: None,
285 counterexample: None,
286 logs: result_logs,
287 labels: call.labels,
288 traces: last_run_traces,
289 breakpoints: last_run_breakpoints,
290 gas_report_traces: traces.into_iter().map(|a| a.arena).collect(),
291 line_coverage: test_data.coverage,
292 deprecated_cheatcodes: test_data.deprecated_cheatcodes,
293 failed_corpus_replays: corpus_manager.failed_replays(),
294 };
295
296 match test_data.failure {
297 Some(TestCaseError::Fail(reason)) => {
298 let reason = reason.to_string();
299 result.reason = (!reason.is_empty()).then_some(reason);
300 let args = if let Some(data) = calldata.get(4..) {
301 func.abi_decode_input(data).unwrap_or_default()
302 } else {
303 vec![]
304 };
305 result.counterexample = Some(CounterExample::Single(
306 BaseCounterExample::from_fuzz_call(calldata, args, call.traces),
307 ));
308 }
309 Some(TestCaseError::Reject(reason)) => {
310 let reason = reason.to_string();
311 result.reason = (!reason.is_empty()).then_some(reason);
312 }
313 None => {}
314 }
315
316 if let Some(reason) = &result.reason
317 && let Some(reason) = SkipReason::decode_self(reason)
318 {
319 result.skipped = true;
320 result.reason = reason.0;
321 }
322
323 state.log_stats();
324
325 Ok(result)
326 }
327
328 fn single_fuzz(
331 &mut self,
332 address: Address,
333 calldata: Bytes,
334 coverage_metrics: &mut CorpusManager,
335 ) -> Result<FuzzOutcome, TestCaseError> {
336 let mut call = self
337 .executor
338 .call_raw(self.sender, address, calldata.clone(), U256::ZERO)
339 .map_err(|e| TestCaseError::fail(e.to_string()))?;
340 let new_coverage = coverage_metrics.merge_edge_coverage(&mut call);
341 coverage_metrics.process_inputs(
342 &[BasicTxDetails {
343 warp: None,
344 roll: None,
345 sender: self.sender,
346 call_details: CallDetails { target: address, calldata: calldata.clone() },
347 }],
348 new_coverage,
349 );
350
351 if call.result.as_ref() == MAGIC_ASSUME {
353 return Err(TestCaseError::reject(FuzzError::AssumeReject));
354 }
355
356 let (breakpoints, deprecated_cheatcodes) =
357 call.cheatcodes.as_ref().map_or_else(Default::default, |cheats| {
358 (cheats.breakpoints.clone(), cheats.deprecated.clone())
359 });
360
361 let success = if !self.config.fail_on_revert
364 && call
365 .reverter
366 .is_some_and(|reverter| reverter != address && reverter != CHEATCODE_ADDRESS)
367 {
368 true
369 } else {
370 self.executor.is_raw_call_mut_success(address, &mut call, false)
371 };
372
373 if success {
374 Ok(FuzzOutcome::Case(CaseOutcome {
375 case: FuzzCase { calldata, gas: call.gas_used, stipend: call.stipend },
376 traces: call.traces,
377 coverage: call.line_coverage,
378 breakpoints,
379 logs: call.logs,
380 deprecated_cheatcodes,
381 }))
382 } else {
383 Ok(FuzzOutcome::CounterExample(CounterExampleOutcome {
384 exit_reason: call.exit_reason,
385 counterexample: (calldata, call),
386 breakpoints,
387 }))
388 }
389 }
390}