Skip to main content

foundry_evm/executors/invariant/
shrink.rs

1use crate::executors::{
2    EarlyExit, EvmError, Executor, RawCallResult,
3    campaign::{CampaignCallKind, apply_block_delay, 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::{constants::MAGIC_ASSUME, decode::RevertDecoder, evm::FoundryEvmNetwork};
16use foundry_evm_fuzz::{BaseCounterExample, BasicTxDetails, invariant::InvariantContract};
17use indicatif::ProgressBar;
18use proptest::bits::{BitSetLike, VarBitSet};
19use std::{borrow::Cow, cell::Cell, fmt::Write, hash::Hash};
20
21const LIVE_SHRINK_SEQUENCE_EDGE_CALLS: usize = 16;
22
23/// Shrinker for a call sequence failure.
24/// Iterates sequence call sequence top down and removes calls one by one.
25/// If the failure is still reproducible with removed call then moves to the next one.
26/// If the failure is not reproducible then restore removed call and moves to next one.
27#[derive(Debug)]
28pub struct SequenceShrink {
29    /// Length of call sequence to be shrunk.
30    call_sequence_len: usize,
31    /// Call ids contained in current shrunk sequence.
32    included_calls: VarBitSet,
33}
34
35impl SequenceShrink {
36    pub fn new(call_sequence_len: usize) -> Self {
37        Self { call_sequence_len, included_calls: VarBitSet::saturated(call_sequence_len) }
38    }
39
40    /// Return candidate shrink sequence to be tested, by removing ids from original sequence.
41    pub fn current(&self) -> impl Iterator<Item = usize> + '_ {
42        (0..self.call_sequence_len).filter(|&call_id| self.included_calls.test(call_id))
43    }
44
45    pub fn contains(&self, call_idx: usize) -> bool {
46        self.included_calls.test(call_idx)
47    }
48
49    pub fn included_count(&self) -> usize {
50        self.included_calls.count()
51    }
52
53    pub fn apply<T: Clone>(&self, calls: &[T]) -> Vec<T> {
54        self.current().map(|idx| calls[idx].clone()).collect()
55    }
56
57    pub fn apply_with_accumulated_delay<T, D, A>(
58        &self,
59        calls: &[T],
60        mut delay: D,
61        mut apply_delay: A,
62    ) -> Vec<T>
63    where
64        T: Clone,
65        D: FnMut(&T) -> (Option<U256>, Option<U256>),
66        A: FnMut(T, U256, U256) -> T,
67    {
68        let mut result = Vec::new();
69        let mut accumulated_warp = U256::ZERO;
70        let mut accumulated_roll = U256::ZERO;
71
72        for (idx, call) in calls.iter().enumerate() {
73            let (warp, roll) = delay(call);
74            accumulated_warp += warp.unwrap_or(U256::ZERO);
75            accumulated_roll += roll.unwrap_or(U256::ZERO);
76
77            if self.contains(idx) {
78                result.push(apply_delay(call.clone(), accumulated_warp, accumulated_roll));
79                accumulated_warp = U256::ZERO;
80                accumulated_roll = U256::ZERO;
81            }
82        }
83
84        result
85    }
86
87    fn remove(&mut self, call_idx: usize) {
88        self.included_calls.clear(call_idx);
89    }
90
91    fn restore(&mut self, call_idx: usize) {
92        self.included_calls.set(call_idx);
93    }
94
95    /// Advance to the next call index, wrapping around to 0 at the end.
96    const fn next_index(&self, call_idx: usize) -> usize {
97        if call_idx + 1 == self.call_sequence_len { 0 } else { call_idx + 1 }
98    }
99}
100
101/// How `run_shrink_loop` handles a predicate error.
102#[derive(Clone, Copy)]
103enum ShrinkErrorPolicy {
104    /// "Bug still present" — keep the call removed (legacy `shrink_sequence` behavior).
105    KeepRemoved,
106    /// "Bug gone" — restore the call. Used by handler shrink so a replay error never
107    /// produces a sequence that no longer reproduces the anchor.
108    RestoreRemoved,
109}
110
111/// Attempt counters collected while trying shrink candidates.
112#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
113pub struct ShrinkRunStats {
114    pub attempts: usize,
115    pub accepted: usize,
116}
117
118/// Shared shrink attempt driver.
119///
120/// Candidate generation stays with each shrinker; this type only centralizes limit enforcement
121/// and the "accept when the candidate still reproduces the bug" accounting.
122#[derive(Clone, Copy, Debug, PartialEq, Eq)]
123pub struct ShrinkRun {
124    max_attempts: usize,
125    stats: ShrinkRunStats,
126}
127
128impl ShrinkRun {
129    pub const fn new(max_attempts: usize) -> Self {
130        Self { max_attempts, stats: ShrinkRunStats { attempts: 0, accepted: 0 } }
131    }
132
133    pub const fn can_try(&self) -> bool {
134        self.stats.attempts < self.max_attempts
135    }
136
137    pub const fn remaining_attempts(&self) -> usize {
138        self.max_attempts - self.stats.attempts
139    }
140
141    pub const fn finish(self) -> ShrinkRunStats {
142        self.stats
143    }
144
145    pub fn try_candidate(&mut self, still_fails: impl FnOnce() -> bool) -> bool {
146        self.try_candidate_decision(|| Some(still_fails())).unwrap_or(false)
147    }
148
149    fn try_candidate_decision(&mut self, decide: impl FnOnce() -> Option<bool>) -> Option<bool> {
150        if !self.can_try() {
151            return None;
152        }
153
154        let accepted = decide()?;
155        self.stats.attempts += 1;
156        if accepted {
157            self.stats.accepted += 1;
158        }
159        Some(accepted)
160    }
161}
162
163/// Shared key set for shrinkers that need to skip duplicate concrete replays.
164#[derive(Clone, Debug)]
165pub struct ShrinkCandidateKeys<K> {
166    seen: HashSet<K>,
167}
168
169impl<K: Eq + Hash> ShrinkCandidateKeys<K> {
170    pub fn new(initial: K) -> Self {
171        Self { seen: HashSet::<_>::from_iter([initial]) }
172    }
173
174    pub fn insert(&mut self, key: K) -> bool {
175        self.seen.insert(key)
176    }
177}
178
179/// Per-call decision returned by callbacks driving `replay_sequence`.
180enum ReplayDecision<T> {
181    Stop(T),
182    Continue,
183}
184
185/// Options controlling how `check_sequence` evaluates a candidate call sequence.
186pub struct CheckSequenceOptions<'a> {
187    pub accumulate_warp_roll: bool,
188    pub fail_on_revert: bool,
189    pub expect_assertion_failure: bool,
190    pub call_after_invariant: bool,
191    pub rd: Option<&'a RevertDecoder>,
192}
193
194/// Concrete failure site observed while replaying a sequence through [`check_sequence`].
195#[derive(Clone, Copy, Debug, PartialEq, Eq)]
196pub enum CheckSequenceFailureSite {
197    SequenceCall { target: Address, selector: Selector, fingerprint: B256 },
198    Invariant { target: Address, selector: Selector, fingerprint: B256 },
199    AfterInvariant { target: Address, selector: Selector, fingerprint: B256 },
200}
201
202/// Outcome from replaying an invariant call sequence through [`check_sequence`].
203#[derive(Clone, Debug)]
204pub struct CheckSequenceOutcome {
205    pub success: bool,
206    pub replayed_entirely: bool,
207    pub reason: Option<String>,
208    pub calls_count: usize,
209    pub reverts: usize,
210    pub failure_site: Option<CheckSequenceFailureSite>,
211    /// Whether replay stopped on an assertion in a sequence call rather than a plain revert or
212    /// terminal invariant check.
213    pub sequence_assertion_failure: bool,
214    /// Innermost reverter for a sequence assertion, used to recognize legacy handler identities.
215    pub sequence_reverter: Option<Address>,
216}
217
218pub struct ShrunkSequence {
219    pub calls: Vec<BasicTxDetails>,
220    pub result: Option<CheckSequenceOutcome>,
221}
222
223/// Result of a strict handler-bug replay: anchor asserts, no earlier call asserts, and the
224/// recomputed edge fingerprint identifies which path the assertion took.
225#[derive(Debug)]
226pub struct HandlerReplayOutcome {
227    pub anchor_asserted: bool,
228    /// Top-level fuzzed handler target used as the canonical failure identity.
229    pub handler_target: Address,
230    /// Innermost reverting frame, retained to recognize legacy persisted identities.
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/// Builds the final shrunk sequence from the shrinker state.
379///
380/// When `accumulate_warp_roll` is enabled, warp/roll from removed calls is folded into the next
381/// kept call so the final sequence remains reproducible.
382fn build_shrunk_sequence(
383    calls: &[BasicTxDetails],
384    shrinker: &SequenceShrink,
385    accumulate_warp_roll: bool,
386) -> Vec<BasicTxDetails> {
387    if !accumulate_warp_roll {
388        return shrinker.apply(calls);
389    }
390
391    shrinker.apply_with_accumulated_delay(calls, |call| (call.warp, call.roll), apply_warp_roll)
392}
393
394/// Shared sequence shrinker. Tries to drop each call; `predicate` decides whether the candidate
395/// should be accepted, rejected, or skipped without spending a replay attempt.
396///
397/// With `drop_blocks`, first tries to drop blocks of kept calls, halving the block size down to two
398/// calls, then drops single calls. Stops when every kept call was tried once since the last
399/// accepted removal: the sequence is then a fixpoint and further passes would replay the same
400/// rejected candidates.
401pub fn shrink_sequence_by_removing<P, S, A>(
402    calls_len: usize,
403    run: &mut ShrinkRun,
404    drop_blocks: bool,
405    mut should_stop: S,
406    mut on_attempt: A,
407    mut predicate: P,
408) -> SequenceShrink
409where
410    P: FnMut(&SequenceShrink) -> Option<bool>,
411    S: FnMut() -> bool,
412    A: FnMut(),
413{
414    let mut shrinker = SequenceShrink::new(calls_len);
415
416    let mut chunk = if drop_blocks { calls_len / 2 } else { 0 };
417    while chunk >= 2 && run.can_try() && !should_stop() {
418        let kept = shrinker.current().collect::<Vec<_>>();
419        let mut accepted_block = false;
420        for block in kept.chunks(chunk) {
421            if !run.can_try() || should_stop() {
422                break;
423            }
424            for &call_idx in block {
425                shrinker.remove(call_idx);
426            }
427            let decision = run.try_candidate_decision(|| predicate(&shrinker));
428            if decision.is_some() {
429                on_attempt();
430            }
431            if decision == Some(true) {
432                accepted_block = true;
433            } else {
434                for &call_idx in block {
435                    shrinker.restore(call_idx);
436                }
437            }
438        }
439        // The first levels cost a few attempts. Once a level has eight or more blocks and none
440        // can go, the needed calls are dense and single removals are the cheaper path.
441        if !accepted_block && kept.len() / chunk >= 8 {
442            break;
443        }
444        chunk /= 2;
445    }
446
447    let mut call_idx = 0;
448    // Kept calls tried without an accepted removal since the last change.
449    let mut unchanged_candidates = 0usize;
450
451    while run.can_try() {
452        if should_stop() {
453            break;
454        }
455        let included_count = shrinker.included_count();
456        if included_count == 0 || unchanged_candidates >= included_count {
457            break;
458        }
459
460        // Already-removed indices have nothing to drop.
461        if !shrinker.contains(call_idx) {
462            call_idx = shrinker.next_index(call_idx);
463            continue;
464        }
465
466        shrinker.remove(call_idx);
467
468        match run.try_candidate_decision(|| predicate(&shrinker)) {
469            Some(true) => {
470                on_attempt();
471                unchanged_candidates = 0;
472                if shrinker.included_count() == 1 {
473                    break;
474                }
475            }
476            Some(false) => {
477                on_attempt();
478                shrinker.restore(call_idx);
479                unchanged_candidates += 1;
480            }
481            None => {
482                shrinker.restore(call_idx);
483                unchanged_candidates += 1;
484            }
485        }
486
487        call_idx = shrinker.next_index(call_idx);
488    }
489
490    shrinker
491}
492
493/// Shared shrink loop driver. Tries to drop each call; `predicate` returns whether the
494/// candidate still triggers the bug.
495fn run_shrink_loop<P>(
496    config: &InvariantConfig,
497    calls: &[BasicTxDetails],
498    progress: &ShrinkProgress<'_>,
499    accumulate_warp_roll: bool,
500    early_exit: &EarlyExit,
501    error_policy: ShrinkErrorPolicy,
502    mut predicate: P,
503) -> SequenceShrink
504where
505    P: FnMut(&SequenceShrink) -> eyre::Result<bool>,
506{
507    let mut run = ShrinkRun::new(config.shrink_run_limit as usize);
508    let initial = SequenceShrink::new(calls.len());
509    progress.update(calls, &initial, accumulate_warp_roll);
510
511    let shrinker = shrink_sequence_by_removing(
512        calls.len(),
513        &mut run,
514        true,
515        || early_exit.should_stop(),
516        || progress.inc(),
517        |shrinker| {
518            progress.update(calls, shrinker, accumulate_warp_roll);
519            match predicate(shrinker) {
520                Ok(bug_still_present) => Some(bug_still_present),
521                Err(_) => Some(matches!(error_policy, ShrinkErrorPolicy::KeepRemoved)),
522            }
523        },
524    );
525    progress.update(calls, &shrinker, accumulate_warp_roll);
526    let stats = run.finish();
527    trace!(
528        target: "forge::test",
529        attempts = stats.attempts,
530        accepted = stats.accepted,
531        "shrink finished"
532    );
533    shrinker
534}
535
536#[expect(clippy::too_many_arguments)]
537pub(crate) fn shrink_sequence<FEN: FoundryEvmNetwork>(
538    config: &InvariantConfig,
539    invariant_contract: &InvariantContract<'_>,
540    target_invariant: &Function,
541    calls: &[BasicTxDetails],
542    expect_assertion_failure: bool,
543    executor: &Executor<FEN>,
544    rd: Option<&RevertDecoder>,
545    progress: &ShrinkProgress<'_>,
546    early_exit: &EarlyExit,
547) -> eyre::Result<ShrunkSequence> {
548    trace!(target: "forge::test", "Shrinking sequence of {} calls.", calls.len());
549
550    let target_address = invariant_contract.address;
551    let calldata: Bytes = target_invariant.selector().into();
552    // Special case test: the invariant is *unsatisfiable* - it took 0 calls to
553    // break the invariant -- consider emitting a warning.
554    let (_, success) = call_invariant_function(executor, target_address, calldata.clone())?;
555    if !success {
556        return Ok(ShrunkSequence { calls: vec![], result: None });
557    }
558
559    let accumulate_warp_roll = config.has_delay();
560    let mut sequence = Vec::with_capacity(calls.len());
561    let mut last_result = None;
562    let mut last_result_matches_shrinker = true;
563    let shrinker = run_shrink_loop(
564        config,
565        calls,
566        progress,
567        accumulate_warp_roll,
568        early_exit,
569        // Preserve legacy invariant-shrink behavior: errors during candidate evaluation
570        // do not roll back the removal.
571        ShrinkErrorPolicy::KeepRemoved,
572        |shrinker| {
573            sequence.clear();
574            sequence.extend(shrinker.current());
575            let result = match check_sequence(
576                executor.clone(),
577                calls,
578                &sequence,
579                target_address,
580                calldata.clone(),
581                CheckSequenceOptions {
582                    accumulate_warp_roll,
583                    fail_on_revert: config.fail_on_revert,
584                    expect_assertion_failure,
585                    call_after_invariant: invariant_contract.call_after_invariant,
586                    rd,
587                },
588            ) {
589                Ok(result) => result,
590                Err(err) => {
591                    last_result_matches_shrinker = false;
592                    return Err(err);
593                }
594            };
595            // Bug still present iff the invariant predicate did not pass.
596            let bug_still_present = !result.success;
597            if bug_still_present {
598                last_result = Some(result);
599                last_result_matches_shrinker = true;
600            }
601            Ok(bug_still_present)
602        },
603    );
604
605    let shrunk = build_shrunk_sequence(calls, &shrinker, accumulate_warp_roll);
606    let result = if last_result_matches_shrinker {
607        last_result
608    } else {
609        match check_sequence(
610            executor.clone(),
611            &shrunk,
612            &(0..shrunk.len()).collect::<Vec<_>>(),
613            target_address,
614            calldata,
615            CheckSequenceOptions {
616                accumulate_warp_roll: false,
617                fail_on_revert: config.fail_on_revert,
618                expect_assertion_failure,
619                call_after_invariant: invariant_contract.call_after_invariant,
620                rd,
621            },
622        ) {
623            Ok(result) => Some(result),
624            Err(err) => {
625                trace!(target: "forge::test", %err, "failed to recompute shrunk replay metrics");
626                None
627            }
628        }
629    };
630
631    Ok(ShrunkSequence { calls: shrunk, result })
632}
633
634/// Replays `sequence` (indices into `calls`) against `executor`. When
635/// `accumulate_warp_roll` is set, warp/roll from skipped calls is folded into the next
636/// included call. `on_call` may stop after each campaign-faithful replay transition.
637fn replay_sequence<FEN, T, F>(
638    executor: &mut Executor<FEN>,
639    calls: &[BasicTxDetails],
640    sequence: &[usize],
641    accumulate_warp_roll: bool,
642    mut on_call: F,
643) -> eyre::Result<Option<T>>
644where
645    FEN: FoundryEvmNetwork,
646    F: FnMut(usize, RawCallResult<FEN>) -> eyre::Result<ReplayDecision<T>>,
647{
648    // Fast path: no warp/roll accumulation → iterate only kept indices (O(k)) and pass
649    // `&calls[idx]` directly to skip the per-call `BasicTxDetails` clone.
650    if !accumulate_warp_roll {
651        for &idx in sequence {
652            let (_, call_result) = execute_invariant_replay_tx(executor, &calls[idx])?;
653            match on_call(idx, call_result)? {
654                ReplayDecision::Stop(val) => return Ok(Some(val)),
655                ReplayDecision::Continue => {}
656            }
657        }
658        return Ok(None);
659    }
660
661    // Accumulating path: must scan the full `calls` so warp/roll from skipped txs lands on
662    // the next kept tx as a concrete delta.
663    let mut accumulated_warp = U256::ZERO;
664    let mut accumulated_roll = U256::ZERO;
665    let mut seq_iter = sequence.iter().peekable();
666
667    for (idx, tx) in calls.iter().enumerate() {
668        accumulated_warp += tx.warp.unwrap_or(U256::ZERO);
669        accumulated_roll += tx.roll.unwrap_or(U256::ZERO);
670        if seq_iter.peek() != Some(&&idx) {
671            continue;
672        }
673        seq_iter.next();
674
675        let executed = apply_warp_roll(tx.clone(), accumulated_warp, accumulated_roll);
676        let (_, call_result) = execute_invariant_replay_tx(executor, &executed)?;
677
678        match on_call(idx, call_result)? {
679            ReplayDecision::Stop(val) => return Ok(Some(val)),
680            ReplayDecision::Continue => {}
681        }
682
683        accumulated_warp = U256::ZERO;
684        accumulated_roll = U256::ZERO;
685    }
686
687    Ok(None)
688}
689
690/// Checks if the given call sequence breaks the invariant.
691///
692/// Used in shrinking phase for checking candidate sequences and in replay failures phase to test
693/// persisted failures.
694/// Returns the result of invariant check (and afterInvariant call if needed) and if sequence was
695/// entirely applied, plus the concrete failure site when replay fails.
696///
697/// When `options.accumulate_warp_roll` is enabled, warp/roll from removed calls is folded into the
698/// next kept call so the candidate sequence stays representable as a concrete counterexample.
699pub fn check_sequence<FEN: FoundryEvmNetwork>(
700    mut executor: Executor<FEN>,
701    calls: &[BasicTxDetails],
702    sequence: &[usize],
703    test_address: Address,
704    calldata: Bytes,
705    options: CheckSequenceOptions<'_>,
706) -> eyre::Result<CheckSequenceOutcome> {
707    let mut calls_executed = 0;
708    let mut reverts = 0;
709    let early = replay_sequence(
710        &mut executor,
711        calls,
712        sequence,
713        options.accumulate_warp_roll,
714        |idx, call_result| {
715            calls_executed += 1;
716            // Ignore calls reverted with `MAGIC_ASSUME`. This is needed to handle failed
717            // scenarios that are replayed with a modified version of test driver (that use
718            // new `vm.assume` cheatcodes).
719            if call_result.result.as_ref() == MAGIC_ASSUME {
720                return Ok(ReplayDecision::Continue);
721            }
722            if call_result.reverted {
723                reverts += 1;
724            }
725            if did_fail_on_assert(&call_result, &call_result.state_changeset) {
726                let call = &calls[idx];
727                let target = call.call_details.target;
728                let selector = selector_from_calldata(&call.call_details.calldata);
729                let fingerprint = handler_edge_fingerprint(
730                    snapshot_edge_fingerprint(&call_result),
731                    target,
732                    selector,
733                );
734                let sequence_reverter = call_result.reverter.or(Some(target));
735                let site = CheckSequenceFailureSite::SequenceCall { target, selector, fingerprint };
736                return Ok(ReplayDecision::Stop(CheckSequenceOutcome {
737                    success: false,
738                    replayed_entirely: false,
739                    reason: assertion_failure_reason(call_result, options.rd),
740                    calls_count: calls_executed,
741                    reverts,
742                    failure_site: Some(site),
743                    sequence_assertion_failure: true,
744                    sequence_reverter,
745                }));
746            }
747            if call_result.reverted && options.fail_on_revert {
748                if options.expect_assertion_failure {
749                    return Ok(ReplayDecision::Stop(CheckSequenceOutcome {
750                        success: true,
751                        replayed_entirely: false,
752                        reason: None,
753                        calls_count: calls_executed,
754                        reverts,
755                        failure_site: None,
756                        sequence_assertion_failure: false,
757                        sequence_reverter: None,
758                    }));
759                }
760                let site = sequence_call_failure_site(&calls[idx], &call_result);
761                return Ok(ReplayDecision::Stop(CheckSequenceOutcome {
762                    success: false,
763                    replayed_entirely: false,
764                    reason: call_failure_reason(call_result, options.rd),
765                    calls_count: calls_executed,
766                    reverts,
767                    failure_site: Some(site),
768                    sequence_assertion_failure: false,
769                    sequence_reverter: None,
770                }));
771            }
772            Ok(ReplayDecision::Continue)
773        },
774    )?;
775    if let Some(result) = early {
776        return Ok(result);
777    }
778
779    // Unlike optimization mode we intentionally do not apply trailing warp/roll before the
780    // invariant call: those delays would not be representable in the final shrunk sequence.
781    let (success, replayed_entirely, reason, failure_site) =
782        finish_sequence_check(&executor, test_address, calldata, &options)?;
783    Ok(CheckSequenceOutcome {
784        success,
785        replayed_entirely,
786        reason,
787        calls_count: calls_executed,
788        reverts,
789        failure_site,
790        sequence_assertion_failure: false,
791        sequence_reverter: None,
792    })
793}
794
795fn finish_sequence_check<FEN: FoundryEvmNetwork>(
796    executor: &Executor<FEN>,
797    test_address: Address,
798    calldata: Bytes,
799    options: &CheckSequenceOptions<'_>,
800) -> eyre::Result<(bool, bool, Option<String>, Option<CheckSequenceFailureSite>)> {
801    let handle_terminal_failure =
802        |call_result: RawCallResult<FEN>, site_kind: TerminalFailureSite| {
803            let should_ignore_failure = options.expect_assertion_failure
804                && !executor.has_global_failure(&call_result.state_changeset)
805                && !did_fail_on_assert(&call_result, &call_result.state_changeset);
806
807            if should_ignore_failure {
808                return (true, true, None, None);
809            }
810
811            let site = terminal_failure_site(site_kind, test_address, &calldata, &call_result);
812            let reason = if options.expect_assertion_failure {
813                assertion_failure_reason(call_result, options.rd)
814            } else {
815                call_failure_reason(call_result, options.rd)
816            };
817
818            (false, true, reason, Some(site))
819        };
820
821    let (invariant_result, mut success) =
822        call_invariant_function(executor, test_address, calldata.clone())?;
823    if !success {
824        return Ok(handle_terminal_failure(invariant_result, TerminalFailureSite::Invariant));
825    }
826
827    // Check after invariant result if invariant is success and `afterInvariant` function is
828    // declared.
829    if success && options.call_after_invariant {
830        let (after_invariant_result, after_invariant_success) =
831            call_after_invariant_function(executor, test_address)?;
832        success = after_invariant_success;
833        if !success {
834            return Ok(handle_terminal_failure(
835                after_invariant_result,
836                TerminalFailureSite::AfterInvariant,
837            ));
838        }
839    }
840
841    Ok((success, true, None, None))
842}
843
844#[derive(Clone, Copy)]
845enum TerminalFailureSite {
846    Invariant,
847    AfterInvariant,
848}
849
850fn sequence_call_failure_site<FEN: FoundryEvmNetwork>(
851    call: &BasicTxDetails,
852    call_result: &RawCallResult<FEN>,
853) -> CheckSequenceFailureSite {
854    let target = call_result.reverter.unwrap_or(call.call_details.target);
855    let selector = selector_from_calldata(&call.call_details.calldata);
856    let fingerprint =
857        handler_edge_fingerprint(snapshot_edge_fingerprint(call_result), target, selector);
858    CheckSequenceFailureSite::SequenceCall { target, selector, fingerprint }
859}
860
861fn terminal_failure_site<FEN: FoundryEvmNetwork>(
862    kind: TerminalFailureSite,
863    target: Address,
864    calldata: &Bytes,
865    call_result: &RawCallResult<FEN>,
866) -> CheckSequenceFailureSite {
867    let target = call_result.reverter.unwrap_or(target);
868    let selector = match kind {
869        TerminalFailureSite::Invariant => selector_from_calldata(calldata),
870        TerminalFailureSite::AfterInvariant => {
871            Selector::from(IInvariantTest::afterInvariantCall::SELECTOR)
872        }
873    };
874    let fingerprint =
875        handler_edge_fingerprint(snapshot_edge_fingerprint(call_result), target, selector);
876    match kind {
877        TerminalFailureSite::Invariant => {
878            CheckSequenceFailureSite::Invariant { target, selector, fingerprint }
879        }
880        TerminalFailureSite::AfterInvariant => {
881            CheckSequenceFailureSite::AfterInvariant { target, selector, fingerprint }
882        }
883    }
884}
885
886fn selector_from_calldata(calldata: &Bytes) -> Selector {
887    let selector: [u8; 4] = calldata.get(..4).and_then(|s| s.try_into().ok()).unwrap_or_default();
888    Selector::from(selector)
889}
890
891fn call_failure_reason<FEN: FoundryEvmNetwork>(
892    call_result: RawCallResult<FEN>,
893    rd: Option<&RevertDecoder>,
894) -> Option<String> {
895    match call_result.into_evm_error(rd) {
896        EvmError::Execution(err) => Some(err.reason),
897        _ => None,
898    }
899}
900
901fn assertion_failure_reason<FEN: FoundryEvmNetwork>(
902    call_result: RawCallResult<FEN>,
903    rd: Option<&RevertDecoder>,
904) -> Option<String> {
905    call_failure_reason(call_result, rd).or_else(|| Some("assertion failed".to_string()))
906}
907
908/// Shrinks an optimization witness while preserving the best value seen during replay.
909/// Warp and roll of removed calls move to the next kept call.
910/// Returns the shrunk sequence and its value, which can exceed `target_value`.
911#[expect(clippy::too_many_arguments)]
912pub(crate) fn shrink_sequence_value<FEN: FoundryEvmNetwork>(
913    config: &InvariantConfig,
914    invariant_contract: &InvariantContract<'_>,
915    target_invariant: &Function,
916    calls: &[BasicTxDetails],
917    executor: &Executor<FEN>,
918    target_value: I256,
919    targets: &[Address],
920    progress: &ShrinkProgress<'_>,
921    early_exit: &EarlyExit,
922) -> eyre::Result<(Vec<BasicTxDetails>, I256)> {
923    trace!(target: "forge::test", "Shrinking optimization sequence of {} calls for target value {}.", calls.len(), target_value);
924
925    let target_address = invariant_contract.address;
926    let calldata: Bytes = target_invariant.selector().into();
927
928    // Candidates replay only up to their last kept call: the returned witness drops the delays of
929    // removed trailing calls, so its value must not count them.
930    // Campaigns score only after a call, so an empty witness must match the target exactly.
931    if check_sequence_value(
932        executor.clone(),
933        &[],
934        &[],
935        target_address,
936        calldata.clone(),
937        config.fail_on_revert,
938        targets,
939    )? == Some(target_value)
940    {
941        return Ok((vec![], target_value));
942    }
943
944    let replay_failed = Cell::new(false);
945    let mut replay_error = None;
946    let mut best = target_value;
947    let mut run = ShrinkRun::new(config.shrink_run_limit as usize);
948    let initial = SequenceShrink::new(calls.len());
949    progress.update(calls, &initial, true);
950    let mut sequence = Vec::with_capacity(calls.len());
951
952    let shrinker = shrink_sequence_by_removing(
953        calls.len(),
954        &mut run,
955        // Dropping a block whose calls cancel out keeps the score but can discard a call that
956        // raises it once a single removal drops the call that lowered it.
957        false,
958        || early_exit.should_stop() || replay_failed.get(),
959        || progress.inc(),
960        |shrinker| {
961            progress.update(calls, shrinker, true);
962            sequence.clear();
963            sequence.extend(shrinker.current());
964            if sequence.is_empty() {
965                return Some(false);
966            }
967            match check_sequence_value(
968                executor.clone(),
969                &calls[..=sequence[sequence.len() - 1]],
970                &sequence,
971                target_address,
972                calldata.clone(),
973                config.fail_on_revert,
974                targets,
975            ) {
976                Ok(Some(value)) if value >= best => {
977                    best = value;
978                    Some(true)
979                }
980                Ok(_) => Some(false),
981                Err(err) => {
982                    replay_error = Some(err);
983                    replay_failed.set(true);
984                    None
985                }
986            }
987        },
988    );
989    progress.update(calls, &shrinker, true);
990    if let Some(err) = replay_error {
991        return Err(err);
992    }
993
994    Ok((build_shrunk_sequence(calls, &shrinker, true), best))
995}
996
997/// Replays a handler-bug sequence and returns whether the anchor still asserts on the same
998/// path. Rejects sequences with a pre-anchor assertion (would be a different bug).
999pub fn replay_handler_failure_sequence<FEN: FoundryEvmNetwork>(
1000    mut executor: Executor<FEN>,
1001    calls: &[BasicTxDetails],
1002    sequence: &[usize],
1003    accumulate_warp_roll: bool,
1004    rd: Option<&RevertDecoder>,
1005) -> eyre::Result<HandlerReplayOutcome> {
1006    let Some(&anchor_idx) = sequence.last() else {
1007        return Ok(HandlerReplayOutcome {
1008            anchor_asserted: false,
1009            handler_target: Address::ZERO,
1010            reverter: Address::ZERO,
1011            selector: Selector::ZERO,
1012            revert_reason: None,
1013            anchor_fingerprint: B256::ZERO,
1014        });
1015    };
1016
1017    let outcome = replay_sequence(
1018        &mut executor,
1019        calls,
1020        sequence,
1021        accumulate_warp_roll,
1022        |idx, call_result| {
1023            let asserted = did_fail_on_assert(&call_result, &call_result.state_changeset);
1024            if idx == anchor_idx {
1025                let snapshot = snapshot_edge_fingerprint(&call_result);
1026                let anchor = &calls[anchor_idx];
1027                // Handler failures are identified by the top-level fuzzed call. The innermost
1028                // reverter can instead be a nested callee or the cheatcode address.
1029                let handler_target = anchor.call_details.target;
1030                let reverter = call_result.reverter.unwrap_or(handler_target);
1031                let selector_bytes: [u8; 4] = anchor
1032                    .call_details
1033                    .calldata
1034                    .get(..4)
1035                    .and_then(|s| s.try_into().ok())
1036                    .unwrap_or_default();
1037                let selector = Selector::from(selector_bytes);
1038                let fingerprint = handler_edge_fingerprint(snapshot, handler_target, selector);
1039                let reason =
1040                    if asserted { assertion_failure_reason(call_result, rd) } else { None };
1041                return Ok(ReplayDecision::Stop(HandlerReplayOutcome {
1042                    anchor_asserted: asserted,
1043                    handler_target,
1044                    reverter,
1045                    selector,
1046                    revert_reason: reason,
1047                    anchor_fingerprint: fingerprint,
1048                }));
1049            }
1050            if asserted {
1051                // Pre-anchor assertion = different bug; reject.
1052                return Ok(ReplayDecision::Stop(HandlerReplayOutcome {
1053                    anchor_asserted: false,
1054                    handler_target: Address::ZERO,
1055                    reverter: Address::ZERO,
1056                    selector: Selector::ZERO,
1057                    revert_reason: None,
1058                    anchor_fingerprint: B256::ZERO,
1059                }));
1060            }
1061            Ok(ReplayDecision::Continue)
1062        },
1063    )?;
1064
1065    Ok(outcome.unwrap_or(HandlerReplayOutcome {
1066        anchor_asserted: false,
1067        handler_target: Address::ZERO,
1068        reverter: Address::ZERO,
1069        selector: Selector::ZERO,
1070        revert_reason: None,
1071        anchor_fingerprint: B256::ZERO,
1072    }))
1073}
1074
1075/// Shrinks a handler-bug sequence to the shortest prefix that still asserts on the anchor
1076/// AND keeps the same edge fingerprint (so we don't change bug identity).
1077pub(crate) fn shrink_handler_sequence<FEN: FoundryEvmNetwork>(
1078    config: &InvariantConfig,
1079    calls: &[BasicTxDetails],
1080    expected_fingerprint: B256,
1081    executor: &Executor<FEN>,
1082    progress: &ShrinkProgress<'_>,
1083    early_exit: &EarlyExit,
1084) -> eyre::Result<Vec<BasicTxDetails>> {
1085    if calls.is_empty() {
1086        return Ok(vec![]);
1087    }
1088    let accumulate_warp_roll = config.has_delay();
1089    let mut sequence = Vec::with_capacity(calls.len());
1090    let shrinker = run_shrink_loop(
1091        config,
1092        calls,
1093        progress,
1094        accumulate_warp_roll,
1095        early_exit,
1096        ShrinkErrorPolicy::RestoreRemoved,
1097        |shrinker| {
1098            sequence.clear();
1099            sequence.extend(shrinker.current());
1100            handler_sequence_still_triggers_bug(
1101                executor.clone(),
1102                calls,
1103                &sequence,
1104                accumulate_warp_roll,
1105                expected_fingerprint,
1106            )
1107        },
1108    );
1109
1110    let shrunk = build_shrunk_sequence(calls, &shrinker, accumulate_warp_roll);
1111
1112    // Verify shrunk repro; fall back to original on any failure.
1113    sequence.clear();
1114    sequence.extend(shrinker.current());
1115    let verified = handler_sequence_still_triggers_bug(
1116        executor.clone(),
1117        calls,
1118        &sequence,
1119        accumulate_warp_roll,
1120        expected_fingerprint,
1121    )
1122    .unwrap_or(false);
1123    if verified { Ok(shrunk) } else { Ok(calls.to_vec()) }
1124}
1125
1126/// Shrink predicate: anchor asserts on the same path as the originally recorded bug.
1127fn handler_sequence_still_triggers_bug<FEN: FoundryEvmNetwork>(
1128    executor: Executor<FEN>,
1129    calls: &[BasicTxDetails],
1130    sequence: &[usize],
1131    accumulate_warp_roll: bool,
1132    expected_fingerprint: B256,
1133) -> eyre::Result<bool> {
1134    let outcome =
1135        replay_handler_failure_sequence(executor, calls, sequence, accumulate_warp_roll, None)?;
1136    Ok(outcome.anchor_asserted && outcome.anchor_fingerprint == expected_fingerprint)
1137}
1138
1139/// Executes a call sequence and returns the optimization value (int256) from the invariant
1140/// function. Used during shrinking for optimization mode.
1141///
1142/// Returns `None` if the invariant call fails or doesn't return a valid int256, or if the campaign
1143/// would not have scored the sequence or would have failed the test: a kept call is rejected by
1144/// `vm.assume`, fails an assertion, or fails its handler gate with `fail_on_revert`, or the last
1145/// kept call fails its handler gate.
1146/// Unlike `check_sequence`, this applies warp/roll from ALL calls (including removed ones).
1147///
1148/// `targets` are the campaign's fuzz targets, whose `failed()` the handler gate checks under legacy
1149/// assertions.
1150pub fn check_sequence_value<FEN: FoundryEvmNetwork>(
1151    mut executor: Executor<FEN>,
1152    calls: &[BasicTxDetails],
1153    sequence: &[usize],
1154    test_address: Address,
1155    calldata: Bytes,
1156    fail_on_revert: bool,
1157    targets: &[Address],
1158) -> eyre::Result<Option<I256>> {
1159    let mut accumulated_warp = U256::ZERO;
1160    let mut accumulated_roll = U256::ZERO;
1161    let mut seq_iter = sequence.iter().peekable();
1162    let mut last_call_scored = true;
1163    // Contracts created during a run also become targets; include the ones the sequence calls.
1164    let mut gate_targets = Vec::new();
1165    if executor.legacy_assertions() {
1166        gate_targets.extend_from_slice(targets);
1167        for tx in calls {
1168            if !gate_targets.contains(&tx.call_details.target) {
1169                gate_targets.push(tx.call_details.target);
1170            }
1171        }
1172    }
1173
1174    for (idx, tx) in calls.iter().enumerate() {
1175        accumulated_warp += tx.warp.unwrap_or(U256::ZERO);
1176        accumulated_roll += tx.roll.unwrap_or(U256::ZERO);
1177
1178        if seq_iter.peek() == Some(&&idx) {
1179            seq_iter.next();
1180
1181            let tx_with_accumulated =
1182                apply_warp_roll(tx.clone(), accumulated_warp, accumulated_roll);
1183            let (kind, call_result) =
1184                execute_invariant_replay_tx(&mut executor, &tx_with_accumulated)?;
1185            // Campaigns sample the score only after a call that passes the handler gate.
1186            last_call_scored =
1187                handlers_succeeded(&executor, test_address, &gate_targets, &call_result);
1188            // Campaigns drop rejected calls. A failed gate stops them under `fail_on_revert`, and
1189            // an assertion failure is a handler bug that fails the test in any configuration.
1190            if kind == CampaignCallKind::AssumptionRejected
1191                || (!last_call_scored
1192                    && (fail_on_revert
1193                        || did_fail_on_assert(&call_result, &call_result.state_changeset)))
1194            {
1195                return Ok(None);
1196            }
1197
1198            accumulated_warp = U256::ZERO;
1199            accumulated_roll = U256::ZERO;
1200        }
1201    }
1202    if !last_call_scored {
1203        return Ok(None);
1204    }
1205
1206    // Apply any remaining accumulated warp/roll before calling invariant.
1207    apply_block_delay(&mut executor, accumulated_warp, accumulated_roll);
1208
1209    let (inv_result, success) = call_invariant_function(&executor, test_address, calldata)?;
1210
1211    if success
1212        && inv_result.result.len() >= 32
1213        && let Some(value) = I256::try_from_be_slice(&inv_result.result[..32])
1214    {
1215        return Ok(Some(value));
1216    }
1217
1218    Ok(None)
1219}
1220
1221/// Mirrors the campaign's handler gate, which checks `failed()` on every target, and not the test
1222/// contract, under legacy assertions.
1223fn handlers_succeeded<FEN: FoundryEvmNetwork>(
1224    executor: &Executor<FEN>,
1225    test_address: Address,
1226    targets: &[Address],
1227    call_result: &RawCallResult<FEN>,
1228) -> bool {
1229    if call_result.reverted {
1230        return false;
1231    }
1232    let changeset = &call_result.state_changeset;
1233    if !executor.legacy_assertions() {
1234        return executor.is_success_handler_gate(test_address, false, Cow::Borrowed(changeset));
1235    }
1236    targets
1237        .iter()
1238        .all(|&address| executor.is_success_handler_gate(address, false, Cow::Borrowed(changeset)))
1239}
1240
1241#[cfg(test)]
1242mod tests {
1243    use super::*;
1244    use crate::executors::ExecutorBuilder;
1245    use alloy_evm::EvmEnv;
1246    use alloy_primitives::hex;
1247    use foundry_cheatcodes::CheatsConfig;
1248    use foundry_evm_core::{FoundryBlock, backend::Backend, evm::EthEvmNetwork};
1249    use foundry_evm_fuzz::CallDetails;
1250    use revm::{
1251        bytecode::Bytecode,
1252        context::{Block, TxEnv},
1253    };
1254    use std::sync::Arc;
1255
1256    fn tx(warp: Option<u64>, roll: Option<u64>) -> BasicTxDetails {
1257        BasicTxDetails {
1258            warp: warp.map(U256::from),
1259            roll: roll.map(U256::from),
1260            sender: Address::ZERO,
1261            call_details: CallDetails {
1262                target: Address::ZERO,
1263                calldata: Bytes::new(),
1264                value: None,
1265            },
1266        }
1267    }
1268
1269    fn tx_with_calldata(byte: u8) -> BasicTxDetails {
1270        BasicTxDetails {
1271            warp: None,
1272            roll: None,
1273            sender: Address::ZERO,
1274            call_details: CallDetails {
1275                target: Address::ZERO,
1276                calldata: Bytes::from(vec![byte]),
1277                value: None,
1278            },
1279        }
1280    }
1281
1282    #[test]
1283    fn build_shrunk_sequence_accumulates_removed_delay_into_next_kept_call() {
1284        let calls = vec![tx(Some(3), Some(5)), tx(Some(7), Some(11)), tx(Some(13), Some(17))];
1285        let mut shrinker = SequenceShrink::new(calls.len());
1286        shrinker.remove(0);
1287
1288        let shrunk = build_shrunk_sequence(&calls, &shrinker, true);
1289
1290        assert_eq!(shrunk.len(), 2);
1291        assert_eq!(shrunk[0].warp, Some(U256::from(10)));
1292        assert_eq!(shrunk[0].roll, Some(U256::from(16)));
1293        assert_eq!(shrunk[1].warp, Some(U256::from(13)));
1294        assert_eq!(shrunk[1].roll, Some(U256::from(17)));
1295    }
1296
1297    #[test]
1298    fn build_shrunk_sequence_does_not_move_trailing_delay_backward() {
1299        let calls = vec![tx(Some(3), Some(5)), tx(Some(7), Some(11))];
1300        let mut shrinker = SequenceShrink::new(calls.len());
1301        shrinker.remove(1);
1302
1303        let shrunk = build_shrunk_sequence(&calls, &shrinker, true);
1304
1305        assert_eq!(shrunk.len(), 1);
1306        assert_eq!(shrunk[0].warp, Some(U256::from(3)));
1307        assert_eq!(shrunk[0].roll, Some(U256::from(5)));
1308    }
1309
1310    #[test]
1311    fn check_sequence_value_applies_trailing_delay_before_invariant() {
1312        let backend = Backend::<EthEvmNetwork>::spawn(None).unwrap();
1313        let mut executor = ExecutorBuilder::default()
1314            .inspectors(|stack| stack.cheatcodes(Arc::new(CheatsConfig::default())))
1315            .gas_limit(1 << 24)
1316            .build(EvmEnv::default(), TxEnv::default(), backend, Default::default());
1317        // Returns `(block.number << 128) + block.timestamp`.
1318        let test_address = Address::repeat_byte(0x11);
1319        let code = hex!("4360801b420160005260206000f3");
1320        executor.set_code(test_address, Bytecode::new_raw(Bytes::from(code))).unwrap();
1321        let block_value =
1322            |timestamp: U256, number: U256| I256::from_raw((number << 128) + timestamp);
1323        let calls = [tx(Some(3), Some(5)), tx(Some(7), Some(11))];
1324        let block = executor.evm_env().block_env.clone();
1325
1326        // Removed calls still contribute their delays to the invariant check, whether the
1327        // cheatcode block mirrors the environment, is unset, or overrides it.
1328        let delayed =
1329            Some(block_value(block.timestamp() + U256::from(10), block.number() + U256::from(16)));
1330        let mut cheatcode_block = block;
1331        cheatcode_block.set_timestamp(U256::from(1000));
1332        cheatcode_block.set_number(U256::from(2000));
1333        let cases = [
1334            (&[0][..], None, delayed),
1335            (&[], None, delayed),
1336            (&[], Some(None), delayed),
1337            (
1338                &[],
1339                Some(Some(cheatcode_block)),
1340                Some(block_value(U256::from(1010), U256::from(2016))),
1341            ),
1342        ];
1343        for (sequence, cheatcode_block, expected) in cases {
1344            let mut executor = executor.clone();
1345            if let Some(block) = cheatcode_block {
1346                executor.inspector_mut().cheatcodes.as_mut().unwrap().block = block;
1347            }
1348            let value = check_sequence_value(
1349                executor,
1350                &calls,
1351                sequence,
1352                test_address,
1353                Bytes::new(),
1354                false,
1355                &[],
1356            )
1357            .unwrap();
1358            assert_eq!(value, expected);
1359        }
1360    }
1361
1362    #[test]
1363    fn shrink_run_counts_attempts_and_accepts() {
1364        let mut run = ShrinkRun::new(2);
1365
1366        assert_eq!(run.remaining_attempts(), 2);
1367        assert!(!run.try_candidate(|| false));
1368        assert!(run.try_candidate(|| true));
1369
1370        let mut called_after_limit = false;
1371        assert!(!run.try_candidate(|| {
1372            called_after_limit = true;
1373            true
1374        }));
1375
1376        assert!(!called_after_limit);
1377        let stats = run.finish();
1378        assert_eq!(stats.attempts, 2);
1379        assert_eq!(stats.accepted, 1);
1380    }
1381
1382    #[test]
1383    fn shrink_candidate_keys_skip_duplicates() {
1384        let mut candidates = ShrinkCandidateKeys::new("initial");
1385
1386        assert!(!candidates.insert("initial"));
1387        assert!(candidates.insert("first"));
1388        assert!(!candidates.insert("first"));
1389        assert!(candidates.insert("second"));
1390    }
1391
1392    #[test]
1393    fn shrink_loop_keep_removed_treats_candidate_error_as_still_failing() {
1394        let config = InvariantConfig { shrink_run_limit: 1, ..Default::default() };
1395        let early_exit = EarlyExit::new(false);
1396        let calls = vec![tx(None, None), tx(None, None)];
1397        let progress = ShrinkProgress::new(&config, None, "test", None, None, false);
1398
1399        let shrinker = run_shrink_loop(
1400            &config,
1401            &calls,
1402            &progress,
1403            false,
1404            &early_exit,
1405            ShrinkErrorPolicy::KeepRemoved,
1406            |_| Err(eyre::eyre!("candidate replay failed")),
1407        );
1408
1409        assert_eq!(shrinker.current().collect::<Vec<_>>(), vec![1]);
1410    }
1411
1412    #[test]
1413    fn sequence_shrinker_skips_duplicate_candidates_without_spending_attempts() {
1414        let mut run = ShrinkRun::new(2);
1415        let mut seen = Vec::new();
1416
1417        let shrinker = shrink_sequence_by_removing(
1418            2,
1419            &mut run,
1420            true,
1421            || false,
1422            || {},
1423            |shrinker| {
1424                let candidate = shrinker.current().collect::<Vec<_>>();
1425                if seen.contains(&candidate) {
1426                    None
1427                } else {
1428                    seen.push(candidate);
1429                    Some(false)
1430                }
1431            },
1432        );
1433
1434        assert_eq!(shrinker.current().collect::<Vec<_>>(), vec![0, 1]);
1435        let stats = run.finish();
1436        assert_eq!(stats.attempts, 2);
1437        assert_eq!(stats.accepted, 0);
1438    }
1439
1440    #[test]
1441    fn shrink_progress_message_renders_current_sequence() {
1442        let calls = vec![tx_with_calldata(1), tx_with_calldata(2)];
1443
1444        let message =
1445            format_shrink_progress_message(" Shrink: invariant_live", &calls, None, false);
1446
1447        assert!(message.contains(" Shrink: invariant_live"));
1448        assert!(message.contains("[Sequence] (shrunk: 2)"));
1449        assert!(message.contains("calldata=0x01 args=[]"));
1450        assert!(message.contains("calldata=0x02 args=[]"));
1451    }
1452
1453    #[test]
1454    fn shrink_progress_message_omits_middle_of_large_sequence() {
1455        let calls = (0..(LIVE_SHRINK_SEQUENCE_EDGE_CALLS * 2 + 3))
1456            .map(|idx| tx_with_calldata(idx as u8))
1457            .collect::<Vec<_>>();
1458
1459        let message =
1460            format_shrink_progress_message(" Shrink: invariant_live", &calls, None, false);
1461
1462        assert!(message.contains("[Sequence] (shrunk: 35)"));
1463        assert!(message.contains("... 3 call(s) omitted ..."));
1464        assert_eq!(message.matches("sender=").count(), LIVE_SHRINK_SEQUENCE_EDGE_CALLS * 2);
1465        assert!(message.contains("calldata=0x00 args=[]"));
1466        assert!(message.contains("calldata=0x22 args=[]"));
1467    }
1468}