Skip to main content

foundry_evm_fuzz/strategies/
param.rs

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
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: &impl FuzzStateReader,
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            state.with_dictionary(|dict| {
131                let values = if bias { dict.samples(&param) } else { None }
132                    .unwrap_or_else(|| dict.values())
133                    .as_slice();
134                values[index.index(values.len())]
135            })
136        })
137    };
138
139    // Convert the value based on the parameter type
140    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); // use deterministic rng
148
149                        // Do not use addresses of deployed libraries as fuzz input, instead return
150                        // a deterministically random address. We cannot filter out this value (via
151                        // `prop_filter_map`) as proptest can invoke this closure after test
152                        // execution, and returning a `None` will cause it to panic.
153                        // See <https://github.com/foundry-rs/foundry/issues/9764> and <https://github.com/foundry-rs/foundry/issues/8639>.
154                        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                        // 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 Some(DynSolValue::String(s.clone()));
187                        }
188                        None
189                    }) {
190                        return Just(value).boxed();
191                    }
192
193                    // Fallback to random string generation
194                    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                        // Try string literals as bytes: 10% chance
215                        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                        // Try hex literals: 20% chance
222                        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                    // Fallback to the generated word from the dictionary: 70% chance
233                    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                    // Extract lower N bits
244                    let uint_n = U256::from_be_bytes(value.0) % U256::from(1).wrapping_shl(n);
245                    // Interpret as signed int (two's complement) --> check sign bit (bit N-1).
246                    let sign_bit = U256::from(1) << (n - 1);
247                    let num = if uint_n >= sign_bit {
248                        // Negative number in two's complement
249                        let modulus = U256::from(1) << n;
250                        I256::from_raw(uint_n.wrapping_sub(modulus))
251                    } else {
252                        // Positive number
253                        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
293/// Selects a random address for mutation, respecting sender filters if provided.
294///
295/// Priority:
296/// 1. If `senders` has targeted addresses, pick randomly from those
297/// 2. Otherwise, pick from the dictionary state values (excluding any in `senders.excluded`)
298/// 3. Returns `None` if no suitable address is found or if the selected address equals `current`
299fn 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            // Pick from targeted senders
308            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        // Pick from dictionary state values, excluding addresses in the exclusion list
314        state.with_dictionary(|dict| {
315            let values = dict.values();
316            if values.is_empty() {
317                return None;
318            }
319
320            // Try a few times to find a non-excluded address
321            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        // No sender filters, just pick from dictionary state values
332        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
345/// Mutates the current value of the given parameter type and value.
346pub 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
355/// Mutates the current value of the given parameter type and value, with optional sender filters.
356///
357/// When `senders` is provided and has targeted addresses, address mutations will prefer
358/// selecting from those targeted addresses (similar to `select_random_sender` behavior).
359pub 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            // flip boolean value
386            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            // Replace with a random address from targeted senders or dictionary.
417            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                    // Decrease array size by removing a random element.
428                    0 => {
429                        values.remove(test_runner.rng().random_range(0..values.len()));
430                    }
431                    // Increase array size.
432                    1 => values.push(new_value(param_type, test_runner)),
433                    // Mutate random array element.
434                    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 struct element.
473                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 element.
484                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
495/// Mutates random value from given tuples.
496fn 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
510/// Mutates random value from given array.
511fn 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
524/// Returns a proptest strategy for generating random msg.value for payable functions.
525///
526/// Most calls carry no value. The configured non-zero percent delegates to [`UintStrategy`],
527/// which biases toward edge cases (around 0 / max) and dictionary fixtures, with
528/// random fallback. Over-budget values are clamped to sender balance at execute time.
529pub 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
541/// Generates a msg.value for payable functions using `TestRunner`'s RNG (corpus mutation path).
542///
543/// Mirrors [`fuzz_msg_value`] by sampling from [`UintStrategy`]. The configured mutation gate is
544/// applied at the call site in `corpus.rs`. Over-budget values are clamped to sender
545/// balance at execute time.
546pub 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        // Seed dict with string values and their hashes --> mimic `CheatcodeAnalysis` behavior.
601        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        // Verify strategies generates the seeded AST literals
614        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        // Add addresses to dictionary via state values.
659        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        // Mutate an address many times and verify we can get addresses from the dictionary.
668        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        // We should have seen at least one dictionary address in 1000 iterations.
697        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        // Add addresses to dictionary (these should NOT be selected when targeted is set).
712        let dict_addr = Address::repeat_byte(0xdd);
713        state.collect_values([dict_addr.into_word()]);
714
715        // Set up targeted senders.
716        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        // Call select_random_address directly to verify it uses targeted senders.
724        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        // Should see targeted addresses, never dictionary address.
745        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        // Add addresses to dictionary.
764        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        // Exclude one address.
770        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        // Call select_random_address directly to verify it respects excluded senders.
776        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}