foundry_evm/executors/fuzz/
mod.rs1use crate::executors::{
2 DURATION_BETWEEN_METRICS_REPORT, Executor, FailFast, 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>,
51 deprecated_cheatcodes: HashMap<&'static str, Option<&'static str>>,
53 runs: u32,
55 rejects: u32,
57 failure: Option<TestCaseError>,
59}
60
61pub struct FuzzedExecutor {
67 executor: Executor,
69 runner: TestRunner,
71 sender: Address,
73 config: FuzzConfig,
75 persisted_failure: Option<BaseCounterExample>,
77}
78
79impl FuzzedExecutor {
80 pub fn new(
82 executor: Executor,
83 runner: TestRunner,
84 sender: Address,
85 config: FuzzConfig,
86 persisted_failure: Option<BaseCounterExample>,
87 ) -> Self {
88 Self { executor, runner, sender, config, persisted_failure }
89 }
90
91 #[allow(clippy::too_many_arguments)]
97 pub fn fuzz(
98 &mut self,
99 func: &Function,
100 fuzz_fixtures: &FuzzFixtures,
101 deployed_libs: &[Address],
102 address: Address,
103 rd: &RevertDecoder,
104 progress: Option<&ProgressBar>,
105 fail_fast: &FailFast,
106 ) -> Result<FuzzTestResult> {
107 let mut test_data = FuzzTestData::default();
109 let state = self.build_fuzz_state(deployed_libs);
110 let dictionary_weight = self.config.dictionary.dictionary_weight.min(100);
111 let strategy = proptest::prop_oneof![
112 100 - dictionary_weight => fuzz_calldata(func.clone(), fuzz_fixtures),
113 dictionary_weight => fuzz_calldata_from_state(func.clone(), &state),
114 ]
115 .prop_map(move |calldata| BasicTxDetails {
116 sender: Default::default(),
117 call_details: CallDetails { target: Default::default(), calldata },
118 });
119 let max_traces_to_collect = std::cmp::max(1, self.config.gas_report_samples) as usize;
121
122 let mut corpus_manager = CorpusManager::new(
123 self.config.corpus.clone(),
124 strategy.boxed(),
125 &self.executor,
126 Some(func),
127 None,
128 )?;
129
130 let timer = FuzzTestTimer::new(self.config.timeout);
132 let mut last_metrics_report = Instant::now();
133 let max_runs = self.config.runs;
134 let continue_campaign = |runs: u32| {
135 if fail_fast.should_stop() {
136 return false;
137 }
138
139 if timer.is_enabled() { !timer.is_timed_out() } else { runs < max_runs }
140 };
141
142 'stop: while continue_campaign(test_data.runs) {
143 let input = if let Some(failure) = self.persisted_failure.take()
145 && func.selector() == failure.calldata[..4]
146 {
147 failure.calldata.clone()
148 } else {
149 if let Some(progress) = progress {
151 progress.inc(1);
152 if self.config.corpus.collect_edge_coverage() {
154 progress.set_message(format!("{}", &corpus_manager.metrics));
155 }
156 } else if self.config.corpus.collect_edge_coverage()
157 && last_metrics_report.elapsed() > DURATION_BETWEEN_METRICS_REPORT
158 {
159 let metrics = json!({
161 "timestamp": SystemTime::now()
162 .duration_since(UNIX_EPOCH)?
163 .as_secs(),
164 "test": func.name,
165 "metrics": &corpus_manager.metrics,
166 });
167 let _ = sh_println!("{}", serde_json::to_string(&metrics)?);
168 last_metrics_report = Instant::now();
169 };
170
171 test_data.runs += 1;
172
173 match corpus_manager.new_input(&mut self.runner, &state, func) {
174 Ok(input) => input,
175 Err(err) => {
176 test_data.failure = Some(TestCaseError::fail(format!(
177 "failed to generate fuzzed input: {err}"
178 )));
179 break 'stop;
180 }
181 }
182 };
183
184 match self.single_fuzz(address, input, &mut corpus_manager) {
185 Ok(fuzz_outcome) => match fuzz_outcome {
186 FuzzOutcome::Case(case) => {
187 test_data.gas_by_case.push((case.case.gas, case.case.stipend));
188
189 if test_data.first_case.is_none() {
190 test_data.first_case.replace(case.case);
191 }
192
193 if let Some(call_traces) = case.traces {
194 if test_data.traces.len() == max_traces_to_collect {
195 test_data.traces.pop();
196 }
197 test_data.traces.push(call_traces);
198 test_data.breakpoints.replace(case.breakpoints);
199 }
200
201 if self.config.show_logs {
202 test_data.logs.extend(case.logs);
203 }
204
205 HitMaps::merge_opt(&mut test_data.coverage, case.coverage);
206 test_data.deprecated_cheatcodes = case.deprecated_cheatcodes;
207 }
208 FuzzOutcome::CounterExample(CounterExampleOutcome {
209 exit_reason: status,
210 counterexample: outcome,
211 ..
212 }) => {
213 let reason = rd.maybe_decode(&outcome.1.result, status);
214 test_data.logs.extend(outcome.1.logs.clone());
215 test_data.counterexample = outcome;
216 test_data.failure = Some(TestCaseError::fail(reason.unwrap_or_default()));
217 break 'stop;
218 }
219 },
220 Err(err) => {
221 match err {
222 TestCaseError::Fail(_) => {
223 test_data.failure = Some(err);
224 break 'stop;
225 }
226 TestCaseError::Reject(_) => {
227 test_data.runs = test_data.runs.saturating_sub(1);
230 if self.config.max_test_rejects > 0 {
231 test_data.rejects += 1;
232 if test_data.rejects >= self.config.max_test_rejects {
233 test_data.failure = Some(err);
234 break 'stop;
235 }
236 }
237 }
238 }
239 }
240 }
241 }
242
243 let (calldata, call) = test_data.counterexample;
244 let mut traces = test_data.traces;
245 let (last_run_traces, last_run_breakpoints) = if test_data.failure.is_none() {
246 (traces.pop(), test_data.breakpoints)
247 } else {
248 (call.traces.clone(), call.cheatcodes.map(|c| c.breakpoints))
249 };
250
251 let mut result = FuzzTestResult {
252 first_case: test_data.first_case.unwrap_or_default(),
253 gas_by_case: test_data.gas_by_case,
254 success: test_data.failure.is_none(),
255 skipped: false,
256 reason: None,
257 counterexample: None,
258 logs: test_data.logs,
259 labels: call.labels,
260 traces: last_run_traces,
261 breakpoints: last_run_breakpoints,
262 gas_report_traces: traces.into_iter().map(|a| a.arena).collect(),
263 line_coverage: test_data.coverage,
264 deprecated_cheatcodes: test_data.deprecated_cheatcodes,
265 failed_corpus_replays: corpus_manager.failed_replays(),
266 };
267
268 match test_data.failure {
269 Some(TestCaseError::Fail(reason)) => {
270 let reason = reason.to_string();
271 result.reason = (!reason.is_empty()).then_some(reason);
272 let args = if let Some(data) = calldata.get(4..) {
273 func.abi_decode_input(data).unwrap_or_default()
274 } else {
275 vec![]
276 };
277 result.counterexample = Some(CounterExample::Single(
278 BaseCounterExample::from_fuzz_call(calldata, args, call.traces),
279 ));
280 }
281 Some(TestCaseError::Reject(reason)) => {
282 let reason = reason.to_string();
283 result.reason = (!reason.is_empty()).then_some(reason);
284 }
285 None => {}
286 }
287
288 if let Some(reason) = &result.reason
289 && let Some(reason) = SkipReason::decode_self(reason)
290 {
291 result.skipped = true;
292 result.reason = reason.0;
293 }
294
295 state.log_stats();
296
297 Ok(result)
298 }
299
300 fn single_fuzz(
303 &mut self,
304 address: Address,
305 calldata: Bytes,
306 coverage_metrics: &mut CorpusManager,
307 ) -> Result<FuzzOutcome, TestCaseError> {
308 let mut call = self
309 .executor
310 .call_raw(self.sender, address, calldata.clone(), U256::ZERO)
311 .map_err(|e| TestCaseError::fail(e.to_string()))?;
312 let new_coverage = coverage_metrics.merge_edge_coverage(&mut call);
313 coverage_metrics.process_inputs(
314 &[BasicTxDetails {
315 sender: self.sender,
316 call_details: CallDetails { target: address, calldata: calldata.clone() },
317 }],
318 new_coverage,
319 );
320
321 if call.result.as_ref() == MAGIC_ASSUME {
323 return Err(TestCaseError::reject(FuzzError::TooManyRejects(
324 self.config.max_test_rejects,
325 )));
326 }
327
328 let (breakpoints, deprecated_cheatcodes) =
329 call.cheatcodes.as_ref().map_or_else(Default::default, |cheats| {
330 (cheats.breakpoints.clone(), cheats.deprecated.clone())
331 });
332
333 let success = if !self.config.fail_on_revert
336 && call
337 .reverter
338 .is_some_and(|reverter| reverter != address && reverter != CHEATCODE_ADDRESS)
339 {
340 true
341 } else {
342 self.executor.is_raw_call_mut_success(address, &mut call, false)
343 };
344
345 if success {
346 Ok(FuzzOutcome::Case(CaseOutcome {
347 case: FuzzCase { calldata, gas: call.gas_used, stipend: call.stipend },
348 traces: call.traces,
349 coverage: call.line_coverage,
350 breakpoints,
351 logs: call.logs,
352 deprecated_cheatcodes,
353 }))
354 } else {
355 Ok(FuzzOutcome::CounterExample(CounterExampleOutcome {
356 exit_reason: call.exit_reason,
357 counterexample: (calldata, call),
358 breakpoints,
359 }))
360 }
361 }
362
363 pub fn build_fuzz_state(&self, deployed_libs: &[Address]) -> EvmFuzzState {
365 if let Some(fork_db) = self.executor.backend().active_fork_db() {
366 EvmFuzzState::new(fork_db, self.config.dictionary, deployed_libs)
367 } else {
368 EvmFuzzState::new(
369 self.executor.backend().mem_db(),
370 self.config.dictionary,
371 deployed_libs,
372 )
373 }
374 }
375}