foundry_evm/executors/fuzz/
mod.rs1use crate::executors::{Executor, FuzzTestTimer, RawCallResult};
2use alloy_dyn_abi::JsonAbiExt;
3use alloy_json_abi::Function;
4use alloy_primitives::{Address, Bytes, Log, U256, map::HashMap};
5use eyre::Result;
6use foundry_common::evm::Breakpoints;
7use foundry_config::FuzzConfig;
8use foundry_evm_core::{
9 constants::{CHEATCODE_ADDRESS, MAGIC_ASSUME, TEST_TIMEOUT},
10 decode::{RevertDecoder, SkipReason},
11};
12use foundry_evm_coverage::HitMaps;
13use foundry_evm_fuzz::{
14 BaseCounterExample, CounterExample, FuzzCase, FuzzError, FuzzFixtures, FuzzTestResult,
15 strategies::{EvmFuzzState, fuzz_calldata, fuzz_calldata_from_state},
16};
17use foundry_evm_traces::SparsedTraceArena;
18use indicatif::ProgressBar;
19use proptest::test_runner::{TestCaseError, TestError, TestRunner};
20use std::{cell::RefCell, collections::BTreeMap};
21
22mod types;
23pub use types::{CaseOutcome, CounterExampleOutcome, FuzzOutcome};
24
25#[derive(Default)]
27pub struct FuzzTestData {
28 pub first_case: Option<FuzzCase>,
30 pub gas_by_case: Vec<(u64, u64)>,
32 pub counterexample: (Bytes, RawCallResult),
34 pub traces: Vec<SparsedTraceArena>,
36 pub breakpoints: Option<Breakpoints>,
38 pub coverage: Option<HitMaps>,
40 pub logs: Vec<Log>,
42 pub gas_snapshots: BTreeMap<String, BTreeMap<String, String>>,
44 pub deprecated_cheatcodes: HashMap<&'static str, Option<&'static str>>,
46}
47
48pub struct FuzzedExecutor {
54 pub executor: Executor,
56 runner: TestRunner,
58 sender: Address,
60 config: FuzzConfig,
62}
63
64impl FuzzedExecutor {
65 pub fn new(
67 executor: Executor,
68 runner: TestRunner,
69 sender: Address,
70 config: FuzzConfig,
71 ) -> Self {
72 Self { executor, runner, sender, config }
73 }
74
75 pub fn fuzz(
81 &self,
82 func: &Function,
83 fuzz_fixtures: &FuzzFixtures,
84 deployed_libs: &[Address],
85 address: Address,
86 rd: &RevertDecoder,
87 progress: Option<&ProgressBar>,
88 ) -> FuzzTestResult {
89 let execution_data = RefCell::new(FuzzTestData::default());
91 let state = self.build_fuzz_state(deployed_libs);
92 let dictionary_weight = self.config.dictionary.dictionary_weight.min(100);
93 let max_fuzz_int = self.config.max_fuzz_int;
94 let strategy = proptest::prop_oneof![
95 100 - dictionary_weight => fuzz_calldata(func.clone(), fuzz_fixtures, max_fuzz_int),
96 dictionary_weight => fuzz_calldata_from_state(func.clone(), &state, max_fuzz_int),
97 ];
98 let max_traces_to_collect = std::cmp::max(1, self.config.gas_report_samples) as usize;
100 let show_logs = self.config.show_logs;
101
102 let timer = FuzzTestTimer::new(self.config.timeout);
104
105 let run_result = self.runner.clone().run(&strategy, |calldata| {
106 if timer.is_timed_out() {
108 return Err(TestCaseError::fail(TEST_TIMEOUT));
109 }
110 self.executor
111 .strategy
112 .runner
113 .start_transaction(self.executor.strategy.context.as_ref());
114 let fuzz_res = self.single_fuzz(address, calldata);
115 self.executor
116 .strategy
117 .runner
118 .rollback_transaction(self.executor.strategy.context.as_ref());
119 let fuzz_res = fuzz_res?;
120 if let Some(progress) = progress {
122 progress.inc(1);
123 };
124
125 match fuzz_res {
126 FuzzOutcome::Case(case) => {
127 let mut data = execution_data.borrow_mut();
128 data.gas_by_case.push((case.case.gas, case.case.stipend));
129
130 if data.first_case.is_none() {
131 data.first_case.replace(case.case);
132 }
133
134 if let Some(call_traces) = case.traces {
135 if data.traces.len() == max_traces_to_collect {
136 data.traces.pop();
137 }
138 data.traces.push(call_traces);
139 data.breakpoints.replace(case.breakpoints);
140 }
141
142 if show_logs {
143 data.logs.extend(case.logs);
144 }
145
146 HitMaps::merge_opt(&mut data.coverage, case.coverage);
147
148 data.deprecated_cheatcodes = case.deprecated_cheatcodes;
149
150 Ok(())
151 }
152 FuzzOutcome::CounterExample(CounterExampleOutcome {
153 exit_reason: status,
154 counterexample: outcome,
155 ..
156 }) => {
157 let reason = rd.maybe_decode(&outcome.1.result, status);
162 execution_data.borrow_mut().logs.extend(outcome.1.logs.clone());
163 execution_data.borrow_mut().counterexample = outcome;
164 Err(TestCaseError::fail(reason.unwrap_or_default()))
166 }
167 }
168 });
169
170 let fuzz_result = execution_data.into_inner();
171 let (calldata, call) = fuzz_result.counterexample;
172
173 let mut traces = fuzz_result.traces;
174 let (last_run_traces, last_run_breakpoints) = if run_result.is_ok() {
175 (traces.pop(), fuzz_result.breakpoints)
176 } else {
177 (call.traces.clone(), call.cheatcodes.map(|c| c.breakpoints))
178 };
179
180 let mut result = FuzzTestResult {
181 first_case: fuzz_result.first_case.unwrap_or_default(),
182 gas_by_case: fuzz_result.gas_by_case,
183 success: run_result.is_ok(),
184 skipped: false,
185 reason: None,
186 counterexample: None,
187 logs: fuzz_result.logs,
188 labeled_addresses: call.labels,
189 traces: last_run_traces,
190 breakpoints: last_run_breakpoints,
191 gas_report_traces: traces.into_iter().map(|a| a.arena).collect(),
192 line_coverage: fuzz_result.coverage,
193 deprecated_cheatcodes: fuzz_result.deprecated_cheatcodes,
194 };
195
196 match run_result {
197 Ok(()) => {}
198 Err(TestError::Abort(reason)) => {
199 let msg = reason.message();
200 result.reason = if msg == "Too many global rejects" {
204 let error = FuzzError::TooManyRejects(self.runner.config().max_global_rejects);
205 Some(error.to_string())
206 } else {
207 Some(msg.to_string())
208 };
209 }
210 Err(TestError::Fail(reason, _)) => {
211 let reason = reason.to_string();
212 if reason == TEST_TIMEOUT {
213 result.success = true;
215 } else {
216 result.reason = (!reason.is_empty()).then_some(reason);
217 let args = if let Some(data) = calldata.get(4..) {
218 func.abi_decode_input(data).unwrap_or_default()
219 } else {
220 vec![]
221 };
222
223 result.counterexample = Some(CounterExample::Single(
224 BaseCounterExample::from_fuzz_call(calldata, args, call.traces),
225 ));
226 }
227 }
228 }
229
230 if let Some(reason) = &result.reason
231 && let Some(reason) = SkipReason::decode_self(reason)
232 {
233 result.skipped = true;
234 result.reason = reason.0;
235 }
236
237 state.log_stats();
238
239 result
240 }
241
242 pub fn single_fuzz(
245 &self,
246 address: Address,
247 calldata: alloy_primitives::Bytes,
248 ) -> Result<FuzzOutcome, TestCaseError> {
249 let mut call = self
250 .executor
251 .call_raw(self.sender, address, calldata.clone(), U256::ZERO)
252 .map_err(|e| TestCaseError::fail(e.to_string()))?;
253 if call.result.as_ref() == MAGIC_ASSUME {
255 return Err(TestCaseError::reject(FuzzError::AssumeReject));
256 }
257
258 let (breakpoints, deprecated_cheatcodes) =
259 call.cheatcodes.as_ref().map_or_else(Default::default, |cheats| {
260 (cheats.breakpoints.clone(), cheats.deprecated.clone())
261 });
262
263 let success = if !self.config.fail_on_revert
266 && call
267 .reverter
268 .is_some_and(|reverter| reverter != address && reverter != CHEATCODE_ADDRESS)
269 {
270 true
271 } else {
272 self.executor.is_raw_call_mut_success(address, &mut call, false)
273 };
274
275 if success {
276 Ok(FuzzOutcome::Case(CaseOutcome {
277 case: FuzzCase { calldata, gas: call.gas_used, stipend: call.stipend },
278 traces: call.traces,
279 coverage: call.line_coverage,
280 breakpoints,
281 logs: call.logs,
282 deprecated_cheatcodes,
283 }))
284 } else {
285 Ok(FuzzOutcome::CounterExample(CounterExampleOutcome {
286 exit_reason: call.exit_reason,
287 counterexample: (calldata, call),
288 breakpoints,
289 }))
290 }
291 }
292
293 pub fn build_fuzz_state(&self, deployed_libs: &[Address]) -> EvmFuzzState {
295 if let Some(fork_db) = self.executor.backend().active_fork_db() {
296 EvmFuzzState::new(fork_db, self.config.dictionary, deployed_libs)
297 } else {
298 EvmFuzzState::new(
299 self.executor.backend().mem_db(),
300 self.config.dictionary,
301 deployed_libs,
302 )
303 }
304 }
305}