1use crate::executors::{
2 EarlyExit, EvmError, Executor, RawCallResult,
3 campaign::{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::{cell::Cell, fmt::Write, hash::Hash};
20
21const LIVE_SHRINK_SEQUENCE_EDGE_CALLS: usize = 16;
22
23#[derive(Debug)]
28pub struct SequenceShrink {
29 call_sequence_len: usize,
31 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 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 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#[derive(Clone, Copy)]
103enum ShrinkErrorPolicy {
104 KeepRemoved,
106 RestoreRemoved,
109}
110
111#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
113pub struct ShrinkRunStats {
114 pub attempts: usize,
115 pub accepted: usize,
116}
117
118#[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#[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
179enum ReplayDecision<T> {
181 Stop(T),
182 Continue,
183}
184
185pub 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#[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#[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 pub sequence_assertion_failure: bool,
214 pub sequence_reverter: Option<Address>,
216}
217
218pub struct ShrunkSequence {
219 pub calls: Vec<BasicTxDetails>,
220 pub result: Option<CheckSequenceOutcome>,
221}
222
223#[derive(Debug)]
226pub struct HandlerReplayOutcome {
227 pub anchor_asserted: bool,
228 pub handler_target: Address,
230 pub reverter: Address,
232 pub selector: Selector,
233 pub revert_reason: Option<String>,
234 pub anchor_fingerprint: B256,
236}
237
238pub(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
260pub(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
367fn 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
378fn 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
394pub fn shrink_sequence_by_removing<P, S, A>(
397 calls_len: usize,
398 run: &mut ShrinkRun,
399 mut should_stop: S,
400 mut on_attempt: A,
401 mut predicate: P,
402) -> SequenceShrink
403where
404 P: FnMut(&SequenceShrink) -> Option<bool>,
405 S: FnMut() -> bool,
406 A: FnMut(),
407{
408 let mut shrinker = SequenceShrink::new(calls_len);
409 let mut call_idx = 0;
410 let mut skipped_candidates = 0usize;
411
412 while run.can_try() {
413 if should_stop() {
414 break;
415 }
416 let included_count = shrinker.included_count();
417 if included_count == 0 || skipped_candidates >= included_count {
418 break;
419 }
420
421 if !shrinker.contains(call_idx) {
423 call_idx = shrinker.next_index(call_idx);
424 continue;
425 }
426
427 shrinker.remove(call_idx);
428
429 let Some(accepted) = run.try_candidate_decision(|| predicate(&shrinker)) else {
430 shrinker.restore(call_idx);
431 skipped_candidates += 1;
432 call_idx = shrinker.next_index(call_idx);
433 continue;
434 };
435
436 on_attempt();
437 skipped_candidates = 0;
438 if accepted {
439 if shrinker.included_count() == 1 {
440 break;
441 }
442 } else {
443 shrinker.restore(call_idx);
444 }
445
446 call_idx = shrinker.next_index(call_idx);
447 }
448
449 shrinker
450}
451
452fn run_shrink_loop<P>(
455 config: &InvariantConfig,
456 calls: &[BasicTxDetails],
457 progress: &ShrinkProgress<'_>,
458 accumulate_warp_roll: bool,
459 early_exit: &EarlyExit,
460 error_policy: ShrinkErrorPolicy,
461 mut predicate: P,
462) -> SequenceShrink
463where
464 P: FnMut(&SequenceShrink) -> eyre::Result<bool>,
465{
466 let mut run = ShrinkRun::new(config.shrink_run_limit as usize);
467 let initial = SequenceShrink::new(calls.len());
468 progress.update(calls, &initial, accumulate_warp_roll);
469
470 let shrinker = shrink_sequence_by_removing(
471 calls.len(),
472 &mut run,
473 || early_exit.should_stop(),
474 || progress.inc(),
475 |shrinker| {
476 progress.update(calls, shrinker, accumulate_warp_roll);
477 match predicate(shrinker) {
478 Ok(bug_still_present) => Some(bug_still_present),
479 Err(_) => Some(matches!(error_policy, ShrinkErrorPolicy::KeepRemoved)),
480 }
481 },
482 );
483 progress.update(calls, &shrinker, accumulate_warp_roll);
484 shrinker
485}
486
487#[expect(clippy::too_many_arguments)]
488pub(crate) fn shrink_sequence<FEN: FoundryEvmNetwork>(
489 config: &InvariantConfig,
490 invariant_contract: &InvariantContract<'_>,
491 target_invariant: &Function,
492 calls: &[BasicTxDetails],
493 expect_assertion_failure: bool,
494 executor: &Executor<FEN>,
495 rd: Option<&RevertDecoder>,
496 progress: &ShrinkProgress<'_>,
497 early_exit: &EarlyExit,
498) -> eyre::Result<ShrunkSequence> {
499 trace!(target: "forge::test", "Shrinking sequence of {} calls.", calls.len());
500
501 let target_address = invariant_contract.address;
502 let calldata: Bytes = target_invariant.selector().into();
503 let (_, success) = call_invariant_function(executor, target_address, calldata.clone())?;
506 if !success {
507 return Ok(ShrunkSequence { calls: vec![], result: None });
508 }
509
510 let accumulate_warp_roll = config.has_delay();
511 let mut sequence = Vec::with_capacity(calls.len());
512 let mut last_result = None;
513 let mut last_result_matches_shrinker = true;
514 let shrinker = run_shrink_loop(
515 config,
516 calls,
517 progress,
518 accumulate_warp_roll,
519 early_exit,
520 ShrinkErrorPolicy::KeepRemoved,
523 |shrinker| {
524 sequence.clear();
525 sequence.extend(shrinker.current());
526 let result = match check_sequence(
527 executor.clone(),
528 calls,
529 &sequence,
530 target_address,
531 calldata.clone(),
532 CheckSequenceOptions {
533 accumulate_warp_roll,
534 fail_on_revert: config.fail_on_revert,
535 expect_assertion_failure,
536 call_after_invariant: invariant_contract.call_after_invariant,
537 rd,
538 },
539 ) {
540 Ok(result) => result,
541 Err(err) => {
542 last_result_matches_shrinker = false;
543 return Err(err);
544 }
545 };
546 let bug_still_present = !result.success;
548 if bug_still_present {
549 last_result = Some(result);
550 last_result_matches_shrinker = true;
551 }
552 Ok(bug_still_present)
553 },
554 );
555
556 let shrunk = build_shrunk_sequence(calls, &shrinker, accumulate_warp_roll);
557 let result = if last_result_matches_shrinker {
558 last_result
559 } else {
560 match check_sequence(
561 executor.clone(),
562 &shrunk,
563 &(0..shrunk.len()).collect::<Vec<_>>(),
564 target_address,
565 calldata,
566 CheckSequenceOptions {
567 accumulate_warp_roll: false,
568 fail_on_revert: config.fail_on_revert,
569 expect_assertion_failure,
570 call_after_invariant: invariant_contract.call_after_invariant,
571 rd,
572 },
573 ) {
574 Ok(result) => Some(result),
575 Err(err) => {
576 trace!(target: "forge::test", %err, "failed to recompute shrunk replay metrics");
577 None
578 }
579 }
580 };
581
582 Ok(ShrunkSequence { calls: shrunk, result })
583}
584
585fn replay_sequence<FEN, T, F>(
589 executor: &mut Executor<FEN>,
590 calls: &[BasicTxDetails],
591 sequence: &[usize],
592 accumulate_warp_roll: bool,
593 mut on_call: F,
594) -> eyre::Result<Option<T>>
595where
596 FEN: FoundryEvmNetwork,
597 F: FnMut(usize, RawCallResult<FEN>) -> eyre::Result<ReplayDecision<T>>,
598{
599 if !accumulate_warp_roll {
602 for &idx in sequence {
603 let (_, call_result) = execute_invariant_replay_tx(executor, &calls[idx])?;
604 match on_call(idx, call_result)? {
605 ReplayDecision::Stop(val) => return Ok(Some(val)),
606 ReplayDecision::Continue => {}
607 }
608 }
609 return Ok(None);
610 }
611
612 let mut accumulated_warp = U256::ZERO;
615 let mut accumulated_roll = U256::ZERO;
616 let mut seq_iter = sequence.iter().peekable();
617
618 for (idx, tx) in calls.iter().enumerate() {
619 accumulated_warp += tx.warp.unwrap_or(U256::ZERO);
620 accumulated_roll += tx.roll.unwrap_or(U256::ZERO);
621 if seq_iter.peek() != Some(&&idx) {
622 continue;
623 }
624 seq_iter.next();
625
626 let executed = apply_warp_roll(tx.clone(), accumulated_warp, accumulated_roll);
627 let (_, call_result) = execute_invariant_replay_tx(executor, &executed)?;
628
629 match on_call(idx, call_result)? {
630 ReplayDecision::Stop(val) => return Ok(Some(val)),
631 ReplayDecision::Continue => {}
632 }
633
634 accumulated_warp = U256::ZERO;
635 accumulated_roll = U256::ZERO;
636 }
637
638 Ok(None)
639}
640
641pub fn check_sequence<FEN: FoundryEvmNetwork>(
651 mut executor: Executor<FEN>,
652 calls: &[BasicTxDetails],
653 sequence: &[usize],
654 test_address: Address,
655 calldata: Bytes,
656 options: CheckSequenceOptions<'_>,
657) -> eyre::Result<CheckSequenceOutcome> {
658 let mut calls_executed = 0;
659 let mut reverts = 0;
660 let early = replay_sequence(
661 &mut executor,
662 calls,
663 sequence,
664 options.accumulate_warp_roll,
665 |idx, call_result| {
666 calls_executed += 1;
667 if call_result.result.as_ref() == MAGIC_ASSUME {
671 return Ok(ReplayDecision::Continue);
672 }
673 if call_result.reverted {
674 reverts += 1;
675 }
676 if did_fail_on_assert(&call_result, &call_result.state_changeset) {
677 let call = &calls[idx];
678 let target = call.call_details.target;
679 let selector = selector_from_calldata(&call.call_details.calldata);
680 let fingerprint = handler_edge_fingerprint(
681 snapshot_edge_fingerprint(&call_result),
682 target,
683 selector,
684 );
685 let sequence_reverter = call_result.reverter.or(Some(target));
686 let site = CheckSequenceFailureSite::SequenceCall { target, selector, fingerprint };
687 return Ok(ReplayDecision::Stop(CheckSequenceOutcome {
688 success: false,
689 replayed_entirely: false,
690 reason: assertion_failure_reason(call_result, options.rd),
691 calls_count: calls_executed,
692 reverts,
693 failure_site: Some(site),
694 sequence_assertion_failure: true,
695 sequence_reverter,
696 }));
697 }
698 if call_result.reverted && options.fail_on_revert {
699 if options.expect_assertion_failure {
700 return Ok(ReplayDecision::Stop(CheckSequenceOutcome {
701 success: true,
702 replayed_entirely: false,
703 reason: None,
704 calls_count: calls_executed,
705 reverts,
706 failure_site: None,
707 sequence_assertion_failure: false,
708 sequence_reverter: None,
709 }));
710 }
711 let site = sequence_call_failure_site(&calls[idx], &call_result);
712 return Ok(ReplayDecision::Stop(CheckSequenceOutcome {
713 success: false,
714 replayed_entirely: false,
715 reason: call_failure_reason(call_result, options.rd),
716 calls_count: calls_executed,
717 reverts,
718 failure_site: Some(site),
719 sequence_assertion_failure: false,
720 sequence_reverter: None,
721 }));
722 }
723 Ok(ReplayDecision::Continue)
724 },
725 )?;
726 if let Some(result) = early {
727 return Ok(result);
728 }
729
730 let (success, replayed_entirely, reason, failure_site) =
733 finish_sequence_check(&executor, test_address, calldata, &options)?;
734 Ok(CheckSequenceOutcome {
735 success,
736 replayed_entirely,
737 reason,
738 calls_count: calls_executed,
739 reverts,
740 failure_site,
741 sequence_assertion_failure: false,
742 sequence_reverter: None,
743 })
744}
745
746fn finish_sequence_check<FEN: FoundryEvmNetwork>(
747 executor: &Executor<FEN>,
748 test_address: Address,
749 calldata: Bytes,
750 options: &CheckSequenceOptions<'_>,
751) -> eyre::Result<(bool, bool, Option<String>, Option<CheckSequenceFailureSite>)> {
752 let handle_terminal_failure =
753 |call_result: RawCallResult<FEN>, site_kind: TerminalFailureSite| {
754 let should_ignore_failure = options.expect_assertion_failure
755 && !executor.has_global_failure(&call_result.state_changeset)
756 && !did_fail_on_assert(&call_result, &call_result.state_changeset);
757
758 if should_ignore_failure {
759 return (true, true, None, None);
760 }
761
762 let site = terminal_failure_site(site_kind, test_address, &calldata, &call_result);
763 let reason = if options.expect_assertion_failure {
764 assertion_failure_reason(call_result, options.rd)
765 } else {
766 call_failure_reason(call_result, options.rd)
767 };
768
769 (false, true, reason, Some(site))
770 };
771
772 let (invariant_result, mut success) =
773 call_invariant_function(executor, test_address, calldata.clone())?;
774 if !success {
775 return Ok(handle_terminal_failure(invariant_result, TerminalFailureSite::Invariant));
776 }
777
778 if success && options.call_after_invariant {
781 let (after_invariant_result, after_invariant_success) =
782 call_after_invariant_function(executor, test_address)?;
783 success = after_invariant_success;
784 if !success {
785 return Ok(handle_terminal_failure(
786 after_invariant_result,
787 TerminalFailureSite::AfterInvariant,
788 ));
789 }
790 }
791
792 Ok((success, true, None, None))
793}
794
795#[derive(Clone, Copy)]
796enum TerminalFailureSite {
797 Invariant,
798 AfterInvariant,
799}
800
801fn sequence_call_failure_site<FEN: FoundryEvmNetwork>(
802 call: &BasicTxDetails,
803 call_result: &RawCallResult<FEN>,
804) -> CheckSequenceFailureSite {
805 let target = call_result.reverter.unwrap_or(call.call_details.target);
806 let selector = selector_from_calldata(&call.call_details.calldata);
807 let fingerprint =
808 handler_edge_fingerprint(snapshot_edge_fingerprint(call_result), target, selector);
809 CheckSequenceFailureSite::SequenceCall { target, selector, fingerprint }
810}
811
812fn terminal_failure_site<FEN: FoundryEvmNetwork>(
813 kind: TerminalFailureSite,
814 target: Address,
815 calldata: &Bytes,
816 call_result: &RawCallResult<FEN>,
817) -> CheckSequenceFailureSite {
818 let target = call_result.reverter.unwrap_or(target);
819 let selector = match kind {
820 TerminalFailureSite::Invariant => selector_from_calldata(calldata),
821 TerminalFailureSite::AfterInvariant => {
822 Selector::from(IInvariantTest::afterInvariantCall::SELECTOR)
823 }
824 };
825 let fingerprint =
826 handler_edge_fingerprint(snapshot_edge_fingerprint(call_result), target, selector);
827 match kind {
828 TerminalFailureSite::Invariant => {
829 CheckSequenceFailureSite::Invariant { target, selector, fingerprint }
830 }
831 TerminalFailureSite::AfterInvariant => {
832 CheckSequenceFailureSite::AfterInvariant { target, selector, fingerprint }
833 }
834 }
835}
836
837fn selector_from_calldata(calldata: &Bytes) -> Selector {
838 let selector: [u8; 4] = calldata.get(..4).and_then(|s| s.try_into().ok()).unwrap_or_default();
839 Selector::from(selector)
840}
841
842fn call_failure_reason<FEN: FoundryEvmNetwork>(
843 call_result: RawCallResult<FEN>,
844 rd: Option<&RevertDecoder>,
845) -> Option<String> {
846 match call_result.into_evm_error(rd) {
847 EvmError::Execution(err) => Some(err.reason),
848 _ => None,
849 }
850}
851
852fn assertion_failure_reason<FEN: FoundryEvmNetwork>(
853 call_result: RawCallResult<FEN>,
854 rd: Option<&RevertDecoder>,
855) -> Option<String> {
856 call_failure_reason(call_result, rd).or_else(|| Some("assertion failed".to_string()))
857}
858
859#[expect(clippy::too_many_arguments)]
867pub(crate) fn shrink_sequence_value<FEN: FoundryEvmNetwork>(
868 config: &InvariantConfig,
869 invariant_contract: &InvariantContract<'_>,
870 target_invariant: &Function,
871 calls: &[BasicTxDetails],
872 executor: &Executor<FEN>,
873 target_value: I256,
874 progress: &ShrinkProgress<'_>,
875 early_exit: &EarlyExit,
876) -> eyre::Result<Vec<BasicTxDetails>> {
877 trace!(target: "forge::test", "Shrinking optimization sequence of {} calls for target value {}.", calls.len(), target_value);
878
879 let target_address = invariant_contract.address;
880 let calldata: Bytes = target_invariant.selector().into();
881
882 if check_sequence_value(executor.clone(), calls, &[], target_address, calldata.clone())?
884 == Some(target_value)
885 {
886 return Ok(vec![]);
887 }
888
889 let replay_failed = Cell::new(false);
890 let mut replay_error = None;
891 let mut run = ShrinkRun::new(config.shrink_run_limit as usize);
892 let initial = SequenceShrink::new(calls.len());
893 progress.update(calls, &initial, true);
894 let mut sequence = Vec::with_capacity(calls.len());
895
896 let shrinker = shrink_sequence_by_removing(
897 calls.len(),
898 &mut run,
899 || early_exit.should_stop() || replay_failed.get(),
900 || progress.inc(),
901 |shrinker| {
902 progress.update(calls, shrinker, true);
903 sequence.clear();
904 sequence.extend(shrinker.current());
905 match check_sequence_value(
906 executor.clone(),
907 calls,
908 &sequence,
909 target_address,
910 calldata.clone(),
911 ) {
912 Ok(Some(value)) => Some(value == target_value),
913 Ok(None) => Some(false),
914 Err(err) => {
915 replay_error = Some(err);
916 replay_failed.set(true);
917 None
918 }
919 }
920 },
921 );
922 progress.update(calls, &shrinker, true);
923 if let Some(err) = replay_error {
924 return Err(err);
925 }
926
927 Ok(build_shrunk_sequence(calls, &shrinker, true))
928}
929
930pub fn replay_handler_failure_sequence<FEN: FoundryEvmNetwork>(
933 mut executor: Executor<FEN>,
934 calls: &[BasicTxDetails],
935 sequence: &[usize],
936 accumulate_warp_roll: bool,
937 rd: Option<&RevertDecoder>,
938) -> eyre::Result<HandlerReplayOutcome> {
939 let Some(&anchor_idx) = sequence.last() else {
940 return Ok(HandlerReplayOutcome {
941 anchor_asserted: false,
942 handler_target: Address::ZERO,
943 reverter: Address::ZERO,
944 selector: Selector::ZERO,
945 revert_reason: None,
946 anchor_fingerprint: B256::ZERO,
947 });
948 };
949
950 let outcome = replay_sequence(
951 &mut executor,
952 calls,
953 sequence,
954 accumulate_warp_roll,
955 |idx, call_result| {
956 let asserted = did_fail_on_assert(&call_result, &call_result.state_changeset);
957 if idx == anchor_idx {
958 let snapshot = snapshot_edge_fingerprint(&call_result);
959 let anchor = &calls[anchor_idx];
960 let handler_target = anchor.call_details.target;
963 let reverter = call_result.reverter.unwrap_or(handler_target);
964 let selector_bytes: [u8; 4] = anchor
965 .call_details
966 .calldata
967 .get(..4)
968 .and_then(|s| s.try_into().ok())
969 .unwrap_or_default();
970 let selector = Selector::from(selector_bytes);
971 let fingerprint = handler_edge_fingerprint(snapshot, handler_target, selector);
972 let reason =
973 if asserted { assertion_failure_reason(call_result, rd) } else { None };
974 return Ok(ReplayDecision::Stop(HandlerReplayOutcome {
975 anchor_asserted: asserted,
976 handler_target,
977 reverter,
978 selector,
979 revert_reason: reason,
980 anchor_fingerprint: fingerprint,
981 }));
982 }
983 if asserted {
984 return Ok(ReplayDecision::Stop(HandlerReplayOutcome {
986 anchor_asserted: false,
987 handler_target: Address::ZERO,
988 reverter: Address::ZERO,
989 selector: Selector::ZERO,
990 revert_reason: None,
991 anchor_fingerprint: B256::ZERO,
992 }));
993 }
994 Ok(ReplayDecision::Continue)
995 },
996 )?;
997
998 Ok(outcome.unwrap_or(HandlerReplayOutcome {
999 anchor_asserted: false,
1000 handler_target: Address::ZERO,
1001 reverter: Address::ZERO,
1002 selector: Selector::ZERO,
1003 revert_reason: None,
1004 anchor_fingerprint: B256::ZERO,
1005 }))
1006}
1007
1008pub(crate) fn shrink_handler_sequence<FEN: FoundryEvmNetwork>(
1011 config: &InvariantConfig,
1012 calls: &[BasicTxDetails],
1013 expected_fingerprint: B256,
1014 executor: &Executor<FEN>,
1015 progress: &ShrinkProgress<'_>,
1016 early_exit: &EarlyExit,
1017) -> eyre::Result<Vec<BasicTxDetails>> {
1018 if calls.is_empty() {
1019 return Ok(vec![]);
1020 }
1021 let accumulate_warp_roll = config.has_delay();
1022 let mut sequence = Vec::with_capacity(calls.len());
1023 let shrinker = run_shrink_loop(
1024 config,
1025 calls,
1026 progress,
1027 accumulate_warp_roll,
1028 early_exit,
1029 ShrinkErrorPolicy::RestoreRemoved,
1030 |shrinker| {
1031 sequence.clear();
1032 sequence.extend(shrinker.current());
1033 handler_sequence_still_triggers_bug(
1034 executor.clone(),
1035 calls,
1036 &sequence,
1037 accumulate_warp_roll,
1038 expected_fingerprint,
1039 )
1040 },
1041 );
1042
1043 let shrunk = build_shrunk_sequence(calls, &shrinker, accumulate_warp_roll);
1044
1045 sequence.clear();
1047 sequence.extend(shrinker.current());
1048 let verified = handler_sequence_still_triggers_bug(
1049 executor.clone(),
1050 calls,
1051 &sequence,
1052 accumulate_warp_roll,
1053 expected_fingerprint,
1054 )
1055 .unwrap_or(false);
1056 if verified { Ok(shrunk) } else { Ok(calls.to_vec()) }
1057}
1058
1059fn handler_sequence_still_triggers_bug<FEN: FoundryEvmNetwork>(
1061 executor: Executor<FEN>,
1062 calls: &[BasicTxDetails],
1063 sequence: &[usize],
1064 accumulate_warp_roll: bool,
1065 expected_fingerprint: B256,
1066) -> eyre::Result<bool> {
1067 let outcome =
1068 replay_handler_failure_sequence(executor, calls, sequence, accumulate_warp_roll, None)?;
1069 Ok(outcome.anchor_asserted && outcome.anchor_fingerprint == expected_fingerprint)
1070}
1071
1072pub fn check_sequence_value<FEN: FoundryEvmNetwork>(
1078 mut executor: Executor<FEN>,
1079 calls: &[BasicTxDetails],
1080 sequence: &[usize],
1081 test_address: Address,
1082 calldata: Bytes,
1083) -> eyre::Result<Option<I256>> {
1084 let mut accumulated_warp = U256::ZERO;
1085 let mut accumulated_roll = U256::ZERO;
1086 let mut seq_iter = sequence.iter().peekable();
1087
1088 for (idx, tx) in calls.iter().enumerate() {
1089 accumulated_warp += tx.warp.unwrap_or(U256::ZERO);
1090 accumulated_roll += tx.roll.unwrap_or(U256::ZERO);
1091
1092 if seq_iter.peek() == Some(&&idx) {
1093 seq_iter.next();
1094
1095 let tx_with_accumulated =
1096 apply_warp_roll(tx.clone(), accumulated_warp, accumulated_roll);
1097 execute_invariant_replay_tx(&mut executor, &tx_with_accumulated)?;
1098
1099 accumulated_warp = U256::ZERO;
1100 accumulated_roll = U256::ZERO;
1101 }
1102 }
1103
1104 apply_block_delay(&mut executor, accumulated_warp, accumulated_roll);
1106
1107 let (inv_result, success) = call_invariant_function(&executor, test_address, calldata)?;
1108
1109 if success
1110 && inv_result.result.len() >= 32
1111 && let Some(value) = I256::try_from_be_slice(&inv_result.result[..32])
1112 {
1113 return Ok(Some(value));
1114 }
1115
1116 Ok(None)
1117}
1118
1119#[cfg(test)]
1120mod tests {
1121 use super::*;
1122 use crate::executors::ExecutorBuilder;
1123 use alloy_evm::EvmEnv;
1124 use alloy_primitives::hex;
1125 use foundry_cheatcodes::CheatsConfig;
1126 use foundry_evm_core::{FoundryBlock, backend::Backend, evm::EthEvmNetwork};
1127 use foundry_evm_fuzz::CallDetails;
1128 use revm::{
1129 bytecode::Bytecode,
1130 context::{Block, TxEnv},
1131 };
1132 use std::sync::Arc;
1133
1134 fn tx(warp: Option<u64>, roll: Option<u64>) -> BasicTxDetails {
1135 BasicTxDetails {
1136 warp: warp.map(U256::from),
1137 roll: roll.map(U256::from),
1138 sender: Address::ZERO,
1139 call_details: CallDetails {
1140 target: Address::ZERO,
1141 calldata: Bytes::new(),
1142 value: None,
1143 },
1144 }
1145 }
1146
1147 fn tx_with_calldata(byte: u8) -> BasicTxDetails {
1148 BasicTxDetails {
1149 warp: None,
1150 roll: None,
1151 sender: Address::ZERO,
1152 call_details: CallDetails {
1153 target: Address::ZERO,
1154 calldata: Bytes::from(vec![byte]),
1155 value: None,
1156 },
1157 }
1158 }
1159
1160 #[test]
1161 fn build_shrunk_sequence_accumulates_removed_delay_into_next_kept_call() {
1162 let calls = vec![tx(Some(3), Some(5)), tx(Some(7), Some(11)), tx(Some(13), Some(17))];
1163 let mut shrinker = SequenceShrink::new(calls.len());
1164 shrinker.remove(0);
1165
1166 let shrunk = build_shrunk_sequence(&calls, &shrinker, true);
1167
1168 assert_eq!(shrunk.len(), 2);
1169 assert_eq!(shrunk[0].warp, Some(U256::from(10)));
1170 assert_eq!(shrunk[0].roll, Some(U256::from(16)));
1171 assert_eq!(shrunk[1].warp, Some(U256::from(13)));
1172 assert_eq!(shrunk[1].roll, Some(U256::from(17)));
1173 }
1174
1175 #[test]
1176 fn build_shrunk_sequence_does_not_move_trailing_delay_backward() {
1177 let calls = vec![tx(Some(3), Some(5)), tx(Some(7), Some(11))];
1178 let mut shrinker = SequenceShrink::new(calls.len());
1179 shrinker.remove(1);
1180
1181 let shrunk = build_shrunk_sequence(&calls, &shrinker, true);
1182
1183 assert_eq!(shrunk.len(), 1);
1184 assert_eq!(shrunk[0].warp, Some(U256::from(3)));
1185 assert_eq!(shrunk[0].roll, Some(U256::from(5)));
1186 }
1187
1188 #[test]
1189 fn check_sequence_value_applies_trailing_delay_before_invariant() {
1190 let backend = Backend::<EthEvmNetwork>::spawn(None).unwrap();
1191 let mut executor = ExecutorBuilder::default()
1192 .inspectors(|stack| stack.cheatcodes(Arc::new(CheatsConfig::default())))
1193 .gas_limit(1 << 24)
1194 .build(EvmEnv::default(), TxEnv::default(), backend, Default::default());
1195 let test_address = Address::repeat_byte(0x11);
1197 let code = hex!("4360801b420160005260206000f3");
1198 executor.set_code(test_address, Bytecode::new_raw(Bytes::from(code))).unwrap();
1199 let block_value =
1200 |timestamp: U256, number: U256| I256::from_raw((number << 128) + timestamp);
1201 let calls = [tx(Some(3), Some(5)), tx(Some(7), Some(11))];
1202 let block = executor.evm_env().block_env.clone();
1203
1204 let delayed =
1207 Some(block_value(block.timestamp() + U256::from(10), block.number() + U256::from(16)));
1208 let mut cheatcode_block = block;
1209 cheatcode_block.set_timestamp(U256::from(1000));
1210 cheatcode_block.set_number(U256::from(2000));
1211 let cases = [
1212 (&[0][..], None, delayed),
1213 (&[], None, delayed),
1214 (&[], Some(None), delayed),
1215 (
1216 &[],
1217 Some(Some(cheatcode_block)),
1218 Some(block_value(U256::from(1010), U256::from(2016))),
1219 ),
1220 ];
1221 for (sequence, cheatcode_block, expected) in cases {
1222 let mut executor = executor.clone();
1223 if let Some(block) = cheatcode_block {
1224 executor.inspector_mut().cheatcodes.as_mut().unwrap().block = block;
1225 }
1226 let value =
1227 check_sequence_value(executor, &calls, sequence, test_address, Bytes::new())
1228 .unwrap();
1229 assert_eq!(value, expected);
1230 }
1231 }
1232
1233 #[test]
1234 fn shrink_run_counts_attempts_and_accepts() {
1235 let mut run = ShrinkRun::new(2);
1236
1237 assert_eq!(run.remaining_attempts(), 2);
1238 assert!(!run.try_candidate(|| false));
1239 assert!(run.try_candidate(|| true));
1240
1241 let mut called_after_limit = false;
1242 assert!(!run.try_candidate(|| {
1243 called_after_limit = true;
1244 true
1245 }));
1246
1247 assert!(!called_after_limit);
1248 let stats = run.finish();
1249 assert_eq!(stats.attempts, 2);
1250 assert_eq!(stats.accepted, 1);
1251 }
1252
1253 #[test]
1254 fn shrink_candidate_keys_skip_duplicates() {
1255 let mut candidates = ShrinkCandidateKeys::new("initial");
1256
1257 assert!(!candidates.insert("initial"));
1258 assert!(candidates.insert("first"));
1259 assert!(!candidates.insert("first"));
1260 assert!(candidates.insert("second"));
1261 }
1262
1263 #[test]
1264 fn shrink_loop_keep_removed_treats_candidate_error_as_still_failing() {
1265 let config = InvariantConfig { shrink_run_limit: 1, ..Default::default() };
1266 let early_exit = EarlyExit::new(false);
1267 let calls = vec![tx(None, None), tx(None, None)];
1268 let progress = ShrinkProgress::new(&config, None, "test", None, None, false);
1269
1270 let shrinker = run_shrink_loop(
1271 &config,
1272 &calls,
1273 &progress,
1274 false,
1275 &early_exit,
1276 ShrinkErrorPolicy::KeepRemoved,
1277 |_| Err(eyre::eyre!("candidate replay failed")),
1278 );
1279
1280 assert_eq!(shrinker.current().collect::<Vec<_>>(), vec![1]);
1281 }
1282
1283 #[test]
1284 fn shrink_loop_limit_counts_candidate_replays_not_skipped_indices() {
1285 let config = InvariantConfig { shrink_run_limit: 4, ..Default::default() };
1286 let early_exit = EarlyExit::new(false);
1287 let calls = vec![tx(None, None), tx(None, None), tx(None, None)];
1288 let progress = ShrinkProgress::new(&config, None, "test", None, None, false);
1289 let mut replay_attempts = 0;
1290
1291 let shrinker = run_shrink_loop(
1292 &config,
1293 &calls,
1294 &progress,
1295 false,
1296 &early_exit,
1297 ShrinkErrorPolicy::RestoreRemoved,
1298 |_| {
1299 replay_attempts += 1;
1300 Ok(matches!(replay_attempts, 1 | 4))
1301 },
1302 );
1303
1304 assert_eq!(replay_attempts, 4);
1305 assert_eq!(shrinker.current().collect::<Vec<_>>(), vec![2]);
1306 }
1307
1308 #[test]
1309 fn sequence_shrinker_skips_duplicate_candidates_without_spending_attempts() {
1310 let mut run = ShrinkRun::new(2);
1311 let mut seen = Vec::new();
1312
1313 let shrinker = shrink_sequence_by_removing(
1314 2,
1315 &mut run,
1316 || false,
1317 || {},
1318 |shrinker| {
1319 let candidate = shrinker.current().collect::<Vec<_>>();
1320 if seen.contains(&candidate) {
1321 None
1322 } else {
1323 seen.push(candidate);
1324 Some(false)
1325 }
1326 },
1327 );
1328
1329 assert_eq!(shrinker.current().collect::<Vec<_>>(), vec![0, 1]);
1330 let stats = run.finish();
1331 assert_eq!(stats.attempts, 2);
1332 assert_eq!(stats.accepted, 0);
1333 }
1334
1335 #[test]
1336 fn shrink_progress_message_renders_current_sequence() {
1337 let calls = vec![tx_with_calldata(1), tx_with_calldata(2)];
1338
1339 let message =
1340 format_shrink_progress_message(" Shrink: invariant_live", &calls, None, false);
1341
1342 assert!(message.contains(" Shrink: invariant_live"));
1343 assert!(message.contains("[Sequence] (shrunk: 2)"));
1344 assert!(message.contains("calldata=0x01 args=[]"));
1345 assert!(message.contains("calldata=0x02 args=[]"));
1346 }
1347
1348 #[test]
1349 fn shrink_progress_message_omits_middle_of_large_sequence() {
1350 let calls = (0..(LIVE_SHRINK_SEQUENCE_EDGE_CALLS * 2 + 3))
1351 .map(|idx| tx_with_calldata(idx as u8))
1352 .collect::<Vec<_>>();
1353
1354 let message =
1355 format_shrink_progress_message(" Shrink: invariant_live", &calls, None, false);
1356
1357 assert!(message.contains("[Sequence] (shrunk: 35)"));
1358 assert!(message.contains("... 3 call(s) omitted ..."));
1359 assert_eq!(message.matches("sender=").count(), LIVE_SHRINK_SEQUENCE_EDGE_CALLS * 2);
1360 assert!(message.contains("calldata=0x00 args=[]"));
1361 assert!(message.contains("calldata=0x22 args=[]"));
1362 }
1363}