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