Skip to main content

foundry_evm_fuzz/strategies/
param.rs

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
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 => {
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
125/// Given a parameter type, returns a strategy for generating values for that type, given some EVM
126/// fuzz state.
127///
128/// Works with ABI Encoder v2 tuples.
129pub(crate) fn fuzz_param_from_state(
130    param: &DynSolType,
131    state: &impl DictionaryRead,
132) -> BoxedStrategy<DynSolValue> {
133    // Value strategy that uses the state.
134    let value = || {
135        let state = state.clone();
136        let param = param.clone();
137        // Generate a bias and use it to pick samples or non-persistent values (50 / 50).
138        // Use `Index` instead of `Selector` when selecting a value to avoid iterating over the
139        // entire dictionary.
140        any::<(bool, prop::sample::Index)>().prop_map(move |(bias, index)| {
141            state.with_dictionary(|dict| {
142                let values = if bias { dict.samples(&param) } else { None }
143                    .unwrap_or_else(|| dict.values())
144                    .as_slice();
145                values[index.index(values.len())]
146            })
147        })
148    };
149
150    // Convert the value based on the parameter type
151    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); // use deterministic rng
159
160                        // Do not use addresses of deployed libraries as fuzz input, instead return
161                        // a deterministically random address. We cannot filter out this value (via
162                        // `prop_filter_map`) as proptest can invoke this closure after test
163                        // execution, and returning a `None` will cause it to panic.
164                        // See <https://github.com/foundry-rs/foundry/issues/9764> and <https://github.com/foundry-rs/foundry/issues/8639>.
165                        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                        // AST string literals available: 30% probability
194                        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                    // Fallback to random string generation
205                    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                        // Try string literals as bytes: 10% chance
226                        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                        // Try hex literals: 20% chance
233                        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                    // Fallback to the generated word from the dictionary: 70% chance
244                    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                    // Extract lower N bits
255                    let uint_n = U256::from_be_bytes(value.0) % U256::from(1).wrapping_shl(n);
256                    // Interpret as signed int (two's complement) --> check sign bit (bit N-1).
257                    let sign_bit = U256::from(1) << (n - 1);
258                    let num = if uint_n >= sign_bit {
259                        // Negative number in two's complement
260                        let modulus = U256::from(1) << n;
261                        I256::from_raw(uint_n.wrapping_sub(modulus))
262                    } else {
263                        // Positive number
264                        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
304/// Selects a random address for mutation, respecting sender filters if provided.
305///
306/// Priority:
307/// 1. If `senders` has targeted addresses, pick randomly from those
308/// 2. Otherwise, pick from the dictionary state values (excluding any in `senders.excluded`)
309/// 3. Returns `None` if no suitable address is found or if the selected address equals `current`
310fn 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            // Pick from targeted senders
319            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        // Pick from dictionary state values, excluding addresses in the exclusion list
325        state.with_dictionary(|dict| {
326            let values = dict.values();
327            if values.is_empty() {
328                return None;
329            }
330
331            // Try a few times to find a non-excluded address
332            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        // No sender filters, just pick from dictionary state values
343        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
356/// Mutates the current value of the given parameter type and value.
357pub(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            // flip boolean value
383            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            // Replace with a random address from targeted senders or dictionary.
414            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                    // Decrease array size by removing a random element.
425                    0 => {
426                        values.remove(test_runner.rng().random_range(0..values.len()));
427                    }
428                    // Increase array size.
429                    1 => values.push(new_value(param_type, test_runner)),
430                    // Mutate random array element.
431                    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 struct element.
470                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 element.
481                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
492/// Mutates random value from given tuples.
493fn 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
507/// Mutates random value from given array.
508fn 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
521/// Returns a proptest strategy for generating random msg.value for payable functions.
522///
523/// Most calls carry no value. The configured non-zero percent delegates to [`UintStrategy`],
524/// which biases toward edge cases (around 0 / max) and dictionary fixtures, with
525/// random fallback. Over-budget values are clamped to sender balance at execute time.
526pub 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
538/// Generates a msg.value for payable functions using `TestRunner`'s RNG (corpus mutation path).
539///
540/// Mirrors [`fuzz_msg_value`] by sampling from [`UintStrategy`]. The configured mutation gate is
541/// applied at the call site in `corpus.rs`. Over-budget values are clamped to sender
542/// balance at execute time.
543pub 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        // Seed dict with string values and their hashes --> mimic `CheatcodeAnalysis` behavior.
637        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        // Verify strategies generates the seeded AST literals
650        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        // Add addresses to dictionary via state values.
695        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        // Mutate an address many times and verify we can get addresses from the dictionary.
704        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        // We should have seen at least one dictionary address in 1000 iterations.
733        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        // Add addresses to dictionary (these should NOT be selected when targeted is set).
748        let dict_addr = Address::repeat_byte(0xdd);
749        state.collect_values([dict_addr.into_word()]);
750
751        // Set up targeted senders.
752        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        // Call select_random_address directly to verify it uses targeted senders.
760        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        // Should see targeted addresses, never dictionary address.
781        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        // Add addresses to dictionary.
800        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        // Exclude one address.
806        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        // Call select_random_address directly to verify it respects excluded senders.
812        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}