Skip to main content

forge/
symbolic_minimizer.rs

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/// Result of deterministic single-call counterexample minimization.
38#[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/// Result of deterministic stateful sequence counterexample minimization.
53#[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
80/// Minimizes a replay-confirmed stateful sequence while preserving the concrete failure.
81///
82/// The caller's `still_fails` predicate must replay the whole candidate sequence and return true
83/// only when it preserves the already-confirmed failure identity.
84pub(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(&current_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
294/// Minimizes a stateless symbolic counterexample with ABI-valid candidates only.
295///
296/// `still_fails` must concretely replay the candidate and return `true` only when it preserves the
297/// already-confirmed failure.
298pub(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, &current_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, &current_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(&current, |candidate| {
958        accept_candidate(value, DynSolValue::Bytes(candidate), try_value)
959    }) {
960        return true;
961    }
962    if try_slice_vec_range(&current, |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(&current, |candidate| {
994        accept_candidate(value, DynSolValue::String(candidate), try_value)
995    }) {
996        return true;
997    }
998    if try_slice_string_range(&current, |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(&current[..start]);
1053            candidate.extend_from_slice(&current[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(&current[..start]);
1072            candidate.push_str(&current[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(&current_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}