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, 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#[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, Hash)]
431pub struct ComparisonHint {
432 pub lhs: U256,
433 pub rhs: U256,
434}
435
436impl ComparisonHint {
437 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 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
492struct 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 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
634const 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
663const SELECTOR_LEN: usize = 4;
665
666fn 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
673fn 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
679fn 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 (
945 "test(bytes)",
946 bytes.clone(),
947 U256::from(40),
948 U256::from(5),
949 replace_byte(bytes, 67, 5),
950 ),
951 (
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 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 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 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 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}