Skip to main content

foundry_evm/executors/invariant/
shrink.rs

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