foundry_evm/executors/invariant/
shrink.rs

1use crate::executors::{
2    invariant::{
3        call_after_invariant_function, call_invariant_function, error::FailedInvariantCaseData,
4    },
5    Executor,
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;
13
14#[derive(Clone, Copy, Debug)]
15struct Shrink {
16    call_index: usize,
17}
18
19/// 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.
26    call_sequence_len: usize,
27    /// Call ids contained in current shrunk sequence.
28    included_calls: VarBitSet,
29    /// Current shrunk call id.
30    shrink: Shrink,
31    /// Previous shrunk call id.
32    prev_shrink: Option<Shrink>,
33}
34
35impl CallSequenceShrinker {
36    fn new(call_sequence_len: usize) -> Self {
37        Self {
38            call_sequence_len,
39            included_calls: VarBitSet::saturated(call_sequence_len),
40            shrink: Shrink { call_index: 0 },
41            prev_shrink: None,
42        }
43    }
44
45    /// Return candidate shrink sequence to be tested, by removing ids from original sequence.
46    fn current(&self) -> impl Iterator<Item = usize> + '_ {
47        (0..self.call_sequence_len).filter(|&call_id| self.included_calls.test(call_id))
48    }
49
50    /// Removes next call from sequence.
51    fn simplify(&mut self) -> bool {
52        if self.shrink.call_index >= self.call_sequence_len {
53            // We reached the end of call sequence, nothing left to simplify.
54            false
55        } else {
56            // Remove current call.
57            self.included_calls.clear(self.shrink.call_index);
58            // Record current call as previous call.
59            self.prev_shrink = Some(self.shrink);
60            // Remove next call index
61            self.shrink = Shrink { call_index: self.shrink.call_index + 1 };
62            true
63        }
64    }
65
66    /// Reverts removed call from sequence and tries to simplify next call.
67    fn complicate(&mut self) -> bool {
68        match self.prev_shrink {
69            Some(shrink) => {
70                // Undo the last call removed.
71                self.included_calls.set(shrink.call_index);
72                self.prev_shrink = None;
73                // Try to simplify next call.
74                self.simplify()
75            }
76            None => false,
77        }
78    }
79}
80
81/// 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>> {
95    trace!(target: "forge::test", "Shrinking sequence of {} calls.", calls.len());
96
97    // Reset run count and display shrinking message.
98    if let Some(progress) = progress {
99        progress.set_length(min(calls.len(), failed_case.shrink_run_limit as usize) as u64);
100        progress.reset();
101        progress.set_message(" Shrink");
102    }
103
104    // Special case test: the invariant is *unsatisfiable* - it took 0 calls to
105    // break the invariant -- consider emitting a warning.
106    let (_, success) =
107        call_invariant_function(executor, failed_case.addr, failed_case.calldata.clone())?;
108    if !success {
109        return Ok(vec![]);
110    }
111
112    let mut shrinker = CallSequenceShrinker::new(calls.len());
113    for _ in 0..failed_case.shrink_run_limit {
114        // Check candidate sequence result.
115        match 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.
125            Ok((false, _)) if !shrinker.simplify() => break,
126            // If candidate sequence pass then restore last removed call and shrink other
127            // calls if possible.
128            Ok((true, _)) if !shrinker.complicate() => break,
129            _ => {}
130        }
131
132        if let Some(progress) = progress {
133            progress.inc(1);
134        }
135    }
136
137    Ok(shrinker.current().map(|idx| &calls[idx]).cloned().collect())
138}
139
140/// 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(
147    mut 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.
156    for call_index in sequence {
157        let tx = &calls[call_index];
158        let 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).
167        if 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.
170            return Ok((false, false));
171        }
172    }
173
174    // Check the invariant for call sequence.
175    let (_, mut success) = call_invariant_function(&executor, test_address, calldata)?;
176    // Check after invariant result if invariant is success and `afterInvariant` function is
177    // declared.
178    if success && call_after_invariant {
179        (_, success) = call_after_invariant_function(&executor, test_address)?;
180    }
181
182    Ok((success, true))
183}