1use crate::result::SymbolicCounterexampleCall;
2use alloy_dyn_abi::{DynSolValue, JsonAbiExt};
3use alloy_json_abi::Function;
4use alloy_primitives::{Address, B256, Bytes, Function as SolFunction, I256, U256};
5use foundry_evm::executors::invariant::{
6 SequenceShrink, ShrinkCandidateKeys, ShrinkRun, shrink_sequence_by_removing,
7};
8use itertools::Itertools;
9
10const MAX_SUBSET_CANDIDATES_PER_PASS: usize = 256;
11
12#[derive(Clone, Debug, PartialEq, Eq, Hash)]
13struct ReplayCallKey {
14 warp: Option<U256>,
15 roll: Option<U256>,
16 sender: Address,
17 target: Address,
18 calldata: Bytes,
19 value: Option<U256>,
20}
21
22fn replay_call_key(call: &SymbolicCounterexampleCall) -> ReplayCallKey {
23 ReplayCallKey {
24 warp: call.warp,
25 roll: call.roll,
26 sender: call.sender,
27 target: call.target,
28 calldata: call.calldata.clone(),
29 value: call.value.filter(|value| !value.is_zero()),
30 }
31}
32
33fn replay_sequence_key(calls: &[SymbolicCounterexampleCall]) -> Vec<ReplayCallKey> {
34 calls.iter().map(replay_call_key).collect()
35}
36
37#[derive(Clone, Debug)]
39pub(crate) struct MinimizedSingleCall {
40 pub original_call: SymbolicCounterexampleCall,
41 pub minimized_call: SymbolicCounterexampleCall,
42 pub attempts: usize,
43 pub accepted: usize,
44}
45
46impl MinimizedSingleCall {
47 pub(crate) fn changed(&self) -> bool {
48 self.original_call.calldata != self.minimized_call.calldata
49 }
50}
51
52#[derive(Clone, Debug)]
54pub(crate) struct MinimizedSequence {
55 pub original_calls: Vec<SymbolicCounterexampleCall>,
56 pub minimized_calls: Vec<SymbolicCounterexampleCall>,
57 pub attempts: usize,
58 pub accepted: usize,
59}
60
61impl MinimizedSequence {
62 #[cfg(test)]
63 pub(crate) fn changed(&self) -> bool {
64 self.original_calls != self.minimized_calls
65 }
66
67 pub(crate) fn original_calldata_bytes(&self) -> usize {
68 sequence_calldata_bytes(&self.original_calls)
69 }
70
71 pub(crate) fn minimized_calldata_bytes(&self) -> usize {
72 sequence_calldata_bytes(&self.minimized_calls)
73 }
74}
75
76fn sequence_calldata_bytes(calls: &[SymbolicCounterexampleCall]) -> usize {
77 calls.iter().map(|call| call.calldata.len()).sum()
78}
79
80pub(crate) fn minimize_sequence_counterexample(
85 calls: &[SymbolicCounterexampleCall],
86 sender_candidates: &[Address],
87 max_attempts: usize,
88 mut still_fails: impl FnMut(&[SymbolicCounterexampleCall]) -> bool,
89) -> Option<MinimizedSequence> {
90 if calls.is_empty() {
91 return None;
92 }
93
94 let original_calls = calls.to_vec();
95 let mut minimizer =
96 SequenceMinimizer::new(original_calls.clone(), max_attempts, &mut still_fails);
97
98 minimizer.minimize_len();
99 minimizer.minimize_calldata();
100 minimizer.minimize_senders(sender_candidates);
101 minimizer.minimize_values();
102
103 let (minimized_calls, attempts, accepted) = minimizer.finish();
104 Some(MinimizedSequence { original_calls, minimized_calls, attempts, accepted })
105}
106
107struct SequenceMinimizer<'a> {
108 current_calls: Vec<SymbolicCounterexampleCall>,
109 tried_candidates: ShrinkCandidateKeys<Vec<ReplayCallKey>>,
110 run: ShrinkRun,
111 still_fails: &'a mut dyn FnMut(&[SymbolicCounterexampleCall]) -> bool,
112}
113
114impl<'a> SequenceMinimizer<'a> {
115 fn new(
116 current_calls: Vec<SymbolicCounterexampleCall>,
117 max_attempts: usize,
118 still_fails: &'a mut dyn FnMut(&[SymbolicCounterexampleCall]) -> bool,
119 ) -> Self {
120 let tried_candidates = ShrinkCandidateKeys::new(replay_sequence_key(¤t_calls));
121 Self { current_calls, tried_candidates, run: ShrinkRun::new(max_attempts), still_fails }
122 }
123
124 const fn can_try(&self) -> bool {
125 self.run.can_try()
126 }
127
128 const fn remaining_attempts(&self) -> usize {
129 self.run.remaining_attempts()
130 }
131
132 fn finish(self) -> (Vec<SymbolicCounterexampleCall>, usize, usize) {
133 let stats = self.run.finish();
134 (self.current_calls, stats.attempts, stats.accepted)
135 }
136
137 fn try_candidate(&mut self, candidate_calls: Vec<SymbolicCounterexampleCall>) -> bool {
138 if !self.can_try() || candidate_calls == self.current_calls {
139 return false;
140 }
141
142 if !self.tried_candidates.insert(replay_sequence_key(&candidate_calls)) {
143 return false;
144 }
145
146 if self.run.try_candidate(|| (self.still_fails)(&candidate_calls)) {
147 self.current_calls = candidate_calls;
148 true
149 } else {
150 false
151 }
152 }
153
154 fn minimize_len(&mut self) {
155 if self.current_calls.len() <= 1 || !self.can_try() {
156 return;
157 }
158
159 let base_calls = self.current_calls.clone();
160 let current_calls = &mut self.current_calls;
161 let tried_candidates = &mut self.tried_candidates;
162 let still_fails = &mut self.still_fails;
163
164 shrink_sequence_by_removing(
165 base_calls.len(),
166 &mut self.run,
167 true,
168 || false,
169 || {},
170 |shrinker| {
171 let candidate_calls = sequence_from_shrink(&base_calls, shrinker);
172 if candidate_calls == *current_calls {
173 return None;
174 }
175
176 if !tried_candidates.insert(replay_sequence_key(&candidate_calls)) {
177 return None;
178 }
179
180 if (*still_fails)(&candidate_calls) {
181 *current_calls = candidate_calls;
182 Some(true)
183 } else {
184 Some(false)
185 }
186 },
187 );
188 }
189
190 fn minimize_calldata(&mut self) {
191 let mut idx = 0usize;
192 while idx < self.current_calls.len() && self.can_try() {
193 let Some(function) = self.current_calls[idx]
194 .signature
195 .as_deref()
196 .and_then(|signature| Function::parse(signature).ok())
197 else {
198 idx += 1;
199 continue;
200 };
201 let template = self.current_calls[idx].clone();
202 let remaining_attempts = self.remaining_attempts();
203 let minimized = minimize_single_call_counterexample(
204 &function,
205 &template,
206 remaining_attempts,
207 |candidate_call| {
208 let mut candidate_calls = self.current_calls.clone();
209 candidate_calls[idx] = candidate_call.clone();
210 self.try_candidate(candidate_calls)
211 },
212 );
213 if let Some(minimized) = minimized
214 && minimized.changed()
215 && let Some(call) = self.current_calls.get_mut(idx)
216 {
217 *call = minimized.minimized_call;
218 }
219 idx += 1;
220 }
221 }
222
223 fn minimize_senders(&mut self, sender_candidates: &[Address]) {
224 let mut idx = 0usize;
225 while idx < self.current_calls.len() && self.can_try() {
226 for sender in sender_candidates.iter().copied() {
227 if !self.can_try() || self.current_calls[idx].sender == sender {
228 continue;
229 }
230 let mut candidate_calls = self.current_calls.clone();
231 candidate_calls[idx].sender = sender;
232 self.try_candidate(candidate_calls);
233 }
234 idx += 1;
235 }
236 }
237
238 fn minimize_values(&mut self) {
239 let mut idx = 0usize;
240 while idx < self.current_calls.len() && self.can_try() {
241 self.minimize_call_value(idx);
242 idx += 1;
243 }
244 }
245
246 fn minimize_call_value(&mut self, idx: usize) {
247 let Some(mut accepted_value) = self.current_calls[idx].value else {
248 return;
249 };
250
251 let mut zero_candidate = self.current_calls.clone();
252 zero_candidate[idx].value = None;
253 if self.try_candidate(zero_candidate) {
254 return;
255 }
256
257 if accepted_value.is_zero() {
258 return;
259 }
260
261 let mut rejected_value = U256::ZERO;
262 while accepted_value > rejected_value + U256::ONE && self.can_try() {
263 let candidate_value = rejected_value + ((accepted_value - rejected_value) >> 1usize);
264 let mut candidate_calls = self.current_calls.clone();
265 candidate_calls[idx].value = Some(candidate_value);
266 if self.try_candidate(candidate_calls) {
267 accepted_value = candidate_value;
268 } else {
269 rejected_value = candidate_value;
270 }
271 }
272 }
273}
274
275fn sequence_from_shrink(
276 calls: &[SymbolicCounterexampleCall],
277 shrinker: &SequenceShrink,
278) -> Vec<SymbolicCounterexampleCall> {
279 shrinker.apply_with_accumulated_delay(
280 calls,
281 |call| (call.warp, call.roll),
282 |mut call, warp, roll| {
283 if !warp.is_zero() {
284 call.warp = Some(warp);
285 }
286 if !roll.is_zero() {
287 call.roll = Some(roll);
288 }
289 call
290 },
291 )
292}
293
294pub(crate) fn minimize_single_call_counterexample(
299 function: &Function,
300 call: &SymbolicCounterexampleCall,
301 max_attempts: usize,
302 mut still_fails: impl FnMut(&SymbolicCounterexampleCall) -> bool,
303) -> Option<MinimizedSingleCall> {
304 if call.calldata.get(..4).is_none_or(|selector| selector != function.selector()) {
305 return None;
306 }
307
308 let original_args = function.abi_decode_input(&call.calldata[4..]).ok()?;
309 let mut current_args = original_args;
310 let mut current_call = call_with_args(function, call, ¤t_args)?;
311 let mut run = ShrinkRun::new(max_attempts);
312 let mut tried_calldata = ShrinkCandidateKeys::new(current_call.calldata.clone());
313
314 let mut try_args = |candidate_args: &[DynSolValue]| {
315 if !run.can_try() {
316 return false;
317 }
318 let Some(candidate_call) = call_with_args(function, call, candidate_args) else {
319 return false;
320 };
321 if candidate_call.calldata == current_call.calldata {
322 return false;
323 }
324
325 if !tried_calldata.insert(candidate_call.calldata.clone()) {
326 return false;
327 }
328
329 if run.try_candidate(|| still_fails(&candidate_call)) {
330 current_call = candidate_call;
331 true
332 } else {
333 false
334 }
335 };
336 minimize_values_batch(&mut current_args, &mut try_args);
337 minimize_value_subsets(&mut current_args, &mut try_args);
338 minimize_value_pairs(&mut current_args, &mut try_args);
339 minimize_values(&mut current_args, &mut try_args);
340
341 current_call = with_formatted_args(current_call, ¤t_args);
342 let stats = run.finish();
343
344 Some(MinimizedSingleCall {
345 original_call: call.clone(),
346 minimized_call: current_call,
347 attempts: stats.attempts,
348 accepted: stats.accepted,
349 })
350}
351
352fn call_with_args(
353 function: &Function,
354 template: &SymbolicCounterexampleCall,
355 args: &[DynSolValue],
356) -> Option<SymbolicCounterexampleCall> {
357 let calldata = Bytes::from(function.abi_encode_input(args).ok()?);
358 Some(SymbolicCounterexampleCall { calldata, args: None, raw_args: None, ..template.clone() })
359}
360
361fn with_formatted_args(
362 mut call: SymbolicCounterexampleCall,
363 args: &[DynSolValue],
364) -> SymbolicCounterexampleCall {
365 call.args = Some(foundry_common::fmt::format_tokens(args).format(", ").to_string());
366 call.raw_args = Some(foundry_common::fmt::format_tokens_raw(args).format(", ").to_string());
367 call
368}
369
370fn minimize_values_batch(
371 values: &mut Vec<DynSolValue>,
372 try_values: &mut dyn FnMut(&[DynSolValue]) -> bool,
373) -> bool {
374 let candidate_values = values.iter().cloned().map(minimally_simple_value).collect::<Vec<_>>();
375 if candidate_values == *values {
376 return false;
377 }
378 if try_values(&candidate_values) {
379 *values = candidate_values;
380 true
381 } else {
382 false
383 }
384}
385
386fn minimize_value_subsets(
387 values: &mut Vec<DynSolValue>,
388 try_values: &mut dyn FnMut(&[DynSolValue]) -> bool,
389) -> bool {
390 let mut changed = false;
391 loop {
392 let simple_values = values.iter().cloned().map(minimally_simple_value).collect::<Vec<_>>();
393 let shrinkable_idxs = values
394 .iter()
395 .zip(&simple_values)
396 .enumerate()
397 .filter_map(|(idx, (current, simple))| (current != simple).then_some(idx))
398 .collect::<Vec<_>>();
399 if shrinkable_idxs.len() < 2 {
400 break;
401 }
402
403 let mut pass_changed = false;
404 for subset_size in subset_sizes(shrinkable_idxs.len()) {
405 let mut subset = Vec::with_capacity(subset_size);
406 if try_value_subset(
407 values,
408 &simple_values,
409 &shrinkable_idxs,
410 subset_size,
411 0,
412 &mut subset,
413 try_values,
414 ) {
415 pass_changed = true;
416 break;
417 }
418 }
419
420 if !pass_changed {
421 break;
422 }
423 changed = true;
424 }
425 changed
426}
427
428fn try_value_subset(
429 values: &mut Vec<DynSolValue>,
430 simple_values: &[DynSolValue],
431 shrinkable_idxs: &[usize],
432 subset_size: usize,
433 start: usize,
434 subset: &mut Vec<usize>,
435 try_values: &mut dyn FnMut(&[DynSolValue]) -> bool,
436) -> bool {
437 if subset.len() == subset_size {
438 let mut candidate_values = values.clone();
439 for idx in subset.iter().copied() {
440 candidate_values[idx] = simple_values[idx].clone();
441 }
442 if try_values(&candidate_values) {
443 *values = candidate_values;
444 return true;
445 }
446 return false;
447 }
448
449 let remaining = subset_size - subset.len();
450 for choice_idx in start..=shrinkable_idxs.len() - remaining {
451 subset.push(shrinkable_idxs[choice_idx]);
452 if try_value_subset(
453 values,
454 simple_values,
455 shrinkable_idxs,
456 subset_size,
457 choice_idx + 1,
458 subset,
459 try_values,
460 ) {
461 return true;
462 }
463 subset.pop();
464 }
465 false
466}
467
468fn minimally_simple_value(mut value: DynSolValue) -> DynSolValue {
469 minimize_value(&mut value, &mut |_| true);
470 value
471}
472
473fn minimize_u256_pair_candidates(
474 current_left: U256,
475 current_right: U256,
476 mut try_candidate: impl FnMut(U256, U256) -> bool,
477) -> bool {
478 if current_left.is_zero() || current_right.is_zero() {
479 return false;
480 }
481
482 let mut accepted_left = current_left;
483 let mut accepted_right = current_right;
484 let mut rejected_left = U256::ZERO;
485 let mut rejected_right = U256::ZERO;
486 let mut changed = false;
487 while accepted_left > rejected_left + U256::ONE || accepted_right > rejected_right + U256::ONE {
488 let candidate_left = if accepted_left > rejected_left + U256::ONE {
489 rejected_left + ((accepted_left - rejected_left) >> 1usize)
490 } else {
491 accepted_left
492 };
493 let candidate_right = if accepted_right > rejected_right + U256::ONE {
494 rejected_right + ((accepted_right - rejected_right) >> 1usize)
495 } else {
496 accepted_right
497 };
498
499 if try_candidate(candidate_left, candidate_right) {
500 accepted_left = candidate_left;
501 accepted_right = candidate_right;
502 changed = true;
503 } else {
504 rejected_left = candidate_left;
505 rejected_right = candidate_right;
506 }
507 }
508 changed
509}
510
511fn minimize_values(values: &mut [DynSolValue], try_values: &mut dyn FnMut(&[DynSolValue]) -> bool) {
512 loop {
513 let mut changed = false;
514 for idx in 0..values.len() {
515 let mut value = values[idx].clone();
516 let value_changed = minimize_value(&mut value, &mut |candidate| {
517 let mut candidate_values = values.to_vec();
518 candidate_values[idx] = candidate.clone();
519 try_values(&candidate_values)
520 });
521 if value_changed {
522 values[idx] = value;
523 changed = true;
524 }
525 }
526 if !changed {
527 break;
528 }
529 }
530}
531
532fn minimize_value_pairs(
533 values: &mut Vec<DynSolValue>,
534 try_values: &mut dyn FnMut(&[DynSolValue]) -> bool,
535) -> bool {
536 let mut changed = false;
537 loop {
538 let mut pass_changed = false;
539 for left_idx in 0..values.len() {
540 for right_idx in left_idx + 1..values.len() {
541 let left = values[left_idx].clone();
542 let right = values[right_idx].clone();
543 if minimize_numeric_value_pair(&left, &right, |left, right| {
544 let mut candidate_values = values.clone();
545 candidate_values[left_idx] = left;
546 candidate_values[right_idx] = right;
547 if try_values(&candidate_values) {
548 *values = candidate_values;
549 true
550 } else {
551 false
552 }
553 }) {
554 pass_changed = true;
555 break;
556 }
557 }
558 if pass_changed {
559 break;
560 }
561 }
562 if !pass_changed {
563 break;
564 }
565 changed = true;
566 }
567 changed
568}
569
570fn minimize_value(
571 value: &mut DynSolValue,
572 try_value: &mut dyn FnMut(&DynSolValue) -> bool,
573) -> bool {
574 let mut changed = false;
575 loop {
576 let pass_changed =
577 minimize_scalar_value(value, try_value) || minimize_compound_value(value, try_value);
578 if !pass_changed {
579 break;
580 }
581 changed = true;
582 }
583 changed
584}
585
586fn minimize_scalar_value(
587 value: &mut DynSolValue,
588 try_value: &mut dyn FnMut(&DynSolValue) -> bool,
589) -> bool {
590 match value.clone() {
591 DynSolValue::Bool(true) => accept_candidate(value, DynSolValue::Bool(false), try_value),
592 DynSolValue::Bool(false) => false,
593 DynSolValue::Uint(current, bits) => minimize_uint(value, current, bits, try_value),
594 DynSolValue::Int(current, bits) => minimize_int(value, current, bits, try_value),
595 DynSolValue::Address(current) => minimize_address(value, current, try_value),
596 DynSolValue::FixedBytes(current, size) => {
597 minimize_fixed_bytes(value, current, size, try_value)
598 }
599 DynSolValue::Function(current) => {
600 if current == SolFunction::ZERO {
601 false
602 } else {
603 accept_candidate(value, DynSolValue::Function(SolFunction::ZERO), try_value)
604 }
605 }
606 DynSolValue::Bytes(current) => minimize_bytes(value, current, try_value),
607 DynSolValue::String(current) => minimize_string(value, current, try_value),
608 DynSolValue::Array(_)
609 | DynSolValue::FixedArray(_)
610 | DynSolValue::Tuple(_)
611 | DynSolValue::CustomStruct { .. } => false,
612 }
613}
614
615fn minimize_compound_value(
616 value: &mut DynSolValue,
617 try_value: &mut dyn FnMut(&DynSolValue) -> bool,
618) -> bool {
619 match value.clone() {
620 DynSolValue::Array(mut elements) => {
621 if minimize_array_len(value, &mut elements, try_value) {
622 return true;
623 }
624 if let Some(candidate) = minimize_elements_batch(
625 &mut elements,
626 |items| DynSolValue::Array(items.to_vec()),
627 try_value,
628 ) {
629 *value = candidate;
630 return true;
631 }
632 if let Some(candidate) = minimize_element_subsets(
633 &mut elements,
634 |items| DynSolValue::Array(items.to_vec()),
635 try_value,
636 ) {
637 *value = candidate;
638 return true;
639 }
640 if let Some(candidate) = minimize_element_pairs(
641 &mut elements,
642 |items| DynSolValue::Array(items.to_vec()),
643 try_value,
644 ) {
645 *value = candidate;
646 return true;
647 }
648 minimize_elements(&mut elements, |items| DynSolValue::Array(items.to_vec()), try_value)
649 .is_some_and(|candidate| {
650 *value = candidate;
651 true
652 })
653 }
654 DynSolValue::FixedArray(mut elements) => {
655 if let Some(candidate) = minimize_elements_batch(
656 &mut elements,
657 |items| DynSolValue::FixedArray(items.to_vec()),
658 try_value,
659 ) {
660 *value = candidate;
661 return true;
662 }
663 if let Some(candidate) = minimize_element_subsets(
664 &mut elements,
665 |items| DynSolValue::FixedArray(items.to_vec()),
666 try_value,
667 ) {
668 *value = candidate;
669 return true;
670 }
671 if let Some(candidate) = minimize_element_pairs(
672 &mut elements,
673 |items| DynSolValue::FixedArray(items.to_vec()),
674 try_value,
675 ) {
676 *value = candidate;
677 return true;
678 }
679 minimize_elements(
680 &mut elements,
681 |items| DynSolValue::FixedArray(items.to_vec()),
682 try_value,
683 )
684 .is_some_and(|candidate| {
685 *value = candidate;
686 true
687 })
688 }
689 DynSolValue::Tuple(mut elements) => {
690 if let Some(candidate) = minimize_elements_batch(
691 &mut elements,
692 |items| DynSolValue::Tuple(items.to_vec()),
693 try_value,
694 ) {
695 *value = candidate;
696 return true;
697 }
698 if let Some(candidate) = minimize_element_subsets(
699 &mut elements,
700 |items| DynSolValue::Tuple(items.to_vec()),
701 try_value,
702 ) {
703 *value = candidate;
704 return true;
705 }
706 if let Some(candidate) = minimize_element_pairs(
707 &mut elements,
708 |items| DynSolValue::Tuple(items.to_vec()),
709 try_value,
710 ) {
711 *value = candidate;
712 return true;
713 }
714 minimize_elements(&mut elements, |items| DynSolValue::Tuple(items.to_vec()), try_value)
715 .is_some_and(|candidate| {
716 *value = candidate;
717 true
718 })
719 }
720 DynSolValue::CustomStruct { name, prop_names, mut tuple } => {
721 if let Some(candidate) = minimize_elements_batch(
722 &mut tuple,
723 |items| DynSolValue::CustomStruct {
724 name: name.clone(),
725 prop_names: prop_names.clone(),
726 tuple: items.to_vec(),
727 },
728 try_value,
729 ) {
730 *value = candidate;
731 return true;
732 }
733 if let Some(candidate) = minimize_element_subsets(
734 &mut tuple,
735 |items| DynSolValue::CustomStruct {
736 name: name.clone(),
737 prop_names: prop_names.clone(),
738 tuple: items.to_vec(),
739 },
740 try_value,
741 ) {
742 *value = candidate;
743 return true;
744 }
745 if let Some(candidate) = minimize_element_pairs(
746 &mut tuple,
747 |items| DynSolValue::CustomStruct {
748 name: name.clone(),
749 prop_names: prop_names.clone(),
750 tuple: items.to_vec(),
751 },
752 try_value,
753 ) {
754 *value = candidate;
755 return true;
756 }
757 minimize_elements(
758 &mut tuple,
759 |items| DynSolValue::CustomStruct {
760 name: name.clone(),
761 prop_names: prop_names.clone(),
762 tuple: items.to_vec(),
763 },
764 try_value,
765 )
766 .is_some_and(|candidate| {
767 *value = candidate;
768 true
769 })
770 }
771 _ => false,
772 }
773}
774
775fn minimize_uint(
776 value: &mut DynSolValue,
777 current: U256,
778 bits: usize,
779 try_value: &mut dyn FnMut(&DynSolValue) -> bool,
780) -> bool {
781 if !current.is_zero() && accept_candidate(value, DynSolValue::Uint(U256::ZERO, bits), try_value)
782 {
783 return true;
784 }
785
786 let one = U256::ONE;
787 if current > one && accept_candidate(value, DynSolValue::Uint(one, bits), try_value) {
788 return true;
789 }
790
791 let bit_limit = bits.min(256);
792 for bit in (0..bit_limit).rev() {
793 let mask = U256::ONE << bit;
794 if (current & mask).is_zero() {
795 continue;
796 }
797 let candidate = current & !mask;
798 if accept_candidate(value, DynSolValue::Uint(candidate, bits), try_value) {
799 return true;
800 }
801 }
802
803 minimize_uint_by_search(value, current, bits, try_value)
804}
805
806fn minimize_uint_by_search(
807 value: &mut DynSolValue,
808 current: U256,
809 bits: usize,
810 try_value: &mut dyn FnMut(&DynSolValue) -> bool,
811) -> bool {
812 if current <= U256::ONE {
813 return false;
814 }
815
816 let mut accepted = current;
817 let mut rejected = U256::ZERO;
818 let mut changed = false;
819 while accepted > rejected + U256::ONE {
820 let candidate: U256 = rejected + ((accepted - rejected) >> 1usize);
821 if accept_candidate(value, DynSolValue::Uint(candidate, bits), try_value) {
822 accepted = candidate;
823 changed = true;
824 } else {
825 rejected = candidate;
826 }
827 }
828
829 changed
830}
831
832fn minimize_int(
833 value: &mut DynSolValue,
834 current: I256,
835 bits: usize,
836 try_value: &mut dyn FnMut(&DynSolValue) -> bool,
837) -> bool {
838 if current != I256::ZERO
839 && accept_candidate(value, DynSolValue::Int(I256::ZERO, bits), try_value)
840 {
841 return true;
842 }
843 if current.is_negative()
844 && current != I256::MINUS_ONE
845 && accept_candidate(value, DynSolValue::Int(I256::MINUS_ONE, bits), try_value)
846 {
847 return true;
848 }
849
850 minimize_int_by_search(value, current, bits, try_value)
851}
852
853fn minimize_int_by_search(
854 value: &mut DynSolValue,
855 current: I256,
856 bits: usize,
857 try_value: &mut dyn FnMut(&DynSolValue) -> bool,
858) -> bool {
859 let mut accepted_abs = current.unsigned_abs();
860 if accepted_abs <= U256::ONE {
861 return false;
862 }
863
864 let mut rejected_abs = U256::ZERO;
865 let mut changed = false;
866 while accepted_abs > rejected_abs + U256::ONE {
867 let candidate_abs: U256 = rejected_abs + ((accepted_abs - rejected_abs) >> 1usize);
868 let candidate = signed_candidate_with_abs(current, candidate_abs);
869 if accept_candidate(value, DynSolValue::Int(candidate, bits), try_value) {
870 accepted_abs = candidate_abs;
871 changed = true;
872 } else {
873 rejected_abs = candidate_abs;
874 }
875 }
876
877 changed
878}
879
880fn minimize_address(
881 value: &mut DynSolValue,
882 current: Address,
883 try_value: &mut dyn FnMut(&DynSolValue) -> bool,
884) -> bool {
885 for candidate in address_candidates(current) {
886 if accept_candidate(value, DynSolValue::Address(candidate), try_value) {
887 return true;
888 }
889 }
890 false
891}
892
893fn address_candidates(current: Address) -> Vec<Address> {
894 if current.is_zero() {
895 return Vec::new();
896 }
897
898 let mut candidates = Vec::new();
899 candidates.push(Address::ZERO);
900
901 let deadbeef = Address::from_word(B256::from(U256::from(0xdeadbeefu64)));
902 if current != deadbeef {
903 candidates.push(deadbeef);
904 }
905
906 let bytes = current.into_array();
907 for idx in 0..bytes.len() {
908 if bytes[idx] == 0 {
909 continue;
910 }
911 let mut candidate = bytes;
912 candidate[idx] = 0;
913 candidates.push(Address::from(candidate));
914 }
915
916 candidates
917}
918
919fn minimize_fixed_bytes(
920 value: &mut DynSolValue,
921 current: B256,
922 size: usize,
923 try_value: &mut dyn FnMut(&DynSolValue) -> bool,
924) -> bool {
925 if !current.is_zero()
926 && accept_candidate(value, DynSolValue::FixedBytes(B256::ZERO, size), try_value)
927 {
928 return true;
929 }
930
931 let mut bytes = [0u8; 32];
932 bytes.copy_from_slice(current.as_slice());
933 for idx in (0..size.min(bytes.len())).rev() {
934 if bytes[idx] == 0 {
935 continue;
936 }
937 let mut candidate = bytes;
938 candidate[idx] = 0;
939 if accept_candidate(value, DynSolValue::FixedBytes(B256::from(candidate), size), try_value)
940 {
941 return true;
942 }
943 }
944 false
945}
946
947fn minimize_bytes(
948 value: &mut DynSolValue,
949 current: Vec<u8>,
950 try_value: &mut dyn FnMut(&DynSolValue) -> bool,
951) -> bool {
952 for len in 0..current.len() {
953 if accept_candidate(value, DynSolValue::Bytes(current[..len].to_vec()), try_value) {
954 return true;
955 }
956 }
957 if try_delete_vec_range(¤t, |candidate| {
958 accept_candidate(value, DynSolValue::Bytes(candidate), try_value)
959 }) {
960 return true;
961 }
962 if try_slice_vec_range(¤t, |candidate| {
963 accept_candidate(value, DynSolValue::Bytes(candidate), try_value)
964 }) {
965 return true;
966 }
967
968 for idx in (0..current.len()).rev() {
969 if current[idx] == 0 {
970 continue;
971 }
972 let mut candidate = current.clone();
973 candidate[idx] = 0;
974 if accept_candidate(value, DynSolValue::Bytes(candidate), try_value) {
975 return true;
976 }
977 }
978 false
979}
980
981fn minimize_string(
982 value: &mut DynSolValue,
983 current: String,
984 try_value: &mut dyn FnMut(&DynSolValue) -> bool,
985) -> bool {
986 for len in 0..current.len() {
987 if current.is_char_boundary(len)
988 && accept_candidate(value, DynSolValue::String(current[..len].to_string()), try_value)
989 {
990 return true;
991 }
992 }
993 if try_delete_string_range(¤t, |candidate| {
994 accept_candidate(value, DynSolValue::String(candidate), try_value)
995 }) {
996 return true;
997 }
998 if try_slice_string_range(¤t, |candidate| {
999 accept_candidate(value, DynSolValue::String(candidate), try_value)
1000 }) {
1001 return true;
1002 }
1003
1004 let current_bytes = current.as_bytes();
1005 for idx in (0..current_bytes.len()).rev() {
1006 if current_bytes[idx] == 0 {
1007 continue;
1008 }
1009 let mut candidate = current_bytes.to_vec();
1010 candidate[idx] = 0;
1011 if let Ok(candidate) = String::from_utf8(candidate)
1012 && accept_candidate(value, DynSolValue::String(candidate), try_value)
1013 {
1014 return true;
1015 }
1016 }
1017 false
1018}
1019
1020fn minimize_array_len(
1021 value: &mut DynSolValue,
1022 current: &mut Vec<DynSolValue>,
1023 try_value: &mut dyn FnMut(&DynSolValue) -> bool,
1024) -> bool {
1025 for len in 0..current.len() {
1026 let candidate = DynSolValue::Array(current[..len].to_vec());
1027 if accept_candidate(value, candidate, try_value) {
1028 current.truncate(len);
1029 return true;
1030 }
1031 }
1032 if try_delete_vec_range(current, |candidate| {
1033 accept_candidate(value, DynSolValue::Array(candidate), try_value)
1034 }) {
1035 return true;
1036 }
1037 if try_slice_vec_range(current, |candidate| {
1038 accept_candidate(value, DynSolValue::Array(candidate), try_value)
1039 }) {
1040 return true;
1041 }
1042 false
1043}
1044
1045fn try_delete_vec_range<T: Clone>(
1046 current: &[T],
1047 mut try_candidate: impl FnMut(Vec<T>) -> bool,
1048) -> bool {
1049 for range_len in deletion_lengths(current.len()) {
1050 for start in 0..=current.len() - range_len {
1051 let mut candidate = Vec::with_capacity(current.len() - range_len);
1052 candidate.extend_from_slice(¤t[..start]);
1053 candidate.extend_from_slice(¤t[start + range_len..]);
1054 if try_candidate(candidate) {
1055 return true;
1056 }
1057 }
1058 }
1059 false
1060}
1061
1062fn try_delete_string_range(current: &str, mut try_candidate: impl FnMut(String) -> bool) -> bool {
1063 let mut boundaries = current.char_indices().map(|(idx, _)| idx).collect::<Vec<_>>();
1064 boundaries.push(current.len());
1065
1066 for range_len in deletion_lengths(boundaries.len().saturating_sub(1)) {
1067 for start_idx in 0..=boundaries.len() - range_len - 1 {
1068 let start = boundaries[start_idx];
1069 let end = boundaries[start_idx + range_len];
1070 let mut candidate = String::with_capacity(current.len() - (end - start));
1071 candidate.push_str(¤t[..start]);
1072 candidate.push_str(¤t[end..]);
1073 if try_candidate(candidate) {
1074 return true;
1075 }
1076 }
1077 }
1078 false
1079}
1080
1081fn try_slice_vec_range<T: Clone>(
1082 current: &[T],
1083 mut try_candidate: impl FnMut(Vec<T>) -> bool,
1084) -> bool {
1085 for len in 1..current.len() {
1086 for start in 1..=current.len() - len {
1087 let candidate = current[start..start + len].to_vec();
1088 if try_candidate(candidate) {
1089 return true;
1090 }
1091 }
1092 }
1093 false
1094}
1095
1096fn try_slice_string_range(current: &str, mut try_candidate: impl FnMut(String) -> bool) -> bool {
1097 let mut boundaries = current.char_indices().map(|(idx, _)| idx).collect::<Vec<_>>();
1098 boundaries.push(current.len());
1099 let char_len = boundaries.len().saturating_sub(1);
1100
1101 for len in 1..char_len {
1102 for start_idx in 1..=char_len - len {
1103 let start = boundaries[start_idx];
1104 let end = boundaries[start_idx + len];
1105 if try_candidate(current[start..end].to_string()) {
1106 return true;
1107 }
1108 }
1109 }
1110 false
1111}
1112
1113fn deletion_lengths(len: usize) -> Vec<usize> {
1114 if len == 0 {
1115 return Vec::new();
1116 }
1117
1118 let mut lengths = Vec::new();
1119 let mut range_len = len;
1120 while range_len > 0 {
1121 lengths.push(range_len);
1122 range_len /= 2;
1123 }
1124 lengths.sort_unstable();
1125 lengths.dedup();
1126 lengths.reverse();
1127 lengths
1128}
1129
1130fn minimize_elements(
1131 elements: &mut [DynSolValue],
1132 rebuild: impl Fn(&[DynSolValue]) -> DynSolValue,
1133 try_value: &mut dyn FnMut(&DynSolValue) -> bool,
1134) -> Option<DynSolValue> {
1135 for idx in 0..elements.len() {
1136 let mut element = elements[idx].clone();
1137 let changed = minimize_value(&mut element, &mut |candidate| {
1138 let mut candidate_elements = elements.to_vec();
1139 candidate_elements[idx] = candidate.clone();
1140 try_value(&rebuild(&candidate_elements))
1141 });
1142 if changed {
1143 elements[idx] = element;
1144 return Some(rebuild(elements));
1145 }
1146 }
1147 None
1148}
1149
1150fn minimize_elements_batch(
1151 elements: &mut Vec<DynSolValue>,
1152 rebuild: impl Fn(&[DynSolValue]) -> DynSolValue,
1153 try_value: &mut dyn FnMut(&DynSolValue) -> bool,
1154) -> Option<DynSolValue> {
1155 let simple_elements = elements.iter().cloned().map(minimally_simple_value).collect::<Vec<_>>();
1156 if simple_elements == *elements {
1157 return None;
1158 }
1159 let candidate = rebuild(&simple_elements);
1160 try_value(&candidate).then(|| {
1161 *elements = simple_elements;
1162 candidate
1163 })
1164}
1165
1166fn minimize_element_pairs(
1167 elements: &mut Vec<DynSolValue>,
1168 rebuild: impl Fn(&[DynSolValue]) -> DynSolValue,
1169 try_value: &mut dyn FnMut(&DynSolValue) -> bool,
1170) -> Option<DynSolValue> {
1171 for left_idx in 0..elements.len() {
1172 for right_idx in left_idx + 1..elements.len() {
1173 let left = elements[left_idx].clone();
1174 let right = elements[right_idx].clone();
1175 if minimize_numeric_value_pair(&left, &right, |left, right| {
1176 let mut candidate_elements = elements.clone();
1177 candidate_elements[left_idx] = left;
1178 candidate_elements[right_idx] = right;
1179 if try_value(&rebuild(&candidate_elements)) {
1180 *elements = candidate_elements;
1181 true
1182 } else {
1183 false
1184 }
1185 }) {
1186 return Some(rebuild(elements));
1187 }
1188 }
1189 }
1190 None
1191}
1192
1193fn minimize_element_subsets(
1194 elements: &mut Vec<DynSolValue>,
1195 rebuild: impl Fn(&[DynSolValue]) -> DynSolValue,
1196 try_value: &mut dyn FnMut(&DynSolValue) -> bool,
1197) -> Option<DynSolValue> {
1198 let simple_elements = elements.iter().cloned().map(minimally_simple_value).collect::<Vec<_>>();
1199 let shrinkable_idxs = elements
1200 .iter()
1201 .zip(&simple_elements)
1202 .enumerate()
1203 .filter_map(|(idx, (current, simple))| (current != simple).then_some(idx))
1204 .collect::<Vec<_>>();
1205 if shrinkable_idxs.len() < 2 {
1206 return None;
1207 }
1208
1209 for subset_size in subset_sizes(shrinkable_idxs.len()) {
1210 let mut search = ElementSubsetSearch {
1211 elements,
1212 simple_elements: &simple_elements,
1213 shrinkable_idxs: &shrinkable_idxs,
1214 rebuild: &rebuild,
1215 try_value,
1216 };
1217 let mut subset = Vec::with_capacity(subset_size);
1218 if let Some(candidate_elements) =
1219 try_element_subset(&mut search, subset_size, 0, &mut subset)
1220 {
1221 *elements = candidate_elements.clone();
1222 return Some(rebuild(&candidate_elements));
1223 }
1224 }
1225 None
1226}
1227
1228fn minimize_numeric_value_pair(
1229 left: &DynSolValue,
1230 right: &DynSolValue,
1231 mut try_pair: impl FnMut(DynSolValue, DynSolValue) -> bool,
1232) -> bool {
1233 match (left, right) {
1234 (DynSolValue::Uint(left, left_bits), DynSolValue::Uint(right, right_bits)) => {
1235 let mut try_uint_pair = |left, right| {
1236 try_pair(DynSolValue::Uint(left, *left_bits), DynSolValue::Uint(right, *right_bits))
1237 };
1238 minimize_u256_pair_delta_candidates(*left, *right, &mut try_uint_pair)
1239 }
1240 (DynSolValue::Int(left, left_bits), DynSolValue::Int(right, right_bits)) => {
1241 minimize_i256_pair_candidates(*left, *right, |left, right| {
1242 try_pair(DynSolValue::Int(left, *left_bits), DynSolValue::Int(right, *right_bits))
1243 })
1244 }
1245 _ => false,
1246 }
1247}
1248
1249fn minimize_u256_pair_delta_candidates(
1250 current_left: U256,
1251 current_right: U256,
1252 try_candidate: &mut impl FnMut(U256, U256) -> bool,
1253) -> bool {
1254 let one = U256::ONE;
1255 if (current_left, current_right) != (one, one)
1256 && !current_left.is_zero()
1257 && !current_right.is_zero()
1258 && try_candidate(one, one)
1259 {
1260 return true;
1261 }
1262
1263 match current_left.cmp(¤t_right) {
1264 std::cmp::Ordering::Equal => {
1265 !current_left.is_zero() && try_candidate(U256::ZERO, U256::ZERO)
1266 }
1267 std::cmp::Ordering::Greater => {
1268 let delta = current_left - current_right;
1269 !current_right.is_zero() && try_candidate(delta, U256::ZERO)
1270 }
1271 std::cmp::Ordering::Less => {
1272 let delta = current_right - current_left;
1273 !current_left.is_zero() && try_candidate(U256::ZERO, delta)
1274 }
1275 }
1276}
1277
1278fn minimize_i256_pair_candidates(
1279 current_left: I256,
1280 current_right: I256,
1281 mut try_candidate: impl FnMut(I256, I256) -> bool,
1282) -> bool {
1283 if current_left == I256::ZERO || current_right == I256::ZERO {
1284 return false;
1285 }
1286 if current_left.is_negative() != current_right.is_negative() {
1287 return false;
1288 }
1289
1290 minimize_u256_pair_candidates(
1291 current_left.unsigned_abs(),
1292 current_right.unsigned_abs(),
1293 |left_abs, right_abs| {
1294 let left = signed_candidate_with_abs(current_left, left_abs);
1295 let right = signed_candidate_with_abs(current_right, right_abs);
1296 try_candidate(left, right)
1297 },
1298 )
1299}
1300
1301const fn signed_candidate_with_abs(current: I256, abs: U256) -> I256 {
1302 if current.is_negative() { I256::from_raw(abs.wrapping_neg()) } else { I256::from_raw(abs) }
1303}
1304
1305fn subset_sizes(shrinkable_len: usize) -> Vec<usize> {
1306 let mut sizes = Vec::new();
1307 for subset_size in 2..shrinkable_len {
1308 if bounded_combination_count(shrinkable_len, subset_size, MAX_SUBSET_CANDIDATES_PER_PASS)
1309 .is_some()
1310 {
1311 sizes.push(subset_size);
1312 }
1313 }
1314 sizes
1315}
1316
1317fn bounded_combination_count(n: usize, k: usize, max: usize) -> Option<usize> {
1318 if k > n {
1319 return None;
1320 }
1321 let k = k.min(n - k);
1322 let mut count = 1usize;
1323 for step in 1..=k {
1324 count = count.checked_mul(n + 1 - step)?;
1325 count /= step;
1326 if count > max {
1327 return None;
1328 }
1329 }
1330 Some(count)
1331}
1332
1333struct ElementSubsetSearch<'a, R>
1334where
1335 R: Fn(&[DynSolValue]) -> DynSolValue,
1336{
1337 elements: &'a [DynSolValue],
1338 simple_elements: &'a [DynSolValue],
1339 shrinkable_idxs: &'a [usize],
1340 rebuild: &'a R,
1341 try_value: &'a mut dyn FnMut(&DynSolValue) -> bool,
1342}
1343
1344fn try_element_subset<R>(
1345 search: &mut ElementSubsetSearch<'_, R>,
1346 subset_size: usize,
1347 start: usize,
1348 subset: &mut Vec<usize>,
1349) -> Option<Vec<DynSolValue>>
1350where
1351 R: Fn(&[DynSolValue]) -> DynSolValue,
1352{
1353 if subset.len() == subset_size {
1354 let mut candidate_elements = search.elements.to_vec();
1355 for idx in subset.iter().copied() {
1356 candidate_elements[idx] = search.simple_elements[idx].clone();
1357 }
1358 if (search.try_value)(&(search.rebuild)(&candidate_elements)) {
1359 return Some(candidate_elements);
1360 }
1361 return None;
1362 }
1363
1364 let remaining = subset_size - subset.len();
1365 for choice_idx in start..=search.shrinkable_idxs.len() - remaining {
1366 subset.push(search.shrinkable_idxs[choice_idx]);
1367 if let Some(candidate) = try_element_subset(search, subset_size, choice_idx + 1, subset) {
1368 return Some(candidate);
1369 }
1370 subset.pop();
1371 }
1372 None
1373}
1374
1375fn accept_candidate(
1376 value: &mut DynSolValue,
1377 candidate: DynSolValue,
1378 try_value: &mut dyn FnMut(&DynSolValue) -> bool,
1379) -> bool {
1380 if *value == candidate {
1381 return false;
1382 }
1383 if try_value(&candidate) {
1384 *value = candidate;
1385 true
1386 } else {
1387 false
1388 }
1389}
1390
1391#[cfg(test)]
1392mod tests {
1393 use super::*;
1394 use alloy_json_abi::JsonAbi;
1395 use std::collections::HashSet;
1396
1397 const TEST_MAX_MINIMIZATION_ATTEMPTS: usize = 5000;
1398
1399 fn call(function: &Function, args: Vec<DynSolValue>) -> SymbolicCounterexampleCall {
1400 SymbolicCounterexampleCall {
1401 warp: None,
1402 roll: None,
1403 sender: Address::ZERO,
1404 target: Address::repeat_byte(0x11),
1405 calldata: Bytes::from(function.abi_encode_input(&args).unwrap()),
1406 value: Some(U256::ZERO),
1407 contract_name: Some("Target".to_string()),
1408 function_name: Some(function.name.clone()),
1409 signature: Some(function.signature()),
1410 args: Some(foundry_common::fmt::format_tokens(&args).format(", ").to_string()),
1411 raw_args: Some(foundry_common::fmt::format_tokens_raw(&args).format(", ").to_string()),
1412 }
1413 }
1414
1415 fn decoded(function: &Function, call: &SymbolicCounterexampleCall) -> Vec<DynSolValue> {
1416 function.abi_decode_input(&call.calldata[4..]).unwrap()
1417 }
1418
1419 fn address(value: u64) -> Address {
1420 Address::from_word(B256::from(U256::from(value)))
1421 }
1422
1423 #[test]
1424 fn minimizes_common_abi_values_with_replay_predicate() {
1425 let abi =
1426 JsonAbi::parse(["function check(uint256,address,bytes,string,uint256[]) external"])
1427 .unwrap();
1428 let function = abi.functions().next().unwrap();
1429 let start = call(
1430 function,
1431 vec![
1432 DynSolValue::Uint(U256::from(0xff), 256),
1433 DynSolValue::Address(Address::repeat_byte(0xaa)),
1434 DynSolValue::Bytes(vec![0x99, 0x42, 0x88]),
1435 DynSolValue::String("abc".to_string()),
1436 DynSolValue::Array(vec![
1437 DynSolValue::Uint(U256::ZERO, 256),
1438 DynSolValue::Uint(U256::from(7), 256),
1439 DynSolValue::Uint(U256::from(9), 256),
1440 ]),
1441 ],
1442 );
1443
1444 let minimized =
1445 minimize_single_call_counterexample(function, &start, TEST_MAX_MINIMIZATION_ATTEMPTS, |candidate| {
1446 let args = decoded(function, candidate);
1447 matches!(&args[0], DynSolValue::Uint(value, _) if *value & U256::from(0x2a) == U256::from(0x2a))
1448 && matches!(&args[1], DynSolValue::Address(address) if address.as_slice()[19] == 0xaa)
1449 && matches!(&args[2], DynSolValue::Bytes(bytes) if bytes.get(1) == Some(&0x42))
1450 && matches!(&args[3], DynSolValue::String(value) if value.starts_with('a'))
1451 && matches!(&args[4], DynSolValue::Array(values) if values.iter().any(|value| matches!(value, DynSolValue::Uint(uint, _) if *uint == U256::from(7))))
1452 })
1453 .unwrap();
1454
1455 let args = decoded(function, &minimized.minimized_call);
1456 assert_eq!(args[0], DynSolValue::Uint(U256::from(0x2a), 256));
1457 assert_eq!(args[1], DynSolValue::Address(Address::with_last_byte(0xaa)));
1458 assert_eq!(args[2], DynSolValue::Bytes(vec![0, 0x42]));
1459 assert_eq!(args[3], DynSolValue::String("a".to_string()));
1460 assert_eq!(args[4], DynSolValue::Array(vec![DynSolValue::Uint(U256::from(7), 256)]));
1461 assert!(minimized.changed());
1462 assert!(minimized.attempts > minimized.accepted);
1463 assert!(minimized.accepted > 0);
1464 }
1465
1466 #[test]
1467 fn minimizes_with_echidna_style_range_deletion_and_numeric_lowering() {
1468 let abi =
1469 JsonAbi::parse(["function check(uint256,int256,bytes,string,uint256[]) external"])
1470 .unwrap();
1471 let function = abi.functions().next().unwrap();
1472 let start = call(
1473 function,
1474 vec![
1475 DynSolValue::Uint(U256::from(100), 256),
1476 DynSolValue::Int(I256::try_from(-100).unwrap(), 256),
1477 DynSolValue::Bytes(vec![1, 2, 3, 0x42, 4]),
1478 DynSolValue::String("abcZ".to_string()),
1479 DynSolValue::Array(vec![
1480 DynSolValue::Uint(U256::ONE, 256),
1481 DynSolValue::Uint(U256::from(2), 256),
1482 DynSolValue::Uint(U256::from(7), 256),
1483 DynSolValue::Uint(U256::from(3), 256),
1484 ]),
1485 ],
1486 );
1487
1488 let minimized = minimize_single_call_counterexample(function, &start, TEST_MAX_MINIMIZATION_ATTEMPTS, |candidate| {
1489 let args = decoded(function, candidate);
1490 matches!(&args[0], DynSolValue::Uint(value, _) if *value > U256::from(42))
1491 && matches!(&args[1], DynSolValue::Int(value, _) if *value < I256::try_from(-42).unwrap())
1492 && matches!(&args[2], DynSolValue::Bytes(bytes) if bytes.contains(&0x42))
1493 && matches!(&args[3], DynSolValue::String(value) if value.contains('Z'))
1494 && matches!(&args[4], DynSolValue::Array(values) if values.iter().any(|value| matches!(value, DynSolValue::Uint(uint, _) if *uint == U256::from(7))))
1495 })
1496 .unwrap();
1497
1498 let args = decoded(function, &minimized.minimized_call);
1499 assert_eq!(args[0], DynSolValue::Uint(U256::from(43), 256));
1500 assert_eq!(args[1], DynSolValue::Int(I256::try_from(-43).unwrap(), 256));
1501 assert_eq!(args[2], DynSolValue::Bytes(vec![0x42]));
1502 assert_eq!(args[3], DynSolValue::String("Z".to_string()));
1503 assert_eq!(args[4], DynSolValue::Array(vec![DynSolValue::Uint(U256::from(7), 256)]));
1504 assert!(minimized.changed());
1505 assert!(minimized.accepted > 0);
1506 }
1507
1508 #[test]
1509 fn matches_echidna_uint8_threshold_shrink_result() {
1510 let abi = JsonAbi::parse(["function check(uint8) external"]).unwrap();
1511 let function = abi.functions().next().unwrap();
1512 let start = call(function, vec![DynSolValue::Uint(U256::from(246), 8)]);
1513
1514 let minimized = minimize_single_call_counterexample(
1515 function,
1516 &start,
1517 TEST_MAX_MINIMIZATION_ATTEMPTS,
1518 |candidate| {
1519 let args = decoded(function, candidate);
1520 matches!(&args[0], DynSolValue::Uint(value, 8) if *value > U256::from(42))
1521 },
1522 )
1523 .unwrap();
1524
1525 assert_eq!(
1526 decoded(function, &minimized.minimized_call),
1527 vec![DynSolValue::Uint(U256::from(43), 8)]
1528 );
1529 assert!(minimized.attempts < TEST_MAX_MINIMIZATION_ATTEMPTS);
1530 }
1531
1532 #[test]
1533 fn uint_minimization_prefers_bit_clearing_before_binary_search() {
1534 let mut value = DynSolValue::Uint(U256::from(100), 256);
1535 let accepted = [U256::from(100), U256::from(50), U256::from(36)];
1536
1537 loop {
1538 let (current, bits) = match &value {
1539 DynSolValue::Uint(current, bits) => (*current, *bits),
1540 _ => unreachable!(),
1541 };
1542 if !minimize_uint(
1543 &mut value,
1544 current,
1545 bits,
1546 &mut |candidate| matches!(candidate, DynSolValue::Uint(candidate, _) if accepted.contains(candidate)),
1547 ) {
1548 break;
1549 }
1550 }
1551
1552 assert_eq!(value, DynSolValue::Uint(U256::from(36), 256));
1553 }
1554
1555 #[test]
1556 fn uint_pair_delta_minimization_tries_simple_nonzero_pair_first() {
1557 let mut candidates = Vec::new();
1558
1559 assert!(minimize_u256_pair_delta_candidates(
1560 U256::from(100),
1561 U256::from(50),
1562 &mut |left, right| {
1563 candidates.push((left, right));
1564 (left, right) == (U256::ONE, U256::ONE)
1565 },
1566 ));
1567
1568 assert_eq!(candidates, vec![(U256::ONE, U256::ONE)]);
1569 }
1570
1571 #[test]
1572 fn skips_duplicate_single_call_replay_candidates() {
1573 let abi = JsonAbi::parse(["function check(uint256,uint256) external"]).unwrap();
1574 let function = abi.functions().next().unwrap();
1575 let start = call(
1576 function,
1577 vec![DynSolValue::Uint(U256::ONE, 256), DynSolValue::Uint(U256::ONE, 256)],
1578 );
1579 let mut replayed = HashSet::new();
1580
1581 let minimized = minimize_single_call_counterexample(
1582 function,
1583 &start,
1584 TEST_MAX_MINIMIZATION_ATTEMPTS,
1585 |candidate| {
1586 assert!(replayed.insert(candidate.calldata.clone()), "duplicate candidate replay");
1587 false
1588 },
1589 )
1590 .unwrap();
1591
1592 assert!(!minimized.changed());
1593 assert_eq!(minimized.accepted, 0);
1594 assert_eq!(minimized.attempts, 3);
1595 assert_eq!(replayed.len(), minimized.attempts);
1596 }
1597
1598 #[test]
1599 fn matches_echidna_contiguous_slice_examples() {
1600 let bytes_abi = JsonAbi::parse(["function check(bytes) external"]).unwrap();
1601 let bytes_function = bytes_abi.functions().next().unwrap();
1602 let bytes_start =
1603 call(bytes_function, vec![DynSolValue::Bytes(vec![0x99, 0x41, 0x42, 0x88])]);
1604 let bytes_minimized = minimize_single_call_counterexample(
1605 bytes_function,
1606 &bytes_start,
1607 TEST_MAX_MINIMIZATION_ATTEMPTS,
1608 |candidate| {
1609 decoded(bytes_function, candidate) == vec![DynSolValue::Bytes(vec![0x41, 0x42])]
1610 },
1611 )
1612 .unwrap();
1613 assert_eq!(
1614 decoded(bytes_function, &bytes_minimized.minimized_call),
1615 vec![DynSolValue::Bytes(vec![0x41, 0x42])]
1616 );
1617
1618 let string_abi = JsonAbi::parse(["function check(string) external"]).unwrap();
1619 let string_function = string_abi.functions().next().unwrap();
1620 let string_start = call(string_function, vec![DynSolValue::String("xOKy".to_string())]);
1621 let string_minimized = minimize_single_call_counterexample(
1622 string_function,
1623 &string_start,
1624 TEST_MAX_MINIMIZATION_ATTEMPTS,
1625 |candidate| {
1626 decoded(string_function, candidate) == vec![DynSolValue::String("OK".to_string())]
1627 },
1628 )
1629 .unwrap();
1630 assert_eq!(
1631 decoded(string_function, &string_minimized.minimized_call),
1632 vec![DynSolValue::String("OK".to_string())]
1633 );
1634
1635 let array_abi = JsonAbi::parse(["function check(uint256[]) external"]).unwrap();
1636 let array_function = array_abi.functions().next().unwrap();
1637 let array_start = call(
1638 array_function,
1639 vec![DynSolValue::Array(vec![
1640 DynSolValue::Uint(U256::from(9), 256),
1641 DynSolValue::Uint(U256::from(4), 256),
1642 DynSolValue::Uint(U256::from(2), 256),
1643 DynSolValue::Uint(U256::from(8), 256),
1644 ])],
1645 );
1646 let array_minimized = minimize_single_call_counterexample(
1647 array_function,
1648 &array_start,
1649 TEST_MAX_MINIMIZATION_ATTEMPTS,
1650 |candidate| {
1651 let args = decoded(array_function, candidate);
1652 matches!(&args[0], DynSolValue::Array(values) if values == &[
1653 DynSolValue::Uint(U256::from(4), 256),
1654 DynSolValue::Uint(U256::from(2), 256),
1655 ])
1656 },
1657 )
1658 .unwrap();
1659 assert_eq!(
1660 decoded(array_function, &array_minimized.minimized_call),
1661 vec![DynSolValue::Array(vec![
1662 DynSolValue::Uint(U256::from(4), 256),
1663 DynSolValue::Uint(U256::from(2), 256),
1664 ])]
1665 );
1666 }
1667
1668 #[test]
1669 fn matches_echidna_address_deadbeef_and_bool_examples() {
1670 let deadbeef = address(0xdeadbeef);
1671
1672 let address_abi = JsonAbi::parse(["function check(address) external"]).unwrap();
1673 let address_function = address_abi.functions().next().unwrap();
1674 let address_start =
1675 call(address_function, vec![DynSolValue::Address(Address::repeat_byte(0xaa))]);
1676 let address_minimized = minimize_single_call_counterexample(
1677 address_function,
1678 &address_start,
1679 TEST_MAX_MINIMIZATION_ATTEMPTS,
1680 |candidate| {
1681 let args = decoded(address_function, candidate);
1682 matches!(&args[..], [DynSolValue::Address(address)] if *address == deadbeef)
1683 },
1684 )
1685 .unwrap();
1686 assert_eq!(
1687 decoded(address_function, &address_minimized.minimized_call),
1688 vec![DynSolValue::Address(deadbeef)]
1689 );
1690
1691 let bool_abi = JsonAbi::parse(["function check(bool) external"]).unwrap();
1692 let bool_function = bool_abi.functions().next().unwrap();
1693 let bool_start = call(bool_function, vec![DynSolValue::Bool(true)]);
1694 let bool_minimized = minimize_single_call_counterexample(
1695 bool_function,
1696 &bool_start,
1697 TEST_MAX_MINIMIZATION_ATTEMPTS,
1698 |candidate| decoded(bool_function, candidate) == vec![DynSolValue::Bool(false)],
1699 )
1700 .unwrap();
1701 assert_eq!(
1702 decoded(bool_function, &bool_minimized.minimized_call),
1703 vec![DynSolValue::Bool(false)]
1704 );
1705 }
1706
1707 #[test]
1708 fn minimizes_correlated_multi_arg_slice_examples() {
1709 let abi = JsonAbi::parse(["function check(bytes,string) external"]).unwrap();
1710 let function = abi.functions().next().unwrap();
1711 let start = call(
1712 function,
1713 vec![
1714 DynSolValue::Bytes(vec![0x99, 0x41, 0x42, 0x88]),
1715 DynSolValue::String("xOKy".to_string()),
1716 ],
1717 );
1718
1719 let minimized = minimize_single_call_counterexample(
1720 function,
1721 &start,
1722 TEST_MAX_MINIMIZATION_ATTEMPTS,
1723 |candidate| {
1724 let args = decoded(function, candidate);
1725 matches!(&args[..], [
1726 DynSolValue::Bytes(bytes),
1727 DynSolValue::String(string),
1728 ] if bytes == &[0x41, 0x42] && string.contains("OK"))
1729 },
1730 )
1731 .unwrap();
1732
1733 assert_eq!(
1734 decoded(function, &minimized.minimized_call),
1735 vec![DynSolValue::Bytes(vec![0x41, 0x42]), DynSolValue::String("OK".to_string()),]
1736 );
1737 assert!(minimized.changed());
1738 }
1739
1740 #[test]
1741 fn adapts_echidna_values_darray_fixture() {
1742 let abi = JsonAbi::parse(["function add_darray(address[]) external"]).unwrap();
1743 let function = abi.functions().next().unwrap();
1744 let target = address(0x123456);
1745 let start = call(
1746 function,
1747 vec![DynSolValue::Array(vec![
1748 DynSolValue::Address(address(0xaaaa)),
1749 DynSolValue::Address(target),
1750 DynSolValue::Address(address(0xbbbb)),
1751 ])],
1752 );
1753
1754 let minimized = minimize_single_call_counterexample(
1755 function,
1756 &start,
1757 TEST_MAX_MINIMIZATION_ATTEMPTS,
1758 |candidate| {
1759 let args = decoded(function, candidate);
1760 matches!(&args[0], DynSolValue::Array(values) if values.iter().any(|value| {
1761 matches!(value, DynSolValue::Address(candidate) if *candidate == target)
1762 }))
1763 },
1764 )
1765 .unwrap();
1766
1767 assert_eq!(
1768 decoded(function, &minimized.minimized_call),
1769 vec![DynSolValue::Array(vec![DynSolValue::Address(target)])]
1770 );
1771 assert!(minimized.changed());
1772 }
1773
1774 #[test]
1775 fn adapts_echidna_abiv2_dynamic_struct_fixture() {
1776 let abi = JsonAbi::parse(["function yolo((uint256,string,address)) external"]).unwrap();
1777 let function = abi.functions().next().unwrap();
1778 let start = call(
1779 function,
1780 vec![DynSolValue::Tuple(vec![
1781 DynSolValue::Uint(U256::from(137), 256),
1782 DynSolValue::String("xyoloy".to_string()),
1783 DynSolValue::Address(Address::repeat_byte(0xaa)),
1784 ])],
1785 );
1786
1787 let minimized = minimize_single_call_counterexample(
1788 function,
1789 &start,
1790 TEST_MAX_MINIMIZATION_ATTEMPTS,
1791 |candidate| {
1792 let args = decoded(function, candidate);
1793 matches!(&args[0], DynSolValue::Tuple(values)
1794 if matches!(&values[..], [
1795 DynSolValue::Uint(_, _),
1796 DynSolValue::String(value),
1797 DynSolValue::Address(_),
1798 ] if value.contains("yolo")))
1799 },
1800 )
1801 .unwrap();
1802
1803 assert_eq!(
1804 decoded(function, &minimized.minimized_call),
1805 vec![DynSolValue::Tuple(vec![
1806 DynSolValue::Uint(U256::ZERO, 256),
1807 DynSolValue::String("yolo".to_string()),
1808 DynSolValue::Address(Address::ZERO),
1809 ])]
1810 );
1811 assert!(minimized.changed());
1812 }
1813
1814 #[test]
1815 fn adapts_echidna_abiv2_multituple_fixture() {
1816 let abi = JsonAbi::parse(["function f(((bytes)),((bytes))) external"]).unwrap();
1817 let function = abi.functions().next().unwrap();
1818 let start = call(
1819 function,
1820 vec![
1821 DynSolValue::Tuple(vec![DynSolValue::Tuple(vec![DynSolValue::Bytes(vec![
1822 0x99, 0x42, 0x88,
1823 ])])]),
1824 DynSolValue::Tuple(vec![DynSolValue::Tuple(vec![DynSolValue::Bytes(vec![
1825 0xaa, 0xbb,
1826 ])])]),
1827 ],
1828 );
1829
1830 let minimized = minimize_single_call_counterexample(
1831 function,
1832 &start,
1833 TEST_MAX_MINIMIZATION_ATTEMPTS,
1834 |candidate| {
1835 let args = decoded(function, candidate);
1836 matches!(&args[0], DynSolValue::Tuple(outer)
1837 if matches!(&outer[0], DynSolValue::Tuple(inner)
1838 if matches!(&inner[0], DynSolValue::Bytes(bytes) if bytes.contains(&0x42))))
1839 },
1840 )
1841 .unwrap();
1842
1843 assert_eq!(
1844 decoded(function, &minimized.minimized_call),
1845 vec![
1846 DynSolValue::Tuple(vec![DynSolValue::Tuple(vec![DynSolValue::Bytes(vec![0x42])])]),
1847 DynSolValue::Tuple(vec![DynSolValue::Tuple(vec![DynSolValue::Bytes(Vec::new())])]),
1848 ]
1849 );
1850 assert!(minimized.changed());
1851 }
1852
1853 #[test]
1854 fn minimizes_correlated_top_level_abi_value_subsets() {
1855 let abi = JsonAbi::parse(["function check(uint256,uint256,bytes) external"]).unwrap();
1856 let function = abi.functions().next().unwrap();
1857 let start = call(
1858 function,
1859 vec![
1860 DynSolValue::Uint(U256::ONE, 256),
1861 DynSolValue::Uint(U256::ONE, 256),
1862 DynSolValue::Bytes(vec![0x99, 0x42, 0x88]),
1863 ],
1864 );
1865
1866 let minimized = minimize_single_call_counterexample(
1867 function,
1868 &start,
1869 TEST_MAX_MINIMIZATION_ATTEMPTS,
1870 |candidate| {
1871 let args = decoded(function, candidate);
1872 matches!(&args[..], [
1873 DynSolValue::Uint(left, _),
1874 DynSolValue::Uint(right, _),
1875 DynSolValue::Bytes(bytes),
1876 ] if left.is_zero() && right.is_zero() && bytes.contains(&0x42))
1877 },
1878 )
1879 .unwrap();
1880
1881 assert_eq!(
1882 decoded(function, &minimized.minimized_call),
1883 vec![
1884 DynSolValue::Uint(U256::ZERO, 256),
1885 DynSolValue::Uint(U256::ZERO, 256),
1886 DynSolValue::Bytes(vec![0x42]),
1887 ]
1888 );
1889 assert!(minimized.accepted > 0);
1890 }
1891
1892 #[test]
1893 fn minimizes_correlated_nested_abi_value_subsets() {
1894 let abi = JsonAbi::parse(["function check((uint256,uint256,bytes)) external"]).unwrap();
1895 let function = abi.functions().next().unwrap();
1896 let start = call(
1897 function,
1898 vec![DynSolValue::Tuple(vec![
1899 DynSolValue::Uint(U256::ONE, 256),
1900 DynSolValue::Uint(U256::ONE, 256),
1901 DynSolValue::Bytes(vec![0x99, 0x42, 0x88]),
1902 ])],
1903 );
1904
1905 let minimized = minimize_single_call_counterexample(
1906 function,
1907 &start,
1908 TEST_MAX_MINIMIZATION_ATTEMPTS,
1909 |candidate| {
1910 let args = decoded(function, candidate);
1911 matches!(&args[0], DynSolValue::Tuple(values)
1912 if matches!(&values[..], [
1913 DynSolValue::Uint(left, _),
1914 DynSolValue::Uint(right, _),
1915 DynSolValue::Bytes(bytes),
1916 ] if left.is_zero() && right.is_zero() && bytes.contains(&0x42)))
1917 },
1918 )
1919 .unwrap();
1920
1921 assert_eq!(
1922 decoded(function, &minimized.minimized_call),
1923 vec![DynSolValue::Tuple(vec![
1924 DynSolValue::Uint(U256::ZERO, 256),
1925 DynSolValue::Uint(U256::ZERO, 256),
1926 DynSolValue::Bytes(vec![0x42]),
1927 ]),]
1928 );
1929 assert!(minimized.accepted > 0);
1930 }
1931
1932 #[test]
1933 fn adapts_echidna_symbolic_fixed_array_relation_fixture() {
1934 let abi = JsonAbi::parse(["function array(uint256[3]) external"]).unwrap();
1935 let function = abi.functions().next().unwrap();
1936 let start = call(
1937 function,
1938 vec![DynSolValue::FixedArray(vec![
1939 DynSolValue::Uint(U256::from(4_370_001), 256),
1940 DynSolValue::Uint(U256::from(1_524_785_991), 256),
1941 DynSolValue::Uint(U256::from(4_370_000), 256),
1942 ])],
1943 );
1944
1945 let minimized = minimize_single_call_counterexample(
1946 function,
1947 &start,
1948 TEST_MAX_MINIMIZATION_ATTEMPTS,
1949 |candidate| {
1950 let args = decoded(function, candidate);
1951 matches!(&args[0], DynSolValue::FixedArray(values)
1952 if matches!(&values[..], [
1953 DynSolValue::Uint(left, _),
1954 DynSolValue::Uint(_, _),
1955 DynSolValue::Uint(right, _),
1956 ] if *left == *right + U256::ONE))
1957 },
1958 )
1959 .unwrap();
1960
1961 assert_eq!(
1962 decoded(function, &minimized.minimized_call),
1963 vec![DynSolValue::FixedArray(vec![
1964 DynSolValue::Uint(U256::ONE, 256),
1965 DynSolValue::Uint(U256::ZERO, 256),
1966 DynSolValue::Uint(U256::ZERO, 256),
1967 ])]
1968 );
1969 assert!(minimized.changed());
1970 assert!(minimized.accepted > 0);
1971 }
1972
1973 #[test]
1974 fn adapts_echidna_addressarrayutils_duplicate_fixture() {
1975 let abi = JsonAbi::parse(["function checkNoDuplicate(address[]) external"]).unwrap();
1976 let function = abi.functions().next().unwrap();
1977 let start = call(
1978 function,
1979 vec![DynSolValue::Array(vec![
1980 DynSolValue::Address(address(0x20000)),
1981 DynSolValue::Address(address(0xffff_ffff)),
1982 DynSolValue::Address(Address::ZERO),
1983 DynSolValue::Address(address(0x20000)),
1984 DynSolValue::Address(address(0x0001_ffff_fffe)),
1985 DynSolValue::Address(address(0x30000)),
1986 ])],
1987 );
1988
1989 let minimized = minimize_single_call_counterexample(function, &start, TEST_MAX_MINIMIZATION_ATTEMPTS, |candidate| {
1990 let args = decoded(function, candidate);
1991 matches!(&args[0], DynSolValue::Array(values) if values.iter().array_combinations().any(|[left, right]| left == right))
1992 })
1993 .unwrap();
1994
1995 assert_eq!(
1996 decoded(function, &minimized.minimized_call),
1997 vec![DynSolValue::Array(vec![
1998 DynSolValue::Address(Address::ZERO),
1999 DynSolValue::Address(Address::ZERO),
2000 ])]
2001 );
2002 assert!(minimized.changed());
2003 assert!(minimized.accepted > 0);
2004 }
2005
2006 #[test]
2007 fn skips_duplicate_sequence_replay_candidates() {
2008 let abi = JsonAbi::parse(["function noop() external"]).unwrap();
2009 let function = abi.functions().next().unwrap();
2010 let mut start = call(function, Vec::new());
2011 start.sender = address(0xaaaa);
2012 start.value = Some(U256::ZERO);
2013 let sender = address(0x100);
2014 let mut replays = 0usize;
2015
2016 let minimized = minimize_sequence_counterexample(
2017 &[start],
2018 &[sender, sender],
2019 TEST_MAX_MINIMIZATION_ATTEMPTS,
2020 |calls| {
2021 replays += 1;
2022 assert_eq!(calls.len(), 1);
2023 assert_eq!(calls[0].sender, sender);
2024 assert_eq!(calls[0].value, Some(U256::ZERO));
2025 false
2026 },
2027 )
2028 .unwrap();
2029
2030 assert!(!minimized.changed());
2031 assert_eq!(minimized.accepted, 0);
2032 assert_eq!(minimized.attempts, 1);
2033 assert_eq!(replays, minimized.attempts);
2034 }
2035
2036 #[test]
2037 fn minimizes_stateful_sequence_length_calldata_senders_and_values() {
2038 let abi = JsonAbi::parse([
2039 "function noise(uint256) external",
2040 "function prime(uint256) external",
2041 "function fire(uint256) external payable",
2042 ])
2043 .unwrap();
2044 let noise = abi.functions().find(|function| function.name == "noise").unwrap();
2045 let prime = abi.functions().find(|function| function.name == "prime").unwrap();
2046 let fire = abi.functions().find(|function| function.name == "fire").unwrap();
2047 let smaller_sender = address(0x100);
2048 let original_sender = address(0xaaaa);
2049 let mut sequence = vec![
2050 call(noise, vec![DynSolValue::Uint(U256::from(999), 256)]),
2051 call(prime, vec![DynSolValue::Uint(U256::from(1_000), 256)]),
2052 call(noise, vec![DynSolValue::Uint(U256::from(123), 256)]),
2053 call(fire, vec![DynSolValue::Uint(U256::from(5_000), 256)]),
2054 ];
2055 for call in &mut sequence {
2056 call.sender = original_sender;
2057 call.value = Some(U256::from(200));
2058 }
2059
2060 let minimized = minimize_sequence_counterexample(
2061 &sequence,
2062 &[smaller_sender],
2063 TEST_MAX_MINIMIZATION_ATTEMPTS,
2064 |candidate| {
2065 let mut primed = false;
2066 for call in candidate {
2067 if call.calldata.get(..4) == Some(prime.selector().as_slice()) {
2068 let args = decoded(prime, call);
2069 primed |= matches!(&args[0], DynSolValue::Uint(value, _) if *value > U256::from(40));
2070 }
2071 if call.calldata.get(..4) == Some(fire.selector().as_slice()) {
2072 let args = decoded(fire, call);
2073 let enough_value = call.value.unwrap_or_default() > U256::from(10);
2074 if primed
2075 && enough_value
2076 && matches!(&args[0], DynSolValue::Uint(value, _) if *value > U256::from(100))
2077 {
2078 return true;
2079 }
2080 }
2081 }
2082 false
2083 },
2084 )
2085 .unwrap();
2086
2087 assert!(minimized.changed());
2088 assert_eq!(minimized.minimized_calls.len(), 2);
2089 assert_eq!(
2090 decoded(prime, &minimized.minimized_calls[0]),
2091 vec![DynSolValue::Uint(U256::from(41), 256)]
2092 );
2093 assert_eq!(
2094 decoded(fire, &minimized.minimized_calls[1]),
2095 vec![DynSolValue::Uint(U256::from(101), 256)]
2096 );
2097 assert_eq!(minimized.minimized_calls[0].sender, smaller_sender);
2098 assert_eq!(minimized.minimized_calls[1].sender, smaller_sender);
2099 assert_eq!(minimized.minimized_calls[0].value, None);
2100 assert_eq!(minimized.minimized_calls[1].value, Some(U256::from(11)));
2101 assert_eq!(minimized.original_calldata_bytes(), sequence_calldata_bytes(&sequence));
2102 assert!(minimized.minimized_calldata_bytes() < minimized.original_calldata_bytes());
2103 assert!(minimized.accepted > 0);
2104 }
2105
2106 #[test]
2107 fn leaves_already_minimal_counterexample_replayable() {
2108 let abi = JsonAbi::parse(["function check(int256,bool,bytes3) external"]).unwrap();
2109 let function = abi.functions().next().unwrap();
2110 let mut fixed_bytes = [0u8; 32];
2111 fixed_bytes[2] = 0x42;
2112 let start = call(
2113 function,
2114 vec![
2115 DynSolValue::Int(I256::MINUS_ONE, 256),
2116 DynSolValue::Bool(false),
2117 DynSolValue::FixedBytes(B256::from(fixed_bytes), 3),
2118 ],
2119 );
2120
2121 let minimized = minimize_single_call_counterexample(
2122 function,
2123 &start,
2124 TEST_MAX_MINIMIZATION_ATTEMPTS,
2125 |candidate| {
2126 let args = decoded(function, candidate);
2127 matches!(&args[0], DynSolValue::Int(value, _) if *value == I256::MINUS_ONE)
2128 && matches!(&args[1], DynSolValue::Bool(false))
2129 && matches!(&args[2], DynSolValue::FixedBytes(bytes, 3) if bytes[2] == 0x42)
2130 },
2131 )
2132 .unwrap();
2133
2134 assert_eq!(decoded(function, &minimized.minimized_call), decoded(function, &start));
2135 assert!(!minimized.changed());
2136 assert_eq!(minimized.accepted, 0);
2137 }
2138}