Skip to content

Commit c976619

Browse files
iFrostizzDaniPopes
andauthored
Debugger Refactor #1: fuzz single (foundry-rs#5692)
* fuzz single refactor * add struct docs * Update crates/evm/src/fuzz/mod.rs Co-authored-by: DaniPopes <[email protected]> * add docs and move types to types.rs * fmt * add docki docs * fmt --------- Co-authored-by: DaniPopes <[email protected]>
1 parent b6607c6 commit c976619

File tree

3 files changed

+148
-76
lines changed

3 files changed

+148
-76
lines changed

crates/evm/src/fuzz/mod.rs

Lines changed: 96 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,12 @@ use strategies::{
2020
build_initial_state, collect_state_from_call, fuzz_calldata, fuzz_calldata_from_state,
2121
EvmFuzzState,
2222
};
23+
use types::{CaseOutcome, CounterExampleOutcome, FuzzCase, FuzzOutcome};
2324

2425
pub mod error;
2526
pub mod invariant;
2627
pub mod strategies;
28+
pub mod types;
2729

2830
/// Wrapper around an [`Executor`] which provides fuzzing support using [`proptest`](https://docs.rs/proptest/1.0.0/proptest/).
2931
///
@@ -101,72 +103,45 @@ impl<'a> FuzzedExecutor<'a> {
101103
let strat = proptest::strategy::Union::new_weighted(weights);
102104
debug!(func = ?func.name, should_fail, "fuzzing");
103105
let run_result = self.runner.clone().run(&strat, |calldata| {
104-
let call = self
105-
.executor
106-
.call_raw(self.sender, address, calldata.0.clone(), 0.into())
107-
.map_err(|_| TestCaseError::fail(FuzzError::FailedContractCall))?;
108-
let state_changeset = call
109-
.state_changeset
110-
.as_ref()
111-
.ok_or_else(|| TestCaseError::fail(FuzzError::EmptyChangeset))?;
112-
113-
// Build fuzzer state
114-
collect_state_from_call(
115-
&call.logs,
116-
state_changeset,
117-
state.clone(),
118-
&self.config.dictionary,
119-
);
120-
121-
// When assume cheat code is triggered return a special string "FOUNDRY::ASSUME"
122-
if call.result.as_ref() == ASSUME_MAGIC_RETURN_CODE {
123-
return Err(TestCaseError::reject(FuzzError::AssumeReject))
124-
}
125-
126-
let success = self.executor.is_success(
127-
address,
128-
call.reverted,
129-
state_changeset.clone(),
130-
should_fail,
131-
);
132-
133-
if success {
134-
let mut first_case = first_case.borrow_mut();
135-
if first_case.is_none() {
136-
first_case.replace(FuzzCase {
137-
calldata,
138-
gas: call.gas_used,
139-
stipend: call.stipend,
140-
});
106+
let fuzz_res = self.single_fuzz(&state, address, should_fail, calldata)?;
107+
108+
match fuzz_res {
109+
FuzzOutcome::Case(case) => {
110+
let mut first_case = first_case.borrow_mut();
111+
gas_by_case.borrow_mut().push((case.case.gas, case.case.stipend));
112+
if first_case.is_none() {
113+
first_case.replace(case.case);
114+
}
115+
116+
traces.replace(case.traces);
117+
118+
if let Some(prev) = coverage.take() {
119+
// Safety: If `Option::or` evaluates to `Some`, then `call.coverage` must
120+
// necessarily also be `Some`
121+
coverage.replace(Some(prev.merge(case.coverage.unwrap())));
122+
} else {
123+
coverage.replace(case.coverage);
124+
}
125+
126+
Ok(())
141127
}
142-
gas_by_case.borrow_mut().push((call.gas_used, call.stipend));
143-
144-
traces.replace(call.traces);
145-
146-
if let Some(prev) = coverage.take() {
147-
// Safety: If `Option::or` evaluates to `Some`, then `call.coverage` must
148-
// necessarily also be `Some`
149-
coverage.replace(Some(prev.merge(call.coverage.unwrap())));
150-
} else {
151-
coverage.replace(call.coverage);
128+
FuzzOutcome::CounterExample(CounterExampleOutcome {
129+
exit_reason,
130+
counterexample: _counterexample,
131+
..
132+
}) => {
133+
let status = exit_reason;
134+
// We cannot use the calldata returned by the test runner in `TestError::Fail`,
135+
// since that input represents the last run case, which may not correspond with
136+
// our failure - when a fuzz case fails, proptest will try
137+
// to run at least one more case to find a minimal failure
138+
// case.
139+
let call_res = _counterexample.1.result.clone();
140+
*counterexample.borrow_mut() = _counterexample;
141+
Err(TestCaseError::fail(
142+
decode::decode_revert(&call_res, errors, Some(status)).unwrap_or_default(),
143+
))
152144
}
153-
154-
Ok(())
155-
} else {
156-
let status = call.exit_reason;
157-
// We cannot use the calldata returned by the test runner in `TestError::Fail`,
158-
// since that input represents the last run case, which may not correspond with our
159-
// failure - when a fuzz case fails, proptest will try to run at least one more
160-
// case to find a minimal failure case.
161-
*counterexample.borrow_mut() = (calldata, call);
162-
Err(TestCaseError::fail(
163-
decode::decode_revert(
164-
counterexample.borrow().1.result.as_ref(),
165-
errors,
166-
Some(status),
167-
)
168-
.unwrap_or_default(),
169-
))
170145
}
171146
});
172147

@@ -216,6 +191,63 @@ impl<'a> FuzzedExecutor<'a> {
216191

217192
result
218193
}
194+
195+
/// Granular and single-step function that runs only one fuzz and returns either a `CaseOutcome`
196+
/// or a `CounterExampleOutcome`
197+
pub fn single_fuzz(
198+
&self,
199+
state: &EvmFuzzState,
200+
address: Address,
201+
should_fail: bool,
202+
calldata: ethers::types::Bytes,
203+
) -> Result<FuzzOutcome, TestCaseError> {
204+
let call = self
205+
.executor
206+
.call_raw(self.sender, address, calldata.0.clone(), 0.into())
207+
.map_err(|_| TestCaseError::fail(FuzzError::FailedContractCall))?;
208+
let state_changeset = call
209+
.state_changeset
210+
.as_ref()
211+
.ok_or_else(|| TestCaseError::fail(FuzzError::EmptyChangeset))?;
212+
213+
// Build fuzzer state
214+
collect_state_from_call(
215+
&call.logs,
216+
state_changeset,
217+
state.clone(),
218+
&self.config.dictionary,
219+
);
220+
221+
// When assume cheat code is triggered return a special string "FOUNDRY::ASSUME"
222+
if call.result.as_ref() == ASSUME_MAGIC_RETURN_CODE {
223+
return Err(TestCaseError::reject(FuzzError::AssumeReject))
224+
}
225+
226+
let breakpoints = call
227+
.cheatcodes
228+
.as_ref()
229+
.map_or_else(Default::default, |cheats| cheats.breakpoints.clone());
230+
231+
let success =
232+
self.executor.is_success(address, call.reverted, state_changeset.clone(), should_fail);
233+
234+
if success {
235+
Ok(FuzzOutcome::Case(CaseOutcome {
236+
case: FuzzCase { calldata, gas: call.gas_used, stipend: call.stipend },
237+
traces: call.traces,
238+
coverage: call.coverage,
239+
debug: call.debug,
240+
breakpoints,
241+
}))
242+
} else {
243+
Ok(FuzzOutcome::CounterExample(CounterExampleOutcome {
244+
debug: call.debug.clone(),
245+
exit_reason: call.exit_reason,
246+
counterexample: (calldata, call),
247+
breakpoints,
248+
}))
249+
}
250+
}
219251
}
220252

221253
#[derive(Clone, Debug, Serialize, Deserialize)]
@@ -444,14 +476,3 @@ impl FuzzedCases {
444476
self.lowest().map(|c| c.gas).unwrap_or_default()
445477
}
446478
}
447-
448-
/// Data of a single fuzz test case
449-
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
450-
pub struct FuzzCase {
451-
/// The calldata used for this fuzz test
452-
pub calldata: Bytes,
453-
/// Consumed gas
454-
pub gas: u64,
455-
/// The initial gas stipend for the transaction
456-
pub stipend: u64,
457-
}

crates/evm/src/fuzz/types.rs

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
use crate::{coverage::HitMaps, debug::DebugArena, executor::RawCallResult, trace::CallTraceArena};
2+
use ethers::types::Bytes;
3+
use foundry_common::evm::Breakpoints;
4+
use revm::interpreter::InstructionResult;
5+
use serde::{Deserialize, Serialize};
6+
7+
/// Data of a single fuzz test case
8+
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
9+
pub struct FuzzCase {
10+
/// The calldata used for this fuzz test
11+
pub calldata: Bytes,
12+
/// Consumed gas
13+
pub gas: u64,
14+
/// The initial gas stipend for the transaction
15+
pub stipend: u64,
16+
}
17+
18+
/// Returned by a single fuzz in the case of a successful run
19+
#[derive(Debug)]
20+
pub struct CaseOutcome {
21+
/// Data of a single fuzz test case
22+
pub case: FuzzCase,
23+
/// The traces of the call
24+
pub traces: Option<CallTraceArena>,
25+
/// The coverage info collected during the call
26+
pub coverage: Option<HitMaps>,
27+
/// The debug nodes of the call
28+
pub debug: Option<DebugArena>,
29+
/// Breakpoints char pc map
30+
pub breakpoints: Breakpoints,
31+
}
32+
33+
/// Returned by a single fuzz when a counterexample has been discovered
34+
#[derive(Debug)]
35+
pub struct CounterExampleOutcome {
36+
/// Minimal reproduction test case for failing test
37+
pub counterexample: (ethers::types::Bytes, RawCallResult),
38+
/// The status of the call
39+
pub exit_reason: InstructionResult,
40+
/// The debug nodes of the call
41+
pub debug: Option<DebugArena>,
42+
/// Breakpoints char pc map
43+
pub breakpoints: Breakpoints,
44+
}
45+
46+
/// Outcome of a single fuzz
47+
#[derive(Debug)]
48+
pub enum FuzzOutcome {
49+
Case(CaseOutcome),
50+
CounterExample(CounterExampleOutcome),
51+
}

crates/forge/src/result.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use foundry_common::evm::Breakpoints;
66
use foundry_evm::{
77
coverage::HitMaps,
88
executor::EvmError,
9-
fuzz::{CounterExample, FuzzCase},
9+
fuzz::{types::FuzzCase, CounterExample},
1010
trace::{TraceKind, Traces},
1111
};
1212
use serde::{Deserialize, Serialize};

0 commit comments

Comments
 (0)