@@ -190,11 +190,7 @@ impl InvariantFailureMetrics {
190190
191191/// Bridges newly-recorded invariant breaks from `failures.errors` into the pulse
192192/// `failure_metrics` so the live progress stream reflects breaks as they happen.
193- ///
194- /// Without this, `unique_failures` only updates when the campaign is *forced to
195- /// stop* (i.e., `can_continue` returns false — which only happens once *all*
196- /// invariants are broken under `assert_all`). Iterates in declaration order so
197- /// the emitted "failure" events are deterministic.
193+ /// Iterates in declaration order so the emitted "failure" events are deterministic.
198194fn record_new_invariant_failures (
199195 failure_metrics : & mut InvariantFailureMetrics ,
200196 invariant_contract : & InvariantContract < ' _ > ,
@@ -511,6 +507,7 @@ impl<'a, FEN: FoundryEvmNetwork> InvariantExecutor<'a, FEN> {
511507 ' stop: while continue_campaign ( runs) {
512508 // Per-run failure count snapshot used to gate `afterInvariant` below.
513509 let failures_before_run = invariant_test. test_data . failures . invariant_count ( ) ;
510+ let mut stop_after_run = false ;
514511
515512 let initial_seq = corpus_manager. new_inputs (
516513 & mut invariant_test. test_data . branch_runner ,
@@ -750,8 +747,7 @@ impl<'a, FEN: FoundryEvmNetwork> InvariantExecutor<'a, FEN> {
750747 invariant_test. set_last_run_inputs ( & current_run. inputs ) ;
751748 }
752749 // Bridge newly-recorded predicate breaks into `failure_metrics` even when
753- // `continues == true` (under `assert_all`, breaks accumulate per tick until
754- // the last invariant falls).
750+ // `continues == true` in multi-predicate campaigns.
755751 if invariant_test. test_data . failures . invariant_count ( ) > errors_before_check
756752 || broken. is_some ( )
757753 {
@@ -761,15 +757,13 @@ impl<'a, FEN: FoundryEvmNetwork> InvariantExecutor<'a, FEN> {
761757 & invariant_test. test_data . failures ,
762758 ) ;
763759 }
764- // Keep the campaign running after a recorded predicate failure only when
765- // `assert_all && !fail_on_revert`; otherwise preserve legacy early-exit.
766- // Handler-side assertions never reach this branch — they go through
767- // `failures.broken_handlers` and leave `continues == true`.
768760 if !continues {
769- if self . config . assert_all && !self . config . fail_on_revert {
761+ if invariant_contract. invariant_fns . len ( ) > 1 && !self . config . fail_on_revert
762+ {
770763 break ;
771764 }
772- break ' stop;
765+ stop_after_run = true ;
766+ break ;
773767 }
774768 current_run. depth += 1 ;
775769 }
@@ -797,8 +791,8 @@ impl<'a, FEN: FoundryEvmNetwork> InvariantExecutor<'a, FEN> {
797791 ) ;
798792
799793 // Call `afterInvariant` only if declared and the current run produced no new
800- // failure. Under `assert_all` the campaign keeps running after earlier failures,
801- // but the hook must still execute on subsequent runs.
794+ // failure. Multi-predicate campaigns keep running after earlier failures, but the
795+ // hook must still execute on subsequent runs.
802796 if invariant_contract. call_after_invariant
803797 && invariant_test. test_data . failures . invariant_count ( ) == failures_before_run
804798 {
@@ -878,6 +872,9 @@ impl<'a, FEN: FoundryEvmNetwork> InvariantExecutor<'a, FEN> {
878872 }
879873
880874 runs += 1 ;
875+ if stop_after_run {
876+ break ' stop;
877+ }
881878 }
882879
883880 trace ! ( ?fuzz_fixtures) ;
@@ -1004,18 +1001,6 @@ impl<'a, FEN: FoundryEvmNetwork> InvariantExecutor<'a, FEN> {
10041001 if let Some ( fuzzer) = self . executor . inspector_mut ( ) . fuzzer . as_mut ( ) {
10051002 fuzz_state. collect_values ( fuzzer. drain_collected_values ( ) ) ;
10061003 }
1007- // First broken invariant in declaration order (anchor first, then secondaries).
1008- // Iterates `invariant_fns` so the lookup is deterministic, unlike HashMap iteration.
1009- if let Some ( error) =
1010- invariant_contract. invariant_fns . iter ( ) . find_map ( |( f, _) | failures. get_failure ( f) )
1011- {
1012- // Under `assert_all` we record the preflight break and let the campaign run for
1013- // the full budget; legacy mode aborts on the first broken invariant.
1014- if !self . config . assert_all {
1015- return Err ( eyre ! ( error. revert_reason( ) . unwrap_or_default( ) ) ) ;
1016- }
1017- }
1018-
10191004 // NOW enable call_override after the initial invariant check has passed.
10201005 // This allows `override_call_strat` to inject calls during actual fuzz runs
10211006 // for reentrancy vulnerability detection.
0 commit comments