1use super::{UintStrategy, state::DictionaryRead};
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::*, strategy::ValueTree, 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 => {
88 let default_strategy = DynSolValue::type_strategy(param).prop_map(move |value| {
89 DynSolValue::String(
90 value.as_str().unwrap().trim().trim_end_matches('\0').to_string(),
91 )
92 });
93 if let Some(fixtures) = fuzz_fixtures {
94 let fixtures = fixtures.to_vec();
95 proptest::prop_oneof![
96 50 => any::<prop::sample::Index>()
97 .prop_map(move |index| index.get(&fixtures).clone()),
98 50 => default_strategy,
99 ]
100 .boxed()
101 } else {
102 default_strategy.boxed()
103 }
104 }
105 DynSolType::Tuple(ref params) => params
106 .iter()
107 .map(|param| fuzz_param_inner(param, None))
108 .collect::<Vec<_>>()
109 .prop_map(DynSolValue::Tuple)
110 .boxed(),
111 DynSolType::FixedArray(ref param, size) => {
112 proptest::collection::vec(fuzz_param_inner(param, None), size)
113 .prop_map(DynSolValue::FixedArray)
114 .boxed()
115 }
116 DynSolType::Array(ref param) => {
117 proptest::collection::vec(fuzz_param_inner(param, None), 0..MAX_ARRAY_LEN)
118 .prop_map(DynSolValue::Array)
119 .boxed()
120 }
121 _ => panic!("unsupported fuzz param type: {param}"),
122 }
123}
124
125pub(crate) fn fuzz_param_from_state(
130 param: &DynSolType,
131 state: &impl DictionaryRead,
132) -> BoxedStrategy<DynSolValue> {
133 let value = || {
135 let state = state.clone();
136 let param = param.clone();
137 any::<(bool, prop::sample::Index)>().prop_map(move |(bias, index)| {
141 state.with_dictionary(|dict| {
142 let values = if bias { dict.samples(¶m) } else { None }
143 .unwrap_or_else(|| dict.values())
144 .as_slice();
145 values[index.index(values.len())]
146 })
147 })
148 };
149
150 match *param {
152 DynSolType::Address => {
153 let deployed_libs = state.deployed_libs().to_vec();
154 value()
155 .prop_map(move |value| {
156 let mut fuzzed_addr = Address::from_word(value);
157 if deployed_libs.contains(&fuzzed_addr) {
158 let mut rng = StdRng::seed_from_u64(0x1337); loop {
166 fuzzed_addr.randomize_with(&mut rng);
167 if !deployed_libs.contains(&fuzzed_addr) {
168 break;
169 }
170 }
171 }
172 DynSolValue::Address(fuzzed_addr)
173 })
174 .boxed()
175 }
176 DynSolType::Function => value()
177 .prop_map(move |value| {
178 DynSolValue::Function(alloy_primitives::Function::from_word(value))
179 })
180 .boxed(),
181 DynSolType::FixedBytes(size @ 1..=32) => value()
182 .prop_map(move |mut v| {
183 v[size..].fill(0);
184 DynSolValue::FixedBytes(B256::from(v), size)
185 })
186 .boxed(),
187 DynSolType::Bool => DynSolValue::type_strategy(param).boxed(),
188 DynSolType::String => {
189 let state = state.clone();
190 (proptest::bool::weighted(0.3), any::<prop::sample::Index>())
191 .prop_flat_map(move |(use_ast, select_index)| {
192 if let Some(value) = state.with_dictionary(|dict| {
193 let ast_strings = dict.ast_strings();
195 if use_ast && !ast_strings.is_empty() {
196 let s = &ast_strings.as_slice()[select_index.index(ast_strings.len())];
197 return Some(DynSolValue::String(s.clone()));
198 }
199 None
200 }) {
201 return Just(value).boxed();
202 }
203
204 DynSolValue::type_strategy(&DynSolType::String)
206 .prop_map(|value| {
207 DynSolValue::String(
208 value.as_str().unwrap().trim().trim_end_matches('\0').to_string(),
209 )
210 })
211 .boxed()
212 })
213 .boxed()
214 }
215 DynSolType::Bytes => {
216 let state_clone = state.clone();
217 (
218 value(),
219 proptest::bool::weighted(0.1),
220 proptest::bool::weighted(0.2),
221 any::<prop::sample::Index>(),
222 )
223 .prop_map(move |(word, use_ast_string, use_ast_bytes, select_index)| {
224 if let Some(value) = state_clone.with_dictionary(|dict| {
225 let ast_strings = dict.ast_strings();
227 if use_ast_string && !ast_strings.is_empty() {
228 let s = &ast_strings.as_slice()[select_index.index(ast_strings.len())];
229 return Some(DynSolValue::Bytes(s.as_bytes().to_vec()));
230 }
231
232 let ast_bytes = dict.ast_bytes();
234 if use_ast_bytes && !ast_bytes.is_empty() {
235 let bytes = &ast_bytes.as_slice()[select_index.index(ast_bytes.len())];
236 return Some(DynSolValue::Bytes(bytes.to_vec()));
237 }
238 None
239 }) {
240 return value;
241 }
242
243 DynSolValue::Bytes(word.0.into())
245 })
246 .boxed()
247 }
248 DynSolType::Int(n @ 8..=256) => match n / 8 {
249 32 => value()
250 .prop_map(move |value| DynSolValue::Int(I256::from_raw(value.into()), 256))
251 .boxed(),
252 1..=31 => value()
253 .prop_map(move |value| {
254 let uint_n = U256::from_be_bytes(value.0) % U256::from(1).wrapping_shl(n);
256 let sign_bit = U256::from(1) << (n - 1);
258 let num = if uint_n >= sign_bit {
259 let modulus = U256::from(1) << n;
261 I256::from_raw(uint_n.wrapping_sub(modulus))
262 } else {
263 I256::from_raw(uint_n)
265 };
266
267 DynSolValue::Int(num, n)
268 })
269 .boxed(),
270 _ => unreachable!(),
271 },
272 DynSolType::Uint(n @ 8..=256) => match n / 8 {
273 32 => value()
274 .prop_map(move |value| DynSolValue::Uint(U256::from_be_bytes(value.0), 256))
275 .boxed(),
276 1..=31 => value()
277 .prop_map(move |value| {
278 let uint = U256::from_be_bytes(value.0) % U256::from(1).wrapping_shl(n);
279 DynSolValue::Uint(uint, n)
280 })
281 .boxed(),
282 _ => unreachable!(),
283 },
284 DynSolType::Tuple(ref params) => params
285 .iter()
286 .map(|p| fuzz_param_from_state(p, state))
287 .collect::<Vec<_>>()
288 .prop_map(DynSolValue::Tuple)
289 .boxed(),
290 DynSolType::FixedArray(ref param, size) => {
291 proptest::collection::vec(fuzz_param_from_state(param, state), size)
292 .prop_map(DynSolValue::FixedArray)
293 .boxed()
294 }
295 DynSolType::Array(ref param) => {
296 proptest::collection::vec(fuzz_param_from_state(param, state), 0..MAX_ARRAY_LEN)
297 .prop_map(DynSolValue::Array)
298 .boxed()
299 }
300 _ => panic!("unsupported fuzz param type: {param}"),
301 }
302}
303
304fn select_random_address(
311 current: Address,
312 test_runner: &mut TestRunner,
313 state: &impl DictionaryRead,
314 senders: Option<&SenderFilters>,
315) -> Option<Address> {
316 if let Some(senders) = senders {
317 if !senders.targeted.is_empty() {
318 let index = test_runner.rng().random_range(0..senders.targeted.len());
320 let addr = senders.targeted[index];
321 return (addr != current).then_some(addr);
322 }
323
324 state.with_dictionary(|dict| {
326 let values = dict.values();
327 if values.is_empty() {
328 return None;
329 }
330
331 for _ in 0..10 {
333 let index = test_runner.rng().random_range(0..values.len());
334 let addr = Address::from_word(values[index]);
335 if addr != current && !senders.excluded.contains(&addr) {
336 return Some(addr);
337 }
338 }
339 None
340 })
341 } else {
342 state.with_dictionary(|dict| {
344 let values = dict.values();
345 if values.is_empty() {
346 None
347 } else {
348 let index = test_runner.rng().random_range(0..values.len());
349 let addr = Address::from_word(values[index]);
350 (addr != current).then_some(addr)
351 }
352 })
353 }
354}
355
356pub(crate) fn mutate_param_value(
358 param: &DynSolType,
359 value: DynSolValue,
360 test_runner: &mut TestRunner,
361 state: &impl DictionaryRead,
362) -> DynSolValue {
363 mutate_param_value_inner(param, value, test_runner, state, None)
364}
365
366fn mutate_param_value_inner(
367 param: &DynSolType,
368 value: DynSolValue,
369 test_runner: &mut TestRunner,
370 state: &impl DictionaryRead,
371 senders: Option<&SenderFilters>,
372) -> DynSolValue {
373 let new_value = |param: &DynSolType, test_runner: &mut TestRunner| {
374 fuzz_param_from_state(param, state)
375 .new_tree(test_runner)
376 .expect("Could not generate case")
377 .current()
378 };
379
380 match value {
381 DynSolValue::Bool(val) => {
382 trace!(target: "mutator", "Bool flip {val}");
384 Some(DynSolValue::Bool(!val))
385 }
386 DynSolValue::Uint(val, size) => match test_runner.rng().random_range(0..=6) {
387 0 => U256::increment_decrement(val, size, test_runner),
388 1 => U256::flip_random_bit(val, size, test_runner),
389 2 => U256::mutate_interesting_byte(val, size, test_runner),
390 3 => U256::mutate_interesting_word(val, size, test_runner),
391 4 => U256::mutate_interesting_dword(val, size, test_runner),
392 5 => U256::mutate_with_gaussian_noise(val, size, test_runner),
393 6 => None,
394 _ => unreachable!(),
395 }
396 .map(|v| DynSolValue::Uint(v, size)),
397 DynSolValue::Int(val, size) => match test_runner.rng().random_range(0..=6) {
398 0 => I256::increment_decrement(val, size, test_runner),
399 1 => I256::flip_random_bit(val, size, test_runner),
400 2 => I256::mutate_interesting_byte(val, size, test_runner),
401 3 => I256::mutate_interesting_word(val, size, test_runner),
402 4 => I256::mutate_interesting_dword(val, size, test_runner),
403 5 => I256::mutate_with_gaussian_noise(val, size, test_runner),
404 6 => None,
405 _ => unreachable!(),
406 }
407 .map(|v| DynSolValue::Int(v, size)),
408 DynSolValue::Address(val) => match test_runner.rng().random_range(0..=5) {
409 0 => Address::flip_random_bit(val, 20, test_runner),
410 1 => Address::mutate_interesting_byte(val, 20, test_runner),
411 2 => Address::mutate_interesting_word(val, 20, test_runner),
412 3 => Address::mutate_interesting_dword(val, 20, test_runner),
413 4 => select_random_address(val, test_runner, state, senders),
415 5 => None,
416 _ => unreachable!(),
417 }
418 .map(DynSolValue::Address),
419 DynSolValue::Array(mut values) => {
420 if let DynSolType::Array(param_type) = param
421 && !values.is_empty()
422 {
423 match test_runner.rng().random_range(0..=2) {
424 0 => {
426 values.remove(test_runner.rng().random_range(0..values.len()));
427 }
428 1 => values.push(new_value(param_type, test_runner)),
430 2 => mutate_random_array_value(
432 &mut values,
433 param_type,
434 test_runner,
435 state,
436 senders,
437 ),
438 _ => unreachable!(),
439 }
440 Some(DynSolValue::Array(values))
441 } else {
442 None
443 }
444 }
445 DynSolValue::FixedArray(mut values) => {
446 if let DynSolType::FixedArray(param_type, _size) = param
447 && !values.is_empty()
448 {
449 mutate_random_array_value(&mut values, param_type, test_runner, state, senders);
450 Some(DynSolValue::FixedArray(values))
451 } else {
452 None
453 }
454 }
455 DynSolValue::FixedBytes(word, size) => match test_runner.rng().random_range(0..=4) {
456 0 => Word::flip_random_bit(word, size, test_runner),
457 1 => Word::mutate_interesting_byte(word, size, test_runner),
458 2 => Word::mutate_interesting_word(word, size, test_runner),
459 3 => Word::mutate_interesting_dword(word, size, test_runner),
460 4 => None,
461 _ => unreachable!(),
462 }
463 .map(|word| DynSolValue::FixedBytes(word, size)),
464 DynSolValue::CustomStruct { name, prop_names, tuple: mut values } => {
465 if let DynSolType::CustomStruct { name: _, prop_names: _, tuple: tuple_types }
466 | DynSolType::Tuple(tuple_types) = param
467 && !values.is_empty()
468 {
469 mutate_random_tuple_value(&mut values, tuple_types, test_runner, state, senders);
471 Some(DynSolValue::CustomStruct { name, prop_names, tuple: values })
472 } else {
473 None
474 }
475 }
476 DynSolValue::Tuple(mut values) => {
477 if let DynSolType::Tuple(tuple_types) = param
478 && !values.is_empty()
479 {
480 mutate_random_tuple_value(&mut values, tuple_types, test_runner, state, senders);
482 Some(DynSolValue::Tuple(values))
483 } else {
484 None
485 }
486 }
487 _ => None,
488 }
489 .unwrap_or_else(|| new_value(param, test_runner))
490}
491
492fn mutate_random_tuple_value(
494 tuple_values: &mut [DynSolValue],
495 tuple_types: &[DynSolType],
496 test_runner: &mut TestRunner,
497 state: &impl DictionaryRead,
498 senders: Option<&SenderFilters>,
499) {
500 let id = test_runner.rng().random_range(0..tuple_values.len());
501 let param_type = &tuple_types[id];
502 let old_val = replace(&mut tuple_values[id], DynSolValue::Bool(false));
503 let new_val = mutate_param_value_inner(param_type, old_val, test_runner, state, senders);
504 tuple_values[id] = new_val;
505}
506
507fn mutate_random_array_value(
509 array_values: &mut [DynSolValue],
510 element_type: &DynSolType,
511 test_runner: &mut TestRunner,
512 state: &impl DictionaryRead,
513 senders: Option<&SenderFilters>,
514) {
515 let elem = array_values.choose_mut(&mut test_runner.rng()).unwrap();
516 let old_val = replace(elem, DynSolValue::Bool(false));
517 let new_val = mutate_param_value_inner(element_type, old_val, test_runner, state, senders);
518 *elem = new_val;
519}
520
521pub fn fuzz_msg_value(payable_value_weight: u32) -> BoxedStrategy<Option<U256>> {
527 match payable_value_weight.min(100) {
528 0 => proptest::strategy::Just(None).boxed(),
529 100 => UintStrategy::new(256, None).prop_map(Some).boxed(),
530 payable_value_weight => proptest::prop_oneof![
531 100 - payable_value_weight => proptest::strategy::Just(None),
532 payable_value_weight => UintStrategy::new(256, None).prop_map(Some),
533 ]
534 .boxed(),
535 }
536}
537
538pub fn generate_msg_value(test_runner: &mut TestRunner) -> U256 {
544 UintStrategy::new(256, None)
545 .new_tree(test_runner)
546 .expect("UintStrategy::new_tree is infallible")
547 .current()
548}
549
550#[cfg(test)]
551mod tests {
552 use crate::{
553 FuzzFixtures,
554 strategies::{EvmFuzzState, fuzz_calldata, fuzz_calldata_from_state},
555 };
556 use alloy_dyn_abi::{DynSolType, DynSolValue};
557 use alloy_primitives::{B256, U256};
558 use foundry_common::abi::get_func;
559 use foundry_config::FuzzDictionaryConfig;
560 use proptest::{
561 strategy::{Strategy, ValueTree},
562 test_runner::TestRunner,
563 };
564 use revm::database::{CacheDB, EmptyDB};
565 use std::collections::HashSet;
566
567 #[test]
568 fn payable_value_weight_controls_non_zero_msg_value() {
569 use super::fuzz_msg_value;
570
571 let cfg = proptest::test_runner::Config { failure_persistence: None, ..Default::default() };
572 let mut runner = proptest::test_runner::TestRunner::new(cfg);
573
574 for _ in 0..32 {
575 assert!(fuzz_msg_value(0).new_tree(&mut runner).unwrap().current().is_none());
576 assert!(fuzz_msg_value(100).new_tree(&mut runner).unwrap().current().is_some());
577 assert!(fuzz_msg_value(250).new_tree(&mut runner).unwrap().current().is_some());
578 }
579 }
580
581 #[test]
582 fn can_fuzz_array() {
583 let f = "testArray(uint64[2] calldata values)";
584 let func = get_func(f).unwrap();
585 let state = EvmFuzzState::test();
586 let strategy = proptest::prop_oneof![
587 60 => fuzz_calldata(func.clone(), &FuzzFixtures::default()),
588 40 => fuzz_calldata_from_state(func, &state, &FuzzFixtures::default()),
589 ];
590 let cfg = proptest::test_runner::Config { failure_persistence: None, ..Default::default() };
591 let mut runner = proptest::test_runner::TestRunner::new(cfg);
592 let _ = runner.run(&strategy, |_| Ok(()));
593 }
594
595 #[test]
596 fn can_fuzz_from_zero_capacity_dictionary() {
597 let state = EvmFuzzState::new(
598 &[],
599 &CacheDB::<EmptyDB>::default(),
600 FuzzDictionaryConfig { max_fuzz_dictionary_values: 0, ..Default::default() },
601 None,
602 );
603 let strategy = super::fuzz_param_from_state(&DynSolType::Uint(256), &state);
604 let mut runner = TestRunner::default();
605
606 assert_eq!(
607 strategy.new_tree(&mut runner).unwrap().current(),
608 DynSolValue::Uint(U256::ZERO, 256)
609 );
610 }
611
612 #[test]
613 fn string_fixtures_are_emitted_verbatim() {
614 let fixture = DynSolValue::String(" padded fixture \0".to_string());
615 let strategy = super::fuzz_param_with_fixtures(
616 &DynSolType::String,
617 Some(std::slice::from_ref(&fixture)),
618 "value",
619 );
620 let mut runner = TestRunner::deterministic();
621
622 let emitted =
623 (0..1000).any(|_| strategy.new_tree(&mut runner).unwrap().current() == fixture);
624
625 assert!(emitted, "string fixture was never emitted verbatim");
626 }
627
628 #[test]
629 fn can_fuzz_string_and_bytes_with_ast_literals_and_hashes() {
630 use super::fuzz_param_from_state;
631 use crate::strategies::LiteralMaps;
632 use alloy_dyn_abi::DynSolType;
633 use alloy_primitives::keccak256;
634 use proptest::strategy::Strategy;
635
636 let mut literals = LiteralMaps::default();
638 literals.strings.insert("hello".to_string());
639 literals.strings.insert("world".to_string());
640 literals.words.entry(DynSolType::FixedBytes(32)).or_default().insert(keccak256("hello"));
641 literals.words.entry(DynSolType::FixedBytes(32)).or_default().insert(keccak256("world"));
642
643 let mut state = EvmFuzzState::test();
644 state.seed_literals(literals);
645
646 let cfg = proptest::test_runner::Config { failure_persistence: None, ..Default::default() };
647 let mut runner = proptest::test_runner::TestRunner::new(cfg);
648
649 let mut generated_bytes = HashSet::new();
651 let mut generated_hashes = HashSet::new();
652 let mut generated_strings = HashSet::new();
653 let bytes_strategy = fuzz_param_from_state(&DynSolType::Bytes, &state);
654 let string_strategy = fuzz_param_from_state(&DynSolType::String, &state);
655 let bytes32_strategy = fuzz_param_from_state(&DynSolType::FixedBytes(32), &state);
656
657 for _ in 0..256 {
658 let tree = bytes_strategy.new_tree(&mut runner).unwrap();
659 if let Some(bytes) = tree.current().as_bytes()
660 && let Ok(s) = std::str::from_utf8(bytes)
661 {
662 generated_bytes.insert(s.to_string());
663 }
664
665 let tree = string_strategy.new_tree(&mut runner).unwrap();
666 if let Some(s) = tree.current().as_str() {
667 generated_strings.insert(s.to_string());
668 }
669
670 let tree = bytes32_strategy.new_tree(&mut runner).unwrap();
671 if let Some((bytes, size)) = tree.current().as_fixed_bytes()
672 && size == 32
673 {
674 generated_hashes.insert(B256::from_slice(bytes));
675 }
676 }
677
678 assert!(generated_bytes.contains("hello"));
679 assert!(generated_bytes.contains("world"));
680 assert!(generated_strings.contains("hello"));
681 assert!(generated_strings.contains("world"));
682 assert!(generated_hashes.contains(&keccak256("hello")));
683 assert!(generated_hashes.contains(&keccak256("world")));
684 }
685
686 #[test]
687 fn mutate_address_can_select_from_dictionary() {
688 use super::mutate_param_value;
689 use alloy_dyn_abi::{DynSolType, DynSolValue};
690 use alloy_primitives::Address;
691
692 let mut state = EvmFuzzState::test();
693
694 let addr1 = Address::repeat_byte(0x11);
696 let addr2 = Address::repeat_byte(0x22);
697 let addr3 = Address::repeat_byte(0x33);
698 state.collect_values([addr1.into_word(), addr2.into_word(), addr3.into_word()]);
699
700 let cfg = proptest::test_runner::Config { failure_persistence: None, ..Default::default() };
701 let mut runner = proptest::test_runner::TestRunner::new(cfg);
702
703 let original = Address::repeat_byte(0xff);
705 let mut got_addr1 = false;
706 let mut got_addr2 = false;
707 let mut got_addr3 = false;
708
709 for _ in 0..1000 {
710 let mutated = mutate_param_value(
711 &DynSolType::Address,
712 DynSolValue::Address(original),
713 &mut runner,
714 &state,
715 );
716 if let DynSolValue::Address(addr) = mutated {
717 if addr == addr1 {
718 got_addr1 = true;
719 }
720 if addr == addr2 {
721 got_addr2 = true;
722 }
723 if addr == addr3 {
724 got_addr3 = true;
725 }
726 }
727 if got_addr1 && got_addr2 && got_addr3 {
728 break;
729 }
730 }
731
732 assert!(
734 got_addr1 || got_addr2 || got_addr3,
735 "Address mutation should select addresses from dictionary"
736 );
737 }
738
739 #[test]
740 fn mutate_address_prefers_targeted_senders() {
741 use super::select_random_address;
742 use crate::invariant::SenderFilters;
743 use alloy_primitives::Address;
744
745 let mut state = EvmFuzzState::test();
746
747 let dict_addr = Address::repeat_byte(0xdd);
749 state.collect_values([dict_addr.into_word()]);
750
751 let targeted1 = Address::repeat_byte(0x11);
753 let targeted2 = Address::repeat_byte(0x22);
754 let senders = SenderFilters::new(vec![targeted1, targeted2], vec![]);
755
756 let cfg = proptest::test_runner::Config { failure_persistence: None, ..Default::default() };
757 let mut runner = proptest::test_runner::TestRunner::new(cfg);
758
759 let original = Address::repeat_byte(0xff);
761 let mut got_targeted1 = false;
762 let mut got_targeted2 = false;
763 let mut got_dict = false;
764
765 for _ in 0..100 {
766 if let Some(addr) = select_random_address(original, &mut runner, &state, Some(&senders))
767 {
768 if addr == targeted1 {
769 got_targeted1 = true;
770 }
771 if addr == targeted2 {
772 got_targeted2 = true;
773 }
774 if addr == dict_addr {
775 got_dict = true;
776 }
777 }
778 }
779
780 assert!(
782 got_targeted1 || got_targeted2,
783 "select_random_address should select from targeted senders"
784 );
785 assert!(
786 !got_dict,
787 "select_random_address should not select from dictionary when targeted senders are set"
788 );
789 }
790
791 #[test]
792 fn mutate_address_respects_excluded_senders() {
793 use super::select_random_address;
794 use crate::invariant::SenderFilters;
795 use alloy_primitives::Address;
796
797 let mut state = EvmFuzzState::test();
798
799 let addr1 = Address::repeat_byte(0x11);
801 let addr2 = Address::repeat_byte(0x22);
802 let excluded_addr = Address::repeat_byte(0xee);
803 state.collect_values([addr1.into_word(), addr2.into_word(), excluded_addr.into_word()]);
804
805 let senders = SenderFilters::new(vec![], vec![excluded_addr]);
807
808 let cfg = proptest::test_runner::Config { failure_persistence: None, ..Default::default() };
809 let mut runner = proptest::test_runner::TestRunner::new(cfg);
810
811 let original = Address::repeat_byte(0xff);
813 let mut got_excluded = false;
814 let mut got_valid = false;
815
816 for _ in 0..100 {
817 if let Some(addr) = select_random_address(original, &mut runner, &state, Some(&senders))
818 {
819 if addr == excluded_addr {
820 got_excluded = true;
821 break;
822 }
823 if addr == addr1 || addr == addr2 {
824 got_valid = true;
825 }
826 }
827 }
828
829 assert!(!got_excluded, "select_random_address should not select excluded addresses");
830 assert!(got_valid, "select_random_address should select valid (non-excluded) addresses");
831 }
832}