Skip to content

Commit a764891

Browse files
committed
feat(invariant): assert all invariants
1 parent 1c57854 commit a764891

File tree

8 files changed

+335
-195
lines changed

8 files changed

+335
-195
lines changed

crates/evm/evm/src/executors/corpus.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ pub(crate) struct CorpusMetrics {
100100
impl fmt::Display for CorpusMetrics {
101101
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
102102
writeln!(f)?;
103+
writeln!(f, " Edge coverage metrics:")?;
103104
writeln!(f, " - cumulative edges seen: {}", self.cumulative_edges_seen)?;
104105
writeln!(f, " - cumulative features seen: {}", self.cumulative_features_seen)?;
105106
writeln!(f, " - corpus count: {}", self.corpus_count)?;

crates/evm/evm/src/executors/invariant/error.rs

Lines changed: 35 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
use super::InvariantContract;
22
use crate::executors::RawCallResult;
3+
use alloy_json_abi::Function;
34
use alloy_primitives::{Address, Bytes};
4-
use foundry_config::InvariantConfig;
55
use foundry_evm_core::decode::RevertDecoder;
66
use foundry_evm_fuzz::{BasicTxDetails, Reason, invariant::FuzzRunIdentifiedContracts};
77
use proptest::test_runner::TestError;
8+
use std::{collections::HashMap, fmt};
89

910
/// Stores information about failures and reverts of the invariant tests.
1011
#[derive(Clone, Default)]
@@ -14,16 +15,40 @@ pub struct InvariantFailures {
1415
/// The latest revert reason of a run.
1516
pub revert_reason: Option<String>,
1617
/// Maps a broken invariant to its specific error.
17-
pub error: Option<InvariantFuzzError>,
18+
pub errors: HashMap<String, InvariantFuzzError>,
1819
}
1920

2021
impl InvariantFailures {
2122
pub fn new() -> Self {
2223
Self::default()
2324
}
2425

25-
pub fn into_inner(self) -> (usize, Option<InvariantFuzzError>) {
26-
(self.reverts, self.error)
26+
pub fn into_inner(self) -> (usize, HashMap<String, InvariantFuzzError>) {
27+
(self.reverts, self.errors)
28+
}
29+
30+
pub fn record_failure(&mut self, invariant: &Function, failure: InvariantFuzzError) {
31+
self.errors.insert(invariant.name.clone(), failure);
32+
}
33+
34+
pub fn has_failure(&self, invariant: &Function) -> bool {
35+
self.errors.contains_key(&invariant.name)
36+
}
37+
38+
pub fn get_failure(&self, invariant: &Function) -> Option<&InvariantFuzzError> {
39+
self.errors.get(&invariant.name)
40+
}
41+
42+
pub fn can_continue(&self, invariants: usize) -> bool {
43+
self.errors.len() < invariants
44+
}
45+
}
46+
47+
impl fmt::Display for InvariantFailures {
48+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
49+
writeln!(f)?;
50+
writeln!(f, " ❌ Failures: {}", self.errors.len())?;
51+
Ok(())
2752
}
2853
}
2954

@@ -70,10 +95,11 @@ pub struct FailedInvariantCaseData {
7095
impl FailedInvariantCaseData {
7196
pub fn new(
7297
invariant_contract: &InvariantContract<'_>,
73-
invariant_config: &InvariantConfig,
98+
shrink_run_limit: u32,
99+
fail_on_revert: bool,
74100
targeted_contracts: &FuzzRunIdentifiedContracts,
75101
calldata: &[BasicTxDetails],
76-
call_result: RawCallResult,
102+
call_result: &RawCallResult,
77103
inner_sequence: &[Option<BasicTxDetails>],
78104
) -> Self {
79105
// Collect abis of fuzzed and invariant contracts to decode custom error.
@@ -82,7 +108,7 @@ impl FailedInvariantCaseData {
82108
.with_abi(invariant_contract.abi)
83109
.decode(call_result.result.as_ref(), call_result.exit_reason);
84110

85-
let func = invariant_contract.invariant_function;
111+
let func = invariant_contract.invariant_fn;
86112
debug_assert!(func.inputs.is_empty());
87113
let origin = func.name.as_str();
88114
Self {
@@ -95,8 +121,8 @@ impl FailedInvariantCaseData {
95121
addr: invariant_contract.address,
96122
calldata: func.selector().to_vec().into(),
97123
inner_sequence: inner_sequence.to_vec(),
98-
shrink_run_limit: invariant_config.shrink_run_limit,
99-
fail_on_revert: invariant_config.fail_on_revert,
124+
shrink_run_limit,
125+
fail_on_revert,
100126
}
101127
}
102128
}

crates/evm/evm/src/executors/invariant/mod.rs

Lines changed: 36 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ use crate::{
22
executors::{Executor, RawCallResult},
33
inspectors::Fuzzer,
44
};
5+
use alloy_json_abi::Function;
56
use alloy_primitives::{
67
Address, Bytes, FixedBytes, Selector, U256,
78
map::{AddressMap, HashMap},
@@ -28,7 +29,7 @@ use foundry_evm_traces::{CallTraceArena, SparsedTraceArena};
2829
use indicatif::ProgressBar;
2930
use parking_lot::RwLock;
3031
use proptest::{strategy::Strategy, test_runner::TestRunner};
31-
use result::{assert_after_invariant, assert_invariants, can_continue};
32+
use result::{assert_after_invariant, can_continue};
3233
use revm::state::Account;
3334
use std::{
3435
collections::{HashMap as Map, btree_map::Entry},
@@ -41,7 +42,7 @@ pub use error::{InvariantFailures, InvariantFuzzError};
4142
use foundry_evm_coverage::HitMaps;
4243

4344
mod replay;
44-
pub use replay::{replay_error, replay_run};
45+
pub use replay::{generate_counterexample, replay_error, replay_run};
4546

4647
mod result;
4748
use foundry_common::{TestFunctionExt, sh_println};
@@ -52,6 +53,7 @@ use serde_json::json;
5253
mod shrink;
5354
use crate::executors::{
5455
DURATION_BETWEEN_METRICS_REPORT, EarlyExit, EvmError, FuzzTestTimer, corpus::CorpusManager,
56+
invariant::result::invariant_preflight_check,
5557
};
5658
pub use shrink::check_sequence;
5759

@@ -130,8 +132,6 @@ struct InvariantTestData {
130132
last_run_inputs: Vec<BasicTxDetails>,
131133
// Additional traces for gas report.
132134
gas_report_traces: Vec<Vec<CallTraceArena>>,
133-
// Last call results of the invariant test.
134-
last_call_results: Option<RawCallResult>,
135135
// Line coverage information collected from all fuzzed calls.
136136
line_coverage: Option<HitMaps>,
137137
// Metrics for each fuzzed selector.
@@ -160,19 +160,13 @@ impl InvariantTest {
160160
fuzz_state: EvmFuzzState,
161161
targeted_contracts: FuzzRunIdentifiedContracts,
162162
failures: InvariantFailures,
163-
last_call_results: Option<RawCallResult>,
164163
branch_runner: TestRunner,
165164
) -> Self {
166-
let mut fuzz_cases = vec![];
167-
if last_call_results.is_none() {
168-
fuzz_cases.push(FuzzedCases::new(vec![]));
169-
}
170165
let test_data = InvariantTestData {
171-
fuzz_cases,
166+
fuzz_cases: vec![],
172167
failures,
173168
last_run_inputs: vec![],
174169
gas_report_traces: vec![],
175-
last_call_results,
176170
line_coverage: None,
177171
metrics: Map::default(),
178172
branch_runner,
@@ -186,18 +180,13 @@ impl InvariantTest {
186180
}
187181

188182
/// Whether invariant test has errors or not.
189-
fn has_errors(&self) -> bool {
190-
self.test_data.failures.error.is_some()
183+
fn has_errors(&self, invariant: &Function) -> bool {
184+
self.test_data.failures.has_failure(invariant)
191185
}
192186

193187
/// Set invariant test error.
194-
fn set_error(&mut self, error: InvariantFuzzError) {
195-
self.test_data.failures.error = Some(error);
196-
}
197-
198-
/// Set last invariant test call results.
199-
fn set_last_call_results(&mut self, call_result: Option<RawCallResult>) {
200-
self.test_data.last_call_results = call_result;
188+
fn set_error(&mut self, invariant: &Function, error: InvariantFuzzError) {
189+
self.test_data.failures.record_failure(invariant, error);
201190
}
202191

203192
/// Set last invariant run call sequence.
@@ -336,7 +325,7 @@ impl<'a> InvariantExecutor<'a> {
336325
early_exit: &EarlyExit,
337326
) -> Result<InvariantFuzzTestResult> {
338327
// Throw an error to abort test run if the invariant function accepts input params
339-
if !invariant_contract.invariant_function.inputs.is_empty() {
328+
if !invariant_contract.invariant_fn.inputs.is_empty() {
340329
return Err(eyre!("Invariant test function should have no inputs"));
341330
}
342331

@@ -421,9 +410,10 @@ impl<'a> InvariantExecutor<'a> {
421410
current_run.inputs.pop();
422411
current_run.rejects += 1;
423412
if current_run.rejects > self.config.max_assume_rejects {
424-
invariant_test.set_error(InvariantFuzzError::MaxAssumeRejects(
425-
self.config.max_assume_rejects,
426-
));
413+
invariant_test.set_error(
414+
invariant_contract.invariant_fn,
415+
InvariantFuzzError::MaxAssumeRejects(self.config.max_assume_rejects),
416+
);
427417
break 'stop;
428418
}
429419
} else {
@@ -468,7 +458,7 @@ impl<'a> InvariantExecutor<'a> {
468458
});
469459

470460
// Determine if test can continue or should exit.
471-
let result = can_continue(
461+
let can_continue = can_continue(
472462
&invariant_contract,
473463
&mut invariant_test,
474464
&mut current_run,
@@ -477,15 +467,13 @@ impl<'a> InvariantExecutor<'a> {
477467
&state_changeset,
478468
)
479469
.map_err(|e| eyre!(e.to_string()))?;
480-
if !result.can_continue || current_run.depth == self.config.depth - 1 {
470+
if !can_continue || current_run.depth == self.config.depth - 1 {
481471
invariant_test.set_last_run_inputs(&current_run.inputs);
482472
}
483473
// If test cannot continue then stop current run and exit test suite.
484-
if !result.can_continue {
474+
if !can_continue {
485475
break 'stop;
486476
}
487-
488-
invariant_test.set_last_call_results(result.call_result);
489477
current_run.depth += 1;
490478
}
491479

@@ -501,7 +489,9 @@ impl<'a> InvariantExecutor<'a> {
501489
corpus_manager.process_inputs(&current_run.inputs, current_run.new_coverage);
502490

503491
// Call `afterInvariant` only if it is declared and test didn't fail already.
504-
if invariant_contract.call_after_invariant && !invariant_test.has_errors() {
492+
if invariant_contract.call_after_invariant
493+
&& !invariant_test.has_errors(invariant_contract.invariant_fn)
494+
{
505495
assert_after_invariant(
506496
&invariant_contract,
507497
&mut invariant_test,
@@ -516,10 +506,18 @@ impl<'a> InvariantExecutor<'a> {
516506
if let Some(progress) = progress {
517507
// If running with progress then increment completed runs.
518508
progress.inc(1);
519-
// Display metrics in progress bar.
509+
510+
let failures = &invariant_test.test_data.failures;
511+
let mut parts = Vec::new();
512+
// Add failures if present
513+
if !failures.errors.is_empty() {
514+
parts.push(format!("{failures}"));
515+
}
516+
// Add edge coverage metrics if enabled
520517
if edge_coverage_enabled {
521-
progress.set_message(format!("{}", &corpus_manager.metrics));
518+
parts.push(format!("{}", corpus_manager.metrics));
522519
}
520+
progress.set_message(parts.join(""));
523521
} else if edge_coverage_enabled
524522
&& last_metrics_report.elapsed() > DURATION_BETWEEN_METRICS_REPORT
525523
{
@@ -528,7 +526,7 @@ impl<'a> InvariantExecutor<'a> {
528526
"timestamp": SystemTime::now()
529527
.duration_since(UNIX_EPOCH)?
530528
.as_secs(),
531-
"invariant": invariant_contract.invariant_function.name,
529+
"invariant": invariant_contract.invariant_fn.name,
532530
"metrics": &corpus_manager.metrics,
533531
});
534532
let _ = sh_println!("{}", serde_json::to_string(&metrics)?);
@@ -543,7 +541,7 @@ impl<'a> InvariantExecutor<'a> {
543541

544542
let result = invariant_test.test_data;
545543
Ok(InvariantFuzzTestResult {
546-
error: result.failures.error,
544+
errors: result.failures.errors,
547545
cases: result.fuzz_cases,
548546
reverts: result.failures.reverts,
549547
last_run_inputs: result.last_run_inputs,
@@ -627,15 +625,15 @@ impl<'a> InvariantExecutor<'a> {
627625
// already know if we can early exit the invariant run.
628626
// This does not count as a fuzz run. It will just register the revert.
629627
let mut failures = InvariantFailures::new();
630-
let last_call_results = assert_invariants(
628+
invariant_preflight_check(
631629
invariant_contract,
632630
&self.config,
633631
&targeted_contracts,
634632
&self.executor,
635633
&[],
636634
&mut failures,
637635
)?;
638-
if let Some(error) = failures.error {
636+
if let Some(error) = failures.get_failure(invariant_contract.invariant_fn) {
639637
return Err(eyre!(error.revert_reason().unwrap_or_default()));
640638
}
641639

@@ -646,13 +644,8 @@ impl<'a> InvariantExecutor<'a> {
646644
None,
647645
Some(&targeted_contracts),
648646
)?;
649-
let invariant_test = InvariantTest::new(
650-
fuzz_state,
651-
targeted_contracts,
652-
failures,
653-
last_call_results,
654-
self.runner.clone(),
655-
);
647+
let invariant_test =
648+
InvariantTest::new(fuzz_state, targeted_contracts, failures, self.runner.clone());
656649

657650
Ok((invariant_test, corpus_manager))
658651
}

crates/evm/evm/src/executors/invariant/replay.rs

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ pub fn replay_run(
7070
let (invariant_result, invariant_success) = call_invariant_function(
7171
&executor,
7272
invariant_contract.address,
73-
invariant_contract.invariant_function.abi_encode_input(&[])?.into(),
73+
invariant_contract.invariant_fn.abi_encode_input(&[])?.into(),
7474
)?;
7575
traces.push((TraceKind::Execution, invariant_result.traces.clone().unwrap()));
7676
logs.extend(invariant_result.logs);
@@ -92,6 +92,47 @@ pub fn replay_run(
9292
Ok(counterexample_sequence)
9393
}
9494

95+
pub fn generate_counterexample(
96+
mut executor: Executor,
97+
known_contracts: &ContractsByArtifact,
98+
mut ided_contracts: ContractsByAddress,
99+
inputs: &[BasicTxDetails],
100+
show_solidity: bool,
101+
) -> Result<Vec<BaseCounterExample>> {
102+
// We want traces for a failed case.
103+
if executor.inspector().tracer.is_none() {
104+
executor.set_tracing(TraceMode::Call);
105+
}
106+
107+
let mut counterexample_sequence = vec![];
108+
109+
// Replay each call from the sequence, collect logs, traces and coverage.
110+
for tx in inputs {
111+
let call_result = executor.transact_raw(
112+
tx.sender,
113+
tx.call_details.target,
114+
tx.call_details.calldata.clone(),
115+
U256::ZERO,
116+
)?;
117+
118+
// Identify newly generated contracts, if they exist.
119+
ided_contracts
120+
.extend(load_contracts(call_result.traces.iter().map(|a| &a.arena), known_contracts));
121+
122+
// Create counter example to be used in failed case.
123+
counterexample_sequence.push(BaseCounterExample::from_invariant_call(
124+
tx.sender,
125+
tx.call_details.target,
126+
&tx.call_details.calldata,
127+
&ided_contracts,
128+
call_result.traces,
129+
show_solidity,
130+
));
131+
}
132+
133+
Ok(counterexample_sequence)
134+
}
135+
95136
/// Replays the error case, shrinks the failing sequence and collects all necessary traces.
96137
#[expect(clippy::too_many_arguments)]
97138
pub fn replay_error(

0 commit comments

Comments
 (0)