1use crate::executors::{
2 EarlyExit, EvmError, Executor, RawCallResult,
3 invariant::{
4 call_after_invariant_function, call_invariant_function,
5 error::{handler_edge_fingerprint, snapshot_edge_fingerprint},
6 execute_tx,
7 result::did_fail_on_assert,
8 },
9};
10use alloy_json_abi::Function;
11use alloy_primitives::{Address, B256, Bytes, I256, Selector, U256};
12use foundry_config::InvariantConfig;
13use foundry_evm_core::{
14 FoundryBlock, constants::MAGIC_ASSUME, decode::RevertDecoder, evm::FoundryEvmNetwork,
15};
16use foundry_evm_fuzz::{BasicTxDetails, invariant::InvariantContract};
17use indicatif::ProgressBar;
18use proptest::bits::{BitSetLike, VarBitSet};
19use revm::context::Block;
20
21#[derive(Debug)]
26struct CallSequenceShrinker {
27 call_sequence_len: usize,
29 included_calls: VarBitSet,
31}
32
33impl CallSequenceShrinker {
34 fn new(call_sequence_len: usize) -> Self {
35 Self { call_sequence_len, included_calls: VarBitSet::saturated(call_sequence_len) }
36 }
37
38 fn current(&self) -> impl Iterator<Item = usize> + '_ {
40 (0..self.call_sequence_len).filter(|&call_id| self.included_calls.test(call_id))
41 }
42
43 const fn next_index(&self, call_idx: usize) -> usize {
45 if call_idx + 1 == self.call_sequence_len { 0 } else { call_idx + 1 }
46 }
47}
48
49#[derive(Clone, Copy)]
51enum ShrinkErrorPolicy {
52 KeepRemoved,
54 RestoreRemoved,
57}
58
59#[expect(clippy::large_enum_variant)]
62enum ReplayDecision<T, FEN: FoundryEvmNetwork> {
63 Stop(T),
64 Continue(RawCallResult<FEN>),
65}
66
67pub struct CheckSequenceOptions<'a> {
69 pub accumulate_warp_roll: bool,
70 pub fail_on_revert: bool,
71 pub expect_assertion_failure: bool,
72 pub call_after_invariant: bool,
73 pub rd: Option<&'a RevertDecoder>,
74}
75
76#[derive(Debug)]
79pub struct HandlerReplayOutcome {
80 pub anchor_asserted: bool,
81 pub revert_reason: Option<String>,
82 pub anchor_fingerprint: B256,
84}
85
86pub(crate) fn reset_shrink_progress(
89 config: &InvariantConfig,
90 progress: Option<&ProgressBar>,
91 label: &str,
92 position: Option<(usize, usize)>,
93) {
94 if let Some(progress) = progress {
95 progress.set_length(config.shrink_run_limit as u64);
96 progress.reset();
97 let message = match position {
98 Some((current, total)) if total > 1 => {
99 format!(" [{current}/{total}] Shrink: {label}")
100 }
101 _ => format!(" Shrink: {label}"),
102 };
103 progress.set_message(message);
104 }
105}
106
107fn apply_warp_roll(call: &BasicTxDetails, warp: U256, roll: U256) -> BasicTxDetails {
109 let mut result = call.clone();
110 if warp > U256::ZERO {
111 result.warp = Some(warp);
112 }
113 if roll > U256::ZERO {
114 result.roll = Some(roll);
115 }
116 result
117}
118
119fn apply_warp_roll_to_env<FEN: FoundryEvmNetwork>(
121 executor: &mut Executor<FEN>,
122 warp: U256,
123 roll: U256,
124) {
125 if warp > U256::ZERO || roll > U256::ZERO {
126 let ts = executor.evm_env().block_env.timestamp();
127 let num = executor.evm_env().block_env.number();
128 executor.evm_env_mut().block_env.set_timestamp(ts + warp);
129 executor.evm_env_mut().block_env.set_number(num + roll);
130
131 let block_env = executor.evm_env().block_env.clone();
132 if let Some(cheatcodes) = executor.inspector_mut().cheatcodes.as_mut() {
133 if let Some(block) = cheatcodes.block.as_mut() {
134 let bts = block.timestamp();
135 let bnum = block.number();
136 block.set_timestamp(bts + warp);
137 block.set_number(bnum + roll);
138 } else {
139 cheatcodes.block = Some(block_env);
140 }
141 }
142 }
143}
144
145fn build_shrunk_sequence(
150 calls: &[BasicTxDetails],
151 shrinker: &CallSequenceShrinker,
152 accumulate_warp_roll: bool,
153) -> Vec<BasicTxDetails> {
154 if !accumulate_warp_roll {
155 return shrinker.current().map(|idx| calls[idx].clone()).collect();
156 }
157
158 let mut result = Vec::new();
159 let mut accumulated_warp = U256::ZERO;
160 let mut accumulated_roll = U256::ZERO;
161
162 for (idx, call) in calls.iter().enumerate() {
163 accumulated_warp += call.warp.unwrap_or(U256::ZERO);
164 accumulated_roll += call.roll.unwrap_or(U256::ZERO);
165
166 if shrinker.included_calls.test(idx) {
167 result.push(apply_warp_roll(call, accumulated_warp, accumulated_roll));
168 accumulated_warp = U256::ZERO;
169 accumulated_roll = U256::ZERO;
170 }
171 }
172
173 result
174}
175
176fn run_shrink_loop<P>(
179 config: &InvariantConfig,
180 calls_len: usize,
181 progress: Option<&ProgressBar>,
182 early_exit: &EarlyExit,
183 error_policy: ShrinkErrorPolicy,
184 mut predicate: P,
185) -> CallSequenceShrinker
186where
187 P: FnMut(&CallSequenceShrinker) -> eyre::Result<bool>,
188{
189 let mut shrinker = CallSequenceShrinker::new(calls_len);
190 let mut call_idx = 0;
191
192 for _ in 0..config.shrink_run_limit {
193 if early_exit.should_stop() {
194 break;
195 }
196
197 if !shrinker.included_calls.test(call_idx) {
199 call_idx = shrinker.next_index(call_idx);
200 continue;
201 }
202
203 shrinker.included_calls.clear(call_idx);
204
205 let bug_still_present = match predicate(&shrinker) {
206 Ok(b) => b,
207 Err(_) => matches!(error_policy, ShrinkErrorPolicy::KeepRemoved),
208 };
209 if bug_still_present {
210 if shrinker.included_calls.count() == 1 {
211 break;
212 }
213 } else {
214 shrinker.included_calls.set(call_idx);
215 }
216
217 if let Some(progress) = progress {
218 progress.inc(1);
219 }
220 call_idx = shrinker.next_index(call_idx);
221 }
222
223 shrinker
224}
225
226#[expect(clippy::too_many_arguments)]
227pub(crate) fn shrink_sequence<FEN: FoundryEvmNetwork>(
228 config: &InvariantConfig,
229 invariant_contract: &InvariantContract<'_>,
230 target_invariant: &Function,
231 calls: &[BasicTxDetails],
232 expect_assertion_failure: bool,
233 executor: &Executor<FEN>,
234 progress: Option<&ProgressBar>,
235 early_exit: &EarlyExit,
236) -> eyre::Result<Vec<BasicTxDetails>> {
237 trace!(target: "forge::test", "Shrinking sequence of {} calls.", calls.len());
238
239 let target_address = invariant_contract.address;
240 let calldata: Bytes = target_invariant.selector().to_vec().into();
241 let (_, success) = call_invariant_function(executor, target_address, calldata.clone())?;
244 if !success {
245 return Ok(vec![]);
246 }
247
248 let accumulate_warp_roll = config.has_delay();
249 let shrinker = run_shrink_loop(
250 config,
251 calls.len(),
252 progress,
253 early_exit,
254 ShrinkErrorPolicy::KeepRemoved,
257 |shrinker| {
258 let (success, _, _) = check_sequence(
259 executor.clone(),
260 calls,
261 shrinker.current().collect(),
262 target_address,
263 calldata.clone(),
264 CheckSequenceOptions {
265 accumulate_warp_roll,
266 fail_on_revert: config.fail_on_revert,
267 expect_assertion_failure,
268 call_after_invariant: invariant_contract.call_after_invariant,
269 rd: None,
270 },
271 )?;
272 Ok(!success)
274 },
275 );
276
277 Ok(build_shrunk_sequence(calls, &shrinker, accumulate_warp_roll))
278}
279
280fn replay_sequence<FEN, T, F>(
284 executor: &mut Executor<FEN>,
285 calls: &[BasicTxDetails],
286 sequence: &[usize],
287 accumulate_warp_roll: bool,
288 mut on_call: F,
289) -> eyre::Result<Option<T>>
290where
291 FEN: FoundryEvmNetwork,
292 F: FnMut(usize, RawCallResult<FEN>) -> eyre::Result<ReplayDecision<T, FEN>>,
293{
294 if !accumulate_warp_roll {
297 for &idx in sequence {
298 let call_result = execute_tx(executor, &calls[idx])?;
299 match on_call(idx, call_result)? {
300 ReplayDecision::Stop(val) => return Ok(Some(val)),
301 ReplayDecision::Continue(mut call_result) => {
302 if !call_result.reverted {
303 executor.commit(&mut call_result);
304 }
305 }
306 }
307 }
308 return Ok(None);
309 }
310
311 let mut accumulated_warp = U256::ZERO;
314 let mut accumulated_roll = U256::ZERO;
315 let mut seq_iter = sequence.iter().peekable();
316
317 for (idx, tx) in calls.iter().enumerate() {
318 accumulated_warp += tx.warp.unwrap_or(U256::ZERO);
319 accumulated_roll += tx.roll.unwrap_or(U256::ZERO);
320 if seq_iter.peek() != Some(&&idx) {
321 continue;
322 }
323 seq_iter.next();
324
325 let executed = apply_warp_roll(tx, accumulated_warp, accumulated_roll);
326 let call_result = execute_tx(executor, &executed)?;
327
328 match on_call(idx, call_result)? {
329 ReplayDecision::Stop(val) => return Ok(Some(val)),
330 ReplayDecision::Continue(mut call_result) => {
331 if !call_result.reverted {
332 executor.commit(&mut call_result);
333 }
334 }
335 }
336
337 accumulated_warp = U256::ZERO;
338 accumulated_roll = U256::ZERO;
339 }
340
341 Ok(None)
342}
343
344pub fn check_sequence<FEN: FoundryEvmNetwork>(
354 mut executor: Executor<FEN>,
355 calls: &[BasicTxDetails],
356 sequence: Vec<usize>,
357 test_address: Address,
358 calldata: Bytes,
359 options: CheckSequenceOptions<'_>,
360) -> eyre::Result<(bool, bool, Option<String>)> {
361 let early = replay_sequence(
362 &mut executor,
363 calls,
364 &sequence,
365 options.accumulate_warp_roll,
366 |_idx, call_result| {
367 if call_result.result.as_ref() == MAGIC_ASSUME {
371 return Ok(ReplayDecision::Continue(call_result));
372 }
373 if did_fail_on_assert(&call_result, &call_result.state_changeset) {
374 return Ok(ReplayDecision::Stop((
375 false,
376 false,
377 assertion_failure_reason(call_result, options.rd),
378 )));
379 }
380 if call_result.reverted && options.fail_on_revert {
381 if options.expect_assertion_failure {
382 return Ok(ReplayDecision::Stop((true, false, None)));
383 }
384 return Ok(ReplayDecision::Stop((
385 false,
386 false,
387 call_failure_reason(call_result, options.rd),
388 )));
389 }
390 Ok(ReplayDecision::Continue(call_result))
391 },
392 )?;
393 if let Some(result) = early {
394 return Ok(result);
395 }
396
397 finish_sequence_check(&executor, test_address, calldata, &options)
400}
401
402fn finish_sequence_check<FEN: FoundryEvmNetwork>(
403 executor: &Executor<FEN>,
404 test_address: Address,
405 calldata: Bytes,
406 options: &CheckSequenceOptions<'_>,
407) -> eyre::Result<(bool, bool, Option<String>)> {
408 let handle_terminal_failure = |call_result: RawCallResult<FEN>| {
409 let should_ignore_failure = options.expect_assertion_failure
410 && !executor.has_global_failure(&call_result.state_changeset)
411 && !did_fail_on_assert(&call_result, &call_result.state_changeset);
412
413 if should_ignore_failure {
414 return (true, true, None);
415 }
416
417 let reason = if options.expect_assertion_failure {
418 assertion_failure_reason(call_result, options.rd)
419 } else {
420 call_failure_reason(call_result, options.rd)
421 };
422
423 (false, true, reason)
424 };
425
426 let (invariant_result, mut success) =
427 call_invariant_function(executor, test_address, calldata)?;
428 if !success {
429 return Ok(handle_terminal_failure(invariant_result));
430 }
431
432 if success && options.call_after_invariant {
435 let (after_invariant_result, after_invariant_success) =
436 call_after_invariant_function(executor, test_address)?;
437 success = after_invariant_success;
438 if !success {
439 return Ok(handle_terminal_failure(after_invariant_result));
440 }
441 }
442
443 Ok((success, true, None))
444}
445
446fn call_failure_reason<FEN: FoundryEvmNetwork>(
447 call_result: RawCallResult<FEN>,
448 rd: Option<&RevertDecoder>,
449) -> Option<String> {
450 match call_result.into_evm_error(rd) {
451 EvmError::Execution(err) => Some(err.reason),
452 _ => None,
453 }
454}
455
456fn assertion_failure_reason<FEN: FoundryEvmNetwork>(
457 call_result: RawCallResult<FEN>,
458 rd: Option<&RevertDecoder>,
459) -> Option<String> {
460 call_failure_reason(call_result, rd).or_else(|| Some("assertion failed".to_string()))
461}
462
463#[expect(clippy::too_many_arguments)]
471pub(crate) fn shrink_sequence_value<FEN: FoundryEvmNetwork>(
472 config: &InvariantConfig,
473 invariant_contract: &InvariantContract<'_>,
474 target_invariant: &Function,
475 calls: &[BasicTxDetails],
476 executor: &Executor<FEN>,
477 target_value: I256,
478 progress: Option<&ProgressBar>,
479 early_exit: &EarlyExit,
480) -> eyre::Result<Vec<BasicTxDetails>> {
481 trace!(target: "forge::test", "Shrinking optimization sequence of {} calls for target value {}.", calls.len(), target_value);
482
483 let target_address = invariant_contract.address;
484 let calldata: Bytes = target_invariant.selector().to_vec().into();
485
486 if check_sequence_value(executor.clone(), calls, vec![], target_address, calldata.clone())?
488 == Some(target_value)
489 {
490 return Ok(vec![]);
491 }
492
493 let mut call_idx = 0;
494 let mut shrinker = CallSequenceShrinker::new(calls.len());
495
496 for _ in 0..config.shrink_run_limit {
497 if early_exit.should_stop() {
498 break;
499 }
500
501 shrinker.included_calls.clear(call_idx);
502
503 let keeps_target = check_sequence_value(
504 executor.clone(),
505 calls,
506 shrinker.current().collect(),
507 target_address,
508 calldata.clone(),
509 )? == Some(target_value);
510
511 if keeps_target {
512 if shrinker.included_calls.count() == 1 {
513 break;
514 }
515 } else {
516 shrinker.included_calls.set(call_idx);
517 }
518
519 if let Some(progress) = progress {
520 progress.inc(1);
521 }
522
523 call_idx = shrinker.next_index(call_idx);
524 }
525
526 Ok(build_shrunk_sequence(calls, &shrinker, true))
527}
528
529pub fn replay_handler_failure_sequence<FEN: FoundryEvmNetwork>(
532 mut executor: Executor<FEN>,
533 calls: &[BasicTxDetails],
534 sequence: Vec<usize>,
535 accumulate_warp_roll: bool,
536 rd: Option<&RevertDecoder>,
537) -> eyre::Result<HandlerReplayOutcome> {
538 let Some(&anchor_idx) = sequence.last() else {
539 return Ok(HandlerReplayOutcome {
540 anchor_asserted: false,
541 revert_reason: None,
542 anchor_fingerprint: B256::ZERO,
543 });
544 };
545
546 let outcome = replay_sequence(
547 &mut executor,
548 calls,
549 &sequence,
550 accumulate_warp_roll,
551 |idx, call_result| {
552 let asserted = did_fail_on_assert(&call_result, &call_result.state_changeset);
553 if idx == anchor_idx {
554 let snapshot = snapshot_edge_fingerprint(&call_result);
555 let anchor = &calls[anchor_idx];
556 let reverter = anchor.call_details.target;
557 let selector_bytes: [u8; 4] = anchor
558 .call_details
559 .calldata
560 .get(..4)
561 .and_then(|s| s.try_into().ok())
562 .unwrap_or_default();
563 let selector = Selector::from(selector_bytes);
564 let fingerprint = handler_edge_fingerprint(snapshot, reverter, selector);
565 let reason =
566 if asserted { assertion_failure_reason(call_result, rd) } else { None };
567 return Ok(ReplayDecision::Stop(HandlerReplayOutcome {
568 anchor_asserted: asserted,
569 revert_reason: reason,
570 anchor_fingerprint: fingerprint,
571 }));
572 }
573 if asserted {
574 return Ok(ReplayDecision::Stop(HandlerReplayOutcome {
576 anchor_asserted: false,
577 revert_reason: None,
578 anchor_fingerprint: B256::ZERO,
579 }));
580 }
581 Ok(ReplayDecision::Continue(call_result))
582 },
583 )?;
584
585 Ok(outcome.unwrap_or(HandlerReplayOutcome {
586 anchor_asserted: false,
587 revert_reason: None,
588 anchor_fingerprint: B256::ZERO,
589 }))
590}
591
592pub(crate) fn shrink_handler_sequence<FEN: FoundryEvmNetwork>(
595 config: &InvariantConfig,
596 calls: &[BasicTxDetails],
597 expected_fingerprint: B256,
598 executor: &Executor<FEN>,
599 progress: Option<&ProgressBar>,
600 early_exit: &EarlyExit,
601) -> eyre::Result<Vec<BasicTxDetails>> {
602 if calls.is_empty() {
603 return Ok(vec![]);
604 }
605 let accumulate_warp_roll = config.has_delay();
606 let shrinker = run_shrink_loop(
607 config,
608 calls.len(),
609 progress,
610 early_exit,
611 ShrinkErrorPolicy::RestoreRemoved,
612 |shrinker| {
613 handler_sequence_still_triggers_bug(
614 executor.clone(),
615 calls,
616 shrinker.current().collect(),
617 accumulate_warp_roll,
618 expected_fingerprint,
619 )
620 },
621 );
622
623 let shrunk = build_shrunk_sequence(calls, &shrinker, accumulate_warp_roll);
624
625 let verified = handler_sequence_still_triggers_bug(
627 executor.clone(),
628 calls,
629 shrinker.current().collect(),
630 accumulate_warp_roll,
631 expected_fingerprint,
632 )
633 .unwrap_or(false);
634 if verified { Ok(shrunk) } else { Ok(calls.to_vec()) }
635}
636
637fn handler_sequence_still_triggers_bug<FEN: FoundryEvmNetwork>(
639 executor: Executor<FEN>,
640 calls: &[BasicTxDetails],
641 sequence: Vec<usize>,
642 accumulate_warp_roll: bool,
643 expected_fingerprint: B256,
644) -> eyre::Result<bool> {
645 let outcome =
646 replay_handler_failure_sequence(executor, calls, sequence, accumulate_warp_roll, None)?;
647 Ok(outcome.anchor_asserted && outcome.anchor_fingerprint == expected_fingerprint)
648}
649
650pub fn check_sequence_value<FEN: FoundryEvmNetwork>(
656 mut executor: Executor<FEN>,
657 calls: &[BasicTxDetails],
658 sequence: Vec<usize>,
659 test_address: Address,
660 calldata: Bytes,
661) -> eyre::Result<Option<I256>> {
662 let mut accumulated_warp = U256::ZERO;
663 let mut accumulated_roll = U256::ZERO;
664 let mut seq_iter = sequence.iter().peekable();
665
666 for (idx, tx) in calls.iter().enumerate() {
667 accumulated_warp += tx.warp.unwrap_or(U256::ZERO);
668 accumulated_roll += tx.roll.unwrap_or(U256::ZERO);
669
670 if seq_iter.peek() == Some(&&idx) {
671 seq_iter.next();
672
673 let tx_with_accumulated = apply_warp_roll(tx, accumulated_warp, accumulated_roll);
674 let mut call_result = execute_tx(&mut executor, &tx_with_accumulated)?;
675
676 if !call_result.reverted {
677 executor.commit(&mut call_result);
678 }
679
680 accumulated_warp = U256::ZERO;
681 accumulated_roll = U256::ZERO;
682 }
683 }
684
685 apply_warp_roll_to_env(&mut executor, accumulated_warp, accumulated_roll);
687
688 let (inv_result, success) = call_invariant_function(&executor, test_address, calldata)?;
689
690 if success
691 && inv_result.result.len() >= 32
692 && let Some(value) = I256::try_from_be_slice(&inv_result.result[..32])
693 {
694 return Ok(Some(value));
695 }
696
697 Ok(None)
698}
699
700#[cfg(test)]
701mod tests {
702 use super::{CallSequenceShrinker, build_shrunk_sequence};
703 use alloy_primitives::{Address, Bytes, U256};
704 use foundry_evm_fuzz::{BasicTxDetails, CallDetails};
705 use proptest::bits::BitSetLike;
706
707 fn tx(warp: Option<u64>, roll: Option<u64>) -> BasicTxDetails {
708 BasicTxDetails {
709 warp: warp.map(U256::from),
710 roll: roll.map(U256::from),
711 sender: Address::ZERO,
712 call_details: CallDetails {
713 target: Address::ZERO,
714 calldata: Bytes::new(),
715 value: None,
716 },
717 }
718 }
719
720 #[test]
721 fn build_shrunk_sequence_accumulates_removed_delay_into_next_kept_call() {
722 let calls = vec![tx(Some(3), Some(5)), tx(Some(7), Some(11)), tx(Some(13), Some(17))];
723 let mut shrinker = CallSequenceShrinker::new(calls.len());
724 shrinker.included_calls.clear(0);
725
726 let shrunk = build_shrunk_sequence(&calls, &shrinker, true);
727
728 assert_eq!(shrunk.len(), 2);
729 assert_eq!(shrunk[0].warp, Some(U256::from(10)));
730 assert_eq!(shrunk[0].roll, Some(U256::from(16)));
731 assert_eq!(shrunk[1].warp, Some(U256::from(13)));
732 assert_eq!(shrunk[1].roll, Some(U256::from(17)));
733 }
734
735 #[test]
736 fn build_shrunk_sequence_does_not_move_trailing_delay_backward() {
737 let calls = vec![tx(Some(3), Some(5)), tx(Some(7), Some(11))];
738 let mut shrinker = CallSequenceShrinker::new(calls.len());
739 shrinker.included_calls.clear(1);
740
741 let shrunk = build_shrunk_sequence(&calls, &shrinker, true);
742
743 assert_eq!(shrunk.len(), 1);
744 assert_eq!(shrunk[0].warp, Some(U256::from(3)));
745 assert_eq!(shrunk[0].roll, Some(U256::from(5)));
746 }
747}