Skip to content
Open
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
2e0240b
feat(invariant): assert all invariants
grandizzy Nov 17, 2025
74f1360
Tests and Nits
grandizzy Apr 24, 2026
0d6858d
fix: check all invariants in afterInvariant gate and preflight
grandizzy Apr 24, 2026
67bbe1a
fix: use per-invariant fail_on_revert when recording handler revert f…
grandizzy Apr 24, 2026
01adfd5
fix: commit state between txs in generate_counterexample
grandizzy Apr 24, 2026
bc4b3ad
fix: preflight check all invariants, not just primary
grandizzy Apr 24, 2026
145b18a
fix: exclude secondary invariants from optimization mode runs
grandizzy Apr 24, 2026
43f7230
refactor: rename invariant_fn to primary_invariant_fn, deterministic …
grandizzy Apr 24, 2026
6c0a977
feat: show broken invariant count in progress bar during continuous runs
grandizzy Apr 24, 2026
b012dbd
feat(invariant): rename continuous_run to assert_all and default to true
grandizzy Apr 27, 2026
640a117
feat(invariant): parameterize shrinker by target invariant + persiste…
grandizzy Apr 27, 2026
20f0c0f
feat(invariant): structured InvariantOtherFailure for assert_all seco…
grandizzy Apr 27, 2026
f06905b
feat(invariant): serial secondary shrinking + Ctrl-C persists un-shru…
grandizzy Apr 27, 2026
714887f
feat(invariant): assert_all polish — [i/N] shrink counter, suite roll…
grandizzy Apr 27, 2026
1b36a6c
feat(invariant): warn when assert_all skips invariants with persisted…
grandizzy Apr 27, 2026
3cf9e71
Merge branch 'master' into gdzzy/issue-9727
grandizzy Apr 27, 2026
8f34444
fix(invariant): gate afterInvariant per-run under assert_all
grandizzy Apr 27, 2026
35e48bd
fix(invariant): re-evaluate secondary persisted failures on settings …
grandizzy Apr 27, 2026
5924ef0
fix(invariant): drop hollow [FAIL] when only secondaries break under …
grandizzy Apr 27, 2026
b3ff301
fix(invariant): scope assert_all hollow [FAIL] suppression to seconda…
grandizzy Apr 27, 2026
c09d7c1
fix(invariant): attribute failure event to first broken invariant in …
grandizzy Apr 27, 2026
eb51a9d
refactor(invariant): rename to InvariantSecondaryFailure / invariant_…
grandizzy Apr 27, 2026
058ef9b
Merge branch 'master' into gdzzy/issue-9727
grandizzy Apr 27, 2026
ffb889a
test(invariant): assert_all + fail_on_revert=false attributes assert(…
grandizzy Apr 28, 2026
1dfc854
Merge branch 'master' into gdzzy/issue-9727
grandizzy Apr 28, 2026
e8197a8
Merge branch 'master' into gdzzy/issue-9727
grandizzy Apr 28, 2026
f1eba3c
Merge branch 'master' into gdzzy/issue-9727
grandizzy Apr 29, 2026
65be9b4
Merge branch 'master' into gdzzy/issue-9727
grandizzy Apr 29, 2026
9b8e678
Merge branch 'master' into gdzzy/issue-9727
grandizzy Apr 30, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions crates/config/src/invariant.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@ pub struct InvariantConfig {
///
/// Example: `check_interval = 10` means assert after calls 10, 20, 30, ... and the last call.
pub check_interval: u32,
/// Assert every invariant declared in the current test suite, continuing the campaign after
/// the first failure until all invariants have been broken (or normal limits are hit).
/// When `false`, the campaign aborts on the first broken invariant (legacy behavior).
pub assert_all: bool,
}

impl Default for InvariantConfig {
Expand All @@ -70,6 +74,7 @@ impl Default for InvariantConfig {
max_time_delay: None,
max_block_delay: None,
check_interval: 1,
assert_all: true,
}
}
}
Expand Down
1 change: 1 addition & 0 deletions crates/evm/evm/src/executors/corpus.rs
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,7 @@ pub(crate) struct CorpusMetrics {
impl fmt::Display for CorpusMetrics {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(f)?;
writeln!(f, " Edge coverage metrics:")?;
writeln!(f, " - cumulative edges seen: {}", self.cumulative_edges_seen)?;
writeln!(f, " - cumulative features seen: {}", self.cumulative_features_seen)?;
writeln!(f, " - corpus count: {}", self.corpus_count)?;
Expand Down
43 changes: 35 additions & 8 deletions crates/evm/evm/src/executors/invariant/error.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
use super::InvariantContract;
use crate::executors::RawCallResult;
use alloy_json_abi::Function;
use alloy_primitives::{Address, Bytes};
use foundry_config::InvariantConfig;
use foundry_evm_core::{
decode::{ASSERTION_FAILED_PREFIX, EMPTY_REVERT_DATA, RevertDecoder},
evm::FoundryEvmNetwork,
};
use foundry_evm_fuzz::{BasicTxDetails, Reason, invariant::FuzzRunIdentifiedContracts};
use proptest::test_runner::TestError;
use std::{collections::HashMap, fmt};

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

impl InvariantFailures {
pub fn new() -> Self {
Self::default()
}

pub fn into_inner(self) -> (usize, Option<InvariantFuzzError>) {
(self.reverts, self.error)
pub fn into_inner(self) -> (usize, HashMap<String, InvariantFuzzError>) {
(self.reverts, self.errors)
}

pub fn record_failure(&mut self, invariant: &Function, failure: InvariantFuzzError) {
self.errors.insert(invariant.name.clone(), failure);
}

pub fn has_failure(&self, invariant: &Function) -> bool {
self.errors.contains_key(&invariant.name)
}

pub fn get_failure(&self, invariant: &Function) -> Option<&InvariantFuzzError> {
self.errors.get(&invariant.name)
}

pub fn can_continue(&self, invariants: usize) -> bool {
debug_assert!(invariants > 0, "invariant_fns must not be empty");
self.errors.len() < invariants
}
}

impl fmt::Display for InvariantFailures {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(f)?;
writeln!(f, " ❌ Failures: {}", self.errors.len())?;
Ok(())
}
}

Expand Down Expand Up @@ -75,7 +101,8 @@ pub struct FailedInvariantCaseData {
impl FailedInvariantCaseData {
pub fn new<FEN: FoundryEvmNetwork>(
invariant_contract: &InvariantContract<'_>,
invariant_config: &InvariantConfig,
shrink_run_limit: u32,
fail_on_revert: bool,
targeted_contracts: &FuzzRunIdentifiedContracts,
calldata: &[BasicTxDetails],
call_result: RawCallResult<FEN>,
Expand All @@ -95,7 +122,7 @@ impl FailedInvariantCaseData {
revert_reason
};

let func = invariant_contract.invariant_function;
let func = invariant_contract.primary_invariant_fn;
debug_assert!(func.inputs.is_empty());
let origin = func.name.as_str();
Self {
Expand All @@ -108,8 +135,8 @@ impl FailedInvariantCaseData {
addr: invariant_contract.address,
calldata: func.selector().to_vec().into(),
inner_sequence: inner_sequence.to_vec(),
shrink_run_limit: invariant_config.shrink_run_limit,
fail_on_revert: invariant_config.fail_on_revert,
shrink_run_limit,
fail_on_revert,
assertion_failure: false,
}
}
Expand Down
Loading
Loading