Skip to content
53 changes: 36 additions & 17 deletions evm/src/fuzz/invariant/executor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,14 @@ use crate::{
},
FuzzCase, FuzzedCases,
},
trace::CallTraceArena,
utils::{get_function, h160_to_b160},
CALLER,
};
use ethers::{
abi::{Abi, Address, Detokenize, FixedBytes, Function, Tokenizable, TokenizableItem},
prelude::U256,
types::Log,
};
use eyre::ContextCompat;
use foundry_common::contracts::{ContractsByAddress, ContractsByArtifact};
Expand Down Expand Up @@ -104,6 +106,8 @@ impl<'a> InvariantExecutor<'a> {
)
.ok(),
);
let last_run_logs = RefCell::new(vec![]);
let last_run_traces = RefCell::new(None);
// Make sure invariants are sound even before starting to fuzz
if last_call_results.borrow().is_none() {
fuzz_cases.borrow_mut().push(FuzzedCases::new(vec![]));
Expand Down Expand Up @@ -139,7 +143,7 @@ impl<'a> InvariantExecutor<'a> {
// Created contracts during a run.
let mut created_contracts = vec![];

'fuzz_run: for _ in 0..self.config.depth {
'fuzz_run: for current_run in 0..self.config.depth {
let (sender, (address, calldata)) =
inputs.last().expect("to have the next randomly generated input.");

Expand Down Expand Up @@ -180,17 +184,23 @@ impl<'a> InvariantExecutor<'a> {
stipend: call_result.stipend,
});

let (can_continue, call_results) = can_continue(
&invariant_contract,
call_result,
&executor,
&inputs,
&mut failures.borrow_mut(),
&targeted_contracts,
state_changeset,
self.config.fail_on_revert,
self.config.shrink_sequence,
);
let (can_continue, call_results, call_result_logs, call_result_traces) =
can_continue(
&invariant_contract,
call_result,
&executor,
&inputs,
&mut failures.borrow_mut(),
&targeted_contracts,
state_changeset,
self.config.fail_on_revert,
self.config.shrink_sequence,
);

if !can_continue || current_run == self.config.depth - 1 {
*last_run_logs.borrow_mut() = call_result_logs;
*last_run_traces.borrow_mut() = call_result_traces;
}

if !can_continue {
break 'fuzz_run
Expand Down Expand Up @@ -231,6 +241,8 @@ impl<'a> InvariantExecutor<'a> {
cases: fuzz_cases.into_inner(),
reverts,
last_call_results: last_call_results.take(),
last_call_logs: last_run_logs.take(),
last_run_traces: last_run_traces.take(),
})
}

Expand Down Expand Up @@ -552,8 +564,12 @@ fn collect_data(
}
}

type RichInvariantResults =
(bool, Option<BTreeMap<String, RawCallResult>>, Vec<Log>, Option<CallTraceArena>);

Comment thread
Evalir marked this conversation as resolved.
Outdated
/// Verifies that the invariant run execution can continue.
/// Returns the mapping of (Invariant Function Name -> Call Result) if invariants were asserted.
/// Returns the mapping of (Invariant Function Name -> Call Result, Logs, Traces) if invariants were
/// asserted.
#[allow(clippy::too_many_arguments)]
fn can_continue(
invariant_contract: &InvariantContract,
Expand All @@ -565,7 +581,7 @@ fn can_continue(
state_changeset: StateChangeset,
fail_on_revert: bool,
shrink_sequence: bool,
) -> (bool, Option<BTreeMap<String, RawCallResult>>) {
) -> RichInvariantResults {
let mut call_results = None;

// Detect handler assertion failures first.
Expand All @@ -578,14 +594,17 @@ fn can_continue(
if !call_result.reverted && !handlers_failed {
call_results = assert_invariants(invariant_contract, executor, calldata, failures).ok();
if call_results.is_none() {
return (false, None)
return (false, None, call_result.logs, call_result.traces)
}
} else {
failures.reverts += 1;

// The user might want to stop all execution if a revert happens to
// better bound their testing space.
if fail_on_revert {
let call_result_logs = call_result.logs.clone();
let call_result_traces = call_result.traces.clone();

let error = InvariantFuzzError::new(
invariant_contract,
None,
Expand All @@ -602,10 +621,10 @@ fn can_continue(
failures.failed_invariants.insert(invariant.name.clone(), Some(error.clone()));
}

return (false, None)
return (false, None, call_result_logs, call_result_traces)
}
}
(true, call_results)
(true, call_results, call_result.logs, call_result.traces)
}

#[derive(Clone)]
Expand Down
4 changes: 4 additions & 0 deletions evm/src/fuzz/invariant/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -135,4 +135,8 @@ pub struct InvariantFuzzTestResult {
pub reverts: usize,

pub last_call_results: Option<BTreeMap<String, RawCallResult>>,

pub last_call_logs: Vec<Log>,

pub last_run_traces: Option<CallTraceArena>,
}
16 changes: 14 additions & 2 deletions forge/src/runner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -449,8 +449,14 @@ impl<'a> ContractRunner<'a> {
let invariant_contract =
InvariantContract { address, invariant_functions: functions, abi: self.contract };

let Ok(InvariantFuzzTestResult { invariants, cases, reverts, mut last_call_results }) =
evm.invariant_fuzz(invariant_contract)
let Ok(InvariantFuzzTestResult {
invariants,
cases,
reverts,
mut last_call_results,
last_call_logs,
last_run_traces,
}) = evm.invariant_fuzz(invariant_contract)
else {
return vec![]
};
Expand Down Expand Up @@ -484,6 +490,7 @@ impl<'a> ContractRunner<'a> {
};

logs.extend(error.logs);
logs.extend(last_call_logs.clone());

if let Some(error_traces) = error.traces {
traces.push((TraceKind::Execution, error_traces));
Expand All @@ -496,10 +503,15 @@ impl<'a> ContractRunner<'a> {
.and_then(|call_results| call_results.remove(&func_name))
{
logs.extend(last_call_result.logs);
logs.extend(last_call_logs.clone());

if let Some(last_call_traces) = last_call_result.traces {
traces.push((TraceKind::Execution, last_call_traces));
}

if let Some(last_run_traces) = last_run_traces.clone() {
traces.push((TraceKind::Execution, last_run_traces));
}
}
}
}
Expand Down