Skip to main content

foundry_evm_fuzz/strategies/
param.rs

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
14/// The max length of arrays we fuzz for is 256.
15const MAX_ARRAY_LEN: usize = 256;
16
17/// Given a parameter type, returns a strategy for generating values for that type.
18///
19/// See [`fuzz_param_with_fixtures`] for more information.
20pub fn fuzz_param(param: &DynSolType) -> BoxedStrategy<DynSolValue> {
21    fuzz_param_inner(param, None)
22}
23
24/// Given a parameter type and configured fixtures for param name, returns a strategy for generating
25/// values for that type.
26///
27/// Fixtures can be currently generated for uint, int, address, bytes and
28/// string types and are defined for parameter name.
29/// For example, fixtures for parameter `owner` of type `address` can be defined in a function with
30/// a `function fixture_owner() public returns (address[] memory)` signature.
31///
32/// Fixtures are matched on parameter name, hence fixtures defined in
33/// `fixture_owner` function can be used in a fuzzed test function with a signature like
34/// `function testFuzz_ownerAddress(address owner, uint amount)`.
35///
36/// Raises an error if all the fixture types are not of the same type as the input parameter.
37///
38/// Works with ABI Encoder v2 tuples.
39pub 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
114/// Given a parameter type, returns a strategy for generating values for that type, given some EVM
115/// fuzz state.
116///
117/// Works with ABI Encoder v2 tuples.
118pub fn fuzz_param_from_state(
119    param: &DynSolType,
120    state: &EvmFuzzState,
121) -> BoxedStrategy<DynSolValue> {
122    // Value strategy that uses the state.
123    let value = || {
124        let state = state.clone();
125        let param = param.clone();
126        // Generate a bias and use it to pick samples or non-persistent values (50 / 50).
127        // Use `Index` instead of `Selector` when selecting a value to avoid iterating over the
128        // entire dictionary.
129        any::<(bool, prop::sample::Index)>().prop_map(move |(bias, index)| {
130            let state = state.dictionary_read();
131            let values = if bias { state.samples(&param) } else { None }
132                .unwrap_or_else(|| state.values())
133                .as_slice();
134            values[index.index(values.len())]
135        })
136    };
137
138    // Convert the value based on the parameter type
139    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); // use deterministic rng
147
148                        // Do not use addresses of deployed libraries as fuzz input, instead return
149                        // a deterministically random address. We cannot filter out this value (via
150                        // `prop_filter_map`) as proptest can invoke this closure after test
151                        // execution, and returning a `None` will cause it to panic.
152                        // See <https://github.com/foundry-rs/foundry/issues/9764> and <https://github.com/foundry-rs/foundry/issues/8639>.
153                        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                    // AST string literals available: 30% probability
183                    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                    // Fallback to random string generation
190                    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                    // Try string literals as bytes: 10% chance
212                    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                    // Try hex literals: 20% chance
219                    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                    // Fallback to the generated word from the dictionary: 70% chance
226                    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                    // Extract lower N bits
237                    let uint_n = U256::from_be_bytes(value.0) % U256::from(1).wrapping_shl(n);
238                    // Interpret as signed int (two's complement) --> check sign bit (bit N-1).
239                    let sign_bit = U256::from(1) << (n - 1);
240                    let num = if uint_n >= sign_bit {
241                        // Negative number in two's complement
242                        let modulus = U256::from(1) << n;
243                        I256::from_raw(uint_n.wrapping_sub(modulus))
244                    } else {
245                        // Positive number
246                        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
286/// Selects a random address for mutation, respecting sender filters if provided.
287///
288/// Priority:
289/// 1. If `senders` has targeted addresses, pick randomly from those
290/// 2. Otherwise, pick from the dictionary state values (excluding any in `senders.excluded`)
291/// 3. Returns `None` if no suitable address is found or if the selected address equals `current`
292fn 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            // Pick from targeted senders
301            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        // Pick from dictionary state values, excluding addresses in the exclusion list
307        let dict = state.dictionary_read();
308        let values = dict.values();
309        if values.is_empty() {
310            return None;
311        }
312
313        // Try a few times to find a non-excluded address
314        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        // No sender filters, just pick from dictionary state values
324        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
336/// Mutates the current value of the given parameter type and value.
337pub 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
346/// Mutates the current value of the given parameter type and value, with optional sender filters.
347///
348/// When `senders` is provided and has targeted addresses, address mutations will prefer
349/// selecting from those targeted addresses (similar to `select_random_sender` behavior).
350pub 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            // flip boolean value
377            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            // Replace with a random address from targeted senders or dictionary.
408            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                    // Decrease array size by removing a random element.
419                    0 => {
420                        values.remove(test_runner.rng().random_range(0..values.len()));
421                    }
422                    // Increase array size.
423                    1 => values.push(new_value(param_type, test_runner)),
424                    // Mutate random array element.
425                    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 struct element.
464                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 element.
475                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
486/// Mutates random value from given tuples.
487fn 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
501/// Mutates random value from given array.
502fn 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        // Seed dict with string values and their hashes --> mimic `CheatcodeAnalysis` behavior.
548        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        // Verify strategies generates the seeded AST literals
561        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        // Add addresses to dictionary via state values.
606        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        // Mutate an address many times and verify we can get addresses from the dictionary.
615        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        // We should have seen at least one dictionary address in 1000 iterations.
644        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        // Add addresses to dictionary (these should NOT be selected when targeted is set).
659        let dict_addr = Address::repeat_byte(0xdd);
660        state.collect_values([dict_addr.into_word()]);
661
662        // Set up targeted senders.
663        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        // Call select_random_address directly to verify it uses targeted senders.
671        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        // Should see targeted addresses, never dictionary address.
692        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        // Add addresses to dictionary.
711        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        // Exclude one address.
717        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        // Call select_random_address directly to verify it respects excluded senders.
723        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}