1use 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#[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#[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
64pub 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#[derive(Clone, Copy, Debug, PartialEq, Eq)]
431pub struct ComparisonHint {
432 pub lhs: U256,
433 pub rhs: U256,
434}
435
436struct 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 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 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 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 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}