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#[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>(
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 if !accepted_block && kept.len() / chunk >= 8 {
442 break;
443 }
444 chunk /= 2;
445 }
446
447 let mut call_idx = 0;
448 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 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
493fn 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 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 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 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
634fn 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 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 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
690pub 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 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 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 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#[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 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 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
997pub 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 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 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
1075pub(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 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
1126fn 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
1139pub 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 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 last_call_scored =
1187 handlers_succeeded(&executor, test_address, &gate_targets, &call_result);
1188 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_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
1221fn 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 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 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}