Skip to main content

foundry_evm/executors/invariant/
shrink.rs

1use crate::executors::{
2    EarlyExit, EvmError, Executor, RawCallResult,
3    invariant::{
4        call_after_invariant_function, call_invariant_function,
5        error::{handler_edge_fingerprint, snapshot_edge_fingerprint},
6        execute_tx,
7        result::did_fail_on_assert,
8    },
9};
10use alloy_json_abi::Function;
11use alloy_primitives::{Address, B256, Bytes, I256, Selector, U256};
12use foundry_config::InvariantConfig;
13use foundry_evm_core::{
14    FoundryBlock, constants::MAGIC_ASSUME, decode::RevertDecoder, evm::FoundryEvmNetwork,
15};
16use foundry_evm_fuzz::{BasicTxDetails, invariant::InvariantContract};
17use indicatif::ProgressBar;
18use proptest::bits::{BitSetLike, VarBitSet};
19use revm::context::Block;
20
21/// Shrinker for a call sequence failure.
22/// Iterates sequence call sequence top down and removes calls one by one.
23/// If the failure is still reproducible with removed call then moves to the next one.
24/// If the failure is not reproducible then restore removed call and moves to next one.
25#[derive(Debug)]
26struct CallSequenceShrinker {
27    /// Length of call sequence to be shrunk.
28    call_sequence_len: usize,
29    /// Call ids contained in current shrunk sequence.
30    included_calls: VarBitSet,
31}
32
33impl CallSequenceShrinker {
34    fn new(call_sequence_len: usize) -> Self {
35        Self { call_sequence_len, included_calls: VarBitSet::saturated(call_sequence_len) }
36    }
37
38    /// Return candidate shrink sequence to be tested, by removing ids from original sequence.
39    fn current(&self) -> impl Iterator<Item = usize> + '_ {
40        (0..self.call_sequence_len).filter(|&call_id| self.included_calls.test(call_id))
41    }
42
43    /// Advance to the next call index, wrapping around to 0 at the end.
44    const fn next_index(&self, call_idx: usize) -> usize {
45        if call_idx + 1 == self.call_sequence_len { 0 } else { call_idx + 1 }
46    }
47}
48
49/// How `run_shrink_loop` handles a predicate error.
50#[derive(Clone, Copy)]
51enum ShrinkErrorPolicy {
52    /// "Bug still present" — keep the call removed (legacy `shrink_sequence` behavior).
53    KeepRemoved,
54    /// "Bug gone" — restore the call. Used by handler shrink so a replay error never
55    /// produces a sequence that no longer reproduces the anchor.
56    RestoreRemoved,
57}
58
59/// Per-call decision returned by callbacks driving `replay_sequence`. `Continue` hands
60/// the result back so non-reverted calls auto-commit; `Stop` short-circuits.
61#[expect(clippy::large_enum_variant)]
62enum ReplayDecision<T, FEN: FoundryEvmNetwork> {
63    Stop(T),
64    Continue(RawCallResult<FEN>),
65}
66
67/// Options controlling how `check_sequence` evaluates a candidate call sequence.
68pub struct CheckSequenceOptions<'a> {
69    pub accumulate_warp_roll: bool,
70    pub fail_on_revert: bool,
71    pub expect_assertion_failure: bool,
72    pub call_after_invariant: bool,
73    pub rd: Option<&'a RevertDecoder>,
74}
75
76/// Result of a strict handler-bug replay: anchor asserts, no earlier call asserts, and the
77/// recomputed edge fingerprint identifies which path the assertion took.
78#[derive(Debug)]
79pub struct HandlerReplayOutcome {
80    pub anchor_asserted: bool,
81    pub revert_reason: Option<String>,
82    /// Normalized via `handler_edge_fingerprint` so callers can compare directly.
83    pub anchor_fingerprint: B256,
84}
85
86/// Resets the progress bar before each shrink. `position = Some((i, N))` renders
87/// `[i/N] Shrink: <label>` for multi-invariant campaigns.
88pub(crate) fn reset_shrink_progress(
89    config: &InvariantConfig,
90    progress: Option<&ProgressBar>,
91    label: &str,
92    position: Option<(usize, usize)>,
93) {
94    if let Some(progress) = progress {
95        progress.set_length(config.shrink_run_limit as u64);
96        progress.reset();
97        let message = match position {
98            Some((current, total)) if total > 1 => {
99                format!(" [{current}/{total}] Shrink: {label}")
100            }
101            _ => format!(" Shrink: {label}"),
102        };
103        progress.set_message(message);
104    }
105}
106
107/// Applies accumulated warp/roll to a call, returning a modified copy.
108fn apply_warp_roll(call: &BasicTxDetails, warp: U256, roll: U256) -> BasicTxDetails {
109    let mut result = call.clone();
110    if warp > U256::ZERO {
111        result.warp = Some(warp);
112    }
113    if roll > U256::ZERO {
114        result.roll = Some(roll);
115    }
116    result
117}
118
119/// Applies warp/roll adjustments directly to the executor's environment.
120fn apply_warp_roll_to_env<FEN: FoundryEvmNetwork>(
121    executor: &mut Executor<FEN>,
122    warp: U256,
123    roll: U256,
124) {
125    if warp > U256::ZERO || roll > U256::ZERO {
126        let ts = executor.evm_env().block_env.timestamp();
127        let num = executor.evm_env().block_env.number();
128        executor.evm_env_mut().block_env.set_timestamp(ts + warp);
129        executor.evm_env_mut().block_env.set_number(num + roll);
130
131        let block_env = executor.evm_env().block_env.clone();
132        if let Some(cheatcodes) = executor.inspector_mut().cheatcodes.as_mut() {
133            if let Some(block) = cheatcodes.block.as_mut() {
134                let bts = block.timestamp();
135                let bnum = block.number();
136                block.set_timestamp(bts + warp);
137                block.set_number(bnum + roll);
138            } else {
139                cheatcodes.block = Some(block_env);
140            }
141        }
142    }
143}
144
145/// Builds the final shrunk sequence from the shrinker state.
146///
147/// When `accumulate_warp_roll` is enabled, warp/roll from removed calls is folded into the next
148/// kept call so the final sequence remains reproducible.
149fn build_shrunk_sequence(
150    calls: &[BasicTxDetails],
151    shrinker: &CallSequenceShrinker,
152    accumulate_warp_roll: bool,
153) -> Vec<BasicTxDetails> {
154    if !accumulate_warp_roll {
155        return shrinker.current().map(|idx| calls[idx].clone()).collect();
156    }
157
158    let mut result = Vec::new();
159    let mut accumulated_warp = U256::ZERO;
160    let mut accumulated_roll = U256::ZERO;
161
162    for (idx, call) in calls.iter().enumerate() {
163        accumulated_warp += call.warp.unwrap_or(U256::ZERO);
164        accumulated_roll += call.roll.unwrap_or(U256::ZERO);
165
166        if shrinker.included_calls.test(idx) {
167            result.push(apply_warp_roll(call, accumulated_warp, accumulated_roll));
168            accumulated_warp = U256::ZERO;
169            accumulated_roll = U256::ZERO;
170        }
171    }
172
173    result
174}
175
176/// Shared shrink loop driver. Tries to drop each call; `predicate` returns whether the
177/// candidate still triggers the bug.
178fn run_shrink_loop<P>(
179    config: &InvariantConfig,
180    calls_len: usize,
181    progress: Option<&ProgressBar>,
182    early_exit: &EarlyExit,
183    error_policy: ShrinkErrorPolicy,
184    mut predicate: P,
185) -> CallSequenceShrinker
186where
187    P: FnMut(&CallSequenceShrinker) -> eyre::Result<bool>,
188{
189    let mut shrinker = CallSequenceShrinker::new(calls_len);
190    let mut call_idx = 0;
191
192    for _ in 0..config.shrink_run_limit {
193        if early_exit.should_stop() {
194            break;
195        }
196
197        // Already-removed indices have nothing to drop.
198        if !shrinker.included_calls.test(call_idx) {
199            call_idx = shrinker.next_index(call_idx);
200            continue;
201        }
202
203        shrinker.included_calls.clear(call_idx);
204
205        let bug_still_present = match predicate(&shrinker) {
206            Ok(b) => b,
207            Err(_) => matches!(error_policy, ShrinkErrorPolicy::KeepRemoved),
208        };
209        if bug_still_present {
210            if shrinker.included_calls.count() == 1 {
211                break;
212            }
213        } else {
214            shrinker.included_calls.set(call_idx);
215        }
216
217        if let Some(progress) = progress {
218            progress.inc(1);
219        }
220        call_idx = shrinker.next_index(call_idx);
221    }
222
223    shrinker
224}
225
226#[expect(clippy::too_many_arguments)]
227pub(crate) fn shrink_sequence<FEN: FoundryEvmNetwork>(
228    config: &InvariantConfig,
229    invariant_contract: &InvariantContract<'_>,
230    target_invariant: &Function,
231    calls: &[BasicTxDetails],
232    expect_assertion_failure: bool,
233    executor: &Executor<FEN>,
234    progress: Option<&ProgressBar>,
235    early_exit: &EarlyExit,
236) -> eyre::Result<Vec<BasicTxDetails>> {
237    trace!(target: "forge::test", "Shrinking sequence of {} calls.", calls.len());
238
239    let target_address = invariant_contract.address;
240    let calldata: Bytes = target_invariant.selector().to_vec().into();
241    // Special case test: the invariant is *unsatisfiable* - it took 0 calls to
242    // break the invariant -- consider emitting a warning.
243    let (_, success) = call_invariant_function(executor, target_address, calldata.clone())?;
244    if !success {
245        return Ok(vec![]);
246    }
247
248    let accumulate_warp_roll = config.has_delay();
249    let shrinker = run_shrink_loop(
250        config,
251        calls.len(),
252        progress,
253        early_exit,
254        // Preserve legacy invariant-shrink behavior: errors during candidate evaluation
255        // do not roll back the removal.
256        ShrinkErrorPolicy::KeepRemoved,
257        |shrinker| {
258            let (success, _, _) = check_sequence(
259                executor.clone(),
260                calls,
261                shrinker.current().collect(),
262                target_address,
263                calldata.clone(),
264                CheckSequenceOptions {
265                    accumulate_warp_roll,
266                    fail_on_revert: config.fail_on_revert,
267                    expect_assertion_failure,
268                    call_after_invariant: invariant_contract.call_after_invariant,
269                    rd: None,
270                },
271            )?;
272            // Bug still present iff the invariant predicate did not pass.
273            Ok(!success)
274        },
275    );
276
277    Ok(build_shrunk_sequence(calls, &shrinker, accumulate_warp_roll))
278}
279
280/// Replays `sequence` (indices into `calls`) against `executor`. When
281/// `accumulate_warp_roll` is set, warp/roll from skipped calls is folded into the next
282/// included call. `on_call` may stop early; otherwise non-reverted calls are committed.
283fn replay_sequence<FEN, T, F>(
284    executor: &mut Executor<FEN>,
285    calls: &[BasicTxDetails],
286    sequence: &[usize],
287    accumulate_warp_roll: bool,
288    mut on_call: F,
289) -> eyre::Result<Option<T>>
290where
291    FEN: FoundryEvmNetwork,
292    F: FnMut(usize, RawCallResult<FEN>) -> eyre::Result<ReplayDecision<T, FEN>>,
293{
294    // Fast path: no warp/roll accumulation → iterate only kept indices (O(k)) and pass
295    // `&calls[idx]` directly to skip the per-call `BasicTxDetails` clone.
296    if !accumulate_warp_roll {
297        for &idx in sequence {
298            let call_result = execute_tx(executor, &calls[idx])?;
299            match on_call(idx, call_result)? {
300                ReplayDecision::Stop(val) => return Ok(Some(val)),
301                ReplayDecision::Continue(mut call_result) => {
302                    if !call_result.reverted {
303                        executor.commit(&mut call_result);
304                    }
305                }
306            }
307        }
308        return Ok(None);
309    }
310
311    // Accumulating path: must scan the full `calls` so warp/roll from skipped txs lands on
312    // the next kept tx as a concrete delta.
313    let mut accumulated_warp = U256::ZERO;
314    let mut accumulated_roll = U256::ZERO;
315    let mut seq_iter = sequence.iter().peekable();
316
317    for (idx, tx) in calls.iter().enumerate() {
318        accumulated_warp += tx.warp.unwrap_or(U256::ZERO);
319        accumulated_roll += tx.roll.unwrap_or(U256::ZERO);
320        if seq_iter.peek() != Some(&&idx) {
321            continue;
322        }
323        seq_iter.next();
324
325        let executed = apply_warp_roll(tx, accumulated_warp, accumulated_roll);
326        let call_result = execute_tx(executor, &executed)?;
327
328        match on_call(idx, call_result)? {
329            ReplayDecision::Stop(val) => return Ok(Some(val)),
330            ReplayDecision::Continue(mut call_result) => {
331                if !call_result.reverted {
332                    executor.commit(&mut call_result);
333                }
334            }
335        }
336
337        accumulated_warp = U256::ZERO;
338        accumulated_roll = U256::ZERO;
339    }
340
341    Ok(None)
342}
343
344/// Checks if the given call sequence breaks the invariant.
345///
346/// Used in shrinking phase for checking candidate sequences and in replay failures phase to test
347/// persisted failures.
348/// Returns the result of invariant check (and afterInvariant call if needed) and if sequence was
349/// entirely applied.
350///
351/// When `options.accumulate_warp_roll` is enabled, warp/roll from removed calls is folded into the
352/// next kept call so the candidate sequence stays representable as a concrete counterexample.
353pub fn check_sequence<FEN: FoundryEvmNetwork>(
354    mut executor: Executor<FEN>,
355    calls: &[BasicTxDetails],
356    sequence: Vec<usize>,
357    test_address: Address,
358    calldata: Bytes,
359    options: CheckSequenceOptions<'_>,
360) -> eyre::Result<(bool, bool, Option<String>)> {
361    let early = replay_sequence(
362        &mut executor,
363        calls,
364        &sequence,
365        options.accumulate_warp_roll,
366        |_idx, call_result| {
367            // Ignore calls reverted with `MAGIC_ASSUME`. This is needed to handle failed
368            // scenarios that are replayed with a modified version of test driver (that use
369            // new `vm.assume` cheatcodes).
370            if call_result.result.as_ref() == MAGIC_ASSUME {
371                return Ok(ReplayDecision::Continue(call_result));
372            }
373            if did_fail_on_assert(&call_result, &call_result.state_changeset) {
374                return Ok(ReplayDecision::Stop((
375                    false,
376                    false,
377                    assertion_failure_reason(call_result, options.rd),
378                )));
379            }
380            if call_result.reverted && options.fail_on_revert {
381                if options.expect_assertion_failure {
382                    return Ok(ReplayDecision::Stop((true, false, None)));
383                }
384                return Ok(ReplayDecision::Stop((
385                    false,
386                    false,
387                    call_failure_reason(call_result, options.rd),
388                )));
389            }
390            Ok(ReplayDecision::Continue(call_result))
391        },
392    )?;
393    if let Some(result) = early {
394        return Ok(result);
395    }
396
397    // Unlike optimization mode we intentionally do not apply trailing warp/roll before the
398    // invariant call: those delays would not be representable in the final shrunk sequence.
399    finish_sequence_check(&executor, test_address, calldata, &options)
400}
401
402fn finish_sequence_check<FEN: FoundryEvmNetwork>(
403    executor: &Executor<FEN>,
404    test_address: Address,
405    calldata: Bytes,
406    options: &CheckSequenceOptions<'_>,
407) -> eyre::Result<(bool, bool, Option<String>)> {
408    let handle_terminal_failure = |call_result: RawCallResult<FEN>| {
409        let should_ignore_failure = options.expect_assertion_failure
410            && !executor.has_global_failure(&call_result.state_changeset)
411            && !did_fail_on_assert(&call_result, &call_result.state_changeset);
412
413        if should_ignore_failure {
414            return (true, true, None);
415        }
416
417        let reason = if options.expect_assertion_failure {
418            assertion_failure_reason(call_result, options.rd)
419        } else {
420            call_failure_reason(call_result, options.rd)
421        };
422
423        (false, true, reason)
424    };
425
426    let (invariant_result, mut success) =
427        call_invariant_function(executor, test_address, calldata)?;
428    if !success {
429        return Ok(handle_terminal_failure(invariant_result));
430    }
431
432    // Check after invariant result if invariant is success and `afterInvariant` function is
433    // declared.
434    if success && options.call_after_invariant {
435        let (after_invariant_result, after_invariant_success) =
436            call_after_invariant_function(executor, test_address)?;
437        success = after_invariant_success;
438        if !success {
439            return Ok(handle_terminal_failure(after_invariant_result));
440        }
441    }
442
443    Ok((success, true, None))
444}
445
446fn call_failure_reason<FEN: FoundryEvmNetwork>(
447    call_result: RawCallResult<FEN>,
448    rd: Option<&RevertDecoder>,
449) -> Option<String> {
450    match call_result.into_evm_error(rd) {
451        EvmError::Execution(err) => Some(err.reason),
452        _ => None,
453    }
454}
455
456fn assertion_failure_reason<FEN: FoundryEvmNetwork>(
457    call_result: RawCallResult<FEN>,
458    rd: Option<&RevertDecoder>,
459) -> Option<String> {
460    call_failure_reason(call_result, rd).or_else(|| Some("assertion failed".to_string()))
461}
462
463/// Shrinks a call sequence to the shortest sequence that still produces the target optimization
464/// value. This is specifically for optimization mode where we want to find the minimal sequence
465/// that achieves the maximum value.
466///
467/// Unlike `shrink_sequence` (for check mode), this function:
468/// - Accumulates warp/roll values from removed calls into the next kept call
469/// - Checks for target value equality rather than invariant failure
470#[expect(clippy::too_many_arguments)]
471pub(crate) fn shrink_sequence_value<FEN: FoundryEvmNetwork>(
472    config: &InvariantConfig,
473    invariant_contract: &InvariantContract<'_>,
474    target_invariant: &Function,
475    calls: &[BasicTxDetails],
476    executor: &Executor<FEN>,
477    target_value: I256,
478    progress: Option<&ProgressBar>,
479    early_exit: &EarlyExit,
480) -> eyre::Result<Vec<BasicTxDetails>> {
481    trace!(target: "forge::test", "Shrinking optimization sequence of {} calls for target value {}.", calls.len(), target_value);
482
483    let target_address = invariant_contract.address;
484    let calldata: Bytes = target_invariant.selector().to_vec().into();
485
486    // Special case: check if target value is achieved with 0 calls.
487    if check_sequence_value(executor.clone(), calls, vec![], target_address, calldata.clone())?
488        == Some(target_value)
489    {
490        return Ok(vec![]);
491    }
492
493    let mut call_idx = 0;
494    let mut shrinker = CallSequenceShrinker::new(calls.len());
495
496    for _ in 0..config.shrink_run_limit {
497        if early_exit.should_stop() {
498            break;
499        }
500
501        shrinker.included_calls.clear(call_idx);
502
503        let keeps_target = check_sequence_value(
504            executor.clone(),
505            calls,
506            shrinker.current().collect(),
507            target_address,
508            calldata.clone(),
509        )? == Some(target_value);
510
511        if keeps_target {
512            if shrinker.included_calls.count() == 1 {
513                break;
514            }
515        } else {
516            shrinker.included_calls.set(call_idx);
517        }
518
519        if let Some(progress) = progress {
520            progress.inc(1);
521        }
522
523        call_idx = shrinker.next_index(call_idx);
524    }
525
526    Ok(build_shrunk_sequence(calls, &shrinker, true))
527}
528
529/// Replays a handler-bug sequence and returns whether the anchor still asserts on the same
530/// path. Rejects sequences with a pre-anchor assertion (would be a different bug).
531pub fn replay_handler_failure_sequence<FEN: FoundryEvmNetwork>(
532    mut executor: Executor<FEN>,
533    calls: &[BasicTxDetails],
534    sequence: Vec<usize>,
535    accumulate_warp_roll: bool,
536    rd: Option<&RevertDecoder>,
537) -> eyre::Result<HandlerReplayOutcome> {
538    let Some(&anchor_idx) = sequence.last() else {
539        return Ok(HandlerReplayOutcome {
540            anchor_asserted: false,
541            revert_reason: None,
542            anchor_fingerprint: B256::ZERO,
543        });
544    };
545
546    let outcome = replay_sequence(
547        &mut executor,
548        calls,
549        &sequence,
550        accumulate_warp_roll,
551        |idx, call_result| {
552            let asserted = did_fail_on_assert(&call_result, &call_result.state_changeset);
553            if idx == anchor_idx {
554                let snapshot = snapshot_edge_fingerprint(&call_result);
555                let anchor = &calls[anchor_idx];
556                let reverter = anchor.call_details.target;
557                let selector_bytes: [u8; 4] = anchor
558                    .call_details
559                    .calldata
560                    .get(..4)
561                    .and_then(|s| s.try_into().ok())
562                    .unwrap_or_default();
563                let selector = Selector::from(selector_bytes);
564                let fingerprint = handler_edge_fingerprint(snapshot, reverter, selector);
565                let reason =
566                    if asserted { assertion_failure_reason(call_result, rd) } else { None };
567                return Ok(ReplayDecision::Stop(HandlerReplayOutcome {
568                    anchor_asserted: asserted,
569                    revert_reason: reason,
570                    anchor_fingerprint: fingerprint,
571                }));
572            }
573            if asserted {
574                // Pre-anchor assertion = different bug; reject.
575                return Ok(ReplayDecision::Stop(HandlerReplayOutcome {
576                    anchor_asserted: false,
577                    revert_reason: None,
578                    anchor_fingerprint: B256::ZERO,
579                }));
580            }
581            Ok(ReplayDecision::Continue(call_result))
582        },
583    )?;
584
585    Ok(outcome.unwrap_or(HandlerReplayOutcome {
586        anchor_asserted: false,
587        revert_reason: None,
588        anchor_fingerprint: B256::ZERO,
589    }))
590}
591
592/// Shrinks a handler-bug sequence to the shortest prefix that still asserts on the anchor
593/// AND keeps the same edge fingerprint (so we don't change bug identity).
594pub(crate) fn shrink_handler_sequence<FEN: FoundryEvmNetwork>(
595    config: &InvariantConfig,
596    calls: &[BasicTxDetails],
597    expected_fingerprint: B256,
598    executor: &Executor<FEN>,
599    progress: Option<&ProgressBar>,
600    early_exit: &EarlyExit,
601) -> eyre::Result<Vec<BasicTxDetails>> {
602    if calls.is_empty() {
603        return Ok(vec![]);
604    }
605    let accumulate_warp_roll = config.has_delay();
606    let shrinker = run_shrink_loop(
607        config,
608        calls.len(),
609        progress,
610        early_exit,
611        ShrinkErrorPolicy::RestoreRemoved,
612        |shrinker| {
613            handler_sequence_still_triggers_bug(
614                executor.clone(),
615                calls,
616                shrinker.current().collect(),
617                accumulate_warp_roll,
618                expected_fingerprint,
619            )
620        },
621    );
622
623    let shrunk = build_shrunk_sequence(calls, &shrinker, accumulate_warp_roll);
624
625    // Verify shrunk repro; fall back to original on any failure.
626    let verified = handler_sequence_still_triggers_bug(
627        executor.clone(),
628        calls,
629        shrinker.current().collect(),
630        accumulate_warp_roll,
631        expected_fingerprint,
632    )
633    .unwrap_or(false);
634    if verified { Ok(shrunk) } else { Ok(calls.to_vec()) }
635}
636
637/// Shrink predicate: anchor asserts on the same path as the originally recorded bug.
638fn handler_sequence_still_triggers_bug<FEN: FoundryEvmNetwork>(
639    executor: Executor<FEN>,
640    calls: &[BasicTxDetails],
641    sequence: Vec<usize>,
642    accumulate_warp_roll: bool,
643    expected_fingerprint: B256,
644) -> eyre::Result<bool> {
645    let outcome =
646        replay_handler_failure_sequence(executor, calls, sequence, accumulate_warp_roll, None)?;
647    Ok(outcome.anchor_asserted && outcome.anchor_fingerprint == expected_fingerprint)
648}
649
650/// Executes a call sequence and returns the optimization value (int256) from the invariant
651/// function. Used during shrinking for optimization mode.
652///
653/// Returns `None` if the invariant call fails or doesn't return a valid int256.
654/// Unlike `check_sequence`, this applies warp/roll from ALL calls (including removed ones).
655pub fn check_sequence_value<FEN: FoundryEvmNetwork>(
656    mut executor: Executor<FEN>,
657    calls: &[BasicTxDetails],
658    sequence: Vec<usize>,
659    test_address: Address,
660    calldata: Bytes,
661) -> eyre::Result<Option<I256>> {
662    let mut accumulated_warp = U256::ZERO;
663    let mut accumulated_roll = U256::ZERO;
664    let mut seq_iter = sequence.iter().peekable();
665
666    for (idx, tx) in calls.iter().enumerate() {
667        accumulated_warp += tx.warp.unwrap_or(U256::ZERO);
668        accumulated_roll += tx.roll.unwrap_or(U256::ZERO);
669
670        if seq_iter.peek() == Some(&&idx) {
671            seq_iter.next();
672
673            let tx_with_accumulated = apply_warp_roll(tx, accumulated_warp, accumulated_roll);
674            let mut call_result = execute_tx(&mut executor, &tx_with_accumulated)?;
675
676            if !call_result.reverted {
677                executor.commit(&mut call_result);
678            }
679
680            accumulated_warp = U256::ZERO;
681            accumulated_roll = U256::ZERO;
682        }
683    }
684
685    // Apply any remaining accumulated warp/roll before calling invariant.
686    apply_warp_roll_to_env(&mut executor, accumulated_warp, accumulated_roll);
687
688    let (inv_result, success) = call_invariant_function(&executor, test_address, calldata)?;
689
690    if success
691        && inv_result.result.len() >= 32
692        && let Some(value) = I256::try_from_be_slice(&inv_result.result[..32])
693    {
694        return Ok(Some(value));
695    }
696
697    Ok(None)
698}
699
700#[cfg(test)]
701mod tests {
702    use super::{CallSequenceShrinker, build_shrunk_sequence};
703    use alloy_primitives::{Address, Bytes, U256};
704    use foundry_evm_fuzz::{BasicTxDetails, CallDetails};
705    use proptest::bits::BitSetLike;
706
707    fn tx(warp: Option<u64>, roll: Option<u64>) -> BasicTxDetails {
708        BasicTxDetails {
709            warp: warp.map(U256::from),
710            roll: roll.map(U256::from),
711            sender: Address::ZERO,
712            call_details: CallDetails {
713                target: Address::ZERO,
714                calldata: Bytes::new(),
715                value: None,
716            },
717        }
718    }
719
720    #[test]
721    fn build_shrunk_sequence_accumulates_removed_delay_into_next_kept_call() {
722        let calls = vec![tx(Some(3), Some(5)), tx(Some(7), Some(11)), tx(Some(13), Some(17))];
723        let mut shrinker = CallSequenceShrinker::new(calls.len());
724        shrinker.included_calls.clear(0);
725
726        let shrunk = build_shrunk_sequence(&calls, &shrinker, true);
727
728        assert_eq!(shrunk.len(), 2);
729        assert_eq!(shrunk[0].warp, Some(U256::from(10)));
730        assert_eq!(shrunk[0].roll, Some(U256::from(16)));
731        assert_eq!(shrunk[1].warp, Some(U256::from(13)));
732        assert_eq!(shrunk[1].roll, Some(U256::from(17)));
733    }
734
735    #[test]
736    fn build_shrunk_sequence_does_not_move_trailing_delay_backward() {
737        let calls = vec![tx(Some(3), Some(5)), tx(Some(7), Some(11))];
738        let mut shrinker = CallSequenceShrinker::new(calls.len());
739        shrinker.included_calls.clear(1);
740
741        let shrunk = build_shrunk_sequence(&calls, &shrinker, true);
742
743        assert_eq!(shrunk.len(), 1);
744        assert_eq!(shrunk[0].warp, Some(U256::from(3)));
745        assert_eq!(shrunk[0].roll, Some(U256::from(5)));
746    }
747}