Skip to content

Commit e707371

Browse files
authored
feat(forge): support for console.log on Invariant handlers (foundry-rs#5488)
* feat: record logs and traces of the last call for all invariants * feat: fill logs and traces with last call returned logs/traces * chore: simplify return type * chore: use struct instead of tuple * chore: show all invariant logs across all depths when failing * chore: insert all logs, even if the function reverts * chore: heavily simplify types * chore: clippy * chore: fmt
1 parent 16208aa commit e707371

File tree

4 files changed

+131
-41
lines changed

4 files changed

+131
-41
lines changed

evm/src/fuzz/invariant/error.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,9 @@ impl InvariantFuzzError {
145145
.call_raw(CALLER, self.addr, func.0.clone(), U256::zero())
146146
.expect("bad call to evm");
147147

148+
traces.push((TraceKind::Execution, error_call_result.traces.clone().unwrap()));
149+
150+
logs.extend(error_call_result.logs);
148151
if error_call_result.reverted {
149152
break
150153
}

evm/src/fuzz/invariant/executor.rs

Lines changed: 38 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,20 @@ use std::{cell::RefCell, collections::BTreeMap, sync::Arc};
4141
type InvariantPreparation =
4242
(EvmFuzzState, FuzzRunIdentifiedContracts, BoxedStrategy<Vec<BasicTxDetails>>);
4343

44+
/// Enriched results of an invariant run check.
45+
///
46+
/// Contains the success condition and call results of the last run
47+
struct RichInvariantResults {
48+
success: bool,
49+
call_results: Option<BTreeMap<String, RawCallResult>>,
50+
}
51+
52+
impl RichInvariantResults {
53+
fn new(success: bool, call_results: Option<BTreeMap<String, RawCallResult>>) -> Self {
54+
Self { success, call_results }
55+
}
56+
}
57+
4458
/// Wrapper around any [`Executor`] implementor which provides fuzzing support using [`proptest`](https://docs.rs/proptest/1.0.0/proptest/).
4559
///
4660
/// After instantiation, calling `fuzz` will proceed to hammer the deployed smart contracts with
@@ -107,6 +121,7 @@ impl<'a> InvariantExecutor<'a> {
107121
)
108122
.ok(),
109123
);
124+
let last_run_calldata: RefCell<Vec<BasicTxDetails>> = RefCell::new(vec![]);
110125
// Make sure invariants are sound even before starting to fuzz
111126
if last_call_results.borrow().is_none() {
112127
fuzz_cases.borrow_mut().push(FuzzedCases::new(vec![]));
@@ -142,7 +157,7 @@ impl<'a> InvariantExecutor<'a> {
142157
// Created contracts during a run.
143158
let mut created_contracts = vec![];
144159

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

@@ -183,7 +198,7 @@ impl<'a> InvariantExecutor<'a> {
183198
stipend: call_result.stipend,
184199
});
185200

186-
let (can_continue, call_results) = can_continue(
201+
let RichInvariantResults { success: can_continue, call_results } = can_continue(
187202
&invariant_contract,
188203
call_result,
189204
&executor,
@@ -195,6 +210,10 @@ impl<'a> InvariantExecutor<'a> {
195210
self.config.shrink_sequence,
196211
);
197212

213+
if !can_continue || current_run == self.config.depth - 1 {
214+
*last_run_calldata.borrow_mut() = inputs.clone();
215+
}
216+
198217
if !can_continue {
199218
break 'fuzz_run
200219
}
@@ -233,7 +252,7 @@ impl<'a> InvariantExecutor<'a> {
233252
invariants,
234253
cases: fuzz_cases.into_inner(),
235254
reverts,
236-
last_call_results: last_call_results.take(),
255+
last_run_inputs: last_run_calldata.take(),
237256
})
238257
}
239258

@@ -556,7 +575,8 @@ fn collect_data(
556575
}
557576

558577
/// Verifies that the invariant run execution can continue.
559-
/// Returns the mapping of (Invariant Function Name -> Call Result) if invariants were asserted.
578+
/// Returns the mapping of (Invariant Function Name -> Call Result, Logs, Traces) if invariants were
579+
/// asserted.
560580
#[allow(clippy::too_many_arguments)]
561581
fn can_continue(
562582
invariant_contract: &InvariantContract,
@@ -568,7 +588,7 @@ fn can_continue(
568588
state_changeset: StateChangeset,
569589
fail_on_revert: bool,
570590
shrink_sequence: bool,
571-
) -> (bool, Option<BTreeMap<String, RawCallResult>>) {
591+
) -> RichInvariantResults {
572592
let mut call_results = None;
573593

574594
// Detect handler assertion failures first.
@@ -583,7 +603,7 @@ fn can_continue(
583603
assert_invariants(invariant_contract, executor, calldata, failures, shrink_sequence)
584604
.ok();
585605
if call_results.is_none() {
586-
return (false, None)
606+
return RichInvariantResults::new(false, None)
587607
}
588608
} else {
589609
failures.reverts += 1;
@@ -604,13 +624,16 @@ fn can_continue(
604624

605625
// Hacky to provide the full error to the user.
606626
for invariant in invariant_contract.invariant_functions.iter() {
607-
failures.failed_invariants.insert(invariant.name.clone(), Some(error.clone()));
627+
failures.failed_invariants.insert(
628+
invariant.name.clone(),
629+
(Some(error.clone()), invariant.to_owned().clone()),
630+
);
608631
}
609632

610-
return (false, None)
633+
return RichInvariantResults::new(false, None)
611634
}
612635
}
613-
(true, call_results)
636+
RichInvariantResults::new(true, call_results)
614637
}
615638

616639
#[derive(Clone)]
@@ -623,21 +646,24 @@ pub struct InvariantFailures {
623646
/// How many different invariants have been broken.
624647
pub broken_invariants_count: usize,
625648
/// Maps a broken invariant to its specific error.
626-
pub failed_invariants: BTreeMap<String, Option<InvariantFuzzError>>,
649+
pub failed_invariants: BTreeMap<String, (Option<InvariantFuzzError>, Function)>,
627650
}
628651

629652
impl InvariantFailures {
630653
fn new(invariants: &[&Function]) -> Self {
631654
InvariantFailures {
632655
reverts: 0,
633656
broken_invariants_count: 0,
634-
failed_invariants: invariants.iter().map(|f| (f.name.to_string(), None)).collect(),
657+
failed_invariants: invariants
658+
.iter()
659+
.map(|f| (f.name.to_string(), (None, f.to_owned().clone())))
660+
.collect(),
635661
revert_reason: None,
636662
}
637663
}
638664

639665
/// Moves `reverts` and `failed_invariants` out of the struct.
640-
fn into_inner(self) -> (usize, BTreeMap<String, Option<InvariantFuzzError>>) {
666+
fn into_inner(self) -> (usize, BTreeMap<String, (Option<InvariantFuzzError>, Function)>) {
641667
(self.reverts, self.failed_invariants)
642668
}
643669
}

evm/src/fuzz/invariant/mod.rs

Lines changed: 71 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
11
//! Fuzzing support abstracted over the [`Evm`](crate::Evm) used
2-
use crate::{fuzz::*, CALLER};
2+
use crate::{
3+
fuzz::*,
4+
trace::{load_contracts, TraceKind, Traces},
5+
CALLER,
6+
};
37
mod error;
48
pub use error::InvariantFuzzError;
59
mod filters;
610
pub use filters::{ArtifactFilters, SenderFilters};
711
mod call_override;
812
pub use call_override::{set_up_inner_replay, RandomCallGenerator};
13+
use foundry_common::ContractsByArtifact;
914
mod executor;
1015
use crate::executor::Executor;
1116
use ethers::{
@@ -87,17 +92,20 @@ pub fn assert_invariants(
8792
.expect("to have been initialized.");
8893

8994
// We only care about invariants which we haven't broken yet.
90-
if invariant_error.is_none() {
95+
if invariant_error.0.is_none() {
9196
invariant_failures.failed_invariants.insert(
9297
broken_invariant.name.clone(),
93-
Some(InvariantFuzzError::new(
94-
invariant_contract,
95-
Some(broken_invariant),
96-
calldata,
97-
call_result,
98-
&inner_sequence,
99-
shrink_sequence,
100-
)),
98+
(
99+
Some(InvariantFuzzError::new(
100+
invariant_contract,
101+
Some(broken_invariant),
102+
calldata,
103+
call_result,
104+
&inner_sequence,
105+
shrink_sequence,
106+
)),
107+
broken_invariant.clone().to_owned(),
108+
),
101109
);
102110
found_case = true;
103111
} else {
@@ -114,7 +122,7 @@ pub fn assert_invariants(
114122
invariant_failures.broken_invariants_count = invariant_failures
115123
.failed_invariants
116124
.iter()
117-
.filter(|(_function, error)| error.is_some())
125+
.filter(|(_function, error)| error.0.is_some())
118126
.count();
119127

120128
eyre::bail!(
@@ -126,14 +134,64 @@ pub fn assert_invariants(
126134
Ok(call_results)
127135
}
128136

137+
/// Replays the provided invariant run for collecting the logs and traces from all depths.
138+
#[allow(clippy::too_many_arguments)]
139+
pub fn replay_run(
140+
invariant_contract: &InvariantContract,
141+
mut executor: Executor,
142+
known_contracts: Option<&ContractsByArtifact>,
143+
mut ided_contracts: ContractsByAddress,
144+
logs: &mut Vec<Log>,
145+
traces: &mut Traces,
146+
func: Function,
147+
inputs: Vec<BasicTxDetails>,
148+
) {
149+
// We want traces for a failed case.
150+
executor.set_tracing(true);
151+
152+
// set_up_inner_replay(&mut executor, &inputs);
153+
154+
// Replay each call from the sequence until we break the invariant.
155+
for (sender, (addr, bytes)) in inputs.iter() {
156+
let call_result = executor
157+
.call_raw_committing(*sender, *addr, bytes.0.clone(), U256::zero())
158+
.expect("bad call to evm");
159+
160+
logs.extend(call_result.logs);
161+
traces.push((TraceKind::Execution, call_result.traces.clone().unwrap()));
162+
163+
// Identify newly generated contracts, if they exist.
164+
ided_contracts.extend(load_contracts(
165+
vec![(TraceKind::Execution, call_result.traces.clone().unwrap())],
166+
known_contracts,
167+
));
168+
169+
// Checks the invariant.
170+
let error_call_result = executor
171+
.call_raw(
172+
CALLER,
173+
invariant_contract.address,
174+
func.encode_input(&[]).expect("invariant should have no inputs").into(),
175+
U256::zero(),
176+
)
177+
.expect("bad call to evm");
178+
179+
traces.push((TraceKind::Execution, error_call_result.traces.clone().unwrap()));
180+
181+
logs.extend(error_call_result.logs);
182+
}
183+
}
184+
129185
/// The outcome of an invariant fuzz test
130186
#[derive(Debug)]
131187
pub struct InvariantFuzzTestResult {
132-
pub invariants: BTreeMap<String, Option<InvariantFuzzError>>,
188+
pub invariants: BTreeMap<String, (Option<InvariantFuzzError>, Function)>,
133189
/// Every successful fuzz test case
134190
pub cases: Vec<FuzzedCases>,
135191
/// Number of reverted fuzz calls
136192
pub reverts: usize,
137193

138-
pub last_call_results: Option<BTreeMap<String, RawCallResult>>,
194+
/// The entire inputs of the last run of the invariant campaign, used for
195+
/// replaying the run for collecting traces.
196+
pub last_run_inputs: Vec<BasicTxDetails>,
139197
}

forge/src/runner.rs

Lines changed: 19 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ use foundry_evm::{
1717
executor::{CallResult, EvmError, ExecutionErr, Executor},
1818
fuzz::{
1919
invariant::{
20-
InvariantContract, InvariantExecutor, InvariantFuzzError, InvariantFuzzTestResult,
20+
replay_run, InvariantContract, InvariantExecutor, InvariantFuzzError,
21+
InvariantFuzzTestResult,
2122
},
2223
FuzzedExecutor,
2324
},
@@ -449,15 +450,16 @@ impl<'a> ContractRunner<'a> {
449450
let invariant_contract =
450451
InvariantContract { address, invariant_functions: functions, abi: self.contract };
451452

452-
let Ok(InvariantFuzzTestResult { invariants, cases, reverts, mut last_call_results }) =
453-
evm.invariant_fuzz(invariant_contract)
453+
let Ok(InvariantFuzzTestResult { invariants, cases, reverts, last_run_inputs }) =
454+
evm.invariant_fuzz(invariant_contract.clone())
454455
else {
455456
return vec![]
456457
};
457458

458459
invariants
459-
.into_iter()
460-
.map(|(func_name, test_error)| {
460+
.into_values()
461+
.map(|test_error| {
462+
let (test_error, invariant) = test_error;
461463
let mut counterexample = None;
462464
let mut logs = logs.clone();
463465
let mut traces = traces.clone();
@@ -489,18 +491,19 @@ impl<'a> ContractRunner<'a> {
489491
traces.push((TraceKind::Execution, error_traces));
490492
}
491493
}
492-
// If invariants ran successfully, collect last call logs and traces
493494
_ => {
494-
if let Some(last_call_result) = last_call_results
495-
.as_mut()
496-
.and_then(|call_results| call_results.remove(&func_name))
497-
{
498-
logs.extend(last_call_result.logs);
499-
500-
if let Some(last_call_traces) = last_call_result.traces {
501-
traces.push((TraceKind::Execution, last_call_traces));
502-
}
503-
}
495+
// If invariants ran successfully, replay the last run to collect logs and
496+
// traces.
497+
replay_run(
498+
&invariant_contract.clone(),
499+
self.executor.clone(),
500+
known_contracts,
501+
identified_contracts.clone(),
502+
&mut logs,
503+
&mut traces,
504+
invariant,
505+
last_run_inputs.clone(),
506+
);
504507
}
505508
}
506509

0 commit comments

Comments
 (0)