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;
11use alloy_json_abi::Function;
12use alloy_primitives::U256;
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)]
431pub struct ComparisonHint {
432    pub lhs: U256,
433    pub rhs: U256,
434}
435
436/// Pure mutations shared by stateless and invariant sequence producers.
437struct SequenceMutator;
438
439impl SequenceMutator {
440    fn splice(
441        first: &[BasicTxDetails],
442        second: &[BasicTxDetails],
443        runner: &mut TestRunner,
444    ) -> Vec<BasicTxDetails> {
445        let rng = runner.rng();
446        let start1 = rng.random_range(0..first.len());
447        let end1 = rng.random_range(start1..first.len());
448        let start2 = rng.random_range(0..second.len());
449        let end2 = rng.random_range(start2..second.len());
450        first[start1..end1].iter().chain(&second[start2..end2]).cloned().collect()
451    }
452
453    fn repeat(sequence: &[BasicTxDetails], runner: &mut TestRunner) -> Vec<BasicTxDetails> {
454        let rng = runner.rng();
455        let start = rng.random_range(0..sequence.len());
456        let end = rng.random_range(start..sequence.len());
457        let repeated = sequence[rng.random_range(0..sequence.len())].clone();
458        let mut result = Vec::with_capacity(sequence.len());
459        result.extend_from_slice(&sequence[..start]);
460        result.extend((start..end).map(|_| repeated.clone()));
461        result.extend_from_slice(&sequence[end..]);
462        result
463    }
464
465    fn interleave(
466        first: &[BasicTxDetails],
467        second: &[BasicTxDetails],
468        runner: &mut TestRunner,
469    ) -> Vec<BasicTxDetails> {
470        first
471            .iter()
472            .zip(second)
473            .map(
474                |(first, second)| {
475                    if runner.rng().random() { first.clone() } else { second.clone() }
476                },
477            )
478            .collect()
479    }
480
481    fn prefix(
482        sequence: &[BasicTxDetails],
483        mut replacements: Vec<BasicTxDetails>,
484    ) -> Vec<BasicTxDetails> {
485        replacements.truncate(sequence.len());
486        let mut result = sequence.to_vec();
487        result[..replacements.len()].clone_from_slice(&replacements);
488        result
489    }
490
491    fn suffix(
492        sequence: &[BasicTxDetails],
493        mut replacements: Vec<BasicTxDetails>,
494    ) -> Vec<BasicTxDetails> {
495        replacements.truncate(sequence.len());
496        let mut result = sequence.to_vec();
497        let retained = result.len() - replacements.len();
498        result[retained..].clone_from_slice(&replacements);
499        result
500    }
501
502    /// Mutates ABI arguments while retaining transaction metadata and optionally changing value.
503    fn abi_mutate(
504        tx: &mut BasicTxDetails,
505        function: &Function,
506        runner: &mut TestRunner,
507        state: &FuzzState,
508        fixtures: &FuzzFixtures,
509        payable_value_weight: u32,
510    ) -> Result<()> {
511        if function.inputs.is_empty() || tx.call_details.calldata.len() < 4 {
512            return Ok(());
513        }
514        if function.state_mutability == alloy_json_abi::StateMutability::Payable
515            && runner.rng().random_ratio(payable_value_weight.min(100), 100)
516        {
517            tx.call_details.value = Some(generate_msg_value(runner));
518        }
519        let mut rounds = runner.rng().random_range(0..=function.inputs.len()).max(1);
520        let indices = if function.inputs.len() <= 1 {
521            vec![0]
522        } else {
523            (0..rounds).map(|_| runner.rng().random_range(0..function.inputs.len())).collect()
524        };
525        let mut inputs = function
526            .abi_decode_input(&tx.call_details.calldata[4..])
527            .map_err(|err| eyre!("failed to load previous inputs: {err}"))?;
528        while rounds > 0 {
529            let index = indices[rounds - 1];
530            inputs[index] = mutate_param_value(
531                &function.inputs[index].selector_type().parse()?,
532                inputs[index].clone(),
533                runner,
534                state,
535            );
536            rounds -= 1;
537        }
538        let inputs = inputs
539            .into_iter()
540            .zip(&function.inputs)
541            .map(|(value, input)| constrain_enum_value(value, input, fixtures))
542            .collect::<Vec<_>>();
543        tx.call_details.calldata =
544            function.abi_encode_input(&inputs).map_err(|err| eyre!(err.to_string()))?.into();
545        Ok(())
546    }
547
548    fn cmp_mutate(
549        tx: &mut BasicTxDetails,
550        function: &Function,
551        hints: &[ComparisonHint],
552        runner: &mut TestRunner,
553        fixtures: &FuzzFixtures,
554    ) -> Result<bool> {
555        if hints.is_empty() || tx.call_details.calldata.len() <= 4 {
556            return Ok(false);
557        }
558        let start = runner.rng().random_range(0..hints.len());
559        for offset in 0..hints.len() {
560            if let Some(calldata) = cmp_mutated_calldata(
561                tx.call_details.calldata.as_ref(),
562                hints[(start + offset) % hints.len()],
563                runner,
564            ) && let Ok(inputs) = function.abi_decode_input(&calldata[4..])
565                && inputs
566                    .iter()
567                    .cloned()
568                    .zip(&function.inputs)
569                    .map(|(value, input)| constrain_enum_value(value, input, fixtures))
570                    .zip(&inputs)
571                    .all(|(constrained, input)| constrained == *input)
572            {
573                tx.call_details.calldata = calldata.into();
574                return Ok(true);
575            }
576        }
577        Ok(false)
578    }
579}
580
581fn cmp_mutated_calldata(
582    calldata: &[u8],
583    hint: ComparisonHint,
584    runner: &mut TestRunner,
585) -> Option<Vec<u8>> {
586    const WIDTHS: [usize; 6] = [32, 16, 8, 4, 2, 1];
587    let lhs = hint.lhs.to_be_bytes::<32>();
588    let rhs = hint.rhs.to_be_bytes::<32>();
589    let start = runner.rng().random_range(0..WIDTHS.len());
590    for offset in 0..WIDTHS.len() {
591        let width = WIDTHS[(start + offset) % WIDTHS.len()];
592        let lhs = &lhs[32 - width..];
593        let rhs = &rhs[32 - width..];
594        if lhs == rhs {
595            continue;
596        }
597        let pairs =
598            if runner.rng().random() { [(lhs, rhs), (rhs, lhs)] } else { [(rhs, lhs), (lhs, rhs)] };
599        for (pattern, replacement) in pairs {
600            if let Some(mutated) = replace_operand(calldata, pattern, replacement, runner) {
601                return Some(mutated);
602            }
603        }
604    }
605    None
606}
607
608fn replace_operand(
609    calldata: &[u8],
610    pattern: &[u8],
611    replacement: &[u8],
612    runner: &mut TestRunner,
613) -> Option<Vec<u8>> {
614    const SELECTOR_LEN: usize = 4;
615    if pattern.is_empty()
616        || pattern.len() != replacement.len()
617        || calldata.len() < SELECTOR_LEN + pattern.len()
618        || (pattern.len() < 32 && pattern.iter().all(|byte| *byte == 0))
619    {
620        return None;
621    }
622    let search_len = calldata.len() - SELECTOR_LEN - pattern.len() + 1;
623    let start = runner.rng().random_range(0..search_len);
624    for offset in 0..search_len {
625        let index = SELECTOR_LEN + ((start + offset) % search_len);
626        if &calldata[index..index + pattern.len()] == pattern {
627            let mut mutated = calldata.to_vec();
628            mutated[index..index + replacement.len()].copy_from_slice(replacement);
629            return Some(mutated);
630        }
631    }
632    None
633}
634
635#[cfg(test)]
636mod tests {
637    use super::*;
638    use crate::{
639        CallDetails,
640        invariant::{TargetedContract, TargetedContracts},
641        strategies::EvmFuzzState,
642    };
643    use alloy_dyn_abi::DynSolValue;
644    use alloy_json_abi::JsonAbi;
645    use alloy_primitives::{Address, Bytes};
646    use foundry_config::FuzzDictionaryConfig;
647    use proptest::{prelude::Just, strategy::Strategy};
648    use revm::database::{CacheDB, EmptyDB};
649    use std::path::PathBuf;
650
651    fn sentinel(runner: &mut TestRunner) -> u64 {
652        runner.rng().random()
653    }
654
655    fn forced_weights(kind: usize) -> FuzzCorpusMutationWeights {
656        let mut weights = FuzzCorpusMutationWeights {
657            mutation_weight_splice: 0,
658            mutation_weight_repeat: 0,
659            mutation_weight_interleave: 0,
660            mutation_weight_prefix: 0,
661            mutation_weight_suffix: 0,
662            mutation_weight_abi: 0,
663            mutation_weight_cmp: 0,
664        };
665        match kind {
666            0 => weights.mutation_weight_splice = 1,
667            1 => weights.mutation_weight_repeat = 1,
668            2 => weights.mutation_weight_interleave = 1,
669            3 => weights.mutation_weight_prefix = 1,
670            4 => weights.mutation_weight_suffix = 1,
671            5 => weights.mutation_weight_abi = 1,
672            _ => weights.mutation_weight_cmp = 1,
673        }
674        weights
675    }
676
677    fn tx(sender: u8) -> BasicTxDetails {
678        BasicTxDetails {
679            warp: Some(U256::from(sender)),
680            roll: Some(U256::from(sender + 1)),
681            sender: Address::with_last_byte(sender),
682            call_details: CallDetails {
683                target: Address::with_last_byte(10),
684                calldata: Bytes::from(vec![sender]),
685                value: Some(U256::from(sender)),
686            },
687        }
688    }
689
690    fn state() -> FuzzState {
691        EvmFuzzState::new(
692            &[],
693            &CacheDB::<EmptyDB>::default(),
694            FuzzDictionaryConfig::default(),
695            None,
696        )
697        .stateless_worker()
698    }
699
700    fn generator_tx(sender: u8) -> TxGenerator {
701        TxGenerator::from_strategy(Just(tx(sender)).boxed())
702    }
703
704    fn config() -> FuzzCorpusConfig {
705        FuzzCorpusConfig { corpus_dir: Some(PathBuf::from("corpus")), ..Default::default() }
706    }
707
708    #[test]
709    fn prefix_and_suffix_handle_sequence_edges() {
710        let sequence = vec![tx(1), tx(2), tx(3)];
711        assert_eq!(SequenceMutator::prefix(&sequence, Vec::new())[0].sender, sequence[0].sender);
712        assert_eq!(SequenceMutator::suffix(&sequence, Vec::new())[2].sender, sequence[2].sender);
713        assert_eq!(
714            SequenceMutator::prefix(&sequence, vec![tx(9), tx(8), tx(7)])[2].sender,
715            tx(7).sender
716        );
717        assert_eq!(SequenceMutator::suffix(&sequence, vec![tx(9)])[0].warp, sequence[0].warp);
718    }
719
720    #[test]
721    fn cmp_mutation_replaces_operand_and_retains_metadata() {
722        let function = Function::parse("testCmp(uint256)").unwrap();
723        let mut input = tx(7);
724        input.call_details.calldata =
725            function.abi_encode_input(&[DynSolValue::Uint(U256::from(7), 256)]).unwrap().into();
726        let metadata = (input.warp, input.roll, input.sender, input.call_details.value);
727        let mut runner = TestRunner::default();
728
729        assert!(
730            SequenceMutator::cmp_mutate(
731                &mut input,
732                &function,
733                &[ComparisonHint { lhs: U256::from(7), rhs: U256::from(42) }],
734                &mut runner,
735                &FuzzFixtures::default(),
736            )
737            .unwrap()
738        );
739        let decoded = function.abi_decode_input(&input.call_details.calldata[4..]).unwrap();
740        assert_eq!(decoded[0].as_uint().unwrap().0, U256::from(42));
741        assert_eq!((input.warp, input.roll, input.sender, input.call_details.value), metadata);
742    }
743
744    #[test]
745    fn stateless_fresh_and_disabled_mutators_generate_one_transaction() {
746        let function = Function::parse("test(uint256)").unwrap();
747        let corpus_tx = tx(1);
748        let corpus = [CorpusEntryView::new(std::slice::from_ref(&corpus_tx), &[]).unwrap()];
749        for disable_mutators in [false, true] {
750            let mut config = config();
751            config.corpus_random_sequence_weight = if disable_mutators { 0 } else { 100 };
752            if disable_mutators {
753                config.mutation_weights.mutation_weight_abi = 0;
754                config.mutation_weights.mutation_weight_cmp = 0;
755            }
756            let generator =
757                SequenceGenerator::stateless(generator_tx(9), state(), function.clone(), &config)
758                    .unwrap();
759            let plan = generator
760                .start(&mut TestRunner::default(), corpus.len(), |i| Ok(corpus[i]), true)
761                .unwrap();
762            assert_eq!(plan.initial().len(), 1);
763            assert_eq!(plan.initial()[0].sender, tx(9).sender);
764            assert_eq!(plan.initial()[0].call_details.value, tx(9).call_details.value);
765            assert_eq!(plan.source(), None);
766        }
767    }
768
769    #[test]
770    fn sequence_plan_continues_corpus_or_generates_when_unavailable() {
771        let mut config = config();
772        config.corpus_random_sequence_weight = 0;
773        config.mutation_weights.mutation_weight_splice = 1;
774        config.mutation_weights.mutation_weight_repeat = 0;
775        config.mutation_weights.mutation_weight_interleave = 0;
776        config.mutation_weights.mutation_weight_prefix = 0;
777        config.mutation_weights.mutation_weight_suffix = 0;
778        config.mutation_weights.mutation_weight_abi = 0;
779        config.mutation_weights.mutation_weight_cmp = 0;
780        let targets = FuzzRunIdentifiedContracts::new(TargetedContracts::new(), false);
781        let generator =
782            SequenceGenerator::invariant(generator_tx(9), state(), targets, &config).unwrap();
783        let mut runner = TestRunner::default();
784        let plan = SequencePlan {
785            initial: InitialSequence::Multiple(vec![tx(1), tx(2)]),
786            tx: generator.tx,
787            fresh_weight: generator.fresh_weight,
788            has_corpus_dir: generator.has_corpus_dir,
789            stateless: false,
790            source: Some(0),
791        };
792
793        assert_eq!(plan.next(&mut runner, false, 1).unwrap().sender, tx(2).sender);
794        assert_eq!(plan.next(&mut runner, true, 1).unwrap().sender, tx(9).sender);
795        assert_eq!(plan.next(&mut runner, false, 2).unwrap().sender, tx(9).sender);
796    }
797
798    #[test]
799    fn continuation_preserves_gate_draw_order() {
800        for (fresh_weight, discarded, depth, expected_sender, draws_gate) in [
801            (0, false, 1, 2, false),
802            (100, false, 1, 9, true),
803            (50, false, 2, 9, true),
804            (50, true, 1, 9, false),
805        ] {
806            let plan = SequencePlan {
807                initial: InitialSequence::Multiple(vec![tx(1), tx(2)]),
808                tx: generator_tx(9),
809                fresh_weight,
810                has_corpus_dir: true,
811                stateless: false,
812                source: Some(0),
813            };
814            let mut actual = TestRunner::deterministic();
815            let mut reference = TestRunner::deterministic();
816            if draws_gate {
817                let _ = reference.rng().random_ratio(fresh_weight, 100);
818            }
819            assert_eq!(
820                plan.next(&mut actual, discarded, depth).unwrap().sender,
821                tx(expected_sender).sender
822            );
823            assert_eq!(sentinel(&mut actual), sentinel(&mut reference));
824        }
825    }
826
827    #[test]
828    fn forced_invariant_mutations_preserve_old_selection_draws() {
829        let sequences = [vec![tx(1), tx(2), tx(3)], vec![tx(4), tx(5), tx(6)]];
830        let entries = sequences
831            .iter()
832            .map(|sequence| CorpusEntryView::new(sequence, &[]).unwrap())
833            .collect::<Vec<_>>();
834        for kind in 0..7 {
835            let mut config = config();
836            config.mutation_weights = forced_weights(kind);
837            let generator = SequenceGenerator::invariant(
838                generator_tx(9),
839                state(),
840                FuzzRunIdentifiedContracts::new(TargetedContracts::new(), false),
841                &config,
842            )
843            .unwrap();
844            let mut actual = TestRunner::deterministic();
845            let mut reference = TestRunner::deterministic();
846
847            // The legacy producer selected the mutation, both corpus entries, and then the source
848            // before making operation-specific draws. Keep these draws explicit: this test is
849            // intended to catch seemingly harmless reordering during further extraction work.
850            let distribution = WeightedIndex::new(
851                [kind == 0, kind == 1, kind == 2, kind == 3, kind == 4, kind == 5, kind == 6]
852                    .map(u32::from),
853            )
854            .unwrap();
855            assert_eq!(distribution.sample(reference.rng()), kind);
856            let a = reference.rng().random_range(0..entries.len());
857            let b = reference.rng().random_range(0..entries.len());
858            let source = match kind {
859                0 | 2 => a,
860                _ => {
861                    if reference.rng().random() {
862                        a
863                    } else {
864                        b
865                    }
866                }
867            };
868            // ABI/CMP always selected a fallback transaction after choosing the source.
869            if kind >= 5 {
870                let _ = reference.rng().random_range(0..entries[source].transactions.len());
871            }
872
873            let plan =
874                generator.start(&mut actual, entries.len(), |i| Ok(entries[i]), true).unwrap();
875            assert_eq!(plan.source(), Some(source), "mutation family {kind}");
876            // For argument mutation there are no matching artifacts, so the fallback index is the
877            // final draw. Structural mutations make additional operation-specific draws and are
878            // covered independently by the mutator unit tests.
879            if kind >= 5 {
880                assert_eq!(
881                    sentinel(&mut actual),
882                    sentinel(&mut reference),
883                    "mutation family {kind}"
884                );
885            }
886        }
887    }
888
889    #[test]
890    fn stateless_plan_never_exposes_continuation() {
891        let generator = SequenceGenerator::stateless(
892            generator_tx(9),
893            state(),
894            Function::parse("test()").unwrap(),
895            &config(),
896        )
897        .unwrap();
898        let plan = generator
899            .start(&mut TestRunner::deterministic(), 0, |_| unreachable!(), false)
900            .unwrap();
901        assert!(plan.next(&mut TestRunner::deterministic(), false, 0).is_err());
902    }
903
904    #[test]
905    fn overflowing_effective_mutation_weight_is_rejected() {
906        let mut config = config();
907        config.mutation_weights.mutation_weight_splice = u32::MAX;
908        config.mutation_weights.mutation_weight_repeat = 1;
909        assert!(
910            SequenceGenerator::stateless(
911                generator_tx(1),
912                state(),
913                Function::parse("test()").unwrap(),
914                &config,
915            )
916            .is_err()
917        );
918    }
919
920    #[test]
921    fn invariant_cmp_only_does_not_fallback_to_abi() {
922        let target = Address::with_last_byte(42);
923        let function = Function::parse("test(uint256)").unwrap();
924        let mut abi = JsonAbi::new();
925        abi.functions.entry(function.name.clone()).or_default().push(function.clone());
926        let mut contracts = TargetedContracts::new();
927        contracts.insert(target, TargetedContract::new("Target".into(), abi));
928        let mut original = tx(1);
929        original.call_details.target = target;
930        original.call_details.calldata =
931            function.abi_encode_input(&[DynSolValue::Uint(U256::from(7), 256)]).unwrap().into();
932        let sequence = [original.clone()];
933        let corpus = [CorpusEntryView::new(&sequence, &[]).unwrap()];
934        let mut config = config();
935        config.mutation_weights = FuzzCorpusMutationWeights {
936            mutation_weight_splice: 0,
937            mutation_weight_repeat: 0,
938            mutation_weight_interleave: 0,
939            mutation_weight_prefix: 0,
940            mutation_weight_suffix: 0,
941            mutation_weight_abi: 0,
942            mutation_weight_cmp: 1,
943        };
944        let generator = SequenceGenerator::invariant(
945            generator_tx(9),
946            state(),
947            FuzzRunIdentifiedContracts::new(contracts, false),
948            &config,
949        )
950        .unwrap();
951        let plan = generator
952            .start(&mut TestRunner::default(), corpus.len(), |i| Ok(corpus[i]), true)
953            .unwrap();
954        assert_eq!(plan.initial()[0].call_details.calldata, original.call_details.calldata);
955    }
956
957    #[test]
958    fn malformed_corpus_views_are_rejected_without_panicking() {
959        assert!(CorpusEntryView::new(&[], &[]).is_err());
960        let sequence = [tx(1)];
961        let comparisons = [Vec::new(), Vec::new()];
962        assert!(CorpusEntryView::new(&sequence, &comparisons).is_err());
963
964        let generator = SequenceGenerator::stateless(
965            generator_tx(9),
966            state(),
967            Function::parse("test(uint256)").unwrap(),
968            &config(),
969        )
970        .unwrap();
971        let result = generator.start(
972            &mut TestRunner::deterministic(),
973            1,
974            |_| CorpusEntryView::new(&[], &[]),
975            true,
976        );
977        assert!(result.is_err());
978    }
979
980    #[test]
981    fn start_accesses_at_most_two_corpus_entries() {
982        let mut config = config();
983        config.mutation_weights.mutation_weight_splice = 1;
984        config.mutation_weights.mutation_weight_repeat = 0;
985        config.mutation_weights.mutation_weight_interleave = 0;
986        config.mutation_weights.mutation_weight_prefix = 0;
987        config.mutation_weights.mutation_weight_suffix = 0;
988        config.mutation_weights.mutation_weight_abi = 0;
989        config.mutation_weights.mutation_weight_cmp = 0;
990        let generator = SequenceGenerator::invariant(
991            generator_tx(9),
992            state(),
993            FuzzRunIdentifiedContracts::new(TargetedContracts::new(), false),
994            &config,
995        )
996        .unwrap();
997        let sequence = [tx(1)];
998        let mut accesses = 0;
999        generator
1000            .start(
1001                &mut TestRunner::deterministic(),
1002                10_000,
1003                |_| {
1004                    accesses += 1;
1005                    CorpusEntryView::new(&sequence, &[])
1006                },
1007                true,
1008            )
1009            .unwrap();
1010        assert_eq!(accesses, 2);
1011    }
1012}