Skip to content

Commit

Permalink
Debugger Refactor #1: fuzz single (foundry-rs#5692)
Browse files Browse the repository at this point in the history
* 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]>
  • Loading branch information
iFrostizz and DaniPopes authored Aug 30, 2023
1 parent b6607c6 commit c976619
Show file tree
Hide file tree
Showing 3 changed files with 148 additions and 76 deletions.
171 changes: 96 additions & 75 deletions crates/evm/src/fuzz/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,12 @@ use strategies::{
build_initial_state, collect_state_from_call, fuzz_calldata, fuzz_calldata_from_state,
EvmFuzzState,
};
use types::{CaseOutcome, CounterExampleOutcome, FuzzCase, FuzzOutcome};

pub mod error;
pub mod invariant;
pub mod strategies;
pub mod types;

/// Wrapper around an [`Executor`] which provides fuzzing support using [`proptest`](https://docs.rs/proptest/1.0.0/proptest/).
///
Expand Down Expand Up @@ -101,72 +103,45 @@ impl<'a> FuzzedExecutor<'a> {
let strat = proptest::strategy::Union::new_weighted(weights);
debug!(func = ?func.name, should_fail, "fuzzing");
let run_result = self.runner.clone().run(&strat, |calldata| {
let call = self
.executor
.call_raw(self.sender, address, calldata.0.clone(), 0.into())
.map_err(|_| TestCaseError::fail(FuzzError::FailedContractCall))?;
let state_changeset = call
.state_changeset
.as_ref()
.ok_or_else(|| TestCaseError::fail(FuzzError::EmptyChangeset))?;

// Build fuzzer state
collect_state_from_call(
&call.logs,
state_changeset,
state.clone(),
&self.config.dictionary,
);

// When assume cheat code is triggered return a special string "FOUNDRY::ASSUME"
if call.result.as_ref() == ASSUME_MAGIC_RETURN_CODE {
return Err(TestCaseError::reject(FuzzError::AssumeReject))
}

let success = self.executor.is_success(
address,
call.reverted,
state_changeset.clone(),
should_fail,
);

if success {
let mut first_case = first_case.borrow_mut();
if first_case.is_none() {
first_case.replace(FuzzCase {
calldata,
gas: call.gas_used,
stipend: call.stipend,
});
let fuzz_res = self.single_fuzz(&state, address, should_fail, calldata)?;

match fuzz_res {
FuzzOutcome::Case(case) => {
let mut first_case = first_case.borrow_mut();
gas_by_case.borrow_mut().push((case.case.gas, case.case.stipend));
if first_case.is_none() {
first_case.replace(case.case);
}

traces.replace(case.traces);

if let Some(prev) = coverage.take() {
// Safety: If `Option::or` evaluates to `Some`, then `call.coverage` must
// necessarily also be `Some`
coverage.replace(Some(prev.merge(case.coverage.unwrap())));
} else {
coverage.replace(case.coverage);
}

Ok(())
}
gas_by_case.borrow_mut().push((call.gas_used, call.stipend));

traces.replace(call.traces);

if let Some(prev) = coverage.take() {
// Safety: If `Option::or` evaluates to `Some`, then `call.coverage` must
// necessarily also be `Some`
coverage.replace(Some(prev.merge(call.coverage.unwrap())));
} else {
coverage.replace(call.coverage);
FuzzOutcome::CounterExample(CounterExampleOutcome {
exit_reason,
counterexample: _counterexample,
..
}) => {
let status = exit_reason;
// We cannot use the calldata returned by the test runner in `TestError::Fail`,
// since that input represents the last run case, which may not correspond with
// our failure - when a fuzz case fails, proptest will try
// to run at least one more case to find a minimal failure
// case.
let call_res = _counterexample.1.result.clone();
*counterexample.borrow_mut() = _counterexample;
Err(TestCaseError::fail(
decode::decode_revert(&call_res, errors, Some(status)).unwrap_or_default(),
))
}

Ok(())
} else {
let status = call.exit_reason;
// We cannot use the calldata returned by the test runner in `TestError::Fail`,
// since that input represents the last run case, which may not correspond with our
// failure - when a fuzz case fails, proptest will try to run at least one more
// case to find a minimal failure case.
*counterexample.borrow_mut() = (calldata, call);
Err(TestCaseError::fail(
decode::decode_revert(
counterexample.borrow().1.result.as_ref(),
errors,
Some(status),
)
.unwrap_or_default(),
))
}
});

Expand Down Expand Up @@ -216,6 +191,63 @@ impl<'a> FuzzedExecutor<'a> {

result
}

/// Granular and single-step function that runs only one fuzz and returns either a `CaseOutcome`
/// or a `CounterExampleOutcome`
pub fn single_fuzz(
&self,
state: &EvmFuzzState,
address: Address,
should_fail: bool,
calldata: ethers::types::Bytes,
) -> Result<FuzzOutcome, TestCaseError> {
let call = self
.executor
.call_raw(self.sender, address, calldata.0.clone(), 0.into())
.map_err(|_| TestCaseError::fail(FuzzError::FailedContractCall))?;
let state_changeset = call
.state_changeset
.as_ref()
.ok_or_else(|| TestCaseError::fail(FuzzError::EmptyChangeset))?;

// Build fuzzer state
collect_state_from_call(
&call.logs,
state_changeset,
state.clone(),
&self.config.dictionary,
);

// When assume cheat code is triggered return a special string "FOUNDRY::ASSUME"
if call.result.as_ref() == ASSUME_MAGIC_RETURN_CODE {
return Err(TestCaseError::reject(FuzzError::AssumeReject))
}

let breakpoints = call
.cheatcodes
.as_ref()
.map_or_else(Default::default, |cheats| cheats.breakpoints.clone());

let success =
self.executor.is_success(address, call.reverted, state_changeset.clone(), should_fail);

if success {
Ok(FuzzOutcome::Case(CaseOutcome {
case: FuzzCase { calldata, gas: call.gas_used, stipend: call.stipend },
traces: call.traces,
coverage: call.coverage,
debug: call.debug,
breakpoints,
}))
} else {
Ok(FuzzOutcome::CounterExample(CounterExampleOutcome {
debug: call.debug.clone(),
exit_reason: call.exit_reason,
counterexample: (calldata, call),
breakpoints,
}))
}
}
}

#[derive(Clone, Debug, Serialize, Deserialize)]
Expand Down Expand Up @@ -444,14 +476,3 @@ impl FuzzedCases {
self.lowest().map(|c| c.gas).unwrap_or_default()
}
}

/// Data of a single fuzz test case
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct FuzzCase {
/// The calldata used for this fuzz test
pub calldata: Bytes,
/// Consumed gas
pub gas: u64,
/// The initial gas stipend for the transaction
pub stipend: u64,
}
51 changes: 51 additions & 0 deletions crates/evm/src/fuzz/types.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
use crate::{coverage::HitMaps, debug::DebugArena, executor::RawCallResult, trace::CallTraceArena};
use ethers::types::Bytes;
use foundry_common::evm::Breakpoints;
use revm::interpreter::InstructionResult;
use serde::{Deserialize, Serialize};

/// Data of a single fuzz test case
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct FuzzCase {
/// The calldata used for this fuzz test
pub calldata: Bytes,
/// Consumed gas
pub gas: u64,
/// The initial gas stipend for the transaction
pub stipend: u64,
}

/// Returned by a single fuzz in the case of a successful run
#[derive(Debug)]
pub struct CaseOutcome {
/// Data of a single fuzz test case
pub case: FuzzCase,
/// The traces of the call
pub traces: Option<CallTraceArena>,
/// The coverage info collected during the call
pub coverage: Option<HitMaps>,
/// The debug nodes of the call
pub debug: Option<DebugArena>,
/// Breakpoints char pc map
pub breakpoints: Breakpoints,
}

/// Returned by a single fuzz when a counterexample has been discovered
#[derive(Debug)]
pub struct CounterExampleOutcome {
/// Minimal reproduction test case for failing test
pub counterexample: (ethers::types::Bytes, RawCallResult),
/// The status of the call
pub exit_reason: InstructionResult,
/// The debug nodes of the call
pub debug: Option<DebugArena>,
/// Breakpoints char pc map
pub breakpoints: Breakpoints,
}

/// Outcome of a single fuzz
#[derive(Debug)]
pub enum FuzzOutcome {
Case(CaseOutcome),
CounterExample(CounterExampleOutcome),
}
2 changes: 1 addition & 1 deletion crates/forge/src/result.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use foundry_common::evm::Breakpoints;
use foundry_evm::{
coverage::HitMaps,
executor::EvmError,
fuzz::{CounterExample, FuzzCase},
fuzz::{types::FuzzCase, CounterExample},
trace::{TraceKind, Traces},
};
use serde::{Deserialize, Serialize};
Expand Down

0 comments on commit c976619

Please sign in to comment.