foundry_evm/executors/fuzz/
mod.rs
1use crate::executors::{Executor, FuzzTestTimer, RawCallResult};
2use alloy_dyn_abi::JsonAbiExt;
3use alloy_json_abi::Function;
4use alloy_primitives::{map::HashMap, Address, Bytes, Log, U256};
5use eyre::Result;
6use foundry_common::evm::Breakpoints;
7use foundry_config::FuzzConfig;
8use foundry_evm_core::{
9 constants::{MAGIC_ASSUME, TEST_TIMEOUT},
10 decode::{RevertDecoder, SkipReason},
11};
12use foundry_evm_coverage::HitMaps;
13use foundry_evm_fuzz::{
14 strategies::{fuzz_calldata, fuzz_calldata_from_state, EvmFuzzState},
15 BaseCounterExample, CounterExample, FuzzCase, FuzzError, FuzzFixtures, FuzzTestResult,
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 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 strategy = proptest::prop_oneof![
94 100 - dictionary_weight => fuzz_calldata(func.clone(), fuzz_fixtures),
95 dictionary_weight => fuzz_calldata_from_state(func.clone(), &state),
96 ];
97 let max_traces_to_collect = std::cmp::max(1, self.config.gas_report_samples) as usize;
99 let show_logs = self.config.show_logs;
100
101 let timer = FuzzTestTimer::new(self.config.timeout);
103
104 let run_result = self.runner.clone().run(&strategy, |calldata| {
105 if timer.is_timed_out() {
107 return Err(TestCaseError::fail(TEST_TIMEOUT));
108 }
109
110 let fuzz_res = self.single_fuzz(address, calldata)?;
111
112 if let Some(progress) = progress {
114 progress.inc(1);
115 };
116
117 match fuzz_res {
118 FuzzOutcome::Case(case) => {
119 let mut data = execution_data.borrow_mut();
120 data.gas_by_case.push((case.case.gas, case.case.stipend));
121
122 if data.first_case.is_none() {
123 data.first_case.replace(case.case);
124 }
125
126 if let Some(call_traces) = case.traces {
127 if data.traces.len() == max_traces_to_collect {
128 data.traces.pop();
129 }
130 data.traces.push(call_traces);
131 data.breakpoints.replace(case.breakpoints);
132 }
133
134 if show_logs {
135 data.logs.extend(case.logs);
136 }
137
138 HitMaps::merge_opt(&mut data.coverage, case.coverage);
139
140 data.deprecated_cheatcodes = case.deprecated_cheatcodes;
141
142 Ok(())
143 }
144 FuzzOutcome::CounterExample(CounterExampleOutcome {
145 exit_reason: status,
146 counterexample: outcome,
147 ..
148 }) => {
149 let reason = rd.maybe_decode(&outcome.1.result, Some(status));
154 execution_data.borrow_mut().logs.extend(outcome.1.logs.clone());
155 execution_data.borrow_mut().counterexample = outcome;
156 Err(TestCaseError::fail(reason.unwrap_or_default()))
158 }
159 }
160 });
161
162 let fuzz_result = execution_data.into_inner();
163 let (calldata, call) = fuzz_result.counterexample;
164
165 let mut traces = fuzz_result.traces;
166 let (last_run_traces, last_run_breakpoints) = if run_result.is_ok() {
167 (traces.pop(), fuzz_result.breakpoints)
168 } else {
169 (call.traces.clone(), call.cheatcodes.map(|c| c.breakpoints))
170 };
171
172 let mut result = FuzzTestResult {
173 first_case: fuzz_result.first_case.unwrap_or_default(),
174 gas_by_case: fuzz_result.gas_by_case,
175 success: run_result.is_ok(),
176 skipped: false,
177 reason: None,
178 counterexample: None,
179 logs: fuzz_result.logs,
180 labeled_addresses: call.labels,
181 traces: last_run_traces,
182 breakpoints: last_run_breakpoints,
183 gas_report_traces: traces.into_iter().map(|a| a.arena).collect(),
184 coverage: fuzz_result.coverage,
185 deprecated_cheatcodes: fuzz_result.deprecated_cheatcodes,
186 };
187
188 match run_result {
189 Ok(()) => {}
190 Err(TestError::Abort(reason)) => {
191 let msg = reason.message();
192 result.reason = if msg == "Too many global rejects" {
196 let error = FuzzError::TooManyRejects(self.runner.config().max_global_rejects);
197 Some(error.to_string())
198 } else {
199 Some(msg.to_string())
200 };
201 }
202 Err(TestError::Fail(reason, _)) => {
203 let reason = reason.to_string();
204 if reason == TEST_TIMEOUT {
205 result.success = true;
207 } else {
208 result.reason = (!reason.is_empty()).then_some(reason);
209 let args = if let Some(data) = calldata.get(4..) {
210 func.abi_decode_input(data, false).unwrap_or_default()
211 } else {
212 vec![]
213 };
214
215 result.counterexample = Some(CounterExample::Single(
216 BaseCounterExample::from_fuzz_call(calldata, args, call.traces),
217 ));
218 }
219 }
220 }
221
222 if let Some(reason) = &result.reason {
223 if let Some(reason) = SkipReason::decode_self(reason) {
224 result.skipped = true;
225 result.reason = reason.0;
226 }
227 }
228
229 state.log_stats();
230
231 result
232 }
233
234 pub fn single_fuzz(
237 &self,
238 address: Address,
239 calldata: alloy_primitives::Bytes,
240 ) -> Result<FuzzOutcome, TestCaseError> {
241 let mut call = self
242 .executor
243 .call_raw(self.sender, address, calldata.clone(), U256::ZERO)
244 .map_err(|e| TestCaseError::fail(e.to_string()))?;
245
246 if call.result.as_ref() == MAGIC_ASSUME {
248 return Err(TestCaseError::reject(FuzzError::AssumeReject))
249 }
250
251 let (breakpoints, deprecated_cheatcodes) =
252 call.cheatcodes.as_ref().map_or_else(Default::default, |cheats| {
253 (cheats.breakpoints.clone(), cheats.deprecated.clone())
254 });
255
256 let success = self.executor.is_raw_call_mut_success(address, &mut call, false);
257 if success {
258 Ok(FuzzOutcome::Case(CaseOutcome {
259 case: FuzzCase { calldata, gas: call.gas_used, stipend: call.stipend },
260 traces: call.traces,
261 coverage: call.coverage,
262 breakpoints,
263 logs: call.logs,
264 deprecated_cheatcodes,
265 }))
266 } else {
267 Ok(FuzzOutcome::CounterExample(CounterExampleOutcome {
268 exit_reason: call.exit_reason,
269 counterexample: (calldata, call),
270 breakpoints,
271 }))
272 }
273 }
274
275 pub fn build_fuzz_state(&self, deployed_libs: &[Address]) -> EvmFuzzState {
277 if let Some(fork_db) = self.executor.backend().active_fork_db() {
278 EvmFuzzState::new(fork_db, self.config.dictionary, deployed_libs)
279 } else {
280 EvmFuzzState::new(
281 self.executor.backend().mem_db(),
282 self.config.dictionary,
283 deployed_libs,
284 )
285 }
286 }
287}