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