1use crate::executors::{
2 EarlyExit, EvmError, Executor, RawCallResult,
3 invariant::{
4 IInvariantTest, 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 alloy_sol_types::SolCall;
13use foundry_common::ContractsByAddress;
14use foundry_config::InvariantConfig;
15use foundry_evm_core::{
16 FoundryBlock, constants::MAGIC_ASSUME, decode::RevertDecoder, evm::FoundryEvmNetwork,
17};
18use foundry_evm_fuzz::{BaseCounterExample, BasicTxDetails, invariant::InvariantContract};
19use indicatif::ProgressBar;
20use proptest::bits::{BitSetLike, VarBitSet};
21use revm::context::Block;
22use std::{cell::Cell, collections::HashSet, fmt::Write, hash::Hash};
23
24const LIVE_SHRINK_SEQUENCE_EDGE_CALLS: usize = 16;
25
26#[derive(Debug)]
31pub struct SequenceShrink {
32 call_sequence_len: usize,
34 included_calls: VarBitSet,
36}
37
38impl SequenceShrink {
39 pub fn new(call_sequence_len: usize) -> Self {
40 Self { call_sequence_len, included_calls: VarBitSet::saturated(call_sequence_len) }
41 }
42
43 pub fn current(&self) -> impl Iterator<Item = usize> + '_ {
45 (0..self.call_sequence_len).filter(|&call_id| self.included_calls.test(call_id))
46 }
47
48 pub fn contains(&self, call_idx: usize) -> bool {
49 self.included_calls.test(call_idx)
50 }
51
52 pub fn included_count(&self) -> usize {
53 self.included_calls.count()
54 }
55
56 pub fn apply<T: Clone>(&self, calls: &[T]) -> Vec<T> {
57 self.current().map(|idx| calls[idx].clone()).collect()
58 }
59
60 pub fn apply_with_accumulated_delay<T, D, A>(
61 &self,
62 calls: &[T],
63 mut delay: D,
64 mut apply_delay: A,
65 ) -> Vec<T>
66 where
67 T: Clone,
68 D: FnMut(&T) -> (Option<U256>, Option<U256>),
69 A: FnMut(T, U256, U256) -> T,
70 {
71 let mut result = Vec::new();
72 let mut accumulated_warp = U256::ZERO;
73 let mut accumulated_roll = U256::ZERO;
74
75 for (idx, call) in calls.iter().enumerate() {
76 let (warp, roll) = delay(call);
77 accumulated_warp += warp.unwrap_or(U256::ZERO);
78 accumulated_roll += roll.unwrap_or(U256::ZERO);
79
80 if self.contains(idx) {
81 result.push(apply_delay(call.clone(), accumulated_warp, accumulated_roll));
82 accumulated_warp = U256::ZERO;
83 accumulated_roll = U256::ZERO;
84 }
85 }
86
87 result
88 }
89
90 fn remove(&mut self, call_idx: usize) {
91 self.included_calls.clear(call_idx);
92 }
93
94 fn restore(&mut self, call_idx: usize) {
95 self.included_calls.set(call_idx);
96 }
97
98 const fn next_index(&self, call_idx: usize) -> usize {
100 if call_idx + 1 == self.call_sequence_len { 0 } else { call_idx + 1 }
101 }
102}
103
104#[derive(Clone, Copy)]
106enum ShrinkErrorPolicy {
107 KeepRemoved,
109 RestoreRemoved,
112}
113
114#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
116pub struct ShrinkRunStats {
117 pub attempts: usize,
118 pub accepted: usize,
119}
120
121#[derive(Clone, Copy, Debug, PartialEq, Eq)]
126pub struct ShrinkRun {
127 max_attempts: usize,
128 stats: ShrinkRunStats,
129}
130
131impl ShrinkRun {
132 pub const fn new(max_attempts: usize) -> Self {
133 Self { max_attempts, stats: ShrinkRunStats { attempts: 0, accepted: 0 } }
134 }
135
136 pub const fn can_try(&self) -> bool {
137 self.stats.attempts < self.max_attempts
138 }
139
140 pub const fn remaining_attempts(&self) -> usize {
141 self.max_attempts - self.stats.attempts
142 }
143
144 pub const fn finish(self) -> ShrinkRunStats {
145 self.stats
146 }
147
148 pub fn try_candidate(&mut self, still_fails: impl FnOnce() -> bool) -> bool {
149 self.try_candidate_decision(|| Some(still_fails())).unwrap_or(false)
150 }
151
152 fn try_candidate_decision(&mut self, decide: impl FnOnce() -> Option<bool>) -> Option<bool> {
153 if !self.can_try() {
154 return None;
155 }
156
157 let accepted = decide()?;
158 self.stats.attempts += 1;
159 if accepted {
160 self.stats.accepted += 1;
161 }
162 Some(accepted)
163 }
164}
165
166#[derive(Clone, Debug)]
168pub struct ShrinkCandidateKeys<K> {
169 seen: HashSet<K>,
170}
171
172impl<K: Eq + Hash> ShrinkCandidateKeys<K> {
173 pub fn new(initial: K) -> Self {
174 Self { seen: HashSet::from([initial]) }
175 }
176
177 pub fn insert(&mut self, key: K) -> bool {
178 self.seen.insert(key)
179 }
180}
181
182#[expect(clippy::large_enum_variant)]
185enum ReplayDecision<T, FEN: FoundryEvmNetwork> {
186 Stop(T),
187 Continue(RawCallResult<FEN>),
188}
189
190pub struct CheckSequenceOptions<'a> {
192 pub accumulate_warp_roll: bool,
193 pub fail_on_revert: bool,
194 pub expect_assertion_failure: bool,
195 pub call_after_invariant: bool,
196 pub rd: Option<&'a RevertDecoder>,
197}
198
199#[derive(Clone, Copy, Debug, PartialEq, Eq)]
201pub enum CheckSequenceFailureSite {
202 SequenceCall { target: Address, selector: Selector, fingerprint: B256 },
203 Invariant { target: Address, selector: Selector, fingerprint: B256 },
204 AfterInvariant { target: Address, selector: Selector, fingerprint: B256 },
205}
206
207#[derive(Clone, Debug)]
209pub struct CheckSequenceOutcome {
210 pub success: bool,
211 pub replayed_entirely: bool,
212 pub reason: Option<String>,
213 pub calls_count: usize,
214 pub reverts: usize,
215 pub failure_site: Option<CheckSequenceFailureSite>,
216 pub sequence_assertion_failure: bool,
219}
220
221pub struct ShrunkSequence {
222 pub calls: Vec<BasicTxDetails>,
223 pub result: Option<CheckSequenceOutcome>,
224}
225
226#[derive(Debug)]
229pub struct HandlerReplayOutcome {
230 pub anchor_asserted: bool,
231 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 apply_warp_roll_to_env<FEN: FoundryEvmNetwork>(
380 executor: &mut Executor<FEN>,
381 warp: U256,
382 roll: U256,
383) {
384 if warp > U256::ZERO || roll > U256::ZERO {
385 let ts = executor.evm_env().block_env.timestamp();
386 let num = executor.evm_env().block_env.number();
387 executor.evm_env_mut().block_env.set_timestamp(ts + warp);
388 executor.evm_env_mut().block_env.set_number(num + roll);
389
390 let block_env = executor.evm_env().block_env.clone();
391 if let Some(cheatcodes) = executor.inspector_mut().cheatcodes.as_mut() {
392 if let Some(block) = cheatcodes.block.as_mut() {
393 let bts = block.timestamp();
394 let bnum = block.number();
395 block.set_timestamp(bts + warp);
396 block.set_number(bnum + roll);
397 } else {
398 cheatcodes.block = Some(block_env);
399 }
400 }
401 }
402}
403
404fn build_shrunk_sequence(
409 calls: &[BasicTxDetails],
410 shrinker: &SequenceShrink,
411 accumulate_warp_roll: bool,
412) -> Vec<BasicTxDetails> {
413 if !accumulate_warp_roll {
414 return shrinker.apply(calls);
415 }
416
417 shrinker.apply_with_accumulated_delay(calls, |call| (call.warp, call.roll), apply_warp_roll)
418}
419
420pub fn shrink_sequence_by_removing<P, S, A>(
423 calls_len: usize,
424 run: &mut ShrinkRun,
425 mut should_stop: S,
426 mut on_attempt: A,
427 mut predicate: P,
428) -> SequenceShrink
429where
430 P: FnMut(&SequenceShrink) -> Option<bool>,
431 S: FnMut() -> bool,
432 A: FnMut(),
433{
434 let mut shrinker = SequenceShrink::new(calls_len);
435 let mut call_idx = 0;
436 let mut skipped_candidates = 0usize;
437
438 while run.can_try() {
439 if should_stop() {
440 break;
441 }
442 let included_count = shrinker.included_count();
443 if included_count == 0 || skipped_candidates >= included_count {
444 break;
445 }
446
447 if !shrinker.contains(call_idx) {
449 call_idx = shrinker.next_index(call_idx);
450 continue;
451 }
452
453 shrinker.remove(call_idx);
454
455 let Some(accepted) = run.try_candidate_decision(|| predicate(&shrinker)) else {
456 shrinker.restore(call_idx);
457 skipped_candidates += 1;
458 call_idx = shrinker.next_index(call_idx);
459 continue;
460 };
461
462 on_attempt();
463 skipped_candidates = 0;
464 if accepted {
465 if shrinker.included_count() == 1 {
466 break;
467 }
468 } else {
469 shrinker.restore(call_idx);
470 }
471
472 call_idx = shrinker.next_index(call_idx);
473 }
474
475 shrinker
476}
477
478fn run_shrink_loop<P>(
481 config: &InvariantConfig,
482 calls: &[BasicTxDetails],
483 progress: &ShrinkProgress<'_>,
484 accumulate_warp_roll: bool,
485 early_exit: &EarlyExit,
486 error_policy: ShrinkErrorPolicy,
487 mut predicate: P,
488) -> SequenceShrink
489where
490 P: FnMut(&SequenceShrink) -> eyre::Result<bool>,
491{
492 let mut run = ShrinkRun::new(config.shrink_run_limit as usize);
493 let initial = SequenceShrink::new(calls.len());
494 progress.update(calls, &initial, accumulate_warp_roll);
495
496 let shrinker = shrink_sequence_by_removing(
497 calls.len(),
498 &mut run,
499 || early_exit.should_stop(),
500 || progress.inc(),
501 |shrinker| {
502 progress.update(calls, shrinker, accumulate_warp_roll);
503 match predicate(shrinker) {
504 Ok(bug_still_present) => Some(bug_still_present),
505 Err(_) => Some(matches!(error_policy, ShrinkErrorPolicy::KeepRemoved)),
506 }
507 },
508 );
509 progress.update(calls, &shrinker, accumulate_warp_roll);
510 shrinker
511}
512
513#[expect(clippy::too_many_arguments)]
514pub(crate) fn shrink_sequence<FEN: FoundryEvmNetwork>(
515 config: &InvariantConfig,
516 invariant_contract: &InvariantContract<'_>,
517 target_invariant: &Function,
518 calls: &[BasicTxDetails],
519 expect_assertion_failure: bool,
520 executor: &Executor<FEN>,
521 rd: Option<&RevertDecoder>,
522 progress: &ShrinkProgress<'_>,
523 early_exit: &EarlyExit,
524) -> eyre::Result<ShrunkSequence> {
525 trace!(target: "forge::test", "Shrinking sequence of {} calls.", calls.len());
526
527 let target_address = invariant_contract.address;
528 let calldata: Bytes = target_invariant.selector().to_vec().into();
529 let (_, success) = call_invariant_function(executor, target_address, calldata.clone())?;
532 if !success {
533 return Ok(ShrunkSequence { calls: vec![], result: None });
534 }
535
536 let accumulate_warp_roll = config.has_delay();
537 let mut sequence = Vec::with_capacity(calls.len());
538 let mut last_result = None;
539 let mut last_result_matches_shrinker = true;
540 let shrinker = run_shrink_loop(
541 config,
542 calls,
543 progress,
544 accumulate_warp_roll,
545 early_exit,
546 ShrinkErrorPolicy::KeepRemoved,
549 |shrinker| {
550 sequence.clear();
551 sequence.extend(shrinker.current());
552 let result = match check_sequence(
553 executor.clone(),
554 calls,
555 &sequence,
556 target_address,
557 calldata.clone(),
558 CheckSequenceOptions {
559 accumulate_warp_roll,
560 fail_on_revert: config.fail_on_revert,
561 expect_assertion_failure,
562 call_after_invariant: invariant_contract.call_after_invariant,
563 rd,
564 },
565 ) {
566 Ok(result) => result,
567 Err(err) => {
568 last_result_matches_shrinker = false;
569 return Err(err);
570 }
571 };
572 let bug_still_present = !result.success;
574 if bug_still_present {
575 last_result = Some(result);
576 last_result_matches_shrinker = true;
577 }
578 Ok(bug_still_present)
579 },
580 );
581
582 let shrunk = build_shrunk_sequence(calls, &shrinker, accumulate_warp_roll);
583 let result = if last_result_matches_shrinker {
584 last_result
585 } else {
586 match check_sequence(
587 executor.clone(),
588 &shrunk,
589 &(0..shrunk.len()).collect::<Vec<_>>(),
590 target_address,
591 calldata,
592 CheckSequenceOptions {
593 accumulate_warp_roll: false,
594 fail_on_revert: config.fail_on_revert,
595 expect_assertion_failure,
596 call_after_invariant: invariant_contract.call_after_invariant,
597 rd,
598 },
599 ) {
600 Ok(result) => Some(result),
601 Err(err) => {
602 trace!(target: "forge::test", %err, "failed to recompute shrunk replay metrics");
603 None
604 }
605 }
606 };
607
608 Ok(ShrunkSequence { calls: shrunk, result })
609}
610
611fn replay_sequence<FEN, T, F>(
615 executor: &mut Executor<FEN>,
616 calls: &[BasicTxDetails],
617 sequence: &[usize],
618 accumulate_warp_roll: bool,
619 mut on_call: F,
620) -> eyre::Result<Option<T>>
621where
622 FEN: FoundryEvmNetwork,
623 F: FnMut(usize, RawCallResult<FEN>) -> eyre::Result<ReplayDecision<T, FEN>>,
624{
625 if !accumulate_warp_roll {
628 for &idx in sequence {
629 let call_result = execute_tx(executor, &calls[idx])?;
630 match on_call(idx, call_result)? {
631 ReplayDecision::Stop(val) => return Ok(Some(val)),
632 ReplayDecision::Continue(mut call_result) => {
633 if !call_result.reverted {
634 executor.commit(&mut call_result);
635 }
636 }
637 }
638 }
639 return Ok(None);
640 }
641
642 let mut accumulated_warp = U256::ZERO;
645 let mut accumulated_roll = U256::ZERO;
646 let mut seq_iter = sequence.iter().peekable();
647
648 for (idx, tx) in calls.iter().enumerate() {
649 accumulated_warp += tx.warp.unwrap_or(U256::ZERO);
650 accumulated_roll += tx.roll.unwrap_or(U256::ZERO);
651 if seq_iter.peek() != Some(&&idx) {
652 continue;
653 }
654 seq_iter.next();
655
656 let executed = apply_warp_roll(tx.clone(), accumulated_warp, accumulated_roll);
657 let call_result = execute_tx(executor, &executed)?;
658
659 match on_call(idx, call_result)? {
660 ReplayDecision::Stop(val) => return Ok(Some(val)),
661 ReplayDecision::Continue(mut call_result) => {
662 if !call_result.reverted {
663 executor.commit(&mut call_result);
664 }
665 }
666 }
667
668 accumulated_warp = U256::ZERO;
669 accumulated_roll = U256::ZERO;
670 }
671
672 Ok(None)
673}
674
675pub fn check_sequence<FEN: FoundryEvmNetwork>(
685 mut executor: Executor<FEN>,
686 calls: &[BasicTxDetails],
687 sequence: &[usize],
688 test_address: Address,
689 calldata: Bytes,
690 options: CheckSequenceOptions<'_>,
691) -> eyre::Result<CheckSequenceOutcome> {
692 let mut calls_executed = 0;
693 let mut reverts = 0;
694 let early = replay_sequence(
695 &mut executor,
696 calls,
697 sequence,
698 options.accumulate_warp_roll,
699 |idx, call_result| {
700 calls_executed += 1;
701 if call_result.result.as_ref() == MAGIC_ASSUME {
705 return Ok(ReplayDecision::Continue(call_result));
706 }
707 if call_result.reverted {
708 reverts += 1;
709 }
710 if did_fail_on_assert(&call_result, &call_result.state_changeset) {
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: assertion_failure_reason(call_result, options.rd),
716 calls_count: calls_executed,
717 reverts,
718 failure_site: Some(site),
719 sequence_assertion_failure: true,
720 }));
721 }
722 if call_result.reverted && options.fail_on_revert {
723 if options.expect_assertion_failure {
724 return Ok(ReplayDecision::Stop(CheckSequenceOutcome {
725 success: true,
726 replayed_entirely: false,
727 reason: None,
728 calls_count: calls_executed,
729 reverts,
730 failure_site: None,
731 sequence_assertion_failure: false,
732 }));
733 }
734 let site = sequence_call_failure_site(&calls[idx], &call_result);
735 return Ok(ReplayDecision::Stop(CheckSequenceOutcome {
736 success: false,
737 replayed_entirely: false,
738 reason: call_failure_reason(call_result, options.rd),
739 calls_count: calls_executed,
740 reverts,
741 failure_site: Some(site),
742 sequence_assertion_failure: false,
743 }));
744 }
745 Ok(ReplayDecision::Continue(call_result))
746 },
747 )?;
748 if let Some(result) = early {
749 return Ok(result);
750 }
751
752 let (success, replayed_entirely, reason, failure_site) =
755 finish_sequence_check(&executor, test_address, calldata, &options)?;
756 Ok(CheckSequenceOutcome {
757 success,
758 replayed_entirely,
759 reason,
760 calls_count: calls_executed,
761 reverts,
762 failure_site,
763 sequence_assertion_failure: false,
764 })
765}
766
767fn finish_sequence_check<FEN: FoundryEvmNetwork>(
768 executor: &Executor<FEN>,
769 test_address: Address,
770 calldata: Bytes,
771 options: &CheckSequenceOptions<'_>,
772) -> eyre::Result<(bool, bool, Option<String>, Option<CheckSequenceFailureSite>)> {
773 let handle_terminal_failure =
774 |call_result: RawCallResult<FEN>, site_kind: TerminalFailureSite| {
775 let should_ignore_failure = options.expect_assertion_failure
776 && !executor.has_global_failure(&call_result.state_changeset)
777 && !did_fail_on_assert(&call_result, &call_result.state_changeset);
778
779 if should_ignore_failure {
780 return (true, true, None, None);
781 }
782
783 let site = terminal_failure_site(site_kind, test_address, &calldata, &call_result);
784 let reason = if options.expect_assertion_failure {
785 assertion_failure_reason(call_result, options.rd)
786 } else {
787 call_failure_reason(call_result, options.rd)
788 };
789
790 (false, true, reason, Some(site))
791 };
792
793 let (invariant_result, mut success) =
794 call_invariant_function(executor, test_address, calldata.clone())?;
795 if !success {
796 return Ok(handle_terminal_failure(invariant_result, TerminalFailureSite::Invariant));
797 }
798
799 if success && options.call_after_invariant {
802 let (after_invariant_result, after_invariant_success) =
803 call_after_invariant_function(executor, test_address)?;
804 success = after_invariant_success;
805 if !success {
806 return Ok(handle_terminal_failure(
807 after_invariant_result,
808 TerminalFailureSite::AfterInvariant,
809 ));
810 }
811 }
812
813 Ok((success, true, None, None))
814}
815
816#[derive(Clone, Copy)]
817enum TerminalFailureSite {
818 Invariant,
819 AfterInvariant,
820}
821
822fn sequence_call_failure_site<FEN: FoundryEvmNetwork>(
823 call: &BasicTxDetails,
824 call_result: &RawCallResult<FEN>,
825) -> CheckSequenceFailureSite {
826 let target = call_result.reverter.unwrap_or(call.call_details.target);
827 let selector = selector_from_calldata(&call.call_details.calldata);
828 let fingerprint =
829 handler_edge_fingerprint(snapshot_edge_fingerprint(call_result), target, selector);
830 CheckSequenceFailureSite::SequenceCall { target, selector, fingerprint }
831}
832
833fn terminal_failure_site<FEN: FoundryEvmNetwork>(
834 kind: TerminalFailureSite,
835 target: Address,
836 calldata: &Bytes,
837 call_result: &RawCallResult<FEN>,
838) -> CheckSequenceFailureSite {
839 let target = call_result.reverter.unwrap_or(target);
840 let selector = match kind {
841 TerminalFailureSite::Invariant => selector_from_calldata(calldata),
842 TerminalFailureSite::AfterInvariant => {
843 Selector::from(IInvariantTest::afterInvariantCall::SELECTOR)
844 }
845 };
846 let fingerprint =
847 handler_edge_fingerprint(snapshot_edge_fingerprint(call_result), target, selector);
848 match kind {
849 TerminalFailureSite::Invariant => {
850 CheckSequenceFailureSite::Invariant { target, selector, fingerprint }
851 }
852 TerminalFailureSite::AfterInvariant => {
853 CheckSequenceFailureSite::AfterInvariant { target, selector, fingerprint }
854 }
855 }
856}
857
858fn selector_from_calldata(calldata: &Bytes) -> Selector {
859 let selector: [u8; 4] = calldata.get(..4).and_then(|s| s.try_into().ok()).unwrap_or_default();
860 Selector::from(selector)
861}
862
863fn call_failure_reason<FEN: FoundryEvmNetwork>(
864 call_result: RawCallResult<FEN>,
865 rd: Option<&RevertDecoder>,
866) -> Option<String> {
867 match call_result.into_evm_error(rd) {
868 EvmError::Execution(err) => Some(err.reason),
869 _ => None,
870 }
871}
872
873fn assertion_failure_reason<FEN: FoundryEvmNetwork>(
874 call_result: RawCallResult<FEN>,
875 rd: Option<&RevertDecoder>,
876) -> Option<String> {
877 call_failure_reason(call_result, rd).or_else(|| Some("assertion failed".to_string()))
878}
879
880#[expect(clippy::too_many_arguments)]
888pub(crate) fn shrink_sequence_value<FEN: FoundryEvmNetwork>(
889 config: &InvariantConfig,
890 invariant_contract: &InvariantContract<'_>,
891 target_invariant: &Function,
892 calls: &[BasicTxDetails],
893 executor: &Executor<FEN>,
894 target_value: I256,
895 progress: &ShrinkProgress<'_>,
896 early_exit: &EarlyExit,
897) -> eyre::Result<Vec<BasicTxDetails>> {
898 trace!(target: "forge::test", "Shrinking optimization sequence of {} calls for target value {}.", calls.len(), target_value);
899
900 let target_address = invariant_contract.address;
901 let calldata: Bytes = target_invariant.selector().to_vec().into();
902
903 if check_sequence_value(executor.clone(), calls, &[], target_address, calldata.clone())?
905 == Some(target_value)
906 {
907 return Ok(vec![]);
908 }
909
910 let replay_failed = Cell::new(false);
911 let mut replay_error = None;
912 let mut run = ShrinkRun::new(config.shrink_run_limit as usize);
913 let initial = SequenceShrink::new(calls.len());
914 progress.update(calls, &initial, true);
915 let mut sequence = Vec::with_capacity(calls.len());
916
917 let shrinker = shrink_sequence_by_removing(
918 calls.len(),
919 &mut run,
920 || early_exit.should_stop() || replay_failed.get(),
921 || progress.inc(),
922 |shrinker| {
923 progress.update(calls, shrinker, true);
924 sequence.clear();
925 sequence.extend(shrinker.current());
926 match check_sequence_value(
927 executor.clone(),
928 calls,
929 &sequence,
930 target_address,
931 calldata.clone(),
932 ) {
933 Ok(Some(value)) => Some(value == target_value),
934 Ok(None) => Some(false),
935 Err(err) => {
936 replay_error = Some(err);
937 replay_failed.set(true);
938 None
939 }
940 }
941 },
942 );
943 progress.update(calls, &shrinker, true);
944 if let Some(err) = replay_error {
945 return Err(err);
946 }
947
948 Ok(build_shrunk_sequence(calls, &shrinker, true))
949}
950
951pub fn replay_handler_failure_sequence<FEN: FoundryEvmNetwork>(
954 mut executor: Executor<FEN>,
955 calls: &[BasicTxDetails],
956 sequence: &[usize],
957 accumulate_warp_roll: bool,
958 rd: Option<&RevertDecoder>,
959) -> eyre::Result<HandlerReplayOutcome> {
960 let Some(&anchor_idx) = sequence.last() else {
961 return Ok(HandlerReplayOutcome {
962 anchor_asserted: false,
963 reverter: Address::ZERO,
964 selector: Selector::ZERO,
965 revert_reason: None,
966 anchor_fingerprint: B256::ZERO,
967 });
968 };
969
970 let outcome = replay_sequence(
971 &mut executor,
972 calls,
973 sequence,
974 accumulate_warp_roll,
975 |idx, call_result| {
976 let asserted = did_fail_on_assert(&call_result, &call_result.state_changeset);
977 if idx == anchor_idx {
978 let snapshot = snapshot_edge_fingerprint(&call_result);
979 let anchor = &calls[anchor_idx];
980 let reverter = call_result.reverter.unwrap_or(anchor.call_details.target);
981 let selector_bytes: [u8; 4] = anchor
982 .call_details
983 .calldata
984 .get(..4)
985 .and_then(|s| s.try_into().ok())
986 .unwrap_or_default();
987 let selector = Selector::from(selector_bytes);
988 let fingerprint = handler_edge_fingerprint(snapshot, reverter, selector);
989 let reason =
990 if asserted { assertion_failure_reason(call_result, rd) } else { None };
991 return Ok(ReplayDecision::Stop(HandlerReplayOutcome {
992 anchor_asserted: asserted,
993 reverter,
994 selector,
995 revert_reason: reason,
996 anchor_fingerprint: fingerprint,
997 }));
998 }
999 if asserted {
1000 return Ok(ReplayDecision::Stop(HandlerReplayOutcome {
1002 anchor_asserted: false,
1003 reverter: Address::ZERO,
1004 selector: Selector::ZERO,
1005 revert_reason: None,
1006 anchor_fingerprint: B256::ZERO,
1007 }));
1008 }
1009 Ok(ReplayDecision::Continue(call_result))
1010 },
1011 )?;
1012
1013 Ok(outcome.unwrap_or(HandlerReplayOutcome {
1014 anchor_asserted: false,
1015 reverter: Address::ZERO,
1016 selector: Selector::ZERO,
1017 revert_reason: None,
1018 anchor_fingerprint: B256::ZERO,
1019 }))
1020}
1021
1022pub(crate) fn shrink_handler_sequence<FEN: FoundryEvmNetwork>(
1025 config: &InvariantConfig,
1026 calls: &[BasicTxDetails],
1027 expected_fingerprint: B256,
1028 executor: &Executor<FEN>,
1029 progress: &ShrinkProgress<'_>,
1030 early_exit: &EarlyExit,
1031) -> eyre::Result<Vec<BasicTxDetails>> {
1032 if calls.is_empty() {
1033 return Ok(vec![]);
1034 }
1035 let accumulate_warp_roll = config.has_delay();
1036 let mut sequence = Vec::with_capacity(calls.len());
1037 let shrinker = run_shrink_loop(
1038 config,
1039 calls,
1040 progress,
1041 accumulate_warp_roll,
1042 early_exit,
1043 ShrinkErrorPolicy::RestoreRemoved,
1044 |shrinker| {
1045 sequence.clear();
1046 sequence.extend(shrinker.current());
1047 handler_sequence_still_triggers_bug(
1048 executor.clone(),
1049 calls,
1050 &sequence,
1051 accumulate_warp_roll,
1052 expected_fingerprint,
1053 )
1054 },
1055 );
1056
1057 let shrunk = build_shrunk_sequence(calls, &shrinker, accumulate_warp_roll);
1058
1059 sequence.clear();
1061 sequence.extend(shrinker.current());
1062 let verified = handler_sequence_still_triggers_bug(
1063 executor.clone(),
1064 calls,
1065 &sequence,
1066 accumulate_warp_roll,
1067 expected_fingerprint,
1068 )
1069 .unwrap_or(false);
1070 if verified { Ok(shrunk) } else { Ok(calls.to_vec()) }
1071}
1072
1073fn handler_sequence_still_triggers_bug<FEN: FoundryEvmNetwork>(
1075 executor: Executor<FEN>,
1076 calls: &[BasicTxDetails],
1077 sequence: &[usize],
1078 accumulate_warp_roll: bool,
1079 expected_fingerprint: B256,
1080) -> eyre::Result<bool> {
1081 let outcome =
1082 replay_handler_failure_sequence(executor, calls, sequence, accumulate_warp_roll, None)?;
1083 Ok(outcome.anchor_asserted && outcome.anchor_fingerprint == expected_fingerprint)
1084}
1085
1086pub fn check_sequence_value<FEN: FoundryEvmNetwork>(
1092 mut executor: Executor<FEN>,
1093 calls: &[BasicTxDetails],
1094 sequence: &[usize],
1095 test_address: Address,
1096 calldata: Bytes,
1097) -> eyre::Result<Option<I256>> {
1098 let mut accumulated_warp = U256::ZERO;
1099 let mut accumulated_roll = U256::ZERO;
1100 let mut seq_iter = sequence.iter().peekable();
1101
1102 for (idx, tx) in calls.iter().enumerate() {
1103 accumulated_warp += tx.warp.unwrap_or(U256::ZERO);
1104 accumulated_roll += tx.roll.unwrap_or(U256::ZERO);
1105
1106 if seq_iter.peek() == Some(&&idx) {
1107 seq_iter.next();
1108
1109 let tx_with_accumulated =
1110 apply_warp_roll(tx.clone(), accumulated_warp, accumulated_roll);
1111 let mut call_result = execute_tx(&mut executor, &tx_with_accumulated)?;
1112
1113 if !call_result.reverted {
1114 executor.commit(&mut call_result);
1115 }
1116
1117 accumulated_warp = U256::ZERO;
1118 accumulated_roll = U256::ZERO;
1119 }
1120 }
1121
1122 apply_warp_roll_to_env(&mut executor, accumulated_warp, accumulated_roll);
1124
1125 let (inv_result, success) = call_invariant_function(&executor, test_address, calldata)?;
1126
1127 if success
1128 && inv_result.result.len() >= 32
1129 && let Some(value) = I256::try_from_be_slice(&inv_result.result[..32])
1130 {
1131 return Ok(Some(value));
1132 }
1133
1134 Ok(None)
1135}
1136
1137#[cfg(test)]
1138mod tests {
1139 use super::{
1140 LIVE_SHRINK_SEQUENCE_EDGE_CALLS, SequenceShrink, ShrinkCandidateKeys, ShrinkErrorPolicy,
1141 ShrinkProgress, ShrinkRun, build_shrunk_sequence, format_shrink_progress_message,
1142 run_shrink_loop, shrink_sequence_by_removing,
1143 };
1144 use crate::executors::EarlyExit;
1145 use alloy_primitives::{Address, Bytes, U256};
1146 use foundry_config::InvariantConfig;
1147 use foundry_evm_fuzz::{BasicTxDetails, CallDetails};
1148
1149 fn tx(warp: Option<u64>, roll: Option<u64>) -> BasicTxDetails {
1150 BasicTxDetails {
1151 warp: warp.map(U256::from),
1152 roll: roll.map(U256::from),
1153 sender: Address::ZERO,
1154 call_details: CallDetails {
1155 target: Address::ZERO,
1156 calldata: Bytes::new(),
1157 value: None,
1158 },
1159 }
1160 }
1161
1162 fn tx_with_calldata(byte: u8) -> BasicTxDetails {
1163 BasicTxDetails {
1164 warp: None,
1165 roll: None,
1166 sender: Address::ZERO,
1167 call_details: CallDetails {
1168 target: Address::ZERO,
1169 calldata: Bytes::from(vec![byte]),
1170 value: None,
1171 },
1172 }
1173 }
1174
1175 #[test]
1176 fn build_shrunk_sequence_accumulates_removed_delay_into_next_kept_call() {
1177 let calls = vec![tx(Some(3), Some(5)), tx(Some(7), Some(11)), tx(Some(13), Some(17))];
1178 let mut shrinker = SequenceShrink::new(calls.len());
1179 shrinker.remove(0);
1180
1181 let shrunk = build_shrunk_sequence(&calls, &shrinker, true);
1182
1183 assert_eq!(shrunk.len(), 2);
1184 assert_eq!(shrunk[0].warp, Some(U256::from(10)));
1185 assert_eq!(shrunk[0].roll, Some(U256::from(16)));
1186 assert_eq!(shrunk[1].warp, Some(U256::from(13)));
1187 assert_eq!(shrunk[1].roll, Some(U256::from(17)));
1188 }
1189
1190 #[test]
1191 fn build_shrunk_sequence_does_not_move_trailing_delay_backward() {
1192 let calls = vec![tx(Some(3), Some(5)), tx(Some(7), Some(11))];
1193 let mut shrinker = SequenceShrink::new(calls.len());
1194 shrinker.remove(1);
1195
1196 let shrunk = build_shrunk_sequence(&calls, &shrinker, true);
1197
1198 assert_eq!(shrunk.len(), 1);
1199 assert_eq!(shrunk[0].warp, Some(U256::from(3)));
1200 assert_eq!(shrunk[0].roll, Some(U256::from(5)));
1201 }
1202
1203 #[test]
1204 fn shrink_run_counts_attempts_and_accepts() {
1205 let mut run = ShrinkRun::new(2);
1206
1207 assert_eq!(run.remaining_attempts(), 2);
1208 assert!(!run.try_candidate(|| false));
1209 assert!(run.try_candidate(|| true));
1210
1211 let mut called_after_limit = false;
1212 assert!(!run.try_candidate(|| {
1213 called_after_limit = true;
1214 true
1215 }));
1216
1217 assert!(!called_after_limit);
1218 let stats = run.finish();
1219 assert_eq!(stats.attempts, 2);
1220 assert_eq!(stats.accepted, 1);
1221 }
1222
1223 #[test]
1224 fn shrink_candidate_keys_skip_duplicates() {
1225 let mut candidates = ShrinkCandidateKeys::new("initial");
1226
1227 assert!(!candidates.insert("initial"));
1228 assert!(candidates.insert("first"));
1229 assert!(!candidates.insert("first"));
1230 assert!(candidates.insert("second"));
1231 }
1232
1233 #[test]
1234 fn shrink_loop_keep_removed_treats_candidate_error_as_still_failing() {
1235 let config = InvariantConfig { shrink_run_limit: 1, ..Default::default() };
1236 let early_exit = EarlyExit::new(false);
1237 let calls = vec![tx(None, None), tx(None, None)];
1238 let progress = ShrinkProgress::new(&config, None, "test", None, None, false);
1239
1240 let shrinker = run_shrink_loop(
1241 &config,
1242 &calls,
1243 &progress,
1244 false,
1245 &early_exit,
1246 ShrinkErrorPolicy::KeepRemoved,
1247 |_| Err(eyre::eyre!("candidate replay failed")),
1248 );
1249
1250 assert_eq!(shrinker.current().collect::<Vec<_>>(), vec![1]);
1251 }
1252
1253 #[test]
1254 fn shrink_loop_limit_counts_candidate_replays_not_skipped_indices() {
1255 let config = InvariantConfig { shrink_run_limit: 4, ..Default::default() };
1256 let early_exit = EarlyExit::new(false);
1257 let calls = vec![tx(None, None), tx(None, None), tx(None, None)];
1258 let progress = ShrinkProgress::new(&config, None, "test", None, None, false);
1259 let mut replay_attempts = 0;
1260
1261 let shrinker = run_shrink_loop(
1262 &config,
1263 &calls,
1264 &progress,
1265 false,
1266 &early_exit,
1267 ShrinkErrorPolicy::RestoreRemoved,
1268 |_| {
1269 replay_attempts += 1;
1270 Ok(matches!(replay_attempts, 1 | 4))
1271 },
1272 );
1273
1274 assert_eq!(replay_attempts, 4);
1275 assert_eq!(shrinker.current().collect::<Vec<_>>(), vec![2]);
1276 }
1277
1278 #[test]
1279 fn sequence_shrinker_skips_duplicate_candidates_without_spending_attempts() {
1280 let mut run = ShrinkRun::new(2);
1281 let mut seen = Vec::new();
1282
1283 let shrinker = shrink_sequence_by_removing(
1284 2,
1285 &mut run,
1286 || false,
1287 || {},
1288 |shrinker| {
1289 let candidate = shrinker.current().collect::<Vec<_>>();
1290 if seen.contains(&candidate) {
1291 None
1292 } else {
1293 seen.push(candidate);
1294 Some(false)
1295 }
1296 },
1297 );
1298
1299 assert_eq!(shrinker.current().collect::<Vec<_>>(), vec![0, 1]);
1300 let stats = run.finish();
1301 assert_eq!(stats.attempts, 2);
1302 assert_eq!(stats.accepted, 0);
1303 }
1304
1305 #[test]
1306 fn shrink_progress_message_renders_current_sequence() {
1307 let calls = vec![tx_with_calldata(1), tx_with_calldata(2)];
1308
1309 let message =
1310 format_shrink_progress_message(" Shrink: invariant_live", &calls, None, false);
1311
1312 assert!(message.contains(" Shrink: invariant_live"));
1313 assert!(message.contains("[Sequence] (shrunk: 2)"));
1314 assert!(message.contains("calldata=0x01 args=[]"));
1315 assert!(message.contains("calldata=0x02 args=[]"));
1316 }
1317
1318 #[test]
1319 fn shrink_progress_message_omits_middle_of_large_sequence() {
1320 let calls = (0..(LIVE_SHRINK_SEQUENCE_EDGE_CALLS * 2 + 3))
1321 .map(|idx| tx_with_calldata(idx as u8))
1322 .collect::<Vec<_>>();
1323
1324 let message =
1325 format_shrink_progress_message(" Shrink: invariant_live", &calls, None, false);
1326
1327 assert!(message.contains("[Sequence] (shrunk: 35)"));
1328 assert!(message.contains("... 3 call(s) omitted ..."));
1329 assert_eq!(message.matches("sender=").count(), LIVE_SHRINK_SEQUENCE_EDGE_CALLS * 2);
1330 assert!(message.contains("calldata=0x00 args=[]"));
1331 assert!(message.contains("calldata=0x22 args=[]"));
1332 }
1333}