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>,
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 early_exit: &EarlyExit,
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 early_exit.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 test_data.rejects += 1;
231
232 if let Some(progress) = progress {
234 progress.set_message(format!("([{}] rejected)", test_data.rejects));
235 progress.dec(1);
236 }
237
238 if self.config.max_test_rejects > 0
239 && test_data.rejects >= self.config.max_test_rejects
240 {
241 test_data.failure = Some(err);
242 break 'stop;
243 }
244 }
245 }
246 }
247 }
248 }
249
250 let (calldata, call) = test_data.counterexample;
251 let mut traces = test_data.traces;
252 let (last_run_traces, last_run_breakpoints) = if test_data.failure.is_none() {
253 (traces.pop(), test_data.breakpoints)
254 } else {
255 (call.traces.clone(), call.cheatcodes.map(|c| c.breakpoints))
256 };
257
258 let mut result = FuzzTestResult {
259 first_case: test_data.first_case.unwrap_or_default(),
260 gas_by_case: test_data.gas_by_case,
261 success: test_data.failure.is_none(),
262 skipped: false,
263 reason: None,
264 counterexample: None,
265 logs: test_data.logs,
266 labels: call.labels,
267 traces: last_run_traces,
268 breakpoints: last_run_breakpoints,
269 gas_report_traces: traces.into_iter().map(|a| a.arena).collect(),
270 line_coverage: test_data.coverage,
271 deprecated_cheatcodes: test_data.deprecated_cheatcodes,
272 failed_corpus_replays: corpus_manager.failed_replays(),
273 };
274
275 match test_data.failure {
276 Some(TestCaseError::Fail(reason)) => {
277 let reason = reason.to_string();
278 result.reason = (!reason.is_empty()).then_some(reason);
279 let args = if let Some(data) = calldata.get(4..) {
280 func.abi_decode_input(data).unwrap_or_default()
281 } else {
282 vec![]
283 };
284 result.counterexample = Some(CounterExample::Single(
285 BaseCounterExample::from_fuzz_call(calldata, args, call.traces),
286 ));
287 }
288 Some(TestCaseError::Reject(reason)) => {
289 let reason = reason.to_string();
290 result.reason = (!reason.is_empty()).then_some(reason);
291 }
292 None => {}
293 }
294
295 if let Some(reason) = &result.reason
296 && let Some(reason) = SkipReason::decode_self(reason)
297 {
298 result.skipped = true;
299 result.reason = reason.0;
300 }
301
302 state.log_stats();
303
304 Ok(result)
305 }
306
307 fn single_fuzz(
310 &mut self,
311 address: Address,
312 calldata: Bytes,
313 coverage_metrics: &mut CorpusManager,
314 ) -> Result<FuzzOutcome, TestCaseError> {
315 let mut call = self
316 .executor
317 .call_raw(self.sender, address, calldata.clone(), U256::ZERO)
318 .map_err(|e| TestCaseError::fail(e.to_string()))?;
319 let new_coverage = coverage_metrics.merge_edge_coverage(&mut call);
320 coverage_metrics.process_inputs(
321 &[BasicTxDetails {
322 sender: self.sender,
323 call_details: CallDetails { target: address, calldata: calldata.clone() },
324 }],
325 new_coverage,
326 );
327
328 if call.result.as_ref() == MAGIC_ASSUME {
330 return Err(TestCaseError::reject(FuzzError::TooManyRejects(
331 self.config.max_test_rejects,
332 )));
333 }
334
335 let (breakpoints, deprecated_cheatcodes) =
336 call.cheatcodes.as_ref().map_or_else(Default::default, |cheats| {
337 (cheats.breakpoints.clone(), cheats.deprecated.clone())
338 });
339
340 let success = if !self.config.fail_on_revert
343 && call
344 .reverter
345 .is_some_and(|reverter| reverter != address && reverter != CHEATCODE_ADDRESS)
346 {
347 true
348 } else {
349 self.executor.is_raw_call_mut_success(address, &mut call, false)
350 };
351
352 if success {
353 Ok(FuzzOutcome::Case(CaseOutcome {
354 case: FuzzCase { calldata, gas: call.gas_used, stipend: call.stipend },
355 traces: call.traces,
356 coverage: call.line_coverage,
357 breakpoints,
358 logs: call.logs,
359 deprecated_cheatcodes,
360 }))
361 } else {
362 Ok(FuzzOutcome::CounterExample(CounterExampleOutcome {
363 exit_reason: call.exit_reason,
364 counterexample: (calldata, call),
365 breakpoints,
366 }))
367 }
368 }
369
370 pub fn build_fuzz_state(&self, deployed_libs: &[Address]) -> EvmFuzzState {
372 let inspector = self.executor.inspector();
373
374 if let Some(fork_db) = self.executor.backend().active_fork_db() {
375 EvmFuzzState::new(
376 fork_db,
377 self.config.dictionary,
378 deployed_libs,
379 inspector.analysis.as_ref(),
380 inspector.paths_config(),
381 )
382 } else {
383 EvmFuzzState::new(
384 self.executor.backend().mem_db(),
385 self.config.dictionary,
386 deployed_libs,
387 inspector.analysis.as_ref(),
388 inspector.paths_config(),
389 )
390 }
391 }
392}