1use crate::executors::{
2 EarlyExit, EvmError, Executor, RawCallResult,
3 campaign::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::{
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, 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_iter([initial]) }
175 }
176
177 pub fn insert(&mut self, key: K) -> bool {
178 self.seen.insert(key)
179 }
180}
181
182enum ReplayDecision<T> {
184 Stop(T),
185 Continue,
186}
187
188pub struct CheckSequenceOptions<'a> {
190 pub accumulate_warp_roll: bool,
191 pub fail_on_revert: bool,
192 pub expect_assertion_failure: bool,
193 pub call_after_invariant: bool,
194 pub rd: Option<&'a RevertDecoder>,
195}
196
197#[derive(Clone, Copy, Debug, PartialEq, Eq)]
199pub enum CheckSequenceFailureSite {
200 SequenceCall { target: Address, selector: Selector, fingerprint: B256 },
201 Invariant { target: Address, selector: Selector, fingerprint: B256 },
202 AfterInvariant { target: Address, selector: Selector, fingerprint: B256 },
203}
204
205#[derive(Clone, Debug)]
207pub struct CheckSequenceOutcome {
208 pub success: bool,
209 pub replayed_entirely: bool,
210 pub reason: Option<String>,
211 pub calls_count: usize,
212 pub reverts: usize,
213 pub failure_site: Option<CheckSequenceFailureSite>,
214 pub sequence_assertion_failure: bool,
217}
218
219pub struct ShrunkSequence {
220 pub calls: Vec<BasicTxDetails>,
221 pub result: Option<CheckSequenceOutcome>,
222}
223
224#[derive(Debug)]
227pub struct HandlerReplayOutcome {
228 pub anchor_asserted: bool,
229 pub reverter: Address,
230 pub selector: Selector,
231 pub revert_reason: Option<String>,
232 pub anchor_fingerprint: B256,
234}
235
236pub(crate) fn reset_shrink_progress(
239 config: &InvariantConfig,
240 progress: Option<&ProgressBar>,
241 label: &str,
242 position: Option<(usize, usize)>,
243) -> String {
244 let message = match position {
245 Some((current, total)) if total > 1 => {
246 format!(" [{current}/{total}] Shrink: {label}")
247 }
248 _ => format!(" Shrink: {label}"),
249 };
250 if let Some(progress) = progress {
251 progress.set_length(config.shrink_run_limit as u64);
252 progress.reset();
253 progress.set_message(message.clone());
254 }
255 message
256}
257
258pub(crate) struct ShrinkProgress<'a> {
261 progress: Option<&'a ProgressBar>,
262 message: String,
263 identified_contracts: Option<&'a ContractsByAddress>,
264 show_solidity: bool,
265}
266
267impl<'a> ShrinkProgress<'a> {
268 pub(crate) fn new(
269 config: &InvariantConfig,
270 progress: Option<&'a ProgressBar>,
271 label: &str,
272 position: Option<(usize, usize)>,
273 identified_contracts: Option<&'a ContractsByAddress>,
274 show_solidity: bool,
275 ) -> Self {
276 let message = reset_shrink_progress(config, progress, label, position);
277 Self { progress, message, identified_contracts, show_solidity }
278 }
279
280 fn inc(&self) {
281 if let Some(progress) = self.progress {
282 progress.inc(1);
283 }
284 }
285
286 fn update(
287 &self,
288 calls: &[BasicTxDetails],
289 shrinker: &SequenceShrink,
290 accumulate_warp_roll: bool,
291 ) {
292 let Some(progress) = self.progress else {
293 return;
294 };
295 if progress.is_hidden() {
296 return;
297 }
298
299 let sequence = build_shrunk_sequence(calls, shrinker, accumulate_warp_roll);
300 let message = format_shrink_progress_message(
301 &self.message,
302 &sequence,
303 self.identified_contracts,
304 self.show_solidity,
305 );
306 progress.set_message(message);
307 }
308}
309
310fn format_shrink_progress_message(
311 phase: &str,
312 sequence: &[BasicTxDetails],
313 identified_contracts: Option<&ContractsByAddress>,
314 show_solidity: bool,
315) -> String {
316 let mut message = String::with_capacity(phase.len() + sequence.len().min(32) * 96);
317 message.push_str(phase);
318 write!(message, "\n\t[Sequence] (shrunk: {})", sequence.len()).unwrap();
319
320 if sequence.len() <= LIVE_SHRINK_SEQUENCE_EDGE_CALLS * 2 {
321 for tx in sequence {
322 push_shrink_progress_call(&mut message, tx, identified_contracts, show_solidity);
323 }
324 return message;
325 }
326
327 for tx in &sequence[..LIVE_SHRINK_SEQUENCE_EDGE_CALLS] {
328 push_shrink_progress_call(&mut message, tx, identified_contracts, show_solidity);
329 }
330 writeln!(
331 message,
332 "\n\t\t... {} call(s) omitted ...",
333 sequence.len() - LIVE_SHRINK_SEQUENCE_EDGE_CALLS * 2
334 )
335 .unwrap();
336 for tx in &sequence[sequence.len() - LIVE_SHRINK_SEQUENCE_EDGE_CALLS..] {
337 push_shrink_progress_call(&mut message, tx, identified_contracts, show_solidity);
338 }
339 message
340}
341
342fn push_shrink_progress_call(
343 message: &mut String,
344 tx: &BasicTxDetails,
345 identified_contracts: Option<&ContractsByAddress>,
346 show_solidity: bool,
347) {
348 let empty_contracts;
349 let identified_contracts = if let Some(identified_contracts) = identified_contracts {
350 identified_contracts
351 } else {
352 empty_contracts = ContractsByAddress::default();
353 &empty_contracts
354 };
355
356 let call =
357 BaseCounterExample::from_invariant_call(tx, identified_contracts, None, show_solidity)
358 .to_string();
359 for line in call.lines() {
360 message.push('\n');
361 message.push_str(line);
362 }
363}
364
365fn apply_warp_roll(mut result: BasicTxDetails, warp: U256, roll: U256) -> BasicTxDetails {
367 if warp > U256::ZERO {
368 result.warp = Some(warp);
369 }
370 if roll > U256::ZERO {
371 result.roll = Some(roll);
372 }
373 result
374}
375
376fn apply_warp_roll_to_env<FEN: FoundryEvmNetwork>(
378 executor: &mut Executor<FEN>,
379 warp: U256,
380 roll: U256,
381) {
382 if warp > U256::ZERO || roll > U256::ZERO {
383 let ts = executor.evm_env().block_env.timestamp();
384 let num = executor.evm_env().block_env.number();
385 executor.evm_env_mut().block_env.set_timestamp(ts + warp);
386 executor.evm_env_mut().block_env.set_number(num + roll);
387
388 let block_env = executor.evm_env().block_env.clone();
389 if let Some(cheatcodes) = executor.inspector_mut().cheatcodes.as_mut() {
390 if let Some(block) = cheatcodes.block.as_mut() {
391 let bts = block.timestamp();
392 let bnum = block.number();
393 block.set_timestamp(bts + warp);
394 block.set_number(bnum + roll);
395 } else {
396 cheatcodes.block = Some(block_env);
397 }
398 }
399 }
400}
401
402fn build_shrunk_sequence(
407 calls: &[BasicTxDetails],
408 shrinker: &SequenceShrink,
409 accumulate_warp_roll: bool,
410) -> Vec<BasicTxDetails> {
411 if !accumulate_warp_roll {
412 return shrinker.apply(calls);
413 }
414
415 shrinker.apply_with_accumulated_delay(calls, |call| (call.warp, call.roll), apply_warp_roll)
416}
417
418pub fn shrink_sequence_by_removing<P, S, A>(
421 calls_len: usize,
422 run: &mut ShrinkRun,
423 mut should_stop: S,
424 mut on_attempt: A,
425 mut predicate: P,
426) -> SequenceShrink
427where
428 P: FnMut(&SequenceShrink) -> Option<bool>,
429 S: FnMut() -> bool,
430 A: FnMut(),
431{
432 let mut shrinker = SequenceShrink::new(calls_len);
433 let mut call_idx = 0;
434 let mut skipped_candidates = 0usize;
435
436 while run.can_try() {
437 if should_stop() {
438 break;
439 }
440 let included_count = shrinker.included_count();
441 if included_count == 0 || skipped_candidates >= included_count {
442 break;
443 }
444
445 if !shrinker.contains(call_idx) {
447 call_idx = shrinker.next_index(call_idx);
448 continue;
449 }
450
451 shrinker.remove(call_idx);
452
453 let Some(accepted) = run.try_candidate_decision(|| predicate(&shrinker)) else {
454 shrinker.restore(call_idx);
455 skipped_candidates += 1;
456 call_idx = shrinker.next_index(call_idx);
457 continue;
458 };
459
460 on_attempt();
461 skipped_candidates = 0;
462 if accepted {
463 if shrinker.included_count() == 1 {
464 break;
465 }
466 } else {
467 shrinker.restore(call_idx);
468 }
469
470 call_idx = shrinker.next_index(call_idx);
471 }
472
473 shrinker
474}
475
476fn run_shrink_loop<P>(
479 config: &InvariantConfig,
480 calls: &[BasicTxDetails],
481 progress: &ShrinkProgress<'_>,
482 accumulate_warp_roll: bool,
483 early_exit: &EarlyExit,
484 error_policy: ShrinkErrorPolicy,
485 mut predicate: P,
486) -> SequenceShrink
487where
488 P: FnMut(&SequenceShrink) -> eyre::Result<bool>,
489{
490 let mut run = ShrinkRun::new(config.shrink_run_limit as usize);
491 let initial = SequenceShrink::new(calls.len());
492 progress.update(calls, &initial, accumulate_warp_roll);
493
494 let shrinker = shrink_sequence_by_removing(
495 calls.len(),
496 &mut run,
497 || early_exit.should_stop(),
498 || progress.inc(),
499 |shrinker| {
500 progress.update(calls, shrinker, accumulate_warp_roll);
501 match predicate(shrinker) {
502 Ok(bug_still_present) => Some(bug_still_present),
503 Err(_) => Some(matches!(error_policy, ShrinkErrorPolicy::KeepRemoved)),
504 }
505 },
506 );
507 progress.update(calls, &shrinker, accumulate_warp_roll);
508 shrinker
509}
510
511#[expect(clippy::too_many_arguments)]
512pub(crate) fn shrink_sequence<FEN: FoundryEvmNetwork>(
513 config: &InvariantConfig,
514 invariant_contract: &InvariantContract<'_>,
515 target_invariant: &Function,
516 calls: &[BasicTxDetails],
517 expect_assertion_failure: bool,
518 executor: &Executor<FEN>,
519 rd: Option<&RevertDecoder>,
520 progress: &ShrinkProgress<'_>,
521 early_exit: &EarlyExit,
522) -> eyre::Result<ShrunkSequence> {
523 trace!(target: "forge::test", "Shrinking sequence of {} calls.", calls.len());
524
525 let target_address = invariant_contract.address;
526 let calldata: Bytes = target_invariant.selector().to_vec().into();
527 let (_, success) = call_invariant_function(executor, target_address, calldata.clone())?;
530 if !success {
531 return Ok(ShrunkSequence { calls: vec![], result: None });
532 }
533
534 let accumulate_warp_roll = config.has_delay();
535 let mut sequence = Vec::with_capacity(calls.len());
536 let mut last_result = None;
537 let mut last_result_matches_shrinker = true;
538 let shrinker = run_shrink_loop(
539 config,
540 calls,
541 progress,
542 accumulate_warp_roll,
543 early_exit,
544 ShrinkErrorPolicy::KeepRemoved,
547 |shrinker| {
548 sequence.clear();
549 sequence.extend(shrinker.current());
550 let result = match check_sequence(
551 executor.clone(),
552 calls,
553 &sequence,
554 target_address,
555 calldata.clone(),
556 CheckSequenceOptions {
557 accumulate_warp_roll,
558 fail_on_revert: config.fail_on_revert,
559 expect_assertion_failure,
560 call_after_invariant: invariant_contract.call_after_invariant,
561 rd,
562 },
563 ) {
564 Ok(result) => result,
565 Err(err) => {
566 last_result_matches_shrinker = false;
567 return Err(err);
568 }
569 };
570 let bug_still_present = !result.success;
572 if bug_still_present {
573 last_result = Some(result);
574 last_result_matches_shrinker = true;
575 }
576 Ok(bug_still_present)
577 },
578 );
579
580 let shrunk = build_shrunk_sequence(calls, &shrinker, accumulate_warp_roll);
581 let result = if last_result_matches_shrinker {
582 last_result
583 } else {
584 match check_sequence(
585 executor.clone(),
586 &shrunk,
587 &(0..shrunk.len()).collect::<Vec<_>>(),
588 target_address,
589 calldata,
590 CheckSequenceOptions {
591 accumulate_warp_roll: false,
592 fail_on_revert: config.fail_on_revert,
593 expect_assertion_failure,
594 call_after_invariant: invariant_contract.call_after_invariant,
595 rd,
596 },
597 ) {
598 Ok(result) => Some(result),
599 Err(err) => {
600 trace!(target: "forge::test", %err, "failed to recompute shrunk replay metrics");
601 None
602 }
603 }
604 };
605
606 Ok(ShrunkSequence { calls: shrunk, result })
607}
608
609fn replay_sequence<FEN, T, F>(
613 executor: &mut Executor<FEN>,
614 calls: &[BasicTxDetails],
615 sequence: &[usize],
616 accumulate_warp_roll: bool,
617 mut on_call: F,
618) -> eyre::Result<Option<T>>
619where
620 FEN: FoundryEvmNetwork,
621 F: FnMut(usize, RawCallResult<FEN>) -> eyre::Result<ReplayDecision<T>>,
622{
623 if !accumulate_warp_roll {
626 for &idx in sequence {
627 let (_, call_result) = execute_invariant_replay_tx(executor, &calls[idx])?;
628 match on_call(idx, call_result)? {
629 ReplayDecision::Stop(val) => return Ok(Some(val)),
630 ReplayDecision::Continue => {}
631 }
632 }
633 return Ok(None);
634 }
635
636 let mut accumulated_warp = U256::ZERO;
639 let mut accumulated_roll = U256::ZERO;
640 let mut seq_iter = sequence.iter().peekable();
641
642 for (idx, tx) in calls.iter().enumerate() {
643 accumulated_warp += tx.warp.unwrap_or(U256::ZERO);
644 accumulated_roll += tx.roll.unwrap_or(U256::ZERO);
645 if seq_iter.peek() != Some(&&idx) {
646 continue;
647 }
648 seq_iter.next();
649
650 let executed = apply_warp_roll(tx.clone(), accumulated_warp, accumulated_roll);
651 let (_, call_result) = execute_invariant_replay_tx(executor, &executed)?;
652
653 match on_call(idx, call_result)? {
654 ReplayDecision::Stop(val) => return Ok(Some(val)),
655 ReplayDecision::Continue => {}
656 }
657
658 accumulated_warp = U256::ZERO;
659 accumulated_roll = U256::ZERO;
660 }
661
662 Ok(None)
663}
664
665pub fn check_sequence<FEN: FoundryEvmNetwork>(
675 mut executor: Executor<FEN>,
676 calls: &[BasicTxDetails],
677 sequence: &[usize],
678 test_address: Address,
679 calldata: Bytes,
680 options: CheckSequenceOptions<'_>,
681) -> eyre::Result<CheckSequenceOutcome> {
682 let mut calls_executed = 0;
683 let mut reverts = 0;
684 let early = replay_sequence(
685 &mut executor,
686 calls,
687 sequence,
688 options.accumulate_warp_roll,
689 |idx, call_result| {
690 calls_executed += 1;
691 if call_result.result.as_ref() == MAGIC_ASSUME {
695 return Ok(ReplayDecision::Continue);
696 }
697 if call_result.reverted {
698 reverts += 1;
699 }
700 if did_fail_on_assert(&call_result, &call_result.state_changeset) {
701 let site = sequence_call_failure_site(&calls[idx], &call_result);
702 return Ok(ReplayDecision::Stop(CheckSequenceOutcome {
703 success: false,
704 replayed_entirely: false,
705 reason: assertion_failure_reason(call_result, options.rd),
706 calls_count: calls_executed,
707 reverts,
708 failure_site: Some(site),
709 sequence_assertion_failure: true,
710 }));
711 }
712 if call_result.reverted && options.fail_on_revert {
713 if options.expect_assertion_failure {
714 return Ok(ReplayDecision::Stop(CheckSequenceOutcome {
715 success: true,
716 replayed_entirely: false,
717 reason: None,
718 calls_count: calls_executed,
719 reverts,
720 failure_site: None,
721 sequence_assertion_failure: false,
722 }));
723 }
724 let site = sequence_call_failure_site(&calls[idx], &call_result);
725 return Ok(ReplayDecision::Stop(CheckSequenceOutcome {
726 success: false,
727 replayed_entirely: false,
728 reason: call_failure_reason(call_result, options.rd),
729 calls_count: calls_executed,
730 reverts,
731 failure_site: Some(site),
732 sequence_assertion_failure: false,
733 }));
734 }
735 Ok(ReplayDecision::Continue)
736 },
737 )?;
738 if let Some(result) = early {
739 return Ok(result);
740 }
741
742 let (success, replayed_entirely, reason, failure_site) =
745 finish_sequence_check(&executor, test_address, calldata, &options)?;
746 Ok(CheckSequenceOutcome {
747 success,
748 replayed_entirely,
749 reason,
750 calls_count: calls_executed,
751 reverts,
752 failure_site,
753 sequence_assertion_failure: false,
754 })
755}
756
757fn finish_sequence_check<FEN: FoundryEvmNetwork>(
758 executor: &Executor<FEN>,
759 test_address: Address,
760 calldata: Bytes,
761 options: &CheckSequenceOptions<'_>,
762) -> eyre::Result<(bool, bool, Option<String>, Option<CheckSequenceFailureSite>)> {
763 let handle_terminal_failure =
764 |call_result: RawCallResult<FEN>, site_kind: TerminalFailureSite| {
765 let should_ignore_failure = options.expect_assertion_failure
766 && !executor.has_global_failure(&call_result.state_changeset)
767 && !did_fail_on_assert(&call_result, &call_result.state_changeset);
768
769 if should_ignore_failure {
770 return (true, true, None, None);
771 }
772
773 let site = terminal_failure_site(site_kind, test_address, &calldata, &call_result);
774 let reason = if options.expect_assertion_failure {
775 assertion_failure_reason(call_result, options.rd)
776 } else {
777 call_failure_reason(call_result, options.rd)
778 };
779
780 (false, true, reason, Some(site))
781 };
782
783 let (invariant_result, mut success) =
784 call_invariant_function(executor, test_address, calldata.clone())?;
785 if !success {
786 return Ok(handle_terminal_failure(invariant_result, TerminalFailureSite::Invariant));
787 }
788
789 if success && options.call_after_invariant {
792 let (after_invariant_result, after_invariant_success) =
793 call_after_invariant_function(executor, test_address)?;
794 success = after_invariant_success;
795 if !success {
796 return Ok(handle_terminal_failure(
797 after_invariant_result,
798 TerminalFailureSite::AfterInvariant,
799 ));
800 }
801 }
802
803 Ok((success, true, None, None))
804}
805
806#[derive(Clone, Copy)]
807enum TerminalFailureSite {
808 Invariant,
809 AfterInvariant,
810}
811
812fn sequence_call_failure_site<FEN: FoundryEvmNetwork>(
813 call: &BasicTxDetails,
814 call_result: &RawCallResult<FEN>,
815) -> CheckSequenceFailureSite {
816 let target = call_result.reverter.unwrap_or(call.call_details.target);
817 let selector = selector_from_calldata(&call.call_details.calldata);
818 let fingerprint =
819 handler_edge_fingerprint(snapshot_edge_fingerprint(call_result), target, selector);
820 CheckSequenceFailureSite::SequenceCall { target, selector, fingerprint }
821}
822
823fn terminal_failure_site<FEN: FoundryEvmNetwork>(
824 kind: TerminalFailureSite,
825 target: Address,
826 calldata: &Bytes,
827 call_result: &RawCallResult<FEN>,
828) -> CheckSequenceFailureSite {
829 let target = call_result.reverter.unwrap_or(target);
830 let selector = match kind {
831 TerminalFailureSite::Invariant => selector_from_calldata(calldata),
832 TerminalFailureSite::AfterInvariant => {
833 Selector::from(IInvariantTest::afterInvariantCall::SELECTOR)
834 }
835 };
836 let fingerprint =
837 handler_edge_fingerprint(snapshot_edge_fingerprint(call_result), target, selector);
838 match kind {
839 TerminalFailureSite::Invariant => {
840 CheckSequenceFailureSite::Invariant { target, selector, fingerprint }
841 }
842 TerminalFailureSite::AfterInvariant => {
843 CheckSequenceFailureSite::AfterInvariant { target, selector, fingerprint }
844 }
845 }
846}
847
848fn selector_from_calldata(calldata: &Bytes) -> Selector {
849 let selector: [u8; 4] = calldata.get(..4).and_then(|s| s.try_into().ok()).unwrap_or_default();
850 Selector::from(selector)
851}
852
853fn call_failure_reason<FEN: FoundryEvmNetwork>(
854 call_result: RawCallResult<FEN>,
855 rd: Option<&RevertDecoder>,
856) -> Option<String> {
857 match call_result.into_evm_error(rd) {
858 EvmError::Execution(err) => Some(err.reason),
859 _ => None,
860 }
861}
862
863fn assertion_failure_reason<FEN: FoundryEvmNetwork>(
864 call_result: RawCallResult<FEN>,
865 rd: Option<&RevertDecoder>,
866) -> Option<String> {
867 call_failure_reason(call_result, rd).or_else(|| Some("assertion failed".to_string()))
868}
869
870#[expect(clippy::too_many_arguments)]
878pub(crate) fn shrink_sequence_value<FEN: FoundryEvmNetwork>(
879 config: &InvariantConfig,
880 invariant_contract: &InvariantContract<'_>,
881 target_invariant: &Function,
882 calls: &[BasicTxDetails],
883 executor: &Executor<FEN>,
884 target_value: I256,
885 progress: &ShrinkProgress<'_>,
886 early_exit: &EarlyExit,
887) -> eyre::Result<Vec<BasicTxDetails>> {
888 trace!(target: "forge::test", "Shrinking optimization sequence of {} calls for target value {}.", calls.len(), target_value);
889
890 let target_address = invariant_contract.address;
891 let calldata: Bytes = target_invariant.selector().to_vec().into();
892
893 if check_sequence_value(executor.clone(), calls, &[], target_address, calldata.clone())?
895 == Some(target_value)
896 {
897 return Ok(vec![]);
898 }
899
900 let replay_failed = Cell::new(false);
901 let mut replay_error = None;
902 let mut run = ShrinkRun::new(config.shrink_run_limit as usize);
903 let initial = SequenceShrink::new(calls.len());
904 progress.update(calls, &initial, true);
905 let mut sequence = Vec::with_capacity(calls.len());
906
907 let shrinker = shrink_sequence_by_removing(
908 calls.len(),
909 &mut run,
910 || early_exit.should_stop() || replay_failed.get(),
911 || progress.inc(),
912 |shrinker| {
913 progress.update(calls, shrinker, true);
914 sequence.clear();
915 sequence.extend(shrinker.current());
916 match check_sequence_value(
917 executor.clone(),
918 calls,
919 &sequence,
920 target_address,
921 calldata.clone(),
922 ) {
923 Ok(Some(value)) => Some(value == target_value),
924 Ok(None) => Some(false),
925 Err(err) => {
926 replay_error = Some(err);
927 replay_failed.set(true);
928 None
929 }
930 }
931 },
932 );
933 progress.update(calls, &shrinker, true);
934 if let Some(err) = replay_error {
935 return Err(err);
936 }
937
938 Ok(build_shrunk_sequence(calls, &shrinker, true))
939}
940
941pub fn replay_handler_failure_sequence<FEN: FoundryEvmNetwork>(
944 mut executor: Executor<FEN>,
945 calls: &[BasicTxDetails],
946 sequence: &[usize],
947 accumulate_warp_roll: bool,
948 rd: Option<&RevertDecoder>,
949) -> eyre::Result<HandlerReplayOutcome> {
950 let Some(&anchor_idx) = sequence.last() else {
951 return Ok(HandlerReplayOutcome {
952 anchor_asserted: false,
953 reverter: Address::ZERO,
954 selector: Selector::ZERO,
955 revert_reason: None,
956 anchor_fingerprint: B256::ZERO,
957 });
958 };
959
960 let outcome = replay_sequence(
961 &mut executor,
962 calls,
963 sequence,
964 accumulate_warp_roll,
965 |idx, call_result| {
966 let asserted = did_fail_on_assert(&call_result, &call_result.state_changeset);
967 if idx == anchor_idx {
968 let snapshot = snapshot_edge_fingerprint(&call_result);
969 let anchor = &calls[anchor_idx];
970 let reverter = call_result.reverter.unwrap_or(anchor.call_details.target);
971 let selector_bytes: [u8; 4] = anchor
972 .call_details
973 .calldata
974 .get(..4)
975 .and_then(|s| s.try_into().ok())
976 .unwrap_or_default();
977 let selector = Selector::from(selector_bytes);
978 let fingerprint = handler_edge_fingerprint(snapshot, reverter, selector);
979 let reason =
980 if asserted { assertion_failure_reason(call_result, rd) } else { None };
981 return Ok(ReplayDecision::Stop(HandlerReplayOutcome {
982 anchor_asserted: asserted,
983 reverter,
984 selector,
985 revert_reason: reason,
986 anchor_fingerprint: fingerprint,
987 }));
988 }
989 if asserted {
990 return Ok(ReplayDecision::Stop(HandlerReplayOutcome {
992 anchor_asserted: false,
993 reverter: Address::ZERO,
994 selector: Selector::ZERO,
995 revert_reason: None,
996 anchor_fingerprint: B256::ZERO,
997 }));
998 }
999 Ok(ReplayDecision::Continue)
1000 },
1001 )?;
1002
1003 Ok(outcome.unwrap_or(HandlerReplayOutcome {
1004 anchor_asserted: false,
1005 reverter: Address::ZERO,
1006 selector: Selector::ZERO,
1007 revert_reason: None,
1008 anchor_fingerprint: B256::ZERO,
1009 }))
1010}
1011
1012pub(crate) fn shrink_handler_sequence<FEN: FoundryEvmNetwork>(
1015 config: &InvariantConfig,
1016 calls: &[BasicTxDetails],
1017 expected_fingerprint: B256,
1018 executor: &Executor<FEN>,
1019 progress: &ShrinkProgress<'_>,
1020 early_exit: &EarlyExit,
1021) -> eyre::Result<Vec<BasicTxDetails>> {
1022 if calls.is_empty() {
1023 return Ok(vec![]);
1024 }
1025 let accumulate_warp_roll = config.has_delay();
1026 let mut sequence = Vec::with_capacity(calls.len());
1027 let shrinker = run_shrink_loop(
1028 config,
1029 calls,
1030 progress,
1031 accumulate_warp_roll,
1032 early_exit,
1033 ShrinkErrorPolicy::RestoreRemoved,
1034 |shrinker| {
1035 sequence.clear();
1036 sequence.extend(shrinker.current());
1037 handler_sequence_still_triggers_bug(
1038 executor.clone(),
1039 calls,
1040 &sequence,
1041 accumulate_warp_roll,
1042 expected_fingerprint,
1043 )
1044 },
1045 );
1046
1047 let shrunk = build_shrunk_sequence(calls, &shrinker, accumulate_warp_roll);
1048
1049 sequence.clear();
1051 sequence.extend(shrinker.current());
1052 let verified = handler_sequence_still_triggers_bug(
1053 executor.clone(),
1054 calls,
1055 &sequence,
1056 accumulate_warp_roll,
1057 expected_fingerprint,
1058 )
1059 .unwrap_or(false);
1060 if verified { Ok(shrunk) } else { Ok(calls.to_vec()) }
1061}
1062
1063fn handler_sequence_still_triggers_bug<FEN: FoundryEvmNetwork>(
1065 executor: Executor<FEN>,
1066 calls: &[BasicTxDetails],
1067 sequence: &[usize],
1068 accumulate_warp_roll: bool,
1069 expected_fingerprint: B256,
1070) -> eyre::Result<bool> {
1071 let outcome =
1072 replay_handler_failure_sequence(executor, calls, sequence, accumulate_warp_roll, None)?;
1073 Ok(outcome.anchor_asserted && outcome.anchor_fingerprint == expected_fingerprint)
1074}
1075
1076pub fn check_sequence_value<FEN: FoundryEvmNetwork>(
1082 mut executor: Executor<FEN>,
1083 calls: &[BasicTxDetails],
1084 sequence: &[usize],
1085 test_address: Address,
1086 calldata: Bytes,
1087) -> eyre::Result<Option<I256>> {
1088 let mut accumulated_warp = U256::ZERO;
1089 let mut accumulated_roll = U256::ZERO;
1090 let mut seq_iter = sequence.iter().peekable();
1091
1092 for (idx, tx) in calls.iter().enumerate() {
1093 accumulated_warp += tx.warp.unwrap_or(U256::ZERO);
1094 accumulated_roll += tx.roll.unwrap_or(U256::ZERO);
1095
1096 if seq_iter.peek() == Some(&&idx) {
1097 seq_iter.next();
1098
1099 let tx_with_accumulated =
1100 apply_warp_roll(tx.clone(), accumulated_warp, accumulated_roll);
1101 execute_invariant_replay_tx(&mut executor, &tx_with_accumulated)?;
1102
1103 accumulated_warp = U256::ZERO;
1104 accumulated_roll = U256::ZERO;
1105 }
1106 }
1107
1108 apply_warp_roll_to_env(&mut executor, accumulated_warp, accumulated_roll);
1110
1111 let (inv_result, success) = call_invariant_function(&executor, test_address, calldata)?;
1112
1113 if success
1114 && inv_result.result.len() >= 32
1115 && let Some(value) = I256::try_from_be_slice(&inv_result.result[..32])
1116 {
1117 return Ok(Some(value));
1118 }
1119
1120 Ok(None)
1121}
1122
1123#[cfg(test)]
1124mod tests {
1125 use super::{
1126 LIVE_SHRINK_SEQUENCE_EDGE_CALLS, SequenceShrink, ShrinkCandidateKeys, ShrinkErrorPolicy,
1127 ShrinkProgress, ShrinkRun, build_shrunk_sequence, format_shrink_progress_message,
1128 run_shrink_loop, shrink_sequence_by_removing,
1129 };
1130 use crate::executors::EarlyExit;
1131 use alloy_primitives::{Address, Bytes, U256};
1132 use foundry_config::InvariantConfig;
1133 use foundry_evm_fuzz::{BasicTxDetails, CallDetails};
1134
1135 fn tx(warp: Option<u64>, roll: Option<u64>) -> BasicTxDetails {
1136 BasicTxDetails {
1137 warp: warp.map(U256::from),
1138 roll: roll.map(U256::from),
1139 sender: Address::ZERO,
1140 call_details: CallDetails {
1141 target: Address::ZERO,
1142 calldata: Bytes::new(),
1143 value: None,
1144 },
1145 }
1146 }
1147
1148 fn tx_with_calldata(byte: u8) -> BasicTxDetails {
1149 BasicTxDetails {
1150 warp: None,
1151 roll: None,
1152 sender: Address::ZERO,
1153 call_details: CallDetails {
1154 target: Address::ZERO,
1155 calldata: Bytes::from(vec![byte]),
1156 value: None,
1157 },
1158 }
1159 }
1160
1161 #[test]
1162 fn build_shrunk_sequence_accumulates_removed_delay_into_next_kept_call() {
1163 let calls = vec![tx(Some(3), Some(5)), tx(Some(7), Some(11)), tx(Some(13), Some(17))];
1164 let mut shrinker = SequenceShrink::new(calls.len());
1165 shrinker.remove(0);
1166
1167 let shrunk = build_shrunk_sequence(&calls, &shrinker, true);
1168
1169 assert_eq!(shrunk.len(), 2);
1170 assert_eq!(shrunk[0].warp, Some(U256::from(10)));
1171 assert_eq!(shrunk[0].roll, Some(U256::from(16)));
1172 assert_eq!(shrunk[1].warp, Some(U256::from(13)));
1173 assert_eq!(shrunk[1].roll, Some(U256::from(17)));
1174 }
1175
1176 #[test]
1177 fn build_shrunk_sequence_does_not_move_trailing_delay_backward() {
1178 let calls = vec![tx(Some(3), Some(5)), tx(Some(7), Some(11))];
1179 let mut shrinker = SequenceShrink::new(calls.len());
1180 shrinker.remove(1);
1181
1182 let shrunk = build_shrunk_sequence(&calls, &shrinker, true);
1183
1184 assert_eq!(shrunk.len(), 1);
1185 assert_eq!(shrunk[0].warp, Some(U256::from(3)));
1186 assert_eq!(shrunk[0].roll, Some(U256::from(5)));
1187 }
1188
1189 #[test]
1190 fn shrink_run_counts_attempts_and_accepts() {
1191 let mut run = ShrinkRun::new(2);
1192
1193 assert_eq!(run.remaining_attempts(), 2);
1194 assert!(!run.try_candidate(|| false));
1195 assert!(run.try_candidate(|| true));
1196
1197 let mut called_after_limit = false;
1198 assert!(!run.try_candidate(|| {
1199 called_after_limit = true;
1200 true
1201 }));
1202
1203 assert!(!called_after_limit);
1204 let stats = run.finish();
1205 assert_eq!(stats.attempts, 2);
1206 assert_eq!(stats.accepted, 1);
1207 }
1208
1209 #[test]
1210 fn shrink_candidate_keys_skip_duplicates() {
1211 let mut candidates = ShrinkCandidateKeys::new("initial");
1212
1213 assert!(!candidates.insert("initial"));
1214 assert!(candidates.insert("first"));
1215 assert!(!candidates.insert("first"));
1216 assert!(candidates.insert("second"));
1217 }
1218
1219 #[test]
1220 fn shrink_loop_keep_removed_treats_candidate_error_as_still_failing() {
1221 let config = InvariantConfig { shrink_run_limit: 1, ..Default::default() };
1222 let early_exit = EarlyExit::new(false);
1223 let calls = vec![tx(None, None), tx(None, None)];
1224 let progress = ShrinkProgress::new(&config, None, "test", None, None, false);
1225
1226 let shrinker = run_shrink_loop(
1227 &config,
1228 &calls,
1229 &progress,
1230 false,
1231 &early_exit,
1232 ShrinkErrorPolicy::KeepRemoved,
1233 |_| Err(eyre::eyre!("candidate replay failed")),
1234 );
1235
1236 assert_eq!(shrinker.current().collect::<Vec<_>>(), vec![1]);
1237 }
1238
1239 #[test]
1240 fn shrink_loop_limit_counts_candidate_replays_not_skipped_indices() {
1241 let config = InvariantConfig { shrink_run_limit: 4, ..Default::default() };
1242 let early_exit = EarlyExit::new(false);
1243 let calls = vec![tx(None, None), tx(None, None), tx(None, None)];
1244 let progress = ShrinkProgress::new(&config, None, "test", None, None, false);
1245 let mut replay_attempts = 0;
1246
1247 let shrinker = run_shrink_loop(
1248 &config,
1249 &calls,
1250 &progress,
1251 false,
1252 &early_exit,
1253 ShrinkErrorPolicy::RestoreRemoved,
1254 |_| {
1255 replay_attempts += 1;
1256 Ok(matches!(replay_attempts, 1 | 4))
1257 },
1258 );
1259
1260 assert_eq!(replay_attempts, 4);
1261 assert_eq!(shrinker.current().collect::<Vec<_>>(), vec![2]);
1262 }
1263
1264 #[test]
1265 fn sequence_shrinker_skips_duplicate_candidates_without_spending_attempts() {
1266 let mut run = ShrinkRun::new(2);
1267 let mut seen = Vec::new();
1268
1269 let shrinker = shrink_sequence_by_removing(
1270 2,
1271 &mut run,
1272 || false,
1273 || {},
1274 |shrinker| {
1275 let candidate = shrinker.current().collect::<Vec<_>>();
1276 if seen.contains(&candidate) {
1277 None
1278 } else {
1279 seen.push(candidate);
1280 Some(false)
1281 }
1282 },
1283 );
1284
1285 assert_eq!(shrinker.current().collect::<Vec<_>>(), vec![0, 1]);
1286 let stats = run.finish();
1287 assert_eq!(stats.attempts, 2);
1288 assert_eq!(stats.accepted, 0);
1289 }
1290
1291 #[test]
1292 fn shrink_progress_message_renders_current_sequence() {
1293 let calls = vec![tx_with_calldata(1), tx_with_calldata(2)];
1294
1295 let message =
1296 format_shrink_progress_message(" Shrink: invariant_live", &calls, None, false);
1297
1298 assert!(message.contains(" Shrink: invariant_live"));
1299 assert!(message.contains("[Sequence] (shrunk: 2)"));
1300 assert!(message.contains("calldata=0x01 args=[]"));
1301 assert!(message.contains("calldata=0x02 args=[]"));
1302 }
1303
1304 #[test]
1305 fn shrink_progress_message_omits_middle_of_large_sequence() {
1306 let calls = (0..(LIVE_SHRINK_SEQUENCE_EDGE_CALLS * 2 + 3))
1307 .map(|idx| tx_with_calldata(idx as u8))
1308 .collect::<Vec<_>>();
1309
1310 let message =
1311 format_shrink_progress_message(" Shrink: invariant_live", &calls, None, false);
1312
1313 assert!(message.contains("[Sequence] (shrunk: 35)"));
1314 assert!(message.contains("... 3 call(s) omitted ..."));
1315 assert_eq!(message.matches("sender=").count(), LIVE_SHRINK_SEQUENCE_EDGE_CALLS * 2);
1316 assert!(message.contains("calldata=0x00 args=[]"));
1317 assert!(message.contains("calldata=0x22 args=[]"));
1318 }
1319}