1use super::state::EvmFuzzState;
2use crate::{
3 invariant::SenderFilters,
4 strategies::mutators::{
5 BitMutator, GaussianNoiseMutator, IncrementDecrementMutator, InterestingWordMutator,
6 },
7};
8use alloy_dyn_abi::{DynSolType, DynSolValue, Word};
9use alloy_primitives::{Address, B256, I256, U256};
10use proptest::{prelude::*, test_runner::TestRunner};
11use rand::{SeedableRng, prelude::IndexedMutRandom, rngs::StdRng};
12use std::mem::replace;
13
14const MAX_ARRAY_LEN: usize = 256;
16
17pub fn fuzz_param(param: &DynSolType) -> BoxedStrategy<DynSolValue> {
21 fuzz_param_inner(param, None)
22}
23
24pub fn fuzz_param_with_fixtures(
40 param: &DynSolType,
41 fixtures: Option<&[DynSolValue]>,
42 name: &str,
43) -> BoxedStrategy<DynSolValue> {
44 fuzz_param_inner(param, fixtures.map(|f| (f, name)))
45}
46
47fn fuzz_param_inner(
48 param: &DynSolType,
49 mut fuzz_fixtures: Option<(&[DynSolValue], &str)>,
50) -> BoxedStrategy<DynSolValue> {
51 if let Some((fixtures, name)) = fuzz_fixtures
52 && !fixtures.iter().all(|f| f.matches(param))
53 {
54 error!("fixtures for {name:?} do not match type {param}");
55 fuzz_fixtures = None;
56 }
57 let fuzz_fixtures = fuzz_fixtures.map(|(f, _)| f);
58
59 let value = || {
60 let default_strategy = DynSolValue::type_strategy(param);
61 if let Some(fixtures) = fuzz_fixtures {
62 proptest::prop_oneof![
63 50 => {
64 let fixtures = fixtures.to_vec();
65 any::<prop::sample::Index>()
66 .prop_map(move |index| index.get(&fixtures).clone())
67 },
68 50 => default_strategy,
69 ]
70 .boxed()
71 } else {
72 default_strategy.boxed()
73 }
74 };
75
76 match *param {
77 DynSolType::Address => value(),
78 DynSolType::Int(n @ 8..=256) => super::IntStrategy::new(n, fuzz_fixtures)
79 .prop_map(move |x| DynSolValue::Int(x, n))
80 .boxed(),
81 DynSolType::Uint(n @ 8..=256) => super::UintStrategy::new(n, fuzz_fixtures)
82 .prop_map(move |x| DynSolValue::Uint(x, n))
83 .boxed(),
84 DynSolType::Function | DynSolType::Bool => DynSolValue::type_strategy(param).boxed(),
85 DynSolType::Bytes => value(),
86 DynSolType::FixedBytes(_size @ 1..=32) => value(),
87 DynSolType::String => value()
88 .prop_map(move |value| {
89 DynSolValue::String(
90 value.as_str().unwrap().trim().trim_end_matches('\0').to_string(),
91 )
92 })
93 .boxed(),
94 DynSolType::Tuple(ref params) => params
95 .iter()
96 .map(|param| fuzz_param_inner(param, None))
97 .collect::<Vec<_>>()
98 .prop_map(DynSolValue::Tuple)
99 .boxed(),
100 DynSolType::FixedArray(ref param, size) => {
101 proptest::collection::vec(fuzz_param_inner(param, None), size)
102 .prop_map(DynSolValue::FixedArray)
103 .boxed()
104 }
105 DynSolType::Array(ref param) => {
106 proptest::collection::vec(fuzz_param_inner(param, None), 0..MAX_ARRAY_LEN)
107 .prop_map(DynSolValue::Array)
108 .boxed()
109 }
110 _ => panic!("unsupported fuzz param type: {param}"),
111 }
112}
113
114pub fn fuzz_param_from_state(
119 param: &DynSolType,
120 state: &EvmFuzzState,
121) -> BoxedStrategy<DynSolValue> {
122 let value = || {
124 let state = state.clone();
125 let param = param.clone();
126 any::<(bool, prop::sample::Index)>().prop_map(move |(bias, index)| {
130 let state = state.dictionary_read();
131 let values = if bias { state.samples(¶m) } else { None }
132 .unwrap_or_else(|| state.values())
133 .as_slice();
134 values[index.index(values.len())]
135 })
136 };
137
138 match *param {
140 DynSolType::Address => {
141 let deployed_libs = state.deployed_libs.clone();
142 value()
143 .prop_map(move |value| {
144 let mut fuzzed_addr = Address::from_word(value);
145 if deployed_libs.contains(&fuzzed_addr) {
146 let mut rng = StdRng::seed_from_u64(0x1337); loop {
154 fuzzed_addr.randomize_with(&mut rng);
155 if !deployed_libs.contains(&fuzzed_addr) {
156 break;
157 }
158 }
159 }
160 DynSolValue::Address(fuzzed_addr)
161 })
162 .boxed()
163 }
164 DynSolType::Function => value()
165 .prop_map(move |value| {
166 DynSolValue::Function(alloy_primitives::Function::from_word(value))
167 })
168 .boxed(),
169 DynSolType::FixedBytes(size @ 1..=32) => value()
170 .prop_map(move |mut v| {
171 v[size..].fill(0);
172 DynSolValue::FixedBytes(B256::from(v), size)
173 })
174 .boxed(),
175 DynSolType::Bool => DynSolValue::type_strategy(param).boxed(),
176 DynSolType::String => {
177 let state = state.clone();
178 (proptest::bool::weighted(0.3), any::<prop::sample::Index>())
179 .prop_flat_map(move |(use_ast, select_index)| {
180 let dict = state.dictionary_read();
181
182 let ast_strings = dict.ast_strings();
184 if use_ast && !ast_strings.is_empty() {
185 let s = &ast_strings.as_slice()[select_index.index(ast_strings.len())];
186 return Just(DynSolValue::String(s.clone())).boxed();
187 }
188
189 DynSolValue::type_strategy(&DynSolType::String)
191 .prop_map(|value| {
192 DynSolValue::String(
193 value.as_str().unwrap().trim().trim_end_matches('\0').to_string(),
194 )
195 })
196 .boxed()
197 })
198 .boxed()
199 }
200 DynSolType::Bytes => {
201 let state_clone = state.clone();
202 (
203 value(),
204 proptest::bool::weighted(0.1),
205 proptest::bool::weighted(0.2),
206 any::<prop::sample::Index>(),
207 )
208 .prop_map(move |(word, use_ast_string, use_ast_bytes, select_index)| {
209 let dict = state_clone.dictionary_read();
210
211 let ast_strings = dict.ast_strings();
213 if use_ast_string && !ast_strings.is_empty() {
214 let s = &ast_strings.as_slice()[select_index.index(ast_strings.len())];
215 return DynSolValue::Bytes(s.as_bytes().to_vec());
216 }
217
218 let ast_bytes = dict.ast_bytes();
220 if use_ast_bytes && !ast_bytes.is_empty() {
221 let bytes = &ast_bytes.as_slice()[select_index.index(ast_bytes.len())];
222 return DynSolValue::Bytes(bytes.to_vec());
223 }
224
225 DynSolValue::Bytes(word.0.into())
227 })
228 .boxed()
229 }
230 DynSolType::Int(n @ 8..=256) => match n / 8 {
231 32 => value()
232 .prop_map(move |value| DynSolValue::Int(I256::from_raw(value.into()), 256))
233 .boxed(),
234 1..=31 => value()
235 .prop_map(move |value| {
236 let uint_n = U256::from_be_bytes(value.0) % U256::from(1).wrapping_shl(n);
238 let sign_bit = U256::from(1) << (n - 1);
240 let num = if uint_n >= sign_bit {
241 let modulus = U256::from(1) << n;
243 I256::from_raw(uint_n.wrapping_sub(modulus))
244 } else {
245 I256::from_raw(uint_n)
247 };
248
249 DynSolValue::Int(num, n)
250 })
251 .boxed(),
252 _ => unreachable!(),
253 },
254 DynSolType::Uint(n @ 8..=256) => match n / 8 {
255 32 => value()
256 .prop_map(move |value| DynSolValue::Uint(U256::from_be_bytes(value.0), 256))
257 .boxed(),
258 1..=31 => value()
259 .prop_map(move |value| {
260 let uint = U256::from_be_bytes(value.0) % U256::from(1).wrapping_shl(n);
261 DynSolValue::Uint(uint, n)
262 })
263 .boxed(),
264 _ => unreachable!(),
265 },
266 DynSolType::Tuple(ref params) => params
267 .iter()
268 .map(|p| fuzz_param_from_state(p, state))
269 .collect::<Vec<_>>()
270 .prop_map(DynSolValue::Tuple)
271 .boxed(),
272 DynSolType::FixedArray(ref param, size) => {
273 proptest::collection::vec(fuzz_param_from_state(param, state), size)
274 .prop_map(DynSolValue::FixedArray)
275 .boxed()
276 }
277 DynSolType::Array(ref param) => {
278 proptest::collection::vec(fuzz_param_from_state(param, state), 0..MAX_ARRAY_LEN)
279 .prop_map(DynSolValue::Array)
280 .boxed()
281 }
282 _ => panic!("unsupported fuzz param type: {param}"),
283 }
284}
285
286fn select_random_address(
293 current: Address,
294 test_runner: &mut TestRunner,
295 state: &EvmFuzzState,
296 senders: Option<&SenderFilters>,
297) -> Option<Address> {
298 if let Some(senders) = senders {
299 if !senders.targeted.is_empty() {
300 let index = test_runner.rng().random_range(0..senders.targeted.len());
302 let addr = senders.targeted[index];
303 return (addr != current).then_some(addr);
304 }
305
306 let dict = state.dictionary_read();
308 let values = dict.values();
309 if values.is_empty() {
310 return None;
311 }
312
313 for _ in 0..10 {
315 let index = test_runner.rng().random_range(0..values.len());
316 let addr = Address::from_word(values[index]);
317 if addr != current && !senders.excluded.contains(&addr) {
318 return Some(addr);
319 }
320 }
321 None
322 } else {
323 let dict = state.dictionary_read();
325 let values = dict.values();
326 if values.is_empty() {
327 None
328 } else {
329 let index = test_runner.rng().random_range(0..values.len());
330 let addr = Address::from_word(values[index]);
331 (addr != current).then_some(addr)
332 }
333 }
334}
335
336pub fn mutate_param_value(
338 param: &DynSolType,
339 value: DynSolValue,
340 test_runner: &mut TestRunner,
341 state: &EvmFuzzState,
342) -> DynSolValue {
343 mutate_param_value_inner(param, value, test_runner, state, None)
344}
345
346pub fn mutate_param_value_with_senders(
351 param: &DynSolType,
352 value: DynSolValue,
353 test_runner: &mut TestRunner,
354 state: &EvmFuzzState,
355 senders: &SenderFilters,
356) -> DynSolValue {
357 mutate_param_value_inner(param, value, test_runner, state, Some(senders))
358}
359
360fn mutate_param_value_inner(
361 param: &DynSolType,
362 value: DynSolValue,
363 test_runner: &mut TestRunner,
364 state: &EvmFuzzState,
365 senders: Option<&SenderFilters>,
366) -> DynSolValue {
367 let new_value = |param: &DynSolType, test_runner: &mut TestRunner| {
368 fuzz_param_from_state(param, state)
369 .new_tree(test_runner)
370 .expect("Could not generate case")
371 .current()
372 };
373
374 match value {
375 DynSolValue::Bool(val) => {
376 trace!(target: "mutator", "Bool flip {val}");
378 Some(DynSolValue::Bool(!val))
379 }
380 DynSolValue::Uint(val, size) => match test_runner.rng().random_range(0..=6) {
381 0 => U256::increment_decrement(val, size, test_runner),
382 1 => U256::flip_random_bit(val, size, test_runner),
383 2 => U256::mutate_interesting_byte(val, size, test_runner),
384 3 => U256::mutate_interesting_word(val, size, test_runner),
385 4 => U256::mutate_interesting_dword(val, size, test_runner),
386 5 => U256::mutate_with_gaussian_noise(val, size, test_runner),
387 6 => None,
388 _ => unreachable!(),
389 }
390 .map(|v| DynSolValue::Uint(v, size)),
391 DynSolValue::Int(val, size) => match test_runner.rng().random_range(0..=6) {
392 0 => I256::increment_decrement(val, size, test_runner),
393 1 => I256::flip_random_bit(val, size, test_runner),
394 2 => I256::mutate_interesting_byte(val, size, test_runner),
395 3 => I256::mutate_interesting_word(val, size, test_runner),
396 4 => I256::mutate_interesting_dword(val, size, test_runner),
397 5 => I256::mutate_with_gaussian_noise(val, size, test_runner),
398 6 => None,
399 _ => unreachable!(),
400 }
401 .map(|v| DynSolValue::Int(v, size)),
402 DynSolValue::Address(val) => match test_runner.rng().random_range(0..=5) {
403 0 => Address::flip_random_bit(val, 20, test_runner),
404 1 => Address::mutate_interesting_byte(val, 20, test_runner),
405 2 => Address::mutate_interesting_word(val, 20, test_runner),
406 3 => Address::mutate_interesting_dword(val, 20, test_runner),
407 4 => select_random_address(val, test_runner, state, senders),
409 5 => None,
410 _ => unreachable!(),
411 }
412 .map(DynSolValue::Address),
413 DynSolValue::Array(mut values) => {
414 if let DynSolType::Array(param_type) = param
415 && !values.is_empty()
416 {
417 match test_runner.rng().random_range(0..=2) {
418 0 => {
420 values.remove(test_runner.rng().random_range(0..values.len()));
421 }
422 1 => values.push(new_value(param_type, test_runner)),
424 2 => mutate_random_array_value(
426 &mut values,
427 param_type,
428 test_runner,
429 state,
430 senders,
431 ),
432 _ => unreachable!(),
433 }
434 Some(DynSolValue::Array(values))
435 } else {
436 None
437 }
438 }
439 DynSolValue::FixedArray(mut values) => {
440 if let DynSolType::FixedArray(param_type, _size) = param
441 && !values.is_empty()
442 {
443 mutate_random_array_value(&mut values, param_type, test_runner, state, senders);
444 Some(DynSolValue::FixedArray(values))
445 } else {
446 None
447 }
448 }
449 DynSolValue::FixedBytes(word, size) => match test_runner.rng().random_range(0..=4) {
450 0 => Word::flip_random_bit(word, size, test_runner),
451 1 => Word::mutate_interesting_byte(word, size, test_runner),
452 2 => Word::mutate_interesting_word(word, size, test_runner),
453 3 => Word::mutate_interesting_dword(word, size, test_runner),
454 4 => None,
455 _ => unreachable!(),
456 }
457 .map(|word| DynSolValue::FixedBytes(word, size)),
458 DynSolValue::CustomStruct { name, prop_names, tuple: mut values } => {
459 if let DynSolType::CustomStruct { name: _, prop_names: _, tuple: tuple_types }
460 | DynSolType::Tuple(tuple_types) = param
461 && !values.is_empty()
462 {
463 mutate_random_tuple_value(&mut values, tuple_types, test_runner, state, senders);
465 Some(DynSolValue::CustomStruct { name, prop_names, tuple: values })
466 } else {
467 None
468 }
469 }
470 DynSolValue::Tuple(mut values) => {
471 if let DynSolType::Tuple(tuple_types) = param
472 && !values.is_empty()
473 {
474 mutate_random_tuple_value(&mut values, tuple_types, test_runner, state, senders);
476 Some(DynSolValue::Tuple(values))
477 } else {
478 None
479 }
480 }
481 _ => None,
482 }
483 .unwrap_or_else(|| new_value(param, test_runner))
484}
485
486fn mutate_random_tuple_value(
488 tuple_values: &mut [DynSolValue],
489 tuple_types: &[DynSolType],
490 test_runner: &mut TestRunner,
491 state: &EvmFuzzState,
492 senders: Option<&SenderFilters>,
493) {
494 let id = test_runner.rng().random_range(0..tuple_values.len());
495 let param_type = &tuple_types[id];
496 let old_val = replace(&mut tuple_values[id], DynSolValue::Bool(false));
497 let new_val = mutate_param_value_inner(param_type, old_val, test_runner, state, senders);
498 tuple_values[id] = new_val;
499}
500
501fn mutate_random_array_value(
503 array_values: &mut [DynSolValue],
504 element_type: &DynSolType,
505 test_runner: &mut TestRunner,
506 state: &EvmFuzzState,
507 senders: Option<&SenderFilters>,
508) {
509 let elem = array_values.choose_mut(&mut test_runner.rng()).unwrap();
510 let old_val = replace(elem, DynSolValue::Bool(false));
511 let new_val = mutate_param_value_inner(element_type, old_val, test_runner, state, senders);
512 *elem = new_val;
513}
514
515#[cfg(test)]
516mod tests {
517 use crate::{
518 FuzzFixtures,
519 strategies::{EvmFuzzState, fuzz_calldata, fuzz_calldata_from_state},
520 };
521 use alloy_primitives::B256;
522 use foundry_common::abi::get_func;
523 use std::collections::HashSet;
524
525 #[test]
526 fn can_fuzz_array() {
527 let f = "testArray(uint64[2] calldata values)";
528 let func = get_func(f).unwrap();
529 let state = EvmFuzzState::test();
530 let strategy = proptest::prop_oneof![
531 60 => fuzz_calldata(func.clone(), &FuzzFixtures::default()),
532 40 => fuzz_calldata_from_state(func, &state),
533 ];
534 let cfg = proptest::test_runner::Config { failure_persistence: None, ..Default::default() };
535 let mut runner = proptest::test_runner::TestRunner::new(cfg);
536 let _ = runner.run(&strategy, |_| Ok(()));
537 }
538
539 #[test]
540 fn can_fuzz_string_and_bytes_with_ast_literals_and_hashes() {
541 use super::fuzz_param_from_state;
542 use crate::strategies::LiteralMaps;
543 use alloy_dyn_abi::DynSolType;
544 use alloy_primitives::keccak256;
545 use proptest::strategy::Strategy;
546
547 let mut literals = LiteralMaps::default();
549 literals.strings.insert("hello".to_string());
550 literals.strings.insert("world".to_string());
551 literals.words.entry(DynSolType::FixedBytes(32)).or_default().insert(keccak256("hello"));
552 literals.words.entry(DynSolType::FixedBytes(32)).or_default().insert(keccak256("world"));
553
554 let state = EvmFuzzState::test();
555 state.seed_literals(literals);
556
557 let cfg = proptest::test_runner::Config { failure_persistence: None, ..Default::default() };
558 let mut runner = proptest::test_runner::TestRunner::new(cfg);
559
560 let mut generated_bytes = HashSet::new();
562 let mut generated_hashes = HashSet::new();
563 let mut generated_strings = HashSet::new();
564 let bytes_strategy = fuzz_param_from_state(&DynSolType::Bytes, &state);
565 let string_strategy = fuzz_param_from_state(&DynSolType::String, &state);
566 let bytes32_strategy = fuzz_param_from_state(&DynSolType::FixedBytes(32), &state);
567
568 for _ in 0..256 {
569 let tree = bytes_strategy.new_tree(&mut runner).unwrap();
570 if let Some(bytes) = tree.current().as_bytes()
571 && let Ok(s) = std::str::from_utf8(bytes)
572 {
573 generated_bytes.insert(s.to_string());
574 }
575
576 let tree = string_strategy.new_tree(&mut runner).unwrap();
577 if let Some(s) = tree.current().as_str() {
578 generated_strings.insert(s.to_string());
579 }
580
581 let tree = bytes32_strategy.new_tree(&mut runner).unwrap();
582 if let Some((bytes, size)) = tree.current().as_fixed_bytes()
583 && size == 32
584 {
585 generated_hashes.insert(B256::from_slice(bytes));
586 }
587 }
588
589 assert!(generated_bytes.contains("hello"));
590 assert!(generated_bytes.contains("world"));
591 assert!(generated_strings.contains("hello"));
592 assert!(generated_strings.contains("world"));
593 assert!(generated_hashes.contains(&keccak256("hello")));
594 assert!(generated_hashes.contains(&keccak256("world")));
595 }
596
597 #[test]
598 fn mutate_address_can_select_from_dictionary() {
599 use super::mutate_param_value;
600 use alloy_dyn_abi::{DynSolType, DynSolValue};
601 use alloy_primitives::Address;
602
603 let state = EvmFuzzState::test();
604
605 let addr1 = Address::repeat_byte(0x11);
607 let addr2 = Address::repeat_byte(0x22);
608 let addr3 = Address::repeat_byte(0x33);
609 state.collect_values([addr1.into_word(), addr2.into_word(), addr3.into_word()]);
610
611 let cfg = proptest::test_runner::Config { failure_persistence: None, ..Default::default() };
612 let mut runner = proptest::test_runner::TestRunner::new(cfg);
613
614 let original = Address::repeat_byte(0xff);
616 let mut got_addr1 = false;
617 let mut got_addr2 = false;
618 let mut got_addr3 = false;
619
620 for _ in 0..1000 {
621 let mutated = mutate_param_value(
622 &DynSolType::Address,
623 DynSolValue::Address(original),
624 &mut runner,
625 &state,
626 );
627 if let DynSolValue::Address(addr) = mutated {
628 if addr == addr1 {
629 got_addr1 = true;
630 }
631 if addr == addr2 {
632 got_addr2 = true;
633 }
634 if addr == addr3 {
635 got_addr3 = true;
636 }
637 }
638 if got_addr1 && got_addr2 && got_addr3 {
639 break;
640 }
641 }
642
643 assert!(
645 got_addr1 || got_addr2 || got_addr3,
646 "Address mutation should select addresses from dictionary"
647 );
648 }
649
650 #[test]
651 fn mutate_address_prefers_targeted_senders() {
652 use super::select_random_address;
653 use crate::invariant::SenderFilters;
654 use alloy_primitives::Address;
655
656 let state = EvmFuzzState::test();
657
658 let dict_addr = Address::repeat_byte(0xdd);
660 state.collect_values([dict_addr.into_word()]);
661
662 let targeted1 = Address::repeat_byte(0x11);
664 let targeted2 = Address::repeat_byte(0x22);
665 let senders = SenderFilters::new(vec![targeted1, targeted2], vec![]);
666
667 let cfg = proptest::test_runner::Config { failure_persistence: None, ..Default::default() };
668 let mut runner = proptest::test_runner::TestRunner::new(cfg);
669
670 let original = Address::repeat_byte(0xff);
672 let mut got_targeted1 = false;
673 let mut got_targeted2 = false;
674 let mut got_dict = false;
675
676 for _ in 0..100 {
677 if let Some(addr) = select_random_address(original, &mut runner, &state, Some(&senders))
678 {
679 if addr == targeted1 {
680 got_targeted1 = true;
681 }
682 if addr == targeted2 {
683 got_targeted2 = true;
684 }
685 if addr == dict_addr {
686 got_dict = true;
687 }
688 }
689 }
690
691 assert!(
693 got_targeted1 || got_targeted2,
694 "select_random_address should select from targeted senders"
695 );
696 assert!(
697 !got_dict,
698 "select_random_address should not select from dictionary when targeted senders are set"
699 );
700 }
701
702 #[test]
703 fn mutate_address_respects_excluded_senders() {
704 use super::select_random_address;
705 use crate::invariant::SenderFilters;
706 use alloy_primitives::Address;
707
708 let state = EvmFuzzState::test();
709
710 let addr1 = Address::repeat_byte(0x11);
712 let addr2 = Address::repeat_byte(0x22);
713 let excluded_addr = Address::repeat_byte(0xee);
714 state.collect_values([addr1.into_word(), addr2.into_word(), excluded_addr.into_word()]);
715
716 let senders = SenderFilters::new(vec![], vec![excluded_addr]);
718
719 let cfg = proptest::test_runner::Config { failure_persistence: None, ..Default::default() };
720 let mut runner = proptest::test_runner::TestRunner::new(cfg);
721
722 let original = Address::repeat_byte(0xff);
724 let mut got_excluded = false;
725 let mut got_valid = false;
726
727 for _ in 0..100 {
728 if let Some(addr) = select_random_address(original, &mut runner, &state, Some(&senders))
729 {
730 if addr == excluded_addr {
731 got_excluded = true;
732 break;
733 }
734 if addr == addr1 || addr == addr2 {
735 got_valid = true;
736 }
737 }
738 }
739
740 assert!(!got_excluded, "select_random_address should not select excluded addresses");
741 assert!(got_valid, "select_random_address should select valid (non-excluded) addresses");
742 }
743}