Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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())));
Comment thread
Evalir marked this conversation as resolved.
} 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(
Comment thread
franfrandev marked this conversation as resolved.
&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(),
Comment thread
Evalir marked this conversation as resolved.
&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,
Comment thread
Evalir marked this conversation as resolved.
}))
}
}
}

#[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