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