foundry_evm/executors/fuzz/
mod.rsuse crate::executors::{Executor, RawCallResult};
use alloy_dyn_abi::JsonAbiExt;
use alloy_json_abi::Function;
use alloy_primitives::{map::HashMap, Address, Bytes, Log, U256};
use eyre::Result;
use foundry_common::evm::Breakpoints;
use foundry_config::FuzzConfig;
use foundry_evm_core::{
constants::MAGIC_ASSUME,
decode::{RevertDecoder, SkipReason},
};
use foundry_evm_coverage::HitMaps;
use foundry_evm_fuzz::{
strategies::{fuzz_calldata, fuzz_calldata_from_state, EvmFuzzState},
BaseCounterExample, CounterExample, FuzzCase, FuzzError, FuzzFixtures, FuzzTestResult,
};
use foundry_evm_traces::SparsedTraceArena;
use indicatif::ProgressBar;
use proptest::test_runner::{TestCaseError, TestError, TestRunner};
use std::{cell::RefCell, collections::BTreeMap};
mod types;
pub use types::{CaseOutcome, CounterExampleOutcome, FuzzOutcome};
#[derive(Default)]
pub struct FuzzTestData {
pub first_case: Option<FuzzCase>,
pub gas_by_case: Vec<(u64, u64)>,
pub counterexample: (Bytes, RawCallResult),
pub traces: Vec<SparsedTraceArena>,
pub breakpoints: Option<Breakpoints>,
pub coverage: Option<HitMaps>,
pub logs: Vec<Log>,
pub gas_snapshots: BTreeMap<String, BTreeMap<String, String>>,
pub deprecated_cheatcodes: HashMap<&'static str, Option<&'static str>>,
}
pub struct FuzzedExecutor {
executor: Executor,
runner: TestRunner,
sender: Address,
config: FuzzConfig,
}
impl FuzzedExecutor {
pub fn new(
executor: Executor,
runner: TestRunner,
sender: Address,
config: FuzzConfig,
) -> Self {
Self { executor, runner, sender, config }
}
pub fn fuzz(
&self,
func: &Function,
fuzz_fixtures: &FuzzFixtures,
address: Address,
should_fail: bool,
rd: &RevertDecoder,
progress: Option<&ProgressBar>,
) -> FuzzTestResult {
let execution_data = RefCell::new(FuzzTestData::default());
let state = self.build_fuzz_state();
let dictionary_weight = self.config.dictionary.dictionary_weight.min(100);
let strategy = proptest::prop_oneof![
100 - dictionary_weight => fuzz_calldata(func.clone(), fuzz_fixtures),
dictionary_weight => fuzz_calldata_from_state(func.clone(), &state),
];
let max_traces_to_collect = std::cmp::max(1, self.config.gas_report_samples) as usize;
let show_logs = self.config.show_logs;
let run_result = self.runner.clone().run(&strategy, |calldata| {
let fuzz_res = self.single_fuzz(address, should_fail, calldata)?;
if let Some(progress) = progress {
progress.inc(1);
};
match fuzz_res {
FuzzOutcome::Case(case) => {
let mut data = execution_data.borrow_mut();
data.gas_by_case.push((case.case.gas, case.case.stipend));
if data.first_case.is_none() {
data.first_case.replace(case.case);
}
if let Some(call_traces) = case.traces {
if data.traces.len() == max_traces_to_collect {
data.traces.pop();
}
data.traces.push(call_traces);
data.breakpoints.replace(case.breakpoints);
}
if show_logs {
data.logs.extend(case.logs);
}
match &mut data.coverage {
Some(prev) => prev.merge(case.coverage.unwrap()),
opt => *opt = case.coverage,
}
data.deprecated_cheatcodes = case.deprecated_cheatcodes;
Ok(())
}
FuzzOutcome::CounterExample(CounterExampleOutcome {
exit_reason: status,
counterexample: outcome,
..
}) => {
let reason = rd.maybe_decode(&outcome.1.result, Some(status));
execution_data.borrow_mut().logs.extend(outcome.1.logs.clone());
execution_data.borrow_mut().counterexample = outcome;
Err(TestCaseError::fail(reason.unwrap_or_default()))
}
}
});
let fuzz_result = execution_data.into_inner();
let (calldata, call) = fuzz_result.counterexample;
let mut traces = fuzz_result.traces;
let (last_run_traces, last_run_breakpoints) = if run_result.is_ok() {
(traces.pop(), fuzz_result.breakpoints)
} else {
(call.traces.clone(), call.cheatcodes.map(|c| c.breakpoints))
};
let mut result = FuzzTestResult {
first_case: fuzz_result.first_case.unwrap_or_default(),
gas_by_case: fuzz_result.gas_by_case,
success: run_result.is_ok(),
skipped: false,
reason: None,
counterexample: None,
logs: fuzz_result.logs,
labeled_addresses: call.labels,
traces: last_run_traces,
breakpoints: last_run_breakpoints,
gas_report_traces: traces.into_iter().map(|a| a.arena).collect(),
coverage: fuzz_result.coverage,
deprecated_cheatcodes: fuzz_result.deprecated_cheatcodes,
};
match run_result {
Ok(()) => {}
Err(TestError::Abort(reason)) => {
let msg = reason.message();
result.reason = if msg == "Too many global rejects" {
let error = FuzzError::TooManyRejects(self.runner.config().max_global_rejects);
Some(error.to_string())
} else {
Some(msg.to_string())
};
}
Err(TestError::Fail(reason, _)) => {
let reason = reason.to_string();
result.reason = (!reason.is_empty()).then_some(reason);
let args = if let Some(data) = calldata.get(4..) {
func.abi_decode_input(data, false).unwrap_or_default()
} else {
vec![]
};
result.counterexample = Some(CounterExample::Single(
BaseCounterExample::from_fuzz_call(calldata, args, call.traces),
));
}
}
if let Some(reason) = &result.reason {
if let Some(reason) = SkipReason::decode_self(reason) {
result.skipped = true;
result.reason = reason.0;
}
}
state.log_stats();
result
}
pub fn single_fuzz(
&self,
address: Address,
should_fail: bool,
calldata: alloy_primitives::Bytes,
) -> Result<FuzzOutcome, TestCaseError> {
let mut call = self
.executor
.call_raw(self.sender, address, calldata.clone(), U256::ZERO)
.map_err(|e| TestCaseError::fail(e.to_string()))?;
if call.result.as_ref() == MAGIC_ASSUME {
return Err(TestCaseError::reject(FuzzError::AssumeReject))
}
let (breakpoints, deprecated_cheatcodes) =
call.cheatcodes.as_ref().map_or_else(Default::default, |cheats| {
(cheats.breakpoints.clone(), cheats.deprecated.clone())
});
let success = self.executor.is_raw_call_mut_success(address, &mut call, should_fail);
if success {
Ok(FuzzOutcome::Case(CaseOutcome {
case: FuzzCase { calldata, gas: call.gas_used, stipend: call.stipend },
traces: call.traces,
coverage: call.coverage,
breakpoints,
logs: call.logs,
deprecated_cheatcodes,
}))
} else {
Ok(FuzzOutcome::CounterExample(CounterExampleOutcome {
exit_reason: call.exit_reason,
counterexample: (calldata, call),
breakpoints,
}))
}
}
pub fn build_fuzz_state(&self) -> EvmFuzzState {
if let Some(fork_db) = self.executor.backend().active_fork_db() {
EvmFuzzState::new(fork_db, self.config.dictionary)
} else {
EvmFuzzState::new(self.executor.backend().mem_db(), self.config.dictionary)
}
}
}