1use crate::executors::{
2 invariant::{
3call_after_invariant_function, call_invariant_function, error::FailedInvariantCaseData,
4 },
5Executor,
6};
7use alloy_primitives::{Address, Bytes, U256};
8use foundry_evm_core::constants::MAGIC_ASSUME;
9use foundry_evm_fuzz::invariant::BasicTxDetails;
10use indicatif::ProgressBar;
11use proptest::bits::{BitSetLike, VarBitSet};
12use std::cmp::min;
1314#[derive(Clone, Copy, Debug)]
15struct Shrink {
16 call_index: usize,
17}
1819/// Shrinker for a call sequence failure.
20/// Iterates sequence call sequence top down and removes calls one by one.
21/// If the failure is still reproducible with removed call then moves to the next one.
22/// If the failure is not reproducible then restore removed call and moves to next one.
23#[derive(Debug)]
24struct CallSequenceShrinker {
25/// Length of call sequence to be shrunk.
26call_sequence_len: usize,
27/// Call ids contained in current shrunk sequence.
28included_calls: VarBitSet,
29/// Current shrunk call id.
30shrink: Shrink,
31/// Previous shrunk call id.
32prev_shrink: Option<Shrink>,
33}
3435impl CallSequenceShrinker {
36fn new(call_sequence_len: usize) -> Self {
37Self {
38call_sequence_len,
39 included_calls: VarBitSet::saturated(call_sequence_len),
40 shrink: Shrink { call_index: 0 },
41 prev_shrink: None,
42 }
43 }
4445/// Return candidate shrink sequence to be tested, by removing ids from original sequence.
46fn current(&self) -> impl Iterator<Item = usize> + '_ {
47 (0..self.call_sequence_len).filter(|&call_id| self.included_calls.test(call_id))
48 }
4950/// Removes next call from sequence.
51fn simplify(&mut self) -> bool {
52if self.shrink.call_index >= self.call_sequence_len {
53// We reached the end of call sequence, nothing left to simplify.
54false
55} else {
56// Remove current call.
57self.included_calls.clear(self.shrink.call_index);
58// Record current call as previous call.
59self.prev_shrink = Some(self.shrink);
60// Remove next call index
61self.shrink = Shrink { call_index: self.shrink.call_index + 1 };
62true
63}
64 }
6566/// Reverts removed call from sequence and tries to simplify next call.
67fn complicate(&mut self) -> bool {
68match self.prev_shrink {
69Some(shrink) => {
70// Undo the last call removed.
71self.included_calls.set(shrink.call_index);
72self.prev_shrink = None;
73// Try to simplify next call.
74self.simplify()
75 }
76None => false,
77 }
78 }
79}
8081/// Shrinks the failure case to its smallest sequence of calls.
82///
83/// Maximal shrinkage is guaranteed if the shrink_run_limit is not set to a value lower than the
84/// length of failed call sequence.
85///
86/// The shrunk call sequence always respect the order failure is reproduced as it is tested
87/// top-down.
88pub(crate) fn shrink_sequence(
89 failed_case: &FailedInvariantCaseData,
90 calls: &[BasicTxDetails],
91 executor: &Executor,
92 call_after_invariant: bool,
93 progress: Option<&ProgressBar>,
94) -> eyre::Result<Vec<BasicTxDetails>> {
95trace!(target: "forge::test", "Shrinking sequence of {} calls.", calls.len());
9697// Reset run count and display shrinking message.
98if let Some(progress) = progress {
99progress.set_length(min(calls.len(), failed_case.shrink_run_limit as usize) as u64);
100progress.reset();
101progress.set_message(" Shrink");
102 }
103104// Special case test: the invariant is *unsatisfiable* - it took 0 calls to
105 // break the invariant -- consider emitting a warning.
106let (_, success) =
107call_invariant_function(executor, failed_case.addr, failed_case.calldata.clone())?;
108if !success {
109return Ok(vec![]);
110 }
111112let mut shrinker = CallSequenceShrinker::new(calls.len());
113for _ in 0..failed_case.shrink_run_limit {
114// Check candidate sequence result.
115match check_sequence(
116 executor.clone(),
117 calls,
118 shrinker.current().collect(),
119 failed_case.addr,
120 failed_case.calldata.clone(),
121 failed_case.fail_on_revert,
122 call_after_invariant,
123 ) {
124// If candidate sequence still fails then shrink more if possible.
125Ok((false, _)) if !shrinker.simplify() => break,
126// If candidate sequence pass then restore last removed call and shrink other
127 // calls if possible.
128Ok((true, _)) if !shrinker.complicate() => break,
129_ => {}
130 }
131132if let Some(progress) = progress {
133 progress.inc(1);
134 }
135 }
136137Ok(shrinker.current().map(|idx| &calls[idx]).cloned().collect())
138}
139140/// Checks if the given call sequence breaks the invariant.
141///
142/// Used in shrinking phase for checking candidate sequences and in replay failures phase to test
143/// persisted failures.
144/// Returns the result of invariant check (and afterInvariant call if needed) and if sequence was
145/// entirely applied.
146pub fn check_sequence(
147mut executor: Executor,
148 calls: &[BasicTxDetails],
149 sequence: Vec<usize>,
150 test_address: Address,
151 calldata: Bytes,
152 fail_on_revert: bool,
153 call_after_invariant: bool,
154) -> eyre::Result<(bool, bool)> {
155// Apply the call sequence.
156for call_index in sequence {
157let tx = &calls[call_index];
158let call_result = executor.transact_raw(
159 tx.sender,
160 tx.call_details.target,
161 tx.call_details.calldata.clone(),
162 U256::ZERO,
163 )?;
164// Ignore calls reverted with `MAGIC_ASSUME`. This is needed to handle failed scenarios that
165 // are replayed with a modified version of test driver (that use new `vm.assume`
166 // cheatcodes).
167if call_result.reverted && fail_on_revert && call_result.result.as_ref() != MAGIC_ASSUME {
168// Candidate sequence fails test.
169 // We don't have to apply remaining calls to check sequence.
170return Ok((false, false));
171 }
172 }
173174// Check the invariant for call sequence.
175let (_, mut success) = call_invariant_function(&executor, test_address, calldata)?;
176// Check after invariant result if invariant is success and `afterInvariant` function is
177 // declared.
178if success && call_after_invariant {
179 (_, success) = call_after_invariant_function(&executor, test_address)?;
180 }
181182Ok((success, true))
183}