Skip to main content

foundry_evm_fuzz/
sequence.rs

1//! Pure transaction-sequence generation and mutation primitives.
2
3use crate::{
4    BasicTxDetails, FuzzFixtures,
5    invariant::FuzzRunIdentifiedContracts,
6    strategies::{
7        FuzzState, TxGenerator, constrain_enum_value, generate_msg_value, mutate_param_value,
8    },
9};
10use alloy_dyn_abi::{JsonAbiExt, abi::AbiDecoderConfig};
11use alloy_json_abi::Function;
12use alloy_primitives::{U256, map::HashSet};
13use eyre::{Result, eyre};
14use foundry_config::{FuzzCorpusConfig, FuzzCorpusMutationWeights};
15use proptest::test_runner::TestRunner;
16use rand::{
17    Rng,
18    distr::{Distribution, weighted::WeightedIndex},
19};
20
21/// A neutral borrowed view of one corpus entry.
22#[derive(Clone, Copy)]
23pub struct CorpusEntryView<'a> {
24    transactions: &'a [BasicTxDetails],
25    comparisons: &'a [Vec<ComparisonHint>],
26}
27
28impl<'a> CorpusEntryView<'a> {
29    pub fn new(
30        transactions: &'a [BasicTxDetails],
31        comparisons: &'a [Vec<ComparisonHint>],
32    ) -> Result<Self> {
33        if transactions.is_empty() {
34            return Err(eyre!("corpus entry has no transactions"));
35        }
36        if comparisons.len() > transactions.len() {
37            return Err(eyre!("corpus entry has more comparison sets than transactions"));
38        }
39        Ok(Self { transactions, comparisons })
40    }
41}
42
43#[derive(Clone)]
44enum SequenceMode {
45    Stateless(Function),
46    Invariant(FuzzRunIdentifiedContracts),
47}
48
49/// Generates initial sequences and their lazy continuations.
50#[derive(Clone)]
51pub struct SequenceGenerator {
52    tx: TxGenerator,
53    state: FuzzState,
54    fixtures: FuzzFixtures,
55    mode: SequenceMode,
56    weights: FuzzCorpusMutationWeights,
57    mutations: WeightedIndex<u32>,
58    arg_mutations: Option<WeightedIndex<u32>>,
59    fresh_weight: u32,
60    payable_weight: u32,
61    has_corpus_dir: bool,
62}
63
64/// An initial sequence and the generator used to lazily continue it.
65pub struct SequencePlan {
66    initial: InitialSequence,
67    tx: TxGenerator,
68    fresh_weight: u32,
69    has_corpus_dir: bool,
70    stateless: bool,
71    source: Option<usize>,
72}
73
74enum InitialSequence {
75    Single(BasicTxDetails),
76    Multiple(Vec<BasicTxDetails>),
77}
78
79impl InitialSequence {
80    pub fn into_first(self) -> BasicTxDetails {
81        match self {
82            Self::Single(tx) => tx,
83            Self::Multiple(mut txs) => txs.remove(0),
84        }
85    }
86
87    pub fn as_slice(&self) -> &[BasicTxDetails] {
88        match self {
89            Self::Single(tx) => std::slice::from_ref(tx),
90            Self::Multiple(txs) => txs,
91        }
92    }
93}
94
95impl SequencePlan {
96    pub fn initial(&self) -> &[BasicTxDetails] {
97        self.initial.as_slice()
98    }
99    pub fn into_first(self) -> BasicTxDetails {
100        self.initial.into_first()
101    }
102    pub const fn source(&self) -> Option<usize> {
103        self.source
104    }
105    pub fn next(
106        &self,
107        runner: &mut TestRunner,
108        discarded: bool,
109        depth: usize,
110    ) -> Result<BasicTxDetails> {
111        if self.stateless {
112            return Err(eyre!("stateless sequence is limited to one transaction"));
113        }
114        if !self.has_corpus_dir || discarded {
115            return self.tx.next_tx(runner);
116        }
117        let fresh = self.fresh_weight > 0 && runner.rng().random_ratio(self.fresh_weight, 100);
118        if depth >= self.initial.as_slice().len() || fresh {
119            self.tx.next_tx(runner)
120        } else {
121            Ok(self.initial.as_slice()[depth].clone())
122        }
123    }
124}
125
126#[derive(Clone, Copy)]
127enum MutationType {
128    Splice,
129    Repeat,
130    Interleave,
131    Prefix,
132    Suffix,
133    Abi,
134    Cmp,
135}
136
137impl SequenceGenerator {
138    pub fn stateless(
139        tx: TxGenerator,
140        state: FuzzState,
141        function: Function,
142        config: &FuzzCorpusConfig,
143    ) -> Result<Self> {
144        Self::stateless_with_fixtures(tx, state, FuzzFixtures::default(), function, config)
145    }
146    pub fn stateless_with_fixtures(
147        tx: TxGenerator,
148        state: FuzzState,
149        fixtures: FuzzFixtures,
150        function: Function,
151        config: &FuzzCorpusConfig,
152    ) -> Result<Self> {
153        Self::new(tx, state, fixtures, SequenceMode::Stateless(function), config)
154    }
155    pub fn invariant(
156        tx: TxGenerator,
157        state: FuzzState,
158        targets: FuzzRunIdentifiedContracts,
159        config: &FuzzCorpusConfig,
160    ) -> Result<Self> {
161        Self::invariant_with_fixtures(tx, state, FuzzFixtures::default(), targets, config)
162    }
163    pub fn invariant_with_fixtures(
164        tx: TxGenerator,
165        state: FuzzState,
166        fixtures: FuzzFixtures,
167        targets: FuzzRunIdentifiedContracts,
168        config: &FuzzCorpusConfig,
169    ) -> Result<Self> {
170        Self::new(tx, state, fixtures, SequenceMode::Invariant(targets), config)
171    }
172    fn new(
173        tx: TxGenerator,
174        state: FuzzState,
175        fixtures: FuzzFixtures,
176        mode: SequenceMode,
177        config: &FuzzCorpusConfig,
178    ) -> Result<Self> {
179        let weights = config.mutation_weights.effective();
180        if weights.total() > u64::from(u32::MAX) {
181            return Err(eyre!(
182                "effective mutation weights sum to {}, which exceeds the maximum supported total {}",
183                weights.total(),
184                u32::MAX
185            ));
186        }
187        let all = [
188            weights.mutation_weight_splice,
189            weights.mutation_weight_repeat,
190            weights.mutation_weight_interleave,
191            weights.mutation_weight_prefix,
192            weights.mutation_weight_suffix,
193            weights.mutation_weight_abi,
194            weights.mutation_weight_cmp,
195        ];
196        let mutations =
197            WeightedIndex::new(all).map_err(|e| eyre!("invalid corpus mutation weights: {e}"))?;
198        let arg_mutations = if weights.mutation_weight_abi == 0 && weights.mutation_weight_cmp == 0
199        {
200            None
201        } else {
202            Some(
203                WeightedIndex::new([weights.mutation_weight_abi, weights.mutation_weight_cmp])
204                    .map_err(|e| eyre!("invalid argument mutation weights: {e}"))?,
205            )
206        };
207        Ok(Self {
208            tx,
209            state,
210            fixtures,
211            mode,
212            weights,
213            mutations,
214            arg_mutations,
215            fresh_weight: config.corpus_random_sequence_weight.min(100),
216            payable_weight: config.payable_value_weight,
217            has_corpus_dir: config.corpus_dir.is_some(),
218        })
219    }
220
221    pub fn start<'a, F>(
222        &self,
223        runner: &mut TestRunner,
224        corpus_len: usize,
225        mut entry_at: F,
226        coverage: bool,
227    ) -> Result<SequencePlan>
228    where
229        F: FnMut(usize) -> Result<CorpusEntryView<'a>>,
230    {
231        let (initial, source) = match &self.mode {
232            SequenceMode::Stateless(function) => {
233                self.start_stateless(runner, corpus_len, &mut entry_at, coverage, function)?
234            }
235            SequenceMode::Invariant(targets) => {
236                self.start_invariant(runner, corpus_len, &mut entry_at, coverage, targets)?
237            }
238        };
239        Ok(SequencePlan {
240            initial,
241            tx: self.tx.clone(),
242            fresh_weight: self.fresh_weight,
243            has_corpus_dir: self.has_corpus_dir,
244            stateless: matches!(self.mode, SequenceMode::Stateless(_)),
245            source,
246        })
247    }
248
249    fn start_stateless<'a>(
250        &self,
251        runner: &mut TestRunner,
252        corpus_len: usize,
253        entry_at: &mut impl FnMut(usize) -> Result<CorpusEntryView<'a>>,
254        coverage: bool,
255        function: &Function,
256    ) -> Result<(InitialSequence, Option<usize>)> {
257        if !coverage
258            || corpus_len == 0
259            || (self.fresh_weight > 0 && runner.rng().random_ratio(self.fresh_weight, 100))
260        {
261            return Ok((InitialSequence::Single(self.tx.next_tx(runner)?), None));
262        }
263        let index = runner.rng().random_range(0..corpus_len);
264        let entry = entry_at(index)?;
265        let mut tx = entry.transactions[0].clone();
266        let hints = entry.comparisons.first().map_or(&[][..], Vec::as_slice);
267        match self.arg_mutations.as_ref().map(|d| d.sample(runner.rng()) == 1) {
268            Some(true)
269                if !SequenceMutator::cmp_mutate(
270                    &mut tx,
271                    function,
272                    hints,
273                    runner,
274                    &self.fixtures,
275                )? && self.weights.mutation_weight_abi > 0
276                    && !function.inputs.is_empty() =>
277            {
278                SequenceMutator::abi_mutate(
279                    &mut tx,
280                    function,
281                    runner,
282                    &self.state,
283                    &self.fixtures,
284                    self.payable_weight,
285                )?
286            }
287            Some(true) => {}
288            Some(false) if self.weights.mutation_weight_abi > 0 && !function.inputs.is_empty() => {
289                SequenceMutator::abi_mutate(
290                    &mut tx,
291                    function,
292                    runner,
293                    &self.state,
294                    &self.fixtures,
295                    self.payable_weight,
296                )?
297            }
298            Some(false) if self.weights.mutation_weight_cmp > 0 => {
299                let _ =
300                    SequenceMutator::cmp_mutate(&mut tx, function, hints, runner, &self.fixtures)?;
301            }
302            None => return Ok((InitialSequence::Single(self.tx.next_tx(runner)?), None)),
303            _ => {}
304        }
305        Ok((InitialSequence::Single(tx), Some(index)))
306    }
307
308    fn start_invariant<'a>(
309        &self,
310        runner: &mut TestRunner,
311        corpus_len: usize,
312        entry_at: &mut impl FnMut(usize) -> Result<CorpusEntryView<'a>>,
313        coverage: bool,
314        targets: &FuzzRunIdentifiedContracts,
315    ) -> Result<(InitialSequence, Option<usize>)> {
316        if !coverage || corpus_len == 0 {
317            return Ok((InitialSequence::Multiple(vec![self.tx.next_tx(runner)?]), None));
318        }
319        let kind = match self.mutations.sample(runner.rng()) {
320            0 => MutationType::Splice,
321            1 => MutationType::Repeat,
322            2 => MutationType::Interleave,
323            3 => MutationType::Prefix,
324            4 => MutationType::Suffix,
325            5 => MutationType::Abi,
326            _ => MutationType::Cmp,
327        };
328        let a = runner.rng().random_range(0..corpus_len);
329        let b = runner.rng().random_range(0..corpus_len);
330        let primary = entry_at(a)?;
331        let secondary = entry_at(b)?;
332        let (mut seq, source) = match kind {
333            MutationType::Splice => {
334                (SequenceMutator::splice(primary.transactions, secondary.transactions, runner), a)
335            }
336            MutationType::Interleave => (
337                SequenceMutator::interleave(primary.transactions, secondary.transactions, runner),
338                a,
339            ),
340            MutationType::Repeat => {
341                let i = if runner.rng().random() { a } else { b };
342                let entry = if i == a { primary } else { secondary };
343                (SequenceMutator::repeat(entry.transactions, runner), i)
344            }
345            MutationType::Prefix | MutationType::Suffix => {
346                let i = if runner.rng().random() { a } else { b };
347                let base = if i == a { primary.transactions } else { secondary.transactions };
348                let len = if matches!(kind, MutationType::Prefix) {
349                    runner.rng().random_range(0..=base.len())
350                } else {
351                    runner.rng().random_range(0..base.len())
352                };
353                let mut r = Vec::with_capacity(len);
354                for _ in 0..len {
355                    r.push(self.tx.next_tx(runner)?)
356                }
357                (
358                    if matches!(kind, MutationType::Prefix) {
359                        SequenceMutator::prefix(base, r)
360                    } else {
361                        SequenceMutator::suffix(base, r)
362                    },
363                    i,
364                )
365            }
366            MutationType::Abi | MutationType::Cmp => {
367                let i = if runner.rng().random() { a } else { b };
368                let entry = if i == a { primary } else { secondary };
369                let mut seq = entry.transactions.to_vec();
370                let fallback = runner.rng().random_range(0..seq.len());
371                if matches!(kind, MutationType::Abi) {
372                    let tx = &mut seq[fallback];
373                    if let (_, Some(f)) = targets.targets().fuzzed_artifacts(tx)
374                        && !f.inputs.is_empty()
375                    {
376                        SequenceMutator::abi_mutate(
377                            tx,
378                            f,
379                            runner,
380                            &self.state,
381                            &self.fixtures,
382                            self.payable_weight,
383                        )?;
384                    }
385                } else {
386                    let candidates =
387                        entry.comparisons.iter().enumerate().filter(|(_, h)| !h.is_empty());
388                    let count = candidates.clone().count();
389                    let mut mutated = false;
390                    if count > 0 {
391                        let start = runner.rng().random_range(0..count);
392                        for (idx, h) in candidates.cycle().skip(start).take(count) {
393                            let tx = &mut seq[idx];
394                            if let (_, Some(f)) = targets.targets().fuzzed_artifacts(tx) {
395                                mutated =
396                                    SequenceMutator::cmp_mutate(tx, f, h, runner, &self.fixtures)?;
397                                if mutated {
398                                    break;
399                                }
400                            }
401                        }
402                    }
403                    if !mutated && self.weights.mutation_weight_abi > 0 {
404                        let tx = &mut seq[fallback];
405                        if let (_, Some(f)) = targets.targets().fuzzed_artifacts(tx)
406                            && !f.inputs.is_empty()
407                        {
408                            SequenceMutator::abi_mutate(
409                                tx,
410                                f,
411                                runner,
412                                &self.state,
413                                &self.fixtures,
414                                self.payable_weight,
415                            )?
416                        }
417                    }
418                }
419                (seq, i)
420            }
421        };
422        if seq.is_empty() {
423            seq.push(self.tx.next_tx(runner)?)
424        }
425        Ok((InitialSequence::Multiple(seq), Some(source)))
426    }
427}
428
429/// An EVM comparison observed while executing an input.
430#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
431pub struct ComparisonHint {
432    pub lhs: U256,
433    pub rhs: U256,
434}
435
436impl ComparisonHint {
437    /// Collects the distinct `hints` that a cmp mutation can apply to `calldata`, in first-seen
438    /// order.
439    ///
440    /// Cmp mutations only rewrite the calldata stored with the hints, so other hints can never
441    /// produce a mutation.
442    ///
443    /// This gives the same result as filtering with [`Self::applies_to`], but scans `calldata`
444    /// once per operand width instead of once per hint and width.
445    pub fn applicable(calldata: &[u8], hints: impl IntoIterator<Item = Self>) -> Vec<Self> {
446        let mut seen = HashSet::<Self>::default();
447        let hints = hints.into_iter().filter(|hint| seen.insert(*hint)).collect::<Vec<_>>();
448        if hints.is_empty() {
449            return hints;
450        }
451        let operands = hints
452            .iter()
453            .map(|hint| (hint.lhs.to_be_bytes::<32>(), hint.rhs.to_be_bytes::<32>()))
454            .collect::<Vec<_>>();
455        let found = CMP_WIDTHS.map(|width| {
456            find_operands(
457                calldata,
458                operands.iter().flat_map(|(lhs, rhs)| {
459                    let lhs = &lhs[32 - width..];
460                    let rhs = &rhs[32 - width..];
461                    (lhs != rhs).then_some([lhs, rhs]).into_iter().flatten()
462                }),
463            )
464        });
465        hints
466            .into_iter()
467            .zip(&operands)
468            .filter(|(_, (lhs, rhs))| {
469                CMP_WIDTHS.iter().zip(&found).any(|(&width, found)| {
470                    let lhs = &lhs[32 - width..];
471                    let rhs = &rhs[32 - width..];
472                    lhs != rhs && (found.contains(lhs) || found.contains(rhs))
473                })
474            })
475            .map(|(hint, _)| hint)
476            .collect()
477    }
478
479    /// Returns whether a cmp mutation can replace one of this comparison's operands in
480    /// `calldata`.
481    pub fn applies_to(&self, calldata: &[u8]) -> bool {
482        let lhs = self.lhs.to_be_bytes::<32>();
483        let rhs = self.rhs.to_be_bytes::<32>();
484        CMP_WIDTHS.iter().any(|&width| {
485            let lhs = &lhs[32 - width..];
486            let rhs = &rhs[32 - width..];
487            lhs != rhs && (contains_operand(calldata, lhs) || contains_operand(calldata, rhs))
488        })
489    }
490}
491
492/// Pure mutations shared by stateless and invariant sequence producers.
493struct SequenceMutator;
494
495impl SequenceMutator {
496    fn splice(
497        first: &[BasicTxDetails],
498        second: &[BasicTxDetails],
499        runner: &mut TestRunner,
500    ) -> Vec<BasicTxDetails> {
501        let rng = runner.rng();
502        let start1 = rng.random_range(0..first.len());
503        let end1 = rng.random_range(start1..first.len());
504        let start2 = rng.random_range(0..second.len());
505        let end2 = rng.random_range(start2..second.len());
506        first[start1..=end1].iter().chain(&second[start2..=end2]).cloned().collect()
507    }
508
509    fn repeat(sequence: &[BasicTxDetails], runner: &mut TestRunner) -> Vec<BasicTxDetails> {
510        let rng = runner.rng();
511        let start = rng.random_range(0..sequence.len());
512        let end = rng.random_range(start..=sequence.len());
513        let repeated = sequence[rng.random_range(0..sequence.len())].clone();
514        let mut result = Vec::with_capacity(sequence.len());
515        result.extend_from_slice(&sequence[..start]);
516        result.extend((start..end).map(|_| repeated.clone()));
517        result.extend_from_slice(&sequence[end..]);
518        result
519    }
520
521    fn interleave(
522        first: &[BasicTxDetails],
523        second: &[BasicTxDetails],
524        runner: &mut TestRunner,
525    ) -> Vec<BasicTxDetails> {
526        first
527            .iter()
528            .zip(second)
529            .map(
530                |(first, second)| {
531                    if runner.rng().random() { first.clone() } else { second.clone() }
532                },
533            )
534            .collect()
535    }
536
537    fn prefix(
538        sequence: &[BasicTxDetails],
539        mut replacements: Vec<BasicTxDetails>,
540    ) -> Vec<BasicTxDetails> {
541        replacements.truncate(sequence.len());
542        let mut result = sequence.to_vec();
543        result[..replacements.len()].clone_from_slice(&replacements);
544        result
545    }
546
547    fn suffix(
548        sequence: &[BasicTxDetails],
549        mut replacements: Vec<BasicTxDetails>,
550    ) -> Vec<BasicTxDetails> {
551        replacements.truncate(sequence.len());
552        let mut result = sequence.to_vec();
553        let retained = result.len() - replacements.len();
554        result[retained..].clone_from_slice(&replacements);
555        result
556    }
557
558    /// Mutates ABI arguments while retaining transaction metadata and optionally changing value.
559    fn abi_mutate(
560        tx: &mut BasicTxDetails,
561        function: &Function,
562        runner: &mut TestRunner,
563        state: &FuzzState,
564        fixtures: &FuzzFixtures,
565        payable_value_weight: u32,
566    ) -> Result<()> {
567        if function.inputs.is_empty() || tx.call_details.calldata.len() < 4 {
568            return Ok(());
569        }
570        if function.state_mutability == alloy_json_abi::StateMutability::Payable
571            && runner.rng().random_ratio(payable_value_weight.min(100), 100)
572        {
573            tx.call_details.value = Some(generate_msg_value(runner));
574        }
575        let mut rounds = runner.rng().random_range(0..=function.inputs.len()).max(1);
576        let indices = if function.inputs.len() <= 1 {
577            vec![0]
578        } else {
579            (0..rounds).map(|_| runner.rng().random_range(0..function.inputs.len())).collect()
580        };
581        let mut inputs = function
582            .abi_decode_input(&tx.call_details.calldata[4..])
583            .map_err(|err| eyre!("failed to load previous inputs: {err}"))?;
584        while rounds > 0 {
585            let index = indices[rounds - 1];
586            inputs[index] = mutate_param_value(
587                &function.inputs[index].selector_type().parse()?,
588                inputs[index].clone(),
589                runner,
590                state,
591            );
592            rounds -= 1;
593        }
594        let inputs = inputs
595            .into_iter()
596            .zip(&function.inputs)
597            .map(|(value, input)| constrain_enum_value(value, input, fixtures))
598            .collect::<Vec<_>>();
599        tx.call_details.calldata =
600            function.abi_encode_input(&inputs).map_err(|err| eyre!(err.to_string()))?.into();
601        Ok(())
602    }
603
604    fn cmp_mutate(
605        tx: &mut BasicTxDetails,
606        function: &Function,
607        hints: &[ComparisonHint],
608        runner: &mut TestRunner,
609        fixtures: &FuzzFixtures,
610    ) -> Result<bool> {
611        if hints.is_empty() || tx.call_details.calldata.len() <= 4 {
612            return Ok(false);
613        }
614        let config = AbiDecoderConfig::new().validate_words(true);
615        let start = runner.rng().random_range(0..hints.len());
616        for offset in 0..hints.len() {
617            if let Some(calldata) = cmp_mutated_calldata(
618                tx.call_details.calldata.as_ref(),
619                hints[(start + offset) % hints.len()],
620                runner,
621            ) && let Ok(inputs) = function.abi_decode_input_with_config(&calldata[4..], config)
622                && inputs.iter().zip(&function.inputs).all(|(value, input)| {
623                    constrain_enum_value(value.clone(), input, fixtures) == *value
624                })
625            {
626                tx.call_details.calldata = calldata.into();
627                return Ok(true);
628            }
629        }
630        Ok(false)
631    }
632}
633
634/// Operand widths, in bytes, that cmp mutations try to replace.
635const CMP_WIDTHS: [usize; 6] = [32, 16, 8, 4, 2, 1];
636
637fn cmp_mutated_calldata(
638    calldata: &[u8],
639    hint: ComparisonHint,
640    runner: &mut TestRunner,
641) -> Option<Vec<u8>> {
642    let lhs = hint.lhs.to_be_bytes::<32>();
643    let rhs = hint.rhs.to_be_bytes::<32>();
644    let start = runner.rng().random_range(0..CMP_WIDTHS.len());
645    for offset in 0..CMP_WIDTHS.len() {
646        let width = CMP_WIDTHS[(start + offset) % CMP_WIDTHS.len()];
647        let lhs = &lhs[32 - width..];
648        let rhs = &rhs[32 - width..];
649        if lhs == rhs {
650            continue;
651        }
652        let pairs =
653            if runner.rng().random() { [(lhs, rhs), (rhs, lhs)] } else { [(rhs, lhs), (lhs, rhs)] };
654        for (pattern, replacement) in pairs {
655            if let Some(mutated) = replace_operand(calldata, pattern, replacement, runner) {
656                return Some(mutated);
657            }
658        }
659    }
660    None
661}
662
663/// Length of the function selector that cmp mutations never rewrite.
664const SELECTOR_LEN: usize = 4;
665
666/// Returns whether `pattern` can be searched for in `calldata` arguments.
667fn is_replaceable_operand(calldata: &[u8], pattern: &[u8]) -> bool {
668    !pattern.is_empty()
669        && calldata.len() >= SELECTOR_LEN + pattern.len()
670        && (pattern.len() == 32 || pattern.iter().any(|byte| *byte != 0))
671}
672
673/// Returns whether `pattern` occurs in `calldata` arguments.
674fn contains_operand(calldata: &[u8], pattern: &[u8]) -> bool {
675    is_replaceable_operand(calldata, pattern)
676        && calldata[SELECTOR_LEN..].windows(pattern.len()).any(|window| window == pattern)
677}
678
679/// Returns the `patterns` that [`contains_operand`] finds in `calldata`.
680///
681/// All `patterns` must have the same width. The calldata is scanned once for all of them.
682fn find_operands<'a>(
683    calldata: &[u8],
684    patterns: impl IntoIterator<Item = &'a [u8]>,
685) -> HashSet<&'a [u8]> {
686    let mut missing = patterns
687        .into_iter()
688        .filter(|pattern| is_replaceable_operand(calldata, pattern))
689        .collect::<HashSet<_>>();
690    let mut found = HashSet::default();
691    let Some(width) = missing.iter().next().map(|pattern| pattern.len()) else { return found };
692    for window in calldata[SELECTOR_LEN..].windows(width) {
693        if let Some(pattern) = missing.take(window) {
694            found.insert(pattern);
695            if missing.is_empty() {
696                break;
697            }
698        }
699    }
700    found
701}
702
703fn replace_operand(
704    calldata: &[u8],
705    pattern: &[u8],
706    replacement: &[u8],
707    runner: &mut TestRunner,
708) -> Option<Vec<u8>> {
709    if pattern.len() != replacement.len() || !is_replaceable_operand(calldata, pattern) {
710        return None;
711    }
712    let search_len = calldata.len() - SELECTOR_LEN - pattern.len() + 1;
713    let start = runner.rng().random_range(0..search_len);
714    for offset in 0..search_len {
715        let index = SELECTOR_LEN + ((start + offset) % search_len);
716        if &calldata[index..index + pattern.len()] == pattern {
717            let mut mutated = calldata.to_vec();
718            mutated[index..index + replacement.len()].copy_from_slice(replacement);
719            return Some(mutated);
720        }
721    }
722    None
723}
724
725#[cfg(test)]
726mod tests {
727    use super::*;
728    use crate::{
729        CallDetails,
730        invariant::{TargetedContract, TargetedContracts},
731        strategies::EvmFuzzState,
732    };
733    use alloy_dyn_abi::DynSolValue;
734    use alloy_json_abi::JsonAbi;
735    use alloy_primitives::{Address, Bytes, I256};
736    use foundry_config::FuzzDictionaryConfig;
737    use proptest::{prelude::Just, strategy::Strategy};
738    use revm::database::InMemoryDB;
739    use std::path::PathBuf;
740
741    fn sentinel(runner: &mut TestRunner) -> u64 {
742        runner.rng().random()
743    }
744
745    fn forced_weights(kind: usize) -> FuzzCorpusMutationWeights {
746        let mut weights = FuzzCorpusMutationWeights {
747            mutation_weight_splice: 0,
748            mutation_weight_repeat: 0,
749            mutation_weight_interleave: 0,
750            mutation_weight_prefix: 0,
751            mutation_weight_suffix: 0,
752            mutation_weight_abi: 0,
753            mutation_weight_cmp: 0,
754        };
755        match kind {
756            0 => weights.mutation_weight_splice = 1,
757            1 => weights.mutation_weight_repeat = 1,
758            2 => weights.mutation_weight_interleave = 1,
759            3 => weights.mutation_weight_prefix = 1,
760            4 => weights.mutation_weight_suffix = 1,
761            5 => weights.mutation_weight_abi = 1,
762            _ => weights.mutation_weight_cmp = 1,
763        }
764        weights
765    }
766
767    fn tx(sender: u8) -> BasicTxDetails {
768        BasicTxDetails {
769            warp: Some(U256::from(sender)),
770            roll: Some(U256::from(sender + 1)),
771            sender: Address::with_last_byte(sender),
772            call_details: CallDetails {
773                target: Address::with_last_byte(10),
774                calldata: Bytes::from(vec![sender]),
775                value: Some(U256::from(sender)),
776            },
777        }
778    }
779
780    fn state() -> FuzzState {
781        EvmFuzzState::new(&[], &InMemoryDB::default(), FuzzDictionaryConfig::default(), None)
782            .stateless_worker()
783    }
784
785    fn generator_tx(sender: u8) -> TxGenerator {
786        TxGenerator::from_strategy(Just(tx(sender)).boxed())
787    }
788
789    fn config() -> FuzzCorpusConfig {
790        FuzzCorpusConfig { corpus_dir: Some(PathBuf::from("corpus")), ..Default::default() }
791    }
792
793    #[test]
794    fn prefix_and_suffix_handle_sequence_edges() {
795        let sequence = vec![tx(1), tx(2), tx(3)];
796        assert_eq!(SequenceMutator::prefix(&sequence, Vec::new())[0].sender, sequence[0].sender);
797        assert_eq!(SequenceMutator::suffix(&sequence, Vec::new())[2].sender, sequence[2].sender);
798        assert_eq!(
799            SequenceMutator::prefix(&sequence, vec![tx(9), tx(8), tx(7)])[2].sender,
800            tx(7).sender
801        );
802        assert_eq!(SequenceMutator::suffix(&sequence, vec![tx(9)])[0].warp, sequence[0].warp);
803    }
804
805    #[test]
806    fn splice_retains_singleton_entries() {
807        let mut runner = TestRunner::deterministic();
808        let result = SequenceMutator::splice(&[tx(1)], &[tx(2)], &mut runner);
809
810        assert_eq!(
811            result.iter().map(|tx| tx.sender).collect::<Vec<_>>(),
812            [tx(1).sender, tx(2).sender]
813        );
814    }
815
816    #[test]
817    fn splice_can_select_final_transactions() {
818        let first = [tx(1), tx(2)];
819        let second = [tx(3), tx(4)];
820        let mut runner = TestRunner::deterministic();
821        let mut selected_first_final = false;
822        let mut selected_second_final = false;
823
824        for _ in 0..1000 {
825            let result = SequenceMutator::splice(&first, &second, &mut runner);
826            selected_first_final |= result.iter().any(|tx| tx.sender == first[1].sender);
827            selected_second_final |= result.iter().any(|tx| tx.sender == second[1].sender);
828        }
829
830        assert!(selected_first_final, "splice never selected the first entry's final transaction");
831        assert!(
832            selected_second_final,
833            "splice never selected the second entry's final transaction"
834        );
835    }
836
837    #[test]
838    fn repeat_can_replace_final_transaction() {
839        let sequence = [tx(1), tx(2)];
840        let mut runner = TestRunner::deterministic();
841        let mut replaced_final = false;
842
843        for _ in 0..1000 {
844            let result = SequenceMutator::repeat(&sequence, &mut runner);
845            replaced_final |= result[1].sender != sequence[1].sender;
846        }
847
848        assert!(replaced_final, "repeat never replaced the final transaction");
849    }
850
851    #[test]
852    fn cmp_mutation_replaces_operand_and_retains_metadata() {
853        let function = Function::parse("testCmp(uint256)").unwrap();
854        let mut input = tx(7);
855        input.call_details.calldata =
856            function.abi_encode_input(&[DynSolValue::Uint(U256::from(7), 256)]).unwrap().into();
857        let metadata = (input.warp, input.roll, input.sender, input.call_details.value);
858        let mut runner = TestRunner::default();
859
860        assert!(
861            SequenceMutator::cmp_mutate(
862                &mut input,
863                &function,
864                &[ComparisonHint { lhs: U256::from(7), rhs: U256::from(42) }],
865                &mut runner,
866                &FuzzFixtures::default(),
867            )
868            .unwrap()
869        );
870        let decoded = function.abi_decode_input(&input.call_details.calldata[4..]).unwrap();
871        assert_eq!(decoded[0].as_uint().unwrap().0, U256::from(42));
872        assert_eq!((input.warp, input.roll, input.sender, input.call_details.value), metadata);
873    }
874
875    #[test]
876    fn cmp_mutation_rejects_dirty_value_words() {
877        let mut runner = TestRunner::deterministic();
878        for (signature, value, lhs, rhs) in [
879            ("test(bool)", DynSolValue::Bool(true), U256::from(1), U256::from(4)),
880            ("test(bool)", DynSolValue::Bool(false), U256::ZERO, U256::from(4)),
881            (
882                "test(bool[2])",
883                DynSolValue::FixedArray(vec![DynSolValue::Bool(false); 2]),
884                U256::ZERO,
885                U256::from(4),
886            ),
887            ("test(uint8)", DynSolValue::Uint(U256::from(7), 8), U256::from(7), U256::from(0x107)),
888            (
889                "test(address)",
890                DynSolValue::Address(Address::with_last_byte(1)),
891                U256::from(1),
892                (U256::from(1) << 160) | U256::from(1),
893            ),
894        ] {
895            let function = Function::parse(signature).unwrap();
896            let original = function.abi_encode_input(&[value]).unwrap();
897            let mut input = tx(7);
898            input.call_details.calldata = original.clone().into();
899            for _ in 0..100 {
900                assert!(
901                    !SequenceMutator::cmp_mutate(
902                        &mut input,
903                        &function,
904                        &[ComparisonHint { lhs, rhs }],
905                        &mut runner,
906                        &FuzzFixtures::default(),
907                    )
908                    .unwrap(),
909                    "{signature}"
910                );
911                assert_eq!(input.call_details.calldata.as_ref(), original);
912            }
913        }
914    }
915
916    #[test]
917    fn cmp_mutation_accepts_values_solidity_accepts() {
918        let mut runner = TestRunner::deterministic();
919        let encode = |signature, value| {
920            Function::parse(signature).unwrap().abi_encode_input(&[value]).unwrap()
921        };
922        let replace_byte = |mut calldata: Vec<u8>, index: usize, byte| {
923            calldata[index] = byte;
924            calldata
925        };
926        let bytes = encode("test(bytes)", DynSolValue::Bytes(vec![0xaa; 40]));
927        let string = encode("test(string)", DynSolValue::String("abcd".into()));
928        for (signature, original, lhs, rhs, expected) in [
929            (
930                "test(bool)",
931                encode("test(bool)", DynSolValue::Bool(false)),
932                U256::ZERO,
933                U256::from(1),
934                encode("test(bool)", DynSolValue::Bool(true)),
935            ),
936            (
937                "test(int8)",
938                encode("test(int8)", DynSolValue::Int(I256::ZERO, 8)),
939                U256::ZERO,
940                U256::MAX,
941                encode("test(int8)", DynSolValue::Int(I256::MINUS_ONE, 8)),
942            ),
943            // Shortened `bytes` length; the stale payload stays in the padding.
944            (
945                "test(bytes)",
946                bytes.clone(),
947                U256::from(40),
948                U256::from(5),
949                replace_byte(bytes, 67, 5),
950            ),
951            // Non-UTF-8 `string` payload.
952            (
953                "test(string)",
954                string.clone(),
955                U256::from(0x61),
956                U256::from(0xff),
957                replace_byte(string, 68, 0xff),
958            ),
959        ] {
960            let function = Function::parse(signature).unwrap();
961            let mut input = tx(7);
962            input.call_details.calldata = original.into();
963            assert!(
964                SequenceMutator::cmp_mutate(
965                    &mut input,
966                    &function,
967                    &[ComparisonHint { lhs, rhs }],
968                    &mut runner,
969                    &FuzzFixtures::default(),
970                )
971                .unwrap(),
972                "{signature}"
973            );
974            assert_eq!(input.call_details.calldata.as_ref(), expected, "{signature}");
975        }
976    }
977
978    #[test]
979    fn stateless_fresh_and_disabled_mutators_generate_one_transaction() {
980        let function = Function::parse("test(uint256)").unwrap();
981        let corpus_tx = tx(1);
982        let corpus = [CorpusEntryView::new(std::slice::from_ref(&corpus_tx), &[]).unwrap()];
983        for disable_mutators in [false, true] {
984            let mut config = config();
985            config.corpus_random_sequence_weight = if disable_mutators { 0 } else { 100 };
986            if disable_mutators {
987                config.mutation_weights.mutation_weight_abi = 0;
988                config.mutation_weights.mutation_weight_cmp = 0;
989            }
990            let generator =
991                SequenceGenerator::stateless(generator_tx(9), state(), function.clone(), &config)
992                    .unwrap();
993            let plan = generator
994                .start(&mut TestRunner::default(), corpus.len(), |i| Ok(corpus[i]), true)
995                .unwrap();
996            assert_eq!(plan.initial().len(), 1);
997            assert_eq!(plan.initial()[0].sender, tx(9).sender);
998            assert_eq!(plan.initial()[0].call_details.value, tx(9).call_details.value);
999            assert_eq!(plan.source(), None);
1000        }
1001    }
1002
1003    #[test]
1004    fn sequence_plan_continues_corpus_or_generates_when_unavailable() {
1005        let mut config = config();
1006        config.corpus_random_sequence_weight = 0;
1007        config.mutation_weights.mutation_weight_splice = 1;
1008        config.mutation_weights.mutation_weight_repeat = 0;
1009        config.mutation_weights.mutation_weight_interleave = 0;
1010        config.mutation_weights.mutation_weight_prefix = 0;
1011        config.mutation_weights.mutation_weight_suffix = 0;
1012        config.mutation_weights.mutation_weight_abi = 0;
1013        config.mutation_weights.mutation_weight_cmp = 0;
1014        let targets = FuzzRunIdentifiedContracts::new(TargetedContracts::new(), false);
1015        let generator =
1016            SequenceGenerator::invariant(generator_tx(9), state(), targets, &config).unwrap();
1017        let mut runner = TestRunner::default();
1018        let plan = SequencePlan {
1019            initial: InitialSequence::Multiple(vec![tx(1), tx(2)]),
1020            tx: generator.tx,
1021            fresh_weight: generator.fresh_weight,
1022            has_corpus_dir: generator.has_corpus_dir,
1023            stateless: false,
1024            source: Some(0),
1025        };
1026
1027        assert_eq!(plan.next(&mut runner, false, 1).unwrap().sender, tx(2).sender);
1028        assert_eq!(plan.next(&mut runner, true, 1).unwrap().sender, tx(9).sender);
1029        assert_eq!(plan.next(&mut runner, false, 2).unwrap().sender, tx(9).sender);
1030    }
1031
1032    #[test]
1033    fn continuation_preserves_gate_draw_order() {
1034        for (fresh_weight, discarded, depth, expected_sender, draws_gate) in [
1035            (0, false, 1, 2, false),
1036            (100, false, 1, 9, true),
1037            (50, false, 2, 9, true),
1038            (50, true, 1, 9, false),
1039        ] {
1040            let plan = SequencePlan {
1041                initial: InitialSequence::Multiple(vec![tx(1), tx(2)]),
1042                tx: generator_tx(9),
1043                fresh_weight,
1044                has_corpus_dir: true,
1045                stateless: false,
1046                source: Some(0),
1047            };
1048            let mut actual = TestRunner::deterministic();
1049            let mut reference = TestRunner::deterministic();
1050            if draws_gate {
1051                let _ = reference.rng().random_ratio(fresh_weight, 100);
1052            }
1053            assert_eq!(
1054                plan.next(&mut actual, discarded, depth).unwrap().sender,
1055                tx(expected_sender).sender
1056            );
1057            assert_eq!(sentinel(&mut actual), sentinel(&mut reference));
1058        }
1059    }
1060
1061    #[test]
1062    fn forced_invariant_mutations_preserve_old_selection_draws() {
1063        let sequences = [vec![tx(1), tx(2), tx(3)], vec![tx(4), tx(5), tx(6)]];
1064        let entries = sequences
1065            .iter()
1066            .map(|sequence| CorpusEntryView::new(sequence, &[]).unwrap())
1067            .collect::<Vec<_>>();
1068        for kind in 0..7 {
1069            let mut config = config();
1070            config.mutation_weights = forced_weights(kind);
1071            let generator = SequenceGenerator::invariant(
1072                generator_tx(9),
1073                state(),
1074                FuzzRunIdentifiedContracts::new(TargetedContracts::new(), false),
1075                &config,
1076            )
1077            .unwrap();
1078            let mut actual = TestRunner::deterministic();
1079            let mut reference = TestRunner::deterministic();
1080
1081            // The legacy producer selected the mutation, both corpus entries, and then the source
1082            // before making operation-specific draws. Keep these draws explicit: this test is
1083            // intended to catch seemingly harmless reordering during further extraction work.
1084            let distribution = WeightedIndex::new(
1085                [kind == 0, kind == 1, kind == 2, kind == 3, kind == 4, kind == 5, kind == 6]
1086                    .map(u32::from),
1087            )
1088            .unwrap();
1089            assert_eq!(distribution.sample(reference.rng()), kind);
1090            let a = reference.rng().random_range(0..entries.len());
1091            let b = reference.rng().random_range(0..entries.len());
1092            let source = match kind {
1093                0 | 2 => a,
1094                _ => {
1095                    if reference.rng().random() {
1096                        a
1097                    } else {
1098                        b
1099                    }
1100                }
1101            };
1102            // ABI/CMP always selected a fallback transaction after choosing the source.
1103            if kind >= 5 {
1104                let _ = reference.rng().random_range(0..entries[source].transactions.len());
1105            }
1106
1107            let plan =
1108                generator.start(&mut actual, entries.len(), |i| Ok(entries[i]), true).unwrap();
1109            assert_eq!(plan.source(), Some(source), "mutation family {kind}");
1110            // For argument mutation there are no matching artifacts, so the fallback index is the
1111            // final draw. Structural mutations make additional operation-specific draws and are
1112            // covered independently by the mutator unit tests.
1113            if kind >= 5 {
1114                assert_eq!(
1115                    sentinel(&mut actual),
1116                    sentinel(&mut reference),
1117                    "mutation family {kind}"
1118                );
1119            }
1120        }
1121    }
1122
1123    #[test]
1124    fn stateless_plan_never_exposes_continuation() {
1125        let generator = SequenceGenerator::stateless(
1126            generator_tx(9),
1127            state(),
1128            Function::parse("test()").unwrap(),
1129            &config(),
1130        )
1131        .unwrap();
1132        let plan = generator
1133            .start(&mut TestRunner::deterministic(), 0, |_| unreachable!(), false)
1134            .unwrap();
1135        assert!(plan.next(&mut TestRunner::deterministic(), false, 0).is_err());
1136    }
1137
1138    #[test]
1139    fn overflowing_effective_mutation_weight_is_rejected() {
1140        let mut config = config();
1141        config.mutation_weights.mutation_weight_splice = u32::MAX;
1142        config.mutation_weights.mutation_weight_repeat = 1;
1143        assert!(
1144            SequenceGenerator::stateless(
1145                generator_tx(1),
1146                state(),
1147                Function::parse("test()").unwrap(),
1148                &config,
1149            )
1150            .is_err()
1151        );
1152    }
1153
1154    #[test]
1155    fn invariant_cmp_only_does_not_fallback_to_abi() {
1156        let target = Address::with_last_byte(42);
1157        let function = Function::parse("test(uint256)").unwrap();
1158        let mut abi = JsonAbi::new();
1159        abi.functions.entry(function.name.clone()).or_default().push(function.clone());
1160        let mut contracts = TargetedContracts::new();
1161        contracts.insert(target, TargetedContract::new("Target".into(), abi));
1162        let mut original = tx(1);
1163        original.call_details.target = target;
1164        original.call_details.calldata =
1165            function.abi_encode_input(&[DynSolValue::Uint(U256::from(7), 256)]).unwrap().into();
1166        let sequence = [original.clone()];
1167        let corpus = [CorpusEntryView::new(&sequence, &[]).unwrap()];
1168        let mut config = config();
1169        config.mutation_weights = FuzzCorpusMutationWeights {
1170            mutation_weight_splice: 0,
1171            mutation_weight_repeat: 0,
1172            mutation_weight_interleave: 0,
1173            mutation_weight_prefix: 0,
1174            mutation_weight_suffix: 0,
1175            mutation_weight_abi: 0,
1176            mutation_weight_cmp: 1,
1177        };
1178        let generator = SequenceGenerator::invariant(
1179            generator_tx(9),
1180            state(),
1181            FuzzRunIdentifiedContracts::new(contracts, false),
1182            &config,
1183        )
1184        .unwrap();
1185        let plan = generator
1186            .start(&mut TestRunner::default(), corpus.len(), |i| Ok(corpus[i]), true)
1187            .unwrap();
1188        assert_eq!(plan.initial()[0].call_details.calldata, original.call_details.calldata);
1189    }
1190
1191    #[test]
1192    fn malformed_corpus_views_are_rejected_without_panicking() {
1193        assert!(CorpusEntryView::new(&[], &[]).is_err());
1194        let sequence = [tx(1)];
1195        let comparisons = [Vec::new(), Vec::new()];
1196        assert!(CorpusEntryView::new(&sequence, &comparisons).is_err());
1197
1198        let generator = SequenceGenerator::stateless(
1199            generator_tx(9),
1200            state(),
1201            Function::parse("test(uint256)").unwrap(),
1202            &config(),
1203        )
1204        .unwrap();
1205        let result = generator.start(
1206            &mut TestRunner::deterministic(),
1207            1,
1208            |_| CorpusEntryView::new(&[], &[]),
1209            true,
1210        );
1211        assert!(result.is_err());
1212    }
1213
1214    #[test]
1215    fn start_accesses_at_most_two_corpus_entries() {
1216        let mut config = config();
1217        config.mutation_weights.mutation_weight_splice = 1;
1218        config.mutation_weights.mutation_weight_repeat = 0;
1219        config.mutation_weights.mutation_weight_interleave = 0;
1220        config.mutation_weights.mutation_weight_prefix = 0;
1221        config.mutation_weights.mutation_weight_suffix = 0;
1222        config.mutation_weights.mutation_weight_abi = 0;
1223        config.mutation_weights.mutation_weight_cmp = 0;
1224        let generator = SequenceGenerator::invariant(
1225            generator_tx(9),
1226            state(),
1227            FuzzRunIdentifiedContracts::new(TargetedContracts::new(), false),
1228            &config,
1229        )
1230        .unwrap();
1231        let sequence = [tx(1)];
1232        let mut accesses = 0;
1233        generator
1234            .start(
1235                &mut TestRunner::deterministic(),
1236                10_000,
1237                |_| {
1238                    accesses += 1;
1239                    CorpusEntryView::new(&sequence, &[])
1240                },
1241                true,
1242            )
1243            .unwrap();
1244        assert_eq!(accesses, 2);
1245    }
1246
1247    #[test]
1248    fn applicable_hints_dedup_and_drop_operands_missing_from_calldata() {
1249        let function = Function::parse("testCmp(uint256)").unwrap();
1250        let calldata = function.abi_encode_input(&[DynSolValue::Uint(U256::from(7), 256)]).unwrap();
1251        let hint =
1252            |lhs: u64, rhs: u64| ComparisonHint { lhs: U256::from(lhs), rhs: U256::from(rhs) };
1253
1254        let hints = [hint(7, 42), hint(1000, 2000), hint(7, 42), hint(5, 5), hint(42, 7)];
1255        let applicable = ComparisonHint::applicable(&calldata, hints);
1256        assert_eq!(applicable, [hint(7, 42), hint(42, 7)]);
1257        for hint in hints {
1258            let mut input = tx(7);
1259            input.call_details.calldata = calldata.clone().into();
1260            let mutated = SequenceMutator::cmp_mutate(
1261                &mut input,
1262                &function,
1263                &[hint],
1264                &mut TestRunner::default(),
1265                &FuzzFixtures::default(),
1266            )
1267            .unwrap();
1268            assert_eq!(mutated, applicable.contains(&hint));
1269        }
1270    }
1271
1272    #[test]
1273    fn applicable_hints_match_per_hint_filter() {
1274        fn word(rng: &mut impl Rng, bytes: &[u8]) -> U256 {
1275            let mut word = [0u8; 32];
1276            let start = 32 - rng.random_range(0..=bytes.len().min(32));
1277            word[start..].copy_from_slice(&bytes[..32 - start]);
1278            U256::from_be_bytes(word)
1279        }
1280
1281        let mut runner = TestRunner::deterministic();
1282        let rng = runner.rng();
1283        let (mut kept, mut dropped) = (0, 0);
1284        for _ in 0..1000 {
1285            // A small byte alphabet makes partial and repeated operand matches common.
1286            let len = rng.random_range(0..100);
1287            let calldata = (0..len).map(|_| rng.random_range(0..3u8)).collect::<Vec<_>>();
1288            let hints = (0..rng.random_range(0..16))
1289                .map(|_| {
1290                    let random = (0..32).map(|_| rng.random_range(0..3u8)).collect::<Vec<_>>();
1291                    let start = rng.random_range(0..=calldata.len());
1292                    let lhs = word(rng, &random);
1293                    let rhs = word(rng, &calldata[start..]);
1294                    if rng.random() {
1295                        ComparisonHint { lhs, rhs }
1296                    } else {
1297                        ComparisonHint { lhs: rhs, rhs: lhs }
1298                    }
1299                })
1300                .collect::<Vec<_>>();
1301            let mut seen = HashSet::<ComparisonHint>::default();
1302            let expected = hints
1303                .iter()
1304                .copied()
1305                .filter(|hint| seen.insert(*hint) && hint.applies_to(&calldata))
1306                .collect::<Vec<_>>();
1307            kept += expected.len();
1308            dropped += seen.len() - expected.len();
1309            assert_eq!(ComparisonHint::applicable(&calldata, hints), expected);
1310        }
1311        assert!(kept > 1000 && dropped > 500, "kept {kept}, dropped {dropped}");
1312    }
1313}