Skip to content

Commit e1db2ff

Browse files
committed
feat(invariant): assert all invariants
1 parent a9c67c2 commit e1db2ff

8 files changed

Lines changed: 381 additions & 197 deletions

File tree

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,7 @@ pub(crate) struct CorpusMetrics {
220220
impl fmt::Display for CorpusMetrics {
221221
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
222222
writeln!(f)?;
223+
writeln!(f, " Edge coverage metrics:")?;
223224
writeln!(f, " - cumulative edges seen: {}", self.cumulative_edges_seen)?;
224225
writeln!(f, " - cumulative features seen: {}", self.cumulative_features_seen)?;
225226
writeln!(f, " - corpus count: {}", self.corpus_count)?;

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

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use super::InvariantContract;
22
use crate::executors::RawCallResult;
3+
use alloy_json_abi::Function;
34
use alloy_primitives::{Address, Bytes};
45
use foundry_config::InvariantConfig;
56
use foundry_evm_core::{
@@ -8,6 +9,7 @@ use foundry_evm_core::{
89
};
910
use foundry_evm_fuzz::{BasicTxDetails, Reason, invariant::FuzzRunIdentifiedContracts};
1011
use proptest::test_runner::TestError;
12+
use std::{collections::HashMap, fmt};
1113

1214
/// Stores information about failures and reverts of the invariant tests.
1315
#[derive(Clone, Default)]
@@ -17,16 +19,40 @@ pub struct InvariantFailures {
1719
/// The latest revert reason of a run.
1820
pub revert_reason: Option<String>,
1921
/// Maps a broken invariant to its specific error.
20-
pub error: Option<InvariantFuzzError>,
22+
pub errors: HashMap<String, InvariantFuzzError>,
2123
}
2224

2325
impl InvariantFailures {
2426
pub fn new() -> Self {
2527
Self::default()
2628
}
2729

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

@@ -75,7 +101,8 @@ pub struct FailedInvariantCaseData {
75101
impl FailedInvariantCaseData {
76102
pub fn new<FEN: FoundryEvmNetwork>(
77103
invariant_contract: &InvariantContract<'_>,
78-
invariant_config: &InvariantConfig,
104+
shrink_run_limit: u32,
105+
fail_on_revert: bool,
79106
targeted_contracts: &FuzzRunIdentifiedContracts,
80107
calldata: &[BasicTxDetails],
81108
call_result: RawCallResult<FEN>,
@@ -95,7 +122,7 @@ impl FailedInvariantCaseData {
95122
revert_reason
96123
};
97124

98-
let func = invariant_contract.invariant_function;
125+
let func = invariant_contract.invariant_fn;
99126
debug_assert!(func.inputs.is_empty());
100127
let origin = func.name.as_str();
101128
Self {
@@ -108,8 +135,8 @@ impl FailedInvariantCaseData {
108135
addr: invariant_contract.address,
109136
calldata: func.selector().to_vec().into(),
110137
inner_sequence: inner_sequence.to_vec(),
111-
shrink_run_limit: invariant_config.shrink_run_limit,
112-
fail_on_revert: invariant_config.fail_on_revert,
138+
shrink_run_limit,
139+
fail_on_revert,
113140
assertion_failure: false,
114141
}
115142
}

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

Lines changed: 39 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ use crate::{
55
},
66
inspectors::Fuzzer,
77
};
8-
use alloy_primitives::{Address, Bytes, FixedBytes, I256, Selector, U256, map::AddressMap};
8+
use alloy_json_abi::Function;
9+
use alloy_primitives::{Address, Bytes, FixedBytes, I256, Selector, U256, map::{AddressMap, HashMap}};
910
use alloy_sol_types::{SolCall, sol};
1011
use eyre::{ContextCompat, Result, eyre};
1112
use foundry_common::{
@@ -34,7 +35,7 @@ use foundry_evm_traces::{CallTraceArena, SparsedTraceArena};
3435
use indicatif::ProgressBar;
3536
use parking_lot::RwLock;
3637
use proptest::{strategy::Strategy, test_runner::TestRunner};
37-
use result::{assert_after_invariant, assert_invariants, can_continue, did_fail_on_assert};
38+
use result::{assert_after_invariant, assert_invariants, can_continue, did_fail_on_assert, invariant_preflight_check};
3839
use revm::{context::Block, state::Account};
3940
use serde::{Deserialize, Serialize};
4041
use serde_json::json;
@@ -49,7 +50,7 @@ pub use error::{InvariantFailures, InvariantFuzzError};
4950
use foundry_evm_coverage::HitMaps;
5051

5152
mod replay;
52-
pub use replay::{replay_error, replay_run};
53+
pub use replay::{generate_counterexample, replay_error, replay_run};
5354

5455
mod result;
5556
pub use result::InvariantFuzzTestResult;
@@ -266,12 +267,8 @@ impl<FEN: FoundryEvmNetwork> InvariantTest<FEN> {
266267
last_call_results: Option<RawCallResult<FEN>>,
267268
branch_runner: TestRunner,
268269
) -> Self {
269-
let mut fuzz_cases = vec![];
270-
if last_call_results.is_none() {
271-
fuzz_cases.push(FuzzedCases::new(vec![]));
272-
}
273270
let test_data = InvariantTestData {
274-
fuzz_cases,
271+
fuzz_cases: vec![],
275272
failures,
276273
last_run_inputs: vec![],
277274
gas_report_traces: vec![],
@@ -291,13 +288,13 @@ impl<FEN: FoundryEvmNetwork> InvariantTest<FEN> {
291288
}
292289

293290
/// Whether invariant test has errors or not.
294-
const fn has_errors(&self) -> bool {
295-
self.test_data.failures.error.is_some()
291+
fn has_errors(&self, invariant: &Function) -> bool {
292+
self.test_data.failures.has_failure(invariant)
296293
}
297294

298295
/// Set invariant test error.
299-
fn set_error(&mut self, error: InvariantFuzzError) {
300-
self.test_data.failures.error = Some(error);
296+
fn set_error(&mut self, invariant: &Function, error: InvariantFuzzError) {
297+
self.test_data.failures.record_failure(invariant, error);
301298
}
302299

303300
/// Set last invariant test call results.
@@ -455,7 +452,7 @@ impl<'a, FEN: FoundryEvmNetwork> InvariantExecutor<'a, FEN> {
455452
early_exit: &EarlyExit,
456453
) -> Result<InvariantFuzzTestResult> {
457454
// Throw an error to abort test run if the invariant function accepts input params
458-
if !invariant_contract.invariant_function.inputs.is_empty() {
455+
if !invariant_contract.invariant_fn.inputs.is_empty() {
459456
return Err(eyre!("Invariant test function should have no inputs"));
460457
}
461458

@@ -534,9 +531,10 @@ impl<'a, FEN: FoundryEvmNetwork> InvariantExecutor<'a, FEN> {
534531
current_run.inputs.pop();
535532
current_run.rejects += 1;
536533
if current_run.rejects > self.config.max_assume_rejects {
537-
invariant_test.set_error(InvariantFuzzError::MaxAssumeRejects(
538-
self.config.max_assume_rejects,
539-
));
534+
invariant_test.set_error(
535+
invariant_contract.invariant_fn,
536+
InvariantFuzzError::MaxAssumeRejects(self.config.max_assume_rejects),
537+
);
540538
break 'stop;
541539
}
542540
} else {
@@ -602,7 +600,7 @@ impl<'a, FEN: FoundryEvmNetwork> InvariantExecutor<'a, FEN> {
602600
|| is_last_call
603601
};
604602

605-
let result = if should_check_invariant {
603+
let can_continue_result = if should_check_invariant {
606604
can_continue(
607605
&invariant_contract,
608606
&mut invariant_test,
@@ -621,7 +619,8 @@ impl<'a, FEN: FoundryEvmNetwork> InvariantExecutor<'a, FEN> {
621619
{
622620
let case_data = error::FailedInvariantCaseData::new(
623621
&invariant_contract,
624-
&self.config,
622+
self.config.shrink_run_limit,
623+
self.config.fail_on_revert,
625624
&invariant_test.targeted_contracts,
626625
&current_run.inputs,
627626
call_result,
@@ -630,12 +629,12 @@ impl<'a, FEN: FoundryEvmNetwork> InvariantExecutor<'a, FEN> {
630629
.with_assertion_failure(assertion_failure);
631630
invariant_test.test_data.failures.revert_reason =
632631
Some(case_data.revert_reason.clone());
633-
invariant_test.test_data.failures.error = Some(if assertion_failure {
632+
invariant_test.set_error(invariant_contract.invariant_fn, if assertion_failure {
634633
InvariantFuzzError::BrokenInvariant(case_data)
635634
} else {
636635
InvariantFuzzError::Revert(case_data)
637636
});
638-
result::RichInvariantResults::new(false, None)
637+
false
639638
} else if call_result.reverted
640639
&& !invariant_contract.is_optimization()
641640
&& !self.config.has_delay()
@@ -644,33 +643,32 @@ impl<'a, FEN: FoundryEvmNetwork> InvariantExecutor<'a, FEN> {
644643
// preserve their warp/roll contribution when building the final
645644
// counterexample.
646645
current_run.inputs.pop();
647-
result::RichInvariantResults::new(true, None)
646+
true
648647
} else {
649-
result::RichInvariantResults::new(true, None)
648+
true
650649
}
651650
};
652651

653-
if !result.can_continue || current_run.depth == self.config.depth - 1 {
652+
if !can_continue_result || current_run.depth == self.config.depth - 1 {
654653
invariant_test.set_last_run_inputs(&current_run.inputs);
655654
}
656655
// If test cannot continue then stop current run and exit test suite.
657-
if !result.can_continue {
656+
if !can_continue_result {
658657
let reason = invariant_test
659658
.test_data
660659
.failures
661-
.error
662-
.as_ref()
660+
.errors
661+
.values()
662+
.next()
663663
.and_then(|e| e.revert_reason())
664664
.unwrap_or_default();
665665
failure_metrics.record_failure(
666-
&invariant_contract.invariant_function.name,
666+
&invariant_contract.invariant_fn.name,
667667
invariant_contract.name,
668668
&reason,
669669
);
670670
break 'stop;
671671
}
672-
673-
invariant_test.set_last_call_results(result.call_result);
674672
current_run.depth += 1;
675673
}
676674

@@ -696,7 +694,9 @@ impl<'a, FEN: FoundryEvmNetwork> InvariantExecutor<'a, FEN> {
696694
);
697695

698696
// Call `afterInvariant` only if it is declared and test didn't fail already.
699-
if invariant_contract.call_after_invariant && !invariant_test.has_errors() {
697+
if invariant_contract.call_after_invariant
698+
&& !invariant_test.has_errors(invariant_contract.invariant_fn)
699+
{
700700
let success = assert_after_invariant(
701701
&invariant_contract,
702702
&mut invariant_test,
@@ -708,12 +708,13 @@ impl<'a, FEN: FoundryEvmNetwork> InvariantExecutor<'a, FEN> {
708708
let reason = invariant_test
709709
.test_data
710710
.failures
711-
.error
712-
.as_ref()
711+
.errors
712+
.values()
713+
.next()
713714
.and_then(|e| e.revert_reason())
714715
.unwrap_or_default();
715716
failure_metrics.record_failure(
716-
&invariant_contract.invariant_function.name,
717+
&invariant_contract.invariant_fn.name,
717718
invariant_contract.name,
718719
&reason,
719720
);
@@ -746,7 +747,7 @@ impl<'a, FEN: FoundryEvmNetwork> InvariantExecutor<'a, FEN> {
746747
// Display corpus metrics inline as JSON.
747748
let metrics = build_invariant_progress_json(
748749
SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs(),
749-
&invariant_contract.invariant_function.name,
750+
&invariant_contract.invariant_fn.name,
750751
&corpus_manager.metrics,
751752
invariant_test.test_data.optimization_best_value,
752753
throughput,
@@ -765,7 +766,7 @@ impl<'a, FEN: FoundryEvmNetwork> InvariantExecutor<'a, FEN> {
765766

766767
let result = invariant_test.test_data;
767768
Ok(InvariantFuzzTestResult {
768-
error: result.failures.error,
769+
errors: result.failures.errors,
769770
cases: result.fuzz_cases,
770771
reverts: result.failures.reverts,
771772
last_run_inputs: result.last_run_inputs,
@@ -825,15 +826,15 @@ impl<'a, FEN: FoundryEvmNetwork> InvariantExecutor<'a, FEN> {
825826
// already know if we can early exit the invariant run.
826827
// This does not count as a fuzz run. It will just register the revert.
827828
let mut failures = InvariantFailures::new();
828-
let last_call_results = assert_invariants(
829+
invariant_preflight_check(
829830
invariant_contract,
830831
&self.config,
831832
&targeted_contracts,
832833
&self.executor,
833834
&[],
834835
&mut failures,
835836
)?;
836-
if let Some(error) = failures.error {
837+
if let Some(error) = failures.get_failure(invariant_contract.invariant_fn) {
837838
return Err(eyre!(error.revert_reason().unwrap_or_default()));
838839
}
839840

@@ -879,7 +880,7 @@ impl<'a, FEN: FoundryEvmNetwork> InvariantExecutor<'a, FEN> {
879880
fuzz_state,
880881
targeted_contracts,
881882
failures,
882-
last_call_results,
883+
None,
883884
self.runner.clone(),
884885
);
885886

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

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use crate::executors::{
44
invariant::shrink::{shrink_sequence, shrink_sequence_value},
55
};
66
use alloy_dyn_abi::JsonAbiExt;
7-
use alloy_primitives::{I256, Log, map::HashMap};
7+
use alloy_primitives::{I256, Log, U256, map::HashMap};
88
use eyre::Result;
99
use foundry_common::{ContractsByAddress, ContractsByArtifact};
1010
use foundry_config::InvariantConfig;
@@ -69,7 +69,7 @@ pub fn replay_run<FEN: FoundryEvmNetwork>(
6969
let (invariant_result, invariant_success) = call_invariant_function(
7070
&executor,
7171
invariant_contract.address,
72-
invariant_contract.invariant_function.abi_encode_input(&[])?.into(),
72+
invariant_contract.invariant_fn.abi_encode_input(&[])?.into(),
7373
)?;
7474
traces.push((TraceKind::Execution, invariant_result.traces.clone().unwrap()));
7575
logs.extend(invariant_result.logs);
@@ -91,6 +91,41 @@ pub fn replay_run<FEN: FoundryEvmNetwork>(
9191
Ok(counterexample_sequence)
9292
}
9393

94+
pub fn generate_counterexample<FEN: FoundryEvmNetwork>(
95+
mut executor: Executor<FEN>,
96+
known_contracts: &ContractsByArtifact,
97+
mut ided_contracts: ContractsByAddress,
98+
inputs: &[BasicTxDetails],
99+
show_solidity: bool,
100+
) -> Result<Vec<BaseCounterExample>> {
101+
if executor.inspector().tracer.is_none() {
102+
executor.set_tracing(TraceMode::Call);
103+
}
104+
105+
let mut counterexample_sequence = vec![];
106+
107+
for tx in inputs {
108+
let call_result = executor.transact_raw(
109+
tx.sender,
110+
tx.call_details.target,
111+
tx.call_details.calldata.clone(),
112+
U256::ZERO,
113+
)?;
114+
115+
ided_contracts
116+
.extend(load_contracts(call_result.traces.iter().map(|a| &a.arena), known_contracts));
117+
118+
counterexample_sequence.push(BaseCounterExample::from_invariant_call(
119+
tx,
120+
&ided_contracts,
121+
call_result.traces,
122+
show_solidity,
123+
));
124+
}
125+
126+
Ok(counterexample_sequence)
127+
}
128+
94129
/// Replays and shrinks a call sequence, collecting logs and traces.
95130
///
96131
/// For check mode (target_value=None): shrinks to find shortest failing sequence.

0 commit comments

Comments
 (0)