Skip to main content

foundry_evm/executors/invariant/
shrink.rs

1use crate::executors::{
2    EarlyExit, EvmError, Executor, RawCallResult,
3    invariant::{
4        IInvariantTest, call_after_invariant_function, call_invariant_function,
5        error::{handler_edge_fingerprint, snapshot_edge_fingerprint},
6        execute_tx,
7        result::did_fail_on_assert,
8    },
9};
10use alloy_json_abi::Function;
11use alloy_primitives::{Address, B256, Bytes, I256, Selector, U256};
12use alloy_sol_types::SolCall;
13use foundry_common::ContractsByAddress;
14use foundry_config::InvariantConfig;
15use foundry_evm_core::{
16    FoundryBlock, constants::MAGIC_ASSUME, decode::RevertDecoder, evm::FoundryEvmNetwork,
17};
18use foundry_evm_fuzz::{BaseCounterExample, BasicTxDetails, invariant::InvariantContract};
19use indicatif::ProgressBar;
20use proptest::bits::{BitSetLike, VarBitSet};
21use revm::context::Block;
22use std::{cell::Cell, collections::HashSet, fmt::Write, hash::Hash};
23
24const LIVE_SHRINK_SEQUENCE_EDGE_CALLS: usize = 16;
25
26/// Shrinker for a call sequence failure.
27/// Iterates sequence call sequence top down and removes calls one by one.
28/// If the failure is still reproducible with removed call then moves to the next one.
29/// If the failure is not reproducible then restore removed call and moves to next one.
30#[derive(Debug)]
31pub struct SequenceShrink {
32    /// Length of call sequence to be shrunk.
33    call_sequence_len: usize,
34    /// Call ids contained in current shrunk sequence.
35    included_calls: VarBitSet,
36}
37
38impl SequenceShrink {
39    pub fn new(call_sequence_len: usize) -> Self {
40        Self { call_sequence_len, included_calls: VarBitSet::saturated(call_sequence_len) }
41    }
42
43    /// Return candidate shrink sequence to be tested, by removing ids from original sequence.
44    pub fn current(&self) -> impl Iterator<Item = usize> + '_ {
45        (0..self.call_sequence_len).filter(|&call_id| self.included_calls.test(call_id))
46    }
47
48    pub fn contains(&self, call_idx: usize) -> bool {
49        self.included_calls.test(call_idx)
50    }
51
52    pub fn included_count(&self) -> usize {
53        self.included_calls.count()
54    }
55
56    pub fn apply<T: Clone>(&self, calls: &[T]) -> Vec<T> {
57        self.current().map(|idx| calls[idx].clone()).collect()
58    }
59
60    pub fn apply_with_accumulated_delay<T, D, A>(
61        &self,
62        calls: &[T],
63        mut delay: D,
64        mut apply_delay: A,
65    ) -> Vec<T>
66    where
67        T: Clone,
68        D: FnMut(&T) -> (Option<U256>, Option<U256>),
69        A: FnMut(T, U256, U256) -> T,
70    {
71        let mut result = Vec::new();
72        let mut accumulated_warp = U256::ZERO;
73        let mut accumulated_roll = U256::ZERO;
74
75        for (idx, call) in calls.iter().enumerate() {
76            let (warp, roll) = delay(call);
77            accumulated_warp += warp.unwrap_or(U256::ZERO);
78            accumulated_roll += roll.unwrap_or(U256::ZERO);
79
80            if self.contains(idx) {
81                result.push(apply_delay(call.clone(), accumulated_warp, accumulated_roll));
82                accumulated_warp = U256::ZERO;
83                accumulated_roll = U256::ZERO;
84            }
85        }
86
87        result
88    }
89
90    fn remove(&mut self, call_idx: usize) {
91        self.included_calls.clear(call_idx);
92    }
93
94    fn restore(&mut self, call_idx: usize) {
95        self.included_calls.set(call_idx);
96    }
97
98    /// Advance to the next call index, wrapping around to 0 at the end.
99    const fn next_index(&self, call_idx: usize) -> usize {
100        if call_idx + 1 == self.call_sequence_len { 0 } else { call_idx + 1 }
101    }
102}
103
104/// How `run_shrink_loop` handles a predicate error.
105#[derive(Clone, Copy)]
106enum ShrinkErrorPolicy {
107    /// "Bug still present" — keep the call removed (legacy `shrink_sequence` behavior).
108    KeepRemoved,
109    /// "Bug gone" — restore the call. Used by handler shrink so a replay error never
110    /// produces a sequence that no longer reproduces the anchor.
111    RestoreRemoved,
112}
113
114/// Attempt counters collected while trying shrink candidates.
115#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
116pub struct ShrinkRunStats {
117    pub attempts: usize,
118    pub accepted: usize,
119}
120
121/// Shared shrink attempt driver.
122///
123/// Candidate generation stays with each shrinker; this type only centralizes limit enforcement
124/// and the "accept when the candidate still reproduces the bug" accounting.
125#[derive(Clone, Copy, Debug, PartialEq, Eq)]
126pub struct ShrinkRun {
127    max_attempts: usize,
128    stats: ShrinkRunStats,
129}
130
131impl ShrinkRun {
132    pub const fn new(max_attempts: usize) -> Self {
133        Self { max_attempts, stats: ShrinkRunStats { attempts: 0, accepted: 0 } }
134    }
135
136    pub const fn can_try(&self) -> bool {
137        self.stats.attempts < self.max_attempts
138    }
139
140    pub const fn remaining_attempts(&self) -> usize {
141        self.max_attempts - self.stats.attempts
142    }
143
144    pub const fn finish(self) -> ShrinkRunStats {
145        self.stats
146    }
147
148    pub fn try_candidate(&mut self, still_fails: impl FnOnce() -> bool) -> bool {
149        self.try_candidate_decision(|| Some(still_fails())).unwrap_or(false)
150    }
151
152    fn try_candidate_decision(&mut self, decide: impl FnOnce() -> Option<bool>) -> Option<bool> {
153        if !self.can_try() {
154            return None;
155        }
156
157        let accepted = decide()?;
158        self.stats.attempts += 1;
159        if accepted {
160            self.stats.accepted += 1;
161        }
162        Some(accepted)
163    }
164}
165
166/// Shared key set for shrinkers that need to skip duplicate concrete replays.
167#[derive(Clone, Debug)]
168pub struct ShrinkCandidateKeys<K> {
169    seen: HashSet<K>,
170}
171
172impl<K: Eq + Hash> ShrinkCandidateKeys<K> {
173    pub fn new(initial: K) -> Self {
174        Self { seen: HashSet::from([initial]) }
175    }
176
177    pub fn insert(&mut self, key: K) -> bool {
178        self.seen.insert(key)
179    }
180}
181
182/// Per-call decision returned by callbacks driving `replay_sequence`. `Continue` hands
183/// the result back so non-reverted calls auto-commit; `Stop` short-circuits.
184#[expect(clippy::large_enum_variant)]
185enum ReplayDecision<T, FEN: FoundryEvmNetwork> {
186    Stop(T),
187    Continue(RawCallResult<FEN>),
188}
189
190/// Options controlling how `check_sequence` evaluates a candidate call sequence.
191pub struct CheckSequenceOptions<'a> {
192    pub accumulate_warp_roll: bool,
193    pub fail_on_revert: bool,
194    pub expect_assertion_failure: bool,
195    pub call_after_invariant: bool,
196    pub rd: Option<&'a RevertDecoder>,
197}
198
199/// Concrete failure site observed while replaying a sequence through [`check_sequence`].
200#[derive(Clone, Copy, Debug, PartialEq, Eq)]
201pub enum CheckSequenceFailureSite {
202    SequenceCall { target: Address, selector: Selector, fingerprint: B256 },
203    Invariant { target: Address, selector: Selector, fingerprint: B256 },
204    AfterInvariant { target: Address, selector: Selector, fingerprint: B256 },
205}
206
207/// Outcome from replaying an invariant call sequence through [`check_sequence`].
208#[derive(Clone, Debug)]
209pub struct CheckSequenceOutcome {
210    pub success: bool,
211    pub replayed_entirely: bool,
212    pub reason: Option<String>,
213    pub calls_count: usize,
214    pub reverts: usize,
215    pub failure_site: Option<CheckSequenceFailureSite>,
216    /// Whether replay stopped on an assertion in a sequence call rather than a plain revert or
217    /// terminal invariant check.
218    pub sequence_assertion_failure: bool,
219}
220
221pub struct ShrunkSequence {
222    pub calls: Vec<BasicTxDetails>,
223    pub result: Option<CheckSequenceOutcome>,
224}
225
226/// Result of a strict handler-bug replay: anchor asserts, no earlier call asserts, and the
227/// recomputed edge fingerprint identifies which path the assertion took.
228#[derive(Debug)]
229pub struct HandlerReplayOutcome {
230    pub anchor_asserted: bool,
231    pub reverter: Address,
232    pub selector: Selector,
233    pub revert_reason: Option<String>,
234    /// Normalized via `handler_edge_fingerprint` so callers can compare directly.
235    pub anchor_fingerprint: B256,
236}
237
238/// Resets the progress bar before each shrink. `position = Some((i, N))` renders
239/// `[i/N] Shrink: <label>` for multi-invariant campaigns.
240pub(crate) fn reset_shrink_progress(
241    config: &InvariantConfig,
242    progress: Option<&ProgressBar>,
243    label: &str,
244    position: Option<(usize, usize)>,
245) -> String {
246    let message = match position {
247        Some((current, total)) if total > 1 => {
248            format!(" [{current}/{total}] Shrink: {label}")
249        }
250        _ => format!(" Shrink: {label}"),
251    };
252    if let Some(progress) = progress {
253        progress.set_length(config.shrink_run_limit as u64);
254        progress.reset();
255        progress.set_message(message.clone());
256    }
257    message
258}
259
260/// Live shrink progress display. The progress bar itself is owned by forge's test runner; this
261/// type only formats the transient message shown while invariant shrinking is active.
262pub(crate) struct ShrinkProgress<'a> {
263    progress: Option<&'a ProgressBar>,
264    message: String,
265    identified_contracts: Option<&'a ContractsByAddress>,
266    show_solidity: bool,
267}
268
269impl<'a> ShrinkProgress<'a> {
270    pub(crate) fn new(
271        config: &InvariantConfig,
272        progress: Option<&'a ProgressBar>,
273        label: &str,
274        position: Option<(usize, usize)>,
275        identified_contracts: Option<&'a ContractsByAddress>,
276        show_solidity: bool,
277    ) -> Self {
278        let message = reset_shrink_progress(config, progress, label, position);
279        Self { progress, message, identified_contracts, show_solidity }
280    }
281
282    fn inc(&self) {
283        if let Some(progress) = self.progress {
284            progress.inc(1);
285        }
286    }
287
288    fn update(
289        &self,
290        calls: &[BasicTxDetails],
291        shrinker: &SequenceShrink,
292        accumulate_warp_roll: bool,
293    ) {
294        let Some(progress) = self.progress else {
295            return;
296        };
297        if progress.is_hidden() {
298            return;
299        }
300
301        let sequence = build_shrunk_sequence(calls, shrinker, accumulate_warp_roll);
302        let message = format_shrink_progress_message(
303            &self.message,
304            &sequence,
305            self.identified_contracts,
306            self.show_solidity,
307        );
308        progress.set_message(message);
309    }
310}
311
312fn format_shrink_progress_message(
313    phase: &str,
314    sequence: &[BasicTxDetails],
315    identified_contracts: Option<&ContractsByAddress>,
316    show_solidity: bool,
317) -> String {
318    let mut message = String::with_capacity(phase.len() + sequence.len().min(32) * 96);
319    message.push_str(phase);
320    write!(message, "\n\t[Sequence] (shrunk: {})", sequence.len()).unwrap();
321
322    if sequence.len() <= LIVE_SHRINK_SEQUENCE_EDGE_CALLS * 2 {
323        for tx in sequence {
324            push_shrink_progress_call(&mut message, tx, identified_contracts, show_solidity);
325        }
326        return message;
327    }
328
329    for tx in &sequence[..LIVE_SHRINK_SEQUENCE_EDGE_CALLS] {
330        push_shrink_progress_call(&mut message, tx, identified_contracts, show_solidity);
331    }
332    writeln!(
333        message,
334        "\n\t\t... {} call(s) omitted ...",
335        sequence.len() - LIVE_SHRINK_SEQUENCE_EDGE_CALLS * 2
336    )
337    .unwrap();
338    for tx in &sequence[sequence.len() - LIVE_SHRINK_SEQUENCE_EDGE_CALLS..] {
339        push_shrink_progress_call(&mut message, tx, identified_contracts, show_solidity);
340    }
341    message
342}
343
344fn push_shrink_progress_call(
345    message: &mut String,
346    tx: &BasicTxDetails,
347    identified_contracts: Option<&ContractsByAddress>,
348    show_solidity: bool,
349) {
350    let empty_contracts;
351    let identified_contracts = if let Some(identified_contracts) = identified_contracts {
352        identified_contracts
353    } else {
354        empty_contracts = ContractsByAddress::default();
355        &empty_contracts
356    };
357
358    let call =
359        BaseCounterExample::from_invariant_call(tx, identified_contracts, None, show_solidity)
360            .to_string();
361    for line in call.lines() {
362        message.push('\n');
363        message.push_str(line);
364    }
365}
366
367/// Applies accumulated warp/roll to a call, returning a modified copy.
368fn apply_warp_roll(mut result: BasicTxDetails, warp: U256, roll: U256) -> BasicTxDetails {
369    if warp > U256::ZERO {
370        result.warp = Some(warp);
371    }
372    if roll > U256::ZERO {
373        result.roll = Some(roll);
374    }
375    result
376}
377
378/// Applies warp/roll adjustments directly to the executor's environment.
379fn apply_warp_roll_to_env<FEN: FoundryEvmNetwork>(
380    executor: &mut Executor<FEN>,
381    warp: U256,
382    roll: U256,
383) {
384    if warp > U256::ZERO || roll > U256::ZERO {
385        let ts = executor.evm_env().block_env.timestamp();
386        let num = executor.evm_env().block_env.number();
387        executor.evm_env_mut().block_env.set_timestamp(ts + warp);
388        executor.evm_env_mut().block_env.set_number(num + roll);
389
390        let block_env = executor.evm_env().block_env.clone();
391        if let Some(cheatcodes) = executor.inspector_mut().cheatcodes.as_mut() {
392            if let Some(block) = cheatcodes.block.as_mut() {
393                let bts = block.timestamp();
394                let bnum = block.number();
395                block.set_timestamp(bts + warp);
396                block.set_number(bnum + roll);
397            } else {
398                cheatcodes.block = Some(block_env);
399            }
400        }
401    }
402}
403
404/// Builds the final shrunk sequence from the shrinker state.
405///
406/// When `accumulate_warp_roll` is enabled, warp/roll from removed calls is folded into the next
407/// kept call so the final sequence remains reproducible.
408fn build_shrunk_sequence(
409    calls: &[BasicTxDetails],
410    shrinker: &SequenceShrink,
411    accumulate_warp_roll: bool,
412) -> Vec<BasicTxDetails> {
413    if !accumulate_warp_roll {
414        return shrinker.apply(calls);
415    }
416
417    shrinker.apply_with_accumulated_delay(calls, |call| (call.warp, call.roll), apply_warp_roll)
418}
419
420/// Shared sequence shrinker. Tries to drop each call; `predicate` decides whether the candidate
421/// should be accepted, rejected, or skipped without spending a replay attempt.
422pub fn shrink_sequence_by_removing<P, S, A>(
423    calls_len: usize,
424    run: &mut ShrinkRun,
425    mut should_stop: S,
426    mut on_attempt: A,
427    mut predicate: P,
428) -> SequenceShrink
429where
430    P: FnMut(&SequenceShrink) -> Option<bool>,
431    S: FnMut() -> bool,
432    A: FnMut(),
433{
434    let mut shrinker = SequenceShrink::new(calls_len);
435    let mut call_idx = 0;
436    let mut skipped_candidates = 0usize;
437
438    while run.can_try() {
439        if should_stop() {
440            break;
441        }
442        let included_count = shrinker.included_count();
443        if included_count == 0 || skipped_candidates >= included_count {
444            break;
445        }
446
447        // Already-removed indices have nothing to drop.
448        if !shrinker.contains(call_idx) {
449            call_idx = shrinker.next_index(call_idx);
450            continue;
451        }
452
453        shrinker.remove(call_idx);
454
455        let Some(accepted) = run.try_candidate_decision(|| predicate(&shrinker)) else {
456            shrinker.restore(call_idx);
457            skipped_candidates += 1;
458            call_idx = shrinker.next_index(call_idx);
459            continue;
460        };
461
462        on_attempt();
463        skipped_candidates = 0;
464        if accepted {
465            if shrinker.included_count() == 1 {
466                break;
467            }
468        } else {
469            shrinker.restore(call_idx);
470        }
471
472        call_idx = shrinker.next_index(call_idx);
473    }
474
475    shrinker
476}
477
478/// Shared shrink loop driver. Tries to drop each call; `predicate` returns whether the
479/// candidate still triggers the bug.
480fn run_shrink_loop<P>(
481    config: &InvariantConfig,
482    calls: &[BasicTxDetails],
483    progress: &ShrinkProgress<'_>,
484    accumulate_warp_roll: bool,
485    early_exit: &EarlyExit,
486    error_policy: ShrinkErrorPolicy,
487    mut predicate: P,
488) -> SequenceShrink
489where
490    P: FnMut(&SequenceShrink) -> eyre::Result<bool>,
491{
492    let mut run = ShrinkRun::new(config.shrink_run_limit as usize);
493    let initial = SequenceShrink::new(calls.len());
494    progress.update(calls, &initial, accumulate_warp_roll);
495
496    let shrinker = shrink_sequence_by_removing(
497        calls.len(),
498        &mut run,
499        || early_exit.should_stop(),
500        || progress.inc(),
501        |shrinker| {
502            progress.update(calls, shrinker, accumulate_warp_roll);
503            match predicate(shrinker) {
504                Ok(bug_still_present) => Some(bug_still_present),
505                Err(_) => Some(matches!(error_policy, ShrinkErrorPolicy::KeepRemoved)),
506            }
507        },
508    );
509    progress.update(calls, &shrinker, accumulate_warp_roll);
510    shrinker
511}
512
513#[expect(clippy::too_many_arguments)]
514pub(crate) fn shrink_sequence<FEN: FoundryEvmNetwork>(
515    config: &InvariantConfig,
516    invariant_contract: &InvariantContract<'_>,
517    target_invariant: &Function,
518    calls: &[BasicTxDetails],
519    expect_assertion_failure: bool,
520    executor: &Executor<FEN>,
521    rd: Option<&RevertDecoder>,
522    progress: &ShrinkProgress<'_>,
523    early_exit: &EarlyExit,
524) -> eyre::Result<ShrunkSequence> {
525    trace!(target: "forge::test", "Shrinking sequence of {} calls.", calls.len());
526
527    let target_address = invariant_contract.address;
528    let calldata: Bytes = target_invariant.selector().to_vec().into();
529    // Special case test: the invariant is *unsatisfiable* - it took 0 calls to
530    // break the invariant -- consider emitting a warning.
531    let (_, success) = call_invariant_function(executor, target_address, calldata.clone())?;
532    if !success {
533        return Ok(ShrunkSequence { calls: vec![], result: None });
534    }
535
536    let accumulate_warp_roll = config.has_delay();
537    let mut sequence = Vec::with_capacity(calls.len());
538    let mut last_result = None;
539    let mut last_result_matches_shrinker = true;
540    let shrinker = run_shrink_loop(
541        config,
542        calls,
543        progress,
544        accumulate_warp_roll,
545        early_exit,
546        // Preserve legacy invariant-shrink behavior: errors during candidate evaluation
547        // do not roll back the removal.
548        ShrinkErrorPolicy::KeepRemoved,
549        |shrinker| {
550            sequence.clear();
551            sequence.extend(shrinker.current());
552            let result = match check_sequence(
553                executor.clone(),
554                calls,
555                &sequence,
556                target_address,
557                calldata.clone(),
558                CheckSequenceOptions {
559                    accumulate_warp_roll,
560                    fail_on_revert: config.fail_on_revert,
561                    expect_assertion_failure,
562                    call_after_invariant: invariant_contract.call_after_invariant,
563                    rd,
564                },
565            ) {
566                Ok(result) => result,
567                Err(err) => {
568                    last_result_matches_shrinker = false;
569                    return Err(err);
570                }
571            };
572            // Bug still present iff the invariant predicate did not pass.
573            let bug_still_present = !result.success;
574            if bug_still_present {
575                last_result = Some(result);
576                last_result_matches_shrinker = true;
577            }
578            Ok(bug_still_present)
579        },
580    );
581
582    let shrunk = build_shrunk_sequence(calls, &shrinker, accumulate_warp_roll);
583    let result = if last_result_matches_shrinker {
584        last_result
585    } else {
586        match check_sequence(
587            executor.clone(),
588            &shrunk,
589            &(0..shrunk.len()).collect::<Vec<_>>(),
590            target_address,
591            calldata,
592            CheckSequenceOptions {
593                accumulate_warp_roll: false,
594                fail_on_revert: config.fail_on_revert,
595                expect_assertion_failure,
596                call_after_invariant: invariant_contract.call_after_invariant,
597                rd,
598            },
599        ) {
600            Ok(result) => Some(result),
601            Err(err) => {
602                trace!(target: "forge::test", %err, "failed to recompute shrunk replay metrics");
603                None
604            }
605        }
606    };
607
608    Ok(ShrunkSequence { calls: shrunk, result })
609}
610
611/// Replays `sequence` (indices into `calls`) against `executor`. When
612/// `accumulate_warp_roll` is set, warp/roll from skipped calls is folded into the next
613/// included call. `on_call` may stop early; otherwise non-reverted calls are committed.
614fn replay_sequence<FEN, T, F>(
615    executor: &mut Executor<FEN>,
616    calls: &[BasicTxDetails],
617    sequence: &[usize],
618    accumulate_warp_roll: bool,
619    mut on_call: F,
620) -> eyre::Result<Option<T>>
621where
622    FEN: FoundryEvmNetwork,
623    F: FnMut(usize, RawCallResult<FEN>) -> eyre::Result<ReplayDecision<T, FEN>>,
624{
625    // Fast path: no warp/roll accumulation → iterate only kept indices (O(k)) and pass
626    // `&calls[idx]` directly to skip the per-call `BasicTxDetails` clone.
627    if !accumulate_warp_roll {
628        for &idx in sequence {
629            let call_result = execute_tx(executor, &calls[idx])?;
630            match on_call(idx, call_result)? {
631                ReplayDecision::Stop(val) => return Ok(Some(val)),
632                ReplayDecision::Continue(mut call_result) => {
633                    if !call_result.reverted {
634                        executor.commit(&mut call_result);
635                    }
636                }
637            }
638        }
639        return Ok(None);
640    }
641
642    // Accumulating path: must scan the full `calls` so warp/roll from skipped txs lands on
643    // the next kept tx as a concrete delta.
644    let mut accumulated_warp = U256::ZERO;
645    let mut accumulated_roll = U256::ZERO;
646    let mut seq_iter = sequence.iter().peekable();
647
648    for (idx, tx) in calls.iter().enumerate() {
649        accumulated_warp += tx.warp.unwrap_or(U256::ZERO);
650        accumulated_roll += tx.roll.unwrap_or(U256::ZERO);
651        if seq_iter.peek() != Some(&&idx) {
652            continue;
653        }
654        seq_iter.next();
655
656        let executed = apply_warp_roll(tx.clone(), accumulated_warp, accumulated_roll);
657        let call_result = execute_tx(executor, &executed)?;
658
659        match on_call(idx, call_result)? {
660            ReplayDecision::Stop(val) => return Ok(Some(val)),
661            ReplayDecision::Continue(mut call_result) => {
662                if !call_result.reverted {
663                    executor.commit(&mut call_result);
664                }
665            }
666        }
667
668        accumulated_warp = U256::ZERO;
669        accumulated_roll = U256::ZERO;
670    }
671
672    Ok(None)
673}
674
675/// Checks if the given call sequence breaks the invariant.
676///
677/// Used in shrinking phase for checking candidate sequences and in replay failures phase to test
678/// persisted failures.
679/// Returns the result of invariant check (and afterInvariant call if needed) and if sequence was
680/// entirely applied, plus the concrete failure site when replay fails.
681///
682/// When `options.accumulate_warp_roll` is enabled, warp/roll from removed calls is folded into the
683/// next kept call so the candidate sequence stays representable as a concrete counterexample.
684pub fn check_sequence<FEN: FoundryEvmNetwork>(
685    mut executor: Executor<FEN>,
686    calls: &[BasicTxDetails],
687    sequence: &[usize],
688    test_address: Address,
689    calldata: Bytes,
690    options: CheckSequenceOptions<'_>,
691) -> eyre::Result<CheckSequenceOutcome> {
692    let mut calls_executed = 0;
693    let mut reverts = 0;
694    let early = replay_sequence(
695        &mut executor,
696        calls,
697        sequence,
698        options.accumulate_warp_roll,
699        |idx, call_result| {
700            calls_executed += 1;
701            // Ignore calls reverted with `MAGIC_ASSUME`. This is needed to handle failed
702            // scenarios that are replayed with a modified version of test driver (that use
703            // new `vm.assume` cheatcodes).
704            if call_result.result.as_ref() == MAGIC_ASSUME {
705                return Ok(ReplayDecision::Continue(call_result));
706            }
707            if call_result.reverted {
708                reverts += 1;
709            }
710            if did_fail_on_assert(&call_result, &call_result.state_changeset) {
711                let site = sequence_call_failure_site(&calls[idx], &call_result);
712                return Ok(ReplayDecision::Stop(CheckSequenceOutcome {
713                    success: false,
714                    replayed_entirely: false,
715                    reason: assertion_failure_reason(call_result, options.rd),
716                    calls_count: calls_executed,
717                    reverts,
718                    failure_site: Some(site),
719                    sequence_assertion_failure: true,
720                }));
721            }
722            if call_result.reverted && options.fail_on_revert {
723                if options.expect_assertion_failure {
724                    return Ok(ReplayDecision::Stop(CheckSequenceOutcome {
725                        success: true,
726                        replayed_entirely: false,
727                        reason: None,
728                        calls_count: calls_executed,
729                        reverts,
730                        failure_site: None,
731                        sequence_assertion_failure: false,
732                    }));
733                }
734                let site = sequence_call_failure_site(&calls[idx], &call_result);
735                return Ok(ReplayDecision::Stop(CheckSequenceOutcome {
736                    success: false,
737                    replayed_entirely: false,
738                    reason: call_failure_reason(call_result, options.rd),
739                    calls_count: calls_executed,
740                    reverts,
741                    failure_site: Some(site),
742                    sequence_assertion_failure: false,
743                }));
744            }
745            Ok(ReplayDecision::Continue(call_result))
746        },
747    )?;
748    if let Some(result) = early {
749        return Ok(result);
750    }
751
752    // Unlike optimization mode we intentionally do not apply trailing warp/roll before the
753    // invariant call: those delays would not be representable in the final shrunk sequence.
754    let (success, replayed_entirely, reason, failure_site) =
755        finish_sequence_check(&executor, test_address, calldata, &options)?;
756    Ok(CheckSequenceOutcome {
757        success,
758        replayed_entirely,
759        reason,
760        calls_count: calls_executed,
761        reverts,
762        failure_site,
763        sequence_assertion_failure: false,
764    })
765}
766
767fn finish_sequence_check<FEN: FoundryEvmNetwork>(
768    executor: &Executor<FEN>,
769    test_address: Address,
770    calldata: Bytes,
771    options: &CheckSequenceOptions<'_>,
772) -> eyre::Result<(bool, bool, Option<String>, Option<CheckSequenceFailureSite>)> {
773    let handle_terminal_failure =
774        |call_result: RawCallResult<FEN>, site_kind: TerminalFailureSite| {
775            let should_ignore_failure = options.expect_assertion_failure
776                && !executor.has_global_failure(&call_result.state_changeset)
777                && !did_fail_on_assert(&call_result, &call_result.state_changeset);
778
779            if should_ignore_failure {
780                return (true, true, None, None);
781            }
782
783            let site = terminal_failure_site(site_kind, test_address, &calldata, &call_result);
784            let reason = if options.expect_assertion_failure {
785                assertion_failure_reason(call_result, options.rd)
786            } else {
787                call_failure_reason(call_result, options.rd)
788            };
789
790            (false, true, reason, Some(site))
791        };
792
793    let (invariant_result, mut success) =
794        call_invariant_function(executor, test_address, calldata.clone())?;
795    if !success {
796        return Ok(handle_terminal_failure(invariant_result, TerminalFailureSite::Invariant));
797    }
798
799    // Check after invariant result if invariant is success and `afterInvariant` function is
800    // declared.
801    if success && options.call_after_invariant {
802        let (after_invariant_result, after_invariant_success) =
803            call_after_invariant_function(executor, test_address)?;
804        success = after_invariant_success;
805        if !success {
806            return Ok(handle_terminal_failure(
807                after_invariant_result,
808                TerminalFailureSite::AfterInvariant,
809            ));
810        }
811    }
812
813    Ok((success, true, None, None))
814}
815
816#[derive(Clone, Copy)]
817enum TerminalFailureSite {
818    Invariant,
819    AfterInvariant,
820}
821
822fn sequence_call_failure_site<FEN: FoundryEvmNetwork>(
823    call: &BasicTxDetails,
824    call_result: &RawCallResult<FEN>,
825) -> CheckSequenceFailureSite {
826    let target = call_result.reverter.unwrap_or(call.call_details.target);
827    let selector = selector_from_calldata(&call.call_details.calldata);
828    let fingerprint =
829        handler_edge_fingerprint(snapshot_edge_fingerprint(call_result), target, selector);
830    CheckSequenceFailureSite::SequenceCall { target, selector, fingerprint }
831}
832
833fn terminal_failure_site<FEN: FoundryEvmNetwork>(
834    kind: TerminalFailureSite,
835    target: Address,
836    calldata: &Bytes,
837    call_result: &RawCallResult<FEN>,
838) -> CheckSequenceFailureSite {
839    let target = call_result.reverter.unwrap_or(target);
840    let selector = match kind {
841        TerminalFailureSite::Invariant => selector_from_calldata(calldata),
842        TerminalFailureSite::AfterInvariant => {
843            Selector::from(IInvariantTest::afterInvariantCall::SELECTOR)
844        }
845    };
846    let fingerprint =
847        handler_edge_fingerprint(snapshot_edge_fingerprint(call_result), target, selector);
848    match kind {
849        TerminalFailureSite::Invariant => {
850            CheckSequenceFailureSite::Invariant { target, selector, fingerprint }
851        }
852        TerminalFailureSite::AfterInvariant => {
853            CheckSequenceFailureSite::AfterInvariant { target, selector, fingerprint }
854        }
855    }
856}
857
858fn selector_from_calldata(calldata: &Bytes) -> Selector {
859    let selector: [u8; 4] = calldata.get(..4).and_then(|s| s.try_into().ok()).unwrap_or_default();
860    Selector::from(selector)
861}
862
863fn call_failure_reason<FEN: FoundryEvmNetwork>(
864    call_result: RawCallResult<FEN>,
865    rd: Option<&RevertDecoder>,
866) -> Option<String> {
867    match call_result.into_evm_error(rd) {
868        EvmError::Execution(err) => Some(err.reason),
869        _ => None,
870    }
871}
872
873fn assertion_failure_reason<FEN: FoundryEvmNetwork>(
874    call_result: RawCallResult<FEN>,
875    rd: Option<&RevertDecoder>,
876) -> Option<String> {
877    call_failure_reason(call_result, rd).or_else(|| Some("assertion failed".to_string()))
878}
879
880/// Shrinks a call sequence to the shortest sequence that still produces the target optimization
881/// value. This is specifically for optimization mode where we want to find the minimal sequence
882/// that achieves the maximum value.
883///
884/// Unlike `shrink_sequence` (for check mode), this function:
885/// - Accumulates warp/roll values from removed calls into the next kept call
886/// - Checks for target value equality rather than invariant failure
887#[expect(clippy::too_many_arguments)]
888pub(crate) fn shrink_sequence_value<FEN: FoundryEvmNetwork>(
889    config: &InvariantConfig,
890    invariant_contract: &InvariantContract<'_>,
891    target_invariant: &Function,
892    calls: &[BasicTxDetails],
893    executor: &Executor<FEN>,
894    target_value: I256,
895    progress: &ShrinkProgress<'_>,
896    early_exit: &EarlyExit,
897) -> eyre::Result<Vec<BasicTxDetails>> {
898    trace!(target: "forge::test", "Shrinking optimization sequence of {} calls for target value {}.", calls.len(), target_value);
899
900    let target_address = invariant_contract.address;
901    let calldata: Bytes = target_invariant.selector().to_vec().into();
902
903    // Special case: check if target value is achieved with 0 calls.
904    if check_sequence_value(executor.clone(), calls, &[], target_address, calldata.clone())?
905        == Some(target_value)
906    {
907        return Ok(vec![]);
908    }
909
910    let replay_failed = Cell::new(false);
911    let mut replay_error = None;
912    let mut run = ShrinkRun::new(config.shrink_run_limit as usize);
913    let initial = SequenceShrink::new(calls.len());
914    progress.update(calls, &initial, true);
915    let mut sequence = Vec::with_capacity(calls.len());
916
917    let shrinker = shrink_sequence_by_removing(
918        calls.len(),
919        &mut run,
920        || early_exit.should_stop() || replay_failed.get(),
921        || progress.inc(),
922        |shrinker| {
923            progress.update(calls, shrinker, true);
924            sequence.clear();
925            sequence.extend(shrinker.current());
926            match check_sequence_value(
927                executor.clone(),
928                calls,
929                &sequence,
930                target_address,
931                calldata.clone(),
932            ) {
933                Ok(Some(value)) => Some(value == target_value),
934                Ok(None) => Some(false),
935                Err(err) => {
936                    replay_error = Some(err);
937                    replay_failed.set(true);
938                    None
939                }
940            }
941        },
942    );
943    progress.update(calls, &shrinker, true);
944    if let Some(err) = replay_error {
945        return Err(err);
946    }
947
948    Ok(build_shrunk_sequence(calls, &shrinker, true))
949}
950
951/// Replays a handler-bug sequence and returns whether the anchor still asserts on the same
952/// path. Rejects sequences with a pre-anchor assertion (would be a different bug).
953pub fn replay_handler_failure_sequence<FEN: FoundryEvmNetwork>(
954    mut executor: Executor<FEN>,
955    calls: &[BasicTxDetails],
956    sequence: &[usize],
957    accumulate_warp_roll: bool,
958    rd: Option<&RevertDecoder>,
959) -> eyre::Result<HandlerReplayOutcome> {
960    let Some(&anchor_idx) = sequence.last() else {
961        return Ok(HandlerReplayOutcome {
962            anchor_asserted: false,
963            reverter: Address::ZERO,
964            selector: Selector::ZERO,
965            revert_reason: None,
966            anchor_fingerprint: B256::ZERO,
967        });
968    };
969
970    let outcome = replay_sequence(
971        &mut executor,
972        calls,
973        sequence,
974        accumulate_warp_roll,
975        |idx, call_result| {
976            let asserted = did_fail_on_assert(&call_result, &call_result.state_changeset);
977            if idx == anchor_idx {
978                let snapshot = snapshot_edge_fingerprint(&call_result);
979                let anchor = &calls[anchor_idx];
980                let reverter = call_result.reverter.unwrap_or(anchor.call_details.target);
981                let selector_bytes: [u8; 4] = anchor
982                    .call_details
983                    .calldata
984                    .get(..4)
985                    .and_then(|s| s.try_into().ok())
986                    .unwrap_or_default();
987                let selector = Selector::from(selector_bytes);
988                let fingerprint = handler_edge_fingerprint(snapshot, reverter, selector);
989                let reason =
990                    if asserted { assertion_failure_reason(call_result, rd) } else { None };
991                return Ok(ReplayDecision::Stop(HandlerReplayOutcome {
992                    anchor_asserted: asserted,
993                    reverter,
994                    selector,
995                    revert_reason: reason,
996                    anchor_fingerprint: fingerprint,
997                }));
998            }
999            if asserted {
1000                // Pre-anchor assertion = different bug; reject.
1001                return Ok(ReplayDecision::Stop(HandlerReplayOutcome {
1002                    anchor_asserted: false,
1003                    reverter: Address::ZERO,
1004                    selector: Selector::ZERO,
1005                    revert_reason: None,
1006                    anchor_fingerprint: B256::ZERO,
1007                }));
1008            }
1009            Ok(ReplayDecision::Continue(call_result))
1010        },
1011    )?;
1012
1013    Ok(outcome.unwrap_or(HandlerReplayOutcome {
1014        anchor_asserted: false,
1015        reverter: Address::ZERO,
1016        selector: Selector::ZERO,
1017        revert_reason: None,
1018        anchor_fingerprint: B256::ZERO,
1019    }))
1020}
1021
1022/// Shrinks a handler-bug sequence to the shortest prefix that still asserts on the anchor
1023/// AND keeps the same edge fingerprint (so we don't change bug identity).
1024pub(crate) fn shrink_handler_sequence<FEN: FoundryEvmNetwork>(
1025    config: &InvariantConfig,
1026    calls: &[BasicTxDetails],
1027    expected_fingerprint: B256,
1028    executor: &Executor<FEN>,
1029    progress: &ShrinkProgress<'_>,
1030    early_exit: &EarlyExit,
1031) -> eyre::Result<Vec<BasicTxDetails>> {
1032    if calls.is_empty() {
1033        return Ok(vec![]);
1034    }
1035    let accumulate_warp_roll = config.has_delay();
1036    let mut sequence = Vec::with_capacity(calls.len());
1037    let shrinker = run_shrink_loop(
1038        config,
1039        calls,
1040        progress,
1041        accumulate_warp_roll,
1042        early_exit,
1043        ShrinkErrorPolicy::RestoreRemoved,
1044        |shrinker| {
1045            sequence.clear();
1046            sequence.extend(shrinker.current());
1047            handler_sequence_still_triggers_bug(
1048                executor.clone(),
1049                calls,
1050                &sequence,
1051                accumulate_warp_roll,
1052                expected_fingerprint,
1053            )
1054        },
1055    );
1056
1057    let shrunk = build_shrunk_sequence(calls, &shrinker, accumulate_warp_roll);
1058
1059    // Verify shrunk repro; fall back to original on any failure.
1060    sequence.clear();
1061    sequence.extend(shrinker.current());
1062    let verified = handler_sequence_still_triggers_bug(
1063        executor.clone(),
1064        calls,
1065        &sequence,
1066        accumulate_warp_roll,
1067        expected_fingerprint,
1068    )
1069    .unwrap_or(false);
1070    if verified { Ok(shrunk) } else { Ok(calls.to_vec()) }
1071}
1072
1073/// Shrink predicate: anchor asserts on the same path as the originally recorded bug.
1074fn handler_sequence_still_triggers_bug<FEN: FoundryEvmNetwork>(
1075    executor: Executor<FEN>,
1076    calls: &[BasicTxDetails],
1077    sequence: &[usize],
1078    accumulate_warp_roll: bool,
1079    expected_fingerprint: B256,
1080) -> eyre::Result<bool> {
1081    let outcome =
1082        replay_handler_failure_sequence(executor, calls, sequence, accumulate_warp_roll, None)?;
1083    Ok(outcome.anchor_asserted && outcome.anchor_fingerprint == expected_fingerprint)
1084}
1085
1086/// Executes a call sequence and returns the optimization value (int256) from the invariant
1087/// function. Used during shrinking for optimization mode.
1088///
1089/// Returns `None` if the invariant call fails or doesn't return a valid int256.
1090/// Unlike `check_sequence`, this applies warp/roll from ALL calls (including removed ones).
1091pub fn check_sequence_value<FEN: FoundryEvmNetwork>(
1092    mut executor: Executor<FEN>,
1093    calls: &[BasicTxDetails],
1094    sequence: &[usize],
1095    test_address: Address,
1096    calldata: Bytes,
1097) -> eyre::Result<Option<I256>> {
1098    let mut accumulated_warp = U256::ZERO;
1099    let mut accumulated_roll = U256::ZERO;
1100    let mut seq_iter = sequence.iter().peekable();
1101
1102    for (idx, tx) in calls.iter().enumerate() {
1103        accumulated_warp += tx.warp.unwrap_or(U256::ZERO);
1104        accumulated_roll += tx.roll.unwrap_or(U256::ZERO);
1105
1106        if seq_iter.peek() == Some(&&idx) {
1107            seq_iter.next();
1108
1109            let tx_with_accumulated =
1110                apply_warp_roll(tx.clone(), accumulated_warp, accumulated_roll);
1111            let mut call_result = execute_tx(&mut executor, &tx_with_accumulated)?;
1112
1113            if !call_result.reverted {
1114                executor.commit(&mut call_result);
1115            }
1116
1117            accumulated_warp = U256::ZERO;
1118            accumulated_roll = U256::ZERO;
1119        }
1120    }
1121
1122    // Apply any remaining accumulated warp/roll before calling invariant.
1123    apply_warp_roll_to_env(&mut executor, accumulated_warp, accumulated_roll);
1124
1125    let (inv_result, success) = call_invariant_function(&executor, test_address, calldata)?;
1126
1127    if success
1128        && inv_result.result.len() >= 32
1129        && let Some(value) = I256::try_from_be_slice(&inv_result.result[..32])
1130    {
1131        return Ok(Some(value));
1132    }
1133
1134    Ok(None)
1135}
1136
1137#[cfg(test)]
1138mod tests {
1139    use super::{
1140        LIVE_SHRINK_SEQUENCE_EDGE_CALLS, SequenceShrink, ShrinkCandidateKeys, ShrinkErrorPolicy,
1141        ShrinkProgress, ShrinkRun, build_shrunk_sequence, format_shrink_progress_message,
1142        run_shrink_loop, shrink_sequence_by_removing,
1143    };
1144    use crate::executors::EarlyExit;
1145    use alloy_primitives::{Address, Bytes, U256};
1146    use foundry_config::InvariantConfig;
1147    use foundry_evm_fuzz::{BasicTxDetails, CallDetails};
1148
1149    fn tx(warp: Option<u64>, roll: Option<u64>) -> BasicTxDetails {
1150        BasicTxDetails {
1151            warp: warp.map(U256::from),
1152            roll: roll.map(U256::from),
1153            sender: Address::ZERO,
1154            call_details: CallDetails {
1155                target: Address::ZERO,
1156                calldata: Bytes::new(),
1157                value: None,
1158            },
1159        }
1160    }
1161
1162    fn tx_with_calldata(byte: u8) -> BasicTxDetails {
1163        BasicTxDetails {
1164            warp: None,
1165            roll: None,
1166            sender: Address::ZERO,
1167            call_details: CallDetails {
1168                target: Address::ZERO,
1169                calldata: Bytes::from(vec![byte]),
1170                value: None,
1171            },
1172        }
1173    }
1174
1175    #[test]
1176    fn build_shrunk_sequence_accumulates_removed_delay_into_next_kept_call() {
1177        let calls = vec![tx(Some(3), Some(5)), tx(Some(7), Some(11)), tx(Some(13), Some(17))];
1178        let mut shrinker = SequenceShrink::new(calls.len());
1179        shrinker.remove(0);
1180
1181        let shrunk = build_shrunk_sequence(&calls, &shrinker, true);
1182
1183        assert_eq!(shrunk.len(), 2);
1184        assert_eq!(shrunk[0].warp, Some(U256::from(10)));
1185        assert_eq!(shrunk[0].roll, Some(U256::from(16)));
1186        assert_eq!(shrunk[1].warp, Some(U256::from(13)));
1187        assert_eq!(shrunk[1].roll, Some(U256::from(17)));
1188    }
1189
1190    #[test]
1191    fn build_shrunk_sequence_does_not_move_trailing_delay_backward() {
1192        let calls = vec![tx(Some(3), Some(5)), tx(Some(7), Some(11))];
1193        let mut shrinker = SequenceShrink::new(calls.len());
1194        shrinker.remove(1);
1195
1196        let shrunk = build_shrunk_sequence(&calls, &shrinker, true);
1197
1198        assert_eq!(shrunk.len(), 1);
1199        assert_eq!(shrunk[0].warp, Some(U256::from(3)));
1200        assert_eq!(shrunk[0].roll, Some(U256::from(5)));
1201    }
1202
1203    #[test]
1204    fn shrink_run_counts_attempts_and_accepts() {
1205        let mut run = ShrinkRun::new(2);
1206
1207        assert_eq!(run.remaining_attempts(), 2);
1208        assert!(!run.try_candidate(|| false));
1209        assert!(run.try_candidate(|| true));
1210
1211        let mut called_after_limit = false;
1212        assert!(!run.try_candidate(|| {
1213            called_after_limit = true;
1214            true
1215        }));
1216
1217        assert!(!called_after_limit);
1218        let stats = run.finish();
1219        assert_eq!(stats.attempts, 2);
1220        assert_eq!(stats.accepted, 1);
1221    }
1222
1223    #[test]
1224    fn shrink_candidate_keys_skip_duplicates() {
1225        let mut candidates = ShrinkCandidateKeys::new("initial");
1226
1227        assert!(!candidates.insert("initial"));
1228        assert!(candidates.insert("first"));
1229        assert!(!candidates.insert("first"));
1230        assert!(candidates.insert("second"));
1231    }
1232
1233    #[test]
1234    fn shrink_loop_keep_removed_treats_candidate_error_as_still_failing() {
1235        let config = InvariantConfig { shrink_run_limit: 1, ..Default::default() };
1236        let early_exit = EarlyExit::new(false);
1237        let calls = vec![tx(None, None), tx(None, None)];
1238        let progress = ShrinkProgress::new(&config, None, "test", None, None, false);
1239
1240        let shrinker = run_shrink_loop(
1241            &config,
1242            &calls,
1243            &progress,
1244            false,
1245            &early_exit,
1246            ShrinkErrorPolicy::KeepRemoved,
1247            |_| Err(eyre::eyre!("candidate replay failed")),
1248        );
1249
1250        assert_eq!(shrinker.current().collect::<Vec<_>>(), vec![1]);
1251    }
1252
1253    #[test]
1254    fn shrink_loop_limit_counts_candidate_replays_not_skipped_indices() {
1255        let config = InvariantConfig { shrink_run_limit: 4, ..Default::default() };
1256        let early_exit = EarlyExit::new(false);
1257        let calls = vec![tx(None, None), tx(None, None), tx(None, None)];
1258        let progress = ShrinkProgress::new(&config, None, "test", None, None, false);
1259        let mut replay_attempts = 0;
1260
1261        let shrinker = run_shrink_loop(
1262            &config,
1263            &calls,
1264            &progress,
1265            false,
1266            &early_exit,
1267            ShrinkErrorPolicy::RestoreRemoved,
1268            |_| {
1269                replay_attempts += 1;
1270                Ok(matches!(replay_attempts, 1 | 4))
1271            },
1272        );
1273
1274        assert_eq!(replay_attempts, 4);
1275        assert_eq!(shrinker.current().collect::<Vec<_>>(), vec![2]);
1276    }
1277
1278    #[test]
1279    fn sequence_shrinker_skips_duplicate_candidates_without_spending_attempts() {
1280        let mut run = ShrinkRun::new(2);
1281        let mut seen = Vec::new();
1282
1283        let shrinker = shrink_sequence_by_removing(
1284            2,
1285            &mut run,
1286            || false,
1287            || {},
1288            |shrinker| {
1289                let candidate = shrinker.current().collect::<Vec<_>>();
1290                if seen.contains(&candidate) {
1291                    None
1292                } else {
1293                    seen.push(candidate);
1294                    Some(false)
1295                }
1296            },
1297        );
1298
1299        assert_eq!(shrinker.current().collect::<Vec<_>>(), vec![0, 1]);
1300        let stats = run.finish();
1301        assert_eq!(stats.attempts, 2);
1302        assert_eq!(stats.accepted, 0);
1303    }
1304
1305    #[test]
1306    fn shrink_progress_message_renders_current_sequence() {
1307        let calls = vec![tx_with_calldata(1), tx_with_calldata(2)];
1308
1309        let message =
1310            format_shrink_progress_message(" Shrink: invariant_live", &calls, None, false);
1311
1312        assert!(message.contains(" Shrink: invariant_live"));
1313        assert!(message.contains("[Sequence] (shrunk: 2)"));
1314        assert!(message.contains("calldata=0x01 args=[]"));
1315        assert!(message.contains("calldata=0x02 args=[]"));
1316    }
1317
1318    #[test]
1319    fn shrink_progress_message_omits_middle_of_large_sequence() {
1320        let calls = (0..(LIVE_SHRINK_SEQUENCE_EDGE_CALLS * 2 + 3))
1321            .map(|idx| tx_with_calldata(idx as u8))
1322            .collect::<Vec<_>>();
1323
1324        let message =
1325            format_shrink_progress_message(" Shrink: invariant_live", &calls, None, false);
1326
1327        assert!(message.contains("[Sequence] (shrunk: 35)"));
1328        assert!(message.contains("... 3 call(s) omitted ..."));
1329        assert_eq!(message.matches("sender=").count(), LIVE_SHRINK_SEQUENCE_EDGE_CALLS * 2);
1330        assert!(message.contains("calldata=0x00 args=[]"));
1331        assert!(message.contains("calldata=0x22 args=[]"));
1332    }
1333}