Skip to content

Commit 4370782

Browse files
authored
feat(invariant): shrink failure when replayed (#12351)
1 parent f377a23 commit 4370782

File tree

4 files changed

+155
-115
lines changed

4 files changed

+155
-115
lines changed

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@ use parking_lot::RwLock;
3030
use proptest::{strategy::Strategy, test_runner::TestRunner};
3131
use result::{assert_after_invariant, assert_invariants, can_continue};
3232
use revm::state::Account;
33-
use shrink::shrink_sequence;
3433
use std::{
3534
collections::{HashMap as Map, btree_map::Entry},
3635
sync::Arc,
@@ -323,6 +322,10 @@ impl<'a> InvariantExecutor<'a> {
323322
}
324323
}
325324

325+
pub fn config(self) -> InvariantConfig {
326+
self.config
327+
}
328+
326329
/// Fuzzes any deployed contract and checks any broken invariant at `invariant_address`.
327330
pub fn invariant_fuzz(
328331
&mut self,

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

Lines changed: 26 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,15 @@
1-
use super::{
2-
call_after_invariant_function, call_invariant_function, error::FailedInvariantCaseData,
3-
shrink_sequence,
4-
};
5-
use crate::executors::{EarlyExit, Executor};
1+
use super::{call_after_invariant_function, call_invariant_function};
2+
use crate::executors::{EarlyExit, Executor, invariant::shrink::shrink_sequence};
63
use alloy_dyn_abi::JsonAbiExt;
74
use alloy_primitives::{Log, U256, map::HashMap};
85
use eyre::Result;
96
use foundry_common::{ContractsByAddress, ContractsByArtifact};
7+
use foundry_config::InvariantConfig;
108
use foundry_evm_coverage::HitMaps;
119
use foundry_evm_fuzz::{BaseCounterExample, BasicTxDetails, invariant::InvariantContract};
1210
use foundry_evm_traces::{TraceKind, TraceMode, Traces, load_contracts};
1311
use indicatif::ProgressBar;
1412
use parking_lot::RwLock;
15-
use proptest::test_runner::TestError;
1613
use std::sync::Arc;
1714

1815
/// Replays a call sequence for collecting logs and traces.
@@ -98,50 +95,41 @@ pub fn replay_run(
9895
/// Replays the error case, shrinks the failing sequence and collects all necessary traces.
9996
#[expect(clippy::too_many_arguments)]
10097
pub fn replay_error(
101-
failed_case: &FailedInvariantCaseData,
102-
invariant_contract: &InvariantContract<'_>,
98+
config: InvariantConfig,
10399
mut executor: Executor,
100+
calls: &[BasicTxDetails],
101+
inner_sequence: Option<Vec<Option<BasicTxDetails>>>,
102+
invariant_contract: &InvariantContract<'_>,
104103
known_contracts: &ContractsByArtifact,
105104
ided_contracts: ContractsByAddress,
106105
logs: &mut Vec<Log>,
107106
traces: &mut Traces,
108107
line_coverage: &mut Option<HitMaps>,
109108
deprecated_cheatcodes: &mut HashMap<&'static str, Option<&'static str>>,
110109
progress: Option<&ProgressBar>,
111-
show_solidity: bool,
112110
early_exit: &EarlyExit,
113111
) -> Result<Vec<BaseCounterExample>> {
114-
match failed_case.test_error {
115-
// Don't use at the moment.
116-
TestError::Abort(_) => Ok(vec![]),
117-
TestError::Fail(_, ref calls) => {
118-
// Shrink sequence of failed calls.
119-
let calls = shrink_sequence(
120-
failed_case,
121-
calls,
122-
&executor,
123-
invariant_contract.call_after_invariant,
124-
progress,
125-
early_exit,
126-
)?;
112+
// Shrink sequence of failed calls.
113+
let calls =
114+
shrink_sequence(&config, invariant_contract, calls, &executor, progress, early_exit)?;
127115

128-
set_up_inner_replay(&mut executor, &failed_case.inner_sequence);
129-
130-
// Replay calls to get the counterexample and to collect logs, traces and coverage.
131-
replay_run(
132-
invariant_contract,
133-
executor,
134-
known_contracts,
135-
ided_contracts,
136-
logs,
137-
traces,
138-
line_coverage,
139-
deprecated_cheatcodes,
140-
&calls,
141-
show_solidity,
142-
)
143-
}
116+
if let Some(sequence) = inner_sequence {
117+
set_up_inner_replay(&mut executor, &sequence);
144118
}
119+
120+
// Replay calls to get the counterexample and to collect logs, traces and coverage.
121+
replay_run(
122+
invariant_contract,
123+
executor,
124+
known_contracts,
125+
ided_contracts,
126+
logs,
127+
traces,
128+
line_coverage,
129+
deprecated_cheatcodes,
130+
&calls,
131+
config.show_solidity,
132+
)
145133
}
146134

147135
/// Sets up the calls generated by the internal fuzzer, if they exist.

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

Lines changed: 14 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
use crate::executors::{
22
EarlyExit, Executor,
3-
invariant::{
4-
call_after_invariant_function, call_invariant_function, error::FailedInvariantCaseData,
5-
},
3+
invariant::{call_after_invariant_function, call_invariant_function},
64
};
75
use alloy_primitives::{Address, Bytes, U256};
6+
use foundry_config::InvariantConfig;
87
use foundry_evm_core::constants::MAGIC_ASSUME;
9-
use foundry_evm_fuzz::BasicTxDetails;
8+
use foundry_evm_fuzz::{BasicTxDetails, invariant::InvariantContract};
109
use indicatif::ProgressBar;
1110
use proptest::bits::{BitSetLike, VarBitSet};
1211

@@ -33,39 +32,36 @@ impl CallSequenceShrinker {
3332
}
3433
}
3534

36-
/// Shrinks the failure case to its smallest sequence of calls.
37-
///
38-
/// The shrunk call sequence always respect the order failure is reproduced as it is tested
39-
/// top-down.
4035
pub(crate) fn shrink_sequence(
41-
failed_case: &FailedInvariantCaseData,
36+
config: &InvariantConfig,
37+
invariant_contract: &InvariantContract<'_>,
4238
calls: &[BasicTxDetails],
4339
executor: &Executor,
44-
call_after_invariant: bool,
4540
progress: Option<&ProgressBar>,
4641
early_exit: &EarlyExit,
4742
) -> eyre::Result<Vec<BasicTxDetails>> {
4843
trace!(target: "forge::test", "Shrinking sequence of {} calls.", calls.len());
4944

5045
// Reset run count and display shrinking message.
5146
if let Some(progress) = progress {
52-
progress.set_length(failed_case.shrink_run_limit as usize as u64);
47+
progress.set_length(config.shrink_run_limit as u64);
5348
progress.reset();
5449
progress.set_message(" Shrink");
5550
}
5651

52+
let target_address = invariant_contract.address;
53+
let calldata: Bytes = invariant_contract.invariant_function.selector().to_vec().into();
5754
// Special case test: the invariant is *unsatisfiable* - it took 0 calls to
5855
// break the invariant -- consider emitting a warning.
59-
let (_, success) =
60-
call_invariant_function(executor, failed_case.addr, failed_case.calldata.clone())?;
56+
let (_, success) = call_invariant_function(executor, target_address, calldata.clone())?;
6157
if !success {
6258
return Ok(vec![]);
6359
}
6460

6561
let mut call_idx = 0;
6662

6763
let mut shrinker = CallSequenceShrinker::new(calls.len());
68-
for _ in 0..failed_case.shrink_run_limit {
64+
for _ in 0..config.shrink_run_limit {
6965
if early_exit.should_stop() {
7066
break;
7167
}
@@ -77,10 +73,10 @@ pub(crate) fn shrink_sequence(
7773
executor.clone(),
7874
calls,
7975
shrinker.current().collect(),
80-
failed_case.addr,
81-
failed_case.calldata.clone(),
82-
failed_case.fail_on_revert,
83-
call_after_invariant,
76+
target_address,
77+
calldata.clone(),
78+
config.fail_on_revert,
79+
invariant_contract.call_after_invariant,
8480
) {
8581
// If candidate sequence still fails, shrink until shortest possible.
8682
Ok((false, _)) if shrinker.included_calls.count() == 1 => break,

0 commit comments

Comments
 (0)