foundry_evm_fuzz/strategies/
param.rs

1use super::state::EvmFuzzState;
2use crate::strategies::mutators::{
3    BitMutator, GaussianNoiseMutator, IncrementDecrementMutator, InterestingWordMutator,
4};
5use alloy_dyn_abi::{DynSolType, DynSolValue, Word};
6use alloy_primitives::{Address, B256, I256, U256};
7use proptest::{prelude::*, test_runner::TestRunner};
8use rand::{SeedableRng, prelude::IndexedMutRandom, rngs::StdRng};
9use std::mem::replace;
10
11/// The max length of arrays we fuzz for is 256.
12const MAX_ARRAY_LEN: usize = 256;
13
14/// Given a parameter type, returns a strategy for generating values for that type.
15///
16/// See [`fuzz_param_with_fixtures`] for more information.
17pub fn fuzz_param(param: &DynSolType) -> BoxedStrategy<DynSolValue> {
18    fuzz_param_inner(param, None)
19}
20
21/// Given a parameter type and configured fixtures for param name, returns a strategy for generating
22/// values for that type.
23///
24/// Fixtures can be currently generated for uint, int, address, bytes and
25/// string types and are defined for parameter name.
26/// For example, fixtures for parameter `owner` of type `address` can be defined in a function with
27/// a `function fixture_owner() public returns (address[] memory)` signature.
28///
29/// Fixtures are matched on parameter name, hence fixtures defined in
30/// `fixture_owner` function can be used in a fuzzed test function with a signature like
31/// `function testFuzz_ownerAddress(address owner, uint amount)`.
32///
33/// Raises an error if all the fixture types are not of the same type as the input parameter.
34///
35/// Works with ABI Encoder v2 tuples.
36pub fn fuzz_param_with_fixtures(
37    param: &DynSolType,
38    fixtures: Option<&[DynSolValue]>,
39    name: &str,
40) -> BoxedStrategy<DynSolValue> {
41    fuzz_param_inner(param, fixtures.map(|f| (f, name)))
42}
43
44fn fuzz_param_inner(
45    param: &DynSolType,
46    mut fuzz_fixtures: Option<(&[DynSolValue], &str)>,
47) -> BoxedStrategy<DynSolValue> {
48    if let Some((fixtures, name)) = fuzz_fixtures
49        && !fixtures.iter().all(|f| f.matches(param))
50    {
51        error!("fixtures for {name:?} do not match type {param}");
52        fuzz_fixtures = None;
53    }
54    let fuzz_fixtures = fuzz_fixtures.map(|(f, _)| f);
55
56    let value = || {
57        let default_strategy = DynSolValue::type_strategy(param);
58        if let Some(fixtures) = fuzz_fixtures {
59            proptest::prop_oneof![
60                50 => {
61                    let fixtures = fixtures.to_vec();
62                    any::<prop::sample::Index>()
63                        .prop_map(move |index| index.get(&fixtures).clone())
64                },
65                50 => default_strategy,
66            ]
67            .boxed()
68        } else {
69            default_strategy.boxed()
70        }
71    };
72
73    match *param {
74        DynSolType::Address => value(),
75        DynSolType::Int(n @ 8..=256) => super::IntStrategy::new(n, fuzz_fixtures)
76            .prop_map(move |x| DynSolValue::Int(x, n))
77            .boxed(),
78        DynSolType::Uint(n @ 8..=256) => super::UintStrategy::new(n, fuzz_fixtures)
79            .prop_map(move |x| DynSolValue::Uint(x, n))
80            .boxed(),
81        DynSolType::Function | DynSolType::Bool => DynSolValue::type_strategy(param).boxed(),
82        DynSolType::Bytes => value(),
83        DynSolType::FixedBytes(_size @ 1..=32) => value(),
84        DynSolType::String => value()
85            .prop_map(move |value| {
86                DynSolValue::String(
87                    value.as_str().unwrap().trim().trim_end_matches('\0').to_string(),
88                )
89            })
90            .boxed(),
91        DynSolType::Tuple(ref params) => params
92            .iter()
93            .map(|param| fuzz_param_inner(param, None))
94            .collect::<Vec<_>>()
95            .prop_map(DynSolValue::Tuple)
96            .boxed(),
97        DynSolType::FixedArray(ref param, size) => {
98            proptest::collection::vec(fuzz_param_inner(param, None), size)
99                .prop_map(DynSolValue::FixedArray)
100                .boxed()
101        }
102        DynSolType::Array(ref param) => {
103            proptest::collection::vec(fuzz_param_inner(param, None), 0..MAX_ARRAY_LEN)
104                .prop_map(DynSolValue::Array)
105                .boxed()
106        }
107        _ => panic!("unsupported fuzz param type: {param}"),
108    }
109}
110
111/// Given a parameter type, returns a strategy for generating values for that type, given some EVM
112/// fuzz state.
113///
114/// Works with ABI Encoder v2 tuples.
115pub fn fuzz_param_from_state(
116    param: &DynSolType,
117    state: &EvmFuzzState,
118) -> BoxedStrategy<DynSolValue> {
119    // Value strategy that uses the state.
120    let value = || {
121        let state = state.clone();
122        let param = param.clone();
123        // Generate a bias and use it to pick samples or non-persistent values (50 / 50).
124        // Use `Index` instead of `Selector` when selecting a value to avoid iterating over the
125        // entire dictionary.
126        any::<(bool, prop::sample::Index)>().prop_map(move |(bias, index)| {
127            let state = state.dictionary_read();
128            let values = if bias { state.samples(&param) } else { None }
129                .unwrap_or_else(|| state.values())
130                .as_slice();
131            values[index.index(values.len())]
132        })
133    };
134
135    // Convert the value based on the parameter type
136    match *param {
137        DynSolType::Address => {
138            let deployed_libs = state.deployed_libs.clone();
139            value()
140                .prop_map(move |value| {
141                    let mut fuzzed_addr = Address::from_word(value);
142                    if deployed_libs.contains(&fuzzed_addr) {
143                        let mut rng = StdRng::seed_from_u64(0x1337); // use deterministic rng
144
145                        // Do not use addresses of deployed libraries as fuzz input, instead return
146                        // a deterministically random address. We cannot filter out this value (via
147                        // `prop_filter_map`) as proptest can invoke this closure after test
148                        // execution, and returning a `None` will cause it to panic.
149                        // See <https://github.com/foundry-rs/foundry/issues/9764> and <https://github.com/foundry-rs/foundry/issues/8639>.
150                        loop {
151                            fuzzed_addr.randomize_with(&mut rng);
152                            if !deployed_libs.contains(&fuzzed_addr) {
153                                break;
154                            }
155                        }
156                    }
157                    DynSolValue::Address(fuzzed_addr)
158                })
159                .boxed()
160        }
161        DynSolType::Function => value()
162            .prop_map(move |value| {
163                DynSolValue::Function(alloy_primitives::Function::from_word(value))
164            })
165            .boxed(),
166        DynSolType::FixedBytes(size @ 1..=32) => value()
167            .prop_map(move |mut v| {
168                v[size..].fill(0);
169                DynSolValue::FixedBytes(B256::from(v), size)
170            })
171            .boxed(),
172        DynSolType::Bool => DynSolValue::type_strategy(param).boxed(),
173        DynSolType::String => {
174            let state = state.clone();
175            (proptest::bool::weighted(0.3), any::<prop::sample::Index>())
176                .prop_flat_map(move |(use_ast, select_index)| {
177                    let dict = state.dictionary_read();
178
179                    // AST string literals available: 30% probability
180                    let ast_strings = dict.ast_strings();
181                    if use_ast && !ast_strings.is_empty() {
182                        let s = &ast_strings.as_slice()[select_index.index(ast_strings.len())];
183                        return Just(DynSolValue::String(s.clone())).boxed();
184                    }
185
186                    // Fallback to random string generation
187                    DynSolValue::type_strategy(&DynSolType::String)
188                        .prop_map(|value| {
189                            DynSolValue::String(
190                                value.as_str().unwrap().trim().trim_end_matches('\0').to_string(),
191                            )
192                        })
193                        .boxed()
194                })
195                .boxed()
196        }
197        DynSolType::Bytes => {
198            let state_clone = state.clone();
199            (
200                value(),
201                proptest::bool::weighted(0.1),
202                proptest::bool::weighted(0.2),
203                any::<prop::sample::Index>(),
204            )
205                .prop_map(move |(word, use_ast_string, use_ast_bytes, select_index)| {
206                    let dict = state_clone.dictionary_read();
207
208                    // Try string literals as bytes: 10% chance
209                    let ast_strings = dict.ast_strings();
210                    if use_ast_string && !ast_strings.is_empty() {
211                        let s = &ast_strings.as_slice()[select_index.index(ast_strings.len())];
212                        return DynSolValue::Bytes(s.as_bytes().to_vec());
213                    }
214
215                    // Try hex literals: 20% chance
216                    let ast_bytes = dict.ast_bytes();
217                    if use_ast_bytes && !ast_bytes.is_empty() {
218                        let bytes = &ast_bytes.as_slice()[select_index.index(ast_bytes.len())];
219                        return DynSolValue::Bytes(bytes.to_vec());
220                    }
221
222                    // Fallback to the generated word from the dictionary: 70% chance
223                    DynSolValue::Bytes(word.0.into())
224                })
225                .boxed()
226        }
227        DynSolType::Int(n @ 8..=256) => match n / 8 {
228            32 => value()
229                .prop_map(move |value| DynSolValue::Int(I256::from_raw(value.into()), 256))
230                .boxed(),
231            1..=31 => value()
232                .prop_map(move |value| {
233                    // Extract lower N bits
234                    let uint_n = U256::from_be_bytes(value.0) % U256::from(1).wrapping_shl(n);
235                    // Interpret as signed int (two's complement) --> check sign bit (bit N-1).
236                    let sign_bit = U256::from(1) << (n - 1);
237                    let num = if uint_n >= sign_bit {
238                        // Negative number in two's complement
239                        let modulus = U256::from(1) << n;
240                        I256::from_raw(uint_n.wrapping_sub(modulus))
241                    } else {
242                        // Positive number
243                        I256::from_raw(uint_n)
244                    };
245
246                    DynSolValue::Int(num, n)
247                })
248                .boxed(),
249            _ => unreachable!(),
250        },
251        DynSolType::Uint(n @ 8..=256) => match n / 8 {
252            32 => value()
253                .prop_map(move |value| DynSolValue::Uint(U256::from_be_bytes(value.0), 256))
254                .boxed(),
255            1..=31 => value()
256                .prop_map(move |value| {
257                    let uint = U256::from_be_bytes(value.0) % U256::from(1).wrapping_shl(n);
258                    DynSolValue::Uint(uint, n)
259                })
260                .boxed(),
261            _ => unreachable!(),
262        },
263        DynSolType::Tuple(ref params) => params
264            .iter()
265            .map(|p| fuzz_param_from_state(p, state))
266            .collect::<Vec<_>>()
267            .prop_map(DynSolValue::Tuple)
268            .boxed(),
269        DynSolType::FixedArray(ref param, size) => {
270            proptest::collection::vec(fuzz_param_from_state(param, state), size)
271                .prop_map(DynSolValue::FixedArray)
272                .boxed()
273        }
274        DynSolType::Array(ref param) => {
275            proptest::collection::vec(fuzz_param_from_state(param, state), 0..MAX_ARRAY_LEN)
276                .prop_map(DynSolValue::Array)
277                .boxed()
278        }
279        _ => panic!("unsupported fuzz param type: {param}"),
280    }
281}
282
283/// Mutates the current value of the given parameter type and value.
284pub fn mutate_param_value(
285    param: &DynSolType,
286    value: DynSolValue,
287    test_runner: &mut TestRunner,
288    state: &EvmFuzzState,
289) -> DynSolValue {
290    let new_value = |param: &DynSolType, test_runner: &mut TestRunner| {
291        fuzz_param_from_state(param, state)
292            .new_tree(test_runner)
293            .expect("Could not generate case")
294            .current()
295    };
296
297    match value {
298        DynSolValue::Bool(val) => {
299            // flip boolean value
300            trace!(target: "mutator", "Bool flip {val}");
301            Some(DynSolValue::Bool(!val))
302        }
303        DynSolValue::Uint(val, size) => match test_runner.rng().random_range(0..=6) {
304            0 => U256::increment_decrement(val, size, test_runner),
305            1 => U256::flip_random_bit(val, size, test_runner),
306            2 => U256::mutate_interesting_byte(val, size, test_runner),
307            3 => U256::mutate_interesting_word(val, size, test_runner),
308            4 => U256::mutate_interesting_dword(val, size, test_runner),
309            5 => U256::mutate_with_gaussian_noise(val, size, test_runner),
310            6 => None,
311            _ => unreachable!(),
312        }
313        .map(|v| DynSolValue::Uint(v, size)),
314        DynSolValue::Int(val, size) => match test_runner.rng().random_range(0..=6) {
315            0 => I256::increment_decrement(val, size, test_runner),
316            1 => I256::flip_random_bit(val, size, test_runner),
317            2 => I256::mutate_interesting_byte(val, size, test_runner),
318            3 => I256::mutate_interesting_word(val, size, test_runner),
319            4 => I256::mutate_interesting_dword(val, size, test_runner),
320            5 => I256::mutate_with_gaussian_noise(val, size, test_runner),
321            6 => None,
322            _ => unreachable!(),
323        }
324        .map(|v| DynSolValue::Int(v, size)),
325        DynSolValue::Address(val) => match test_runner.rng().random_range(0..=4) {
326            0 => Address::flip_random_bit(val, 20, test_runner),
327            1 => Address::mutate_interesting_byte(val, 20, test_runner),
328            2 => Address::mutate_interesting_word(val, 20, test_runner),
329            3 => Address::mutate_interesting_dword(val, 20, test_runner),
330            4 => None,
331            _ => unreachable!(),
332        }
333        .map(DynSolValue::Address),
334        DynSolValue::Array(mut values) => {
335            if let DynSolType::Array(param_type) = param
336                && !values.is_empty()
337            {
338                match test_runner.rng().random_range(0..=2) {
339                    // Decrease array size by removing a random element.
340                    0 => {
341                        values.remove(test_runner.rng().random_range(0..values.len()));
342                    }
343                    // Increase array size.
344                    1 => values.push(new_value(param_type, test_runner)),
345                    // Mutate random array element.
346                    2 => mutate_random_array_value(&mut values, param_type, test_runner, state),
347                    _ => unreachable!(),
348                }
349                Some(DynSolValue::Array(values))
350            } else {
351                None
352            }
353        }
354        DynSolValue::FixedArray(mut values) => {
355            if let DynSolType::FixedArray(param_type, _size) = param
356                && !values.is_empty()
357            {
358                mutate_random_array_value(&mut values, param_type, test_runner, state);
359                Some(DynSolValue::FixedArray(values))
360            } else {
361                None
362            }
363        }
364        DynSolValue::FixedBytes(word, size) => match test_runner.rng().random_range(0..=4) {
365            0 => Word::flip_random_bit(word, size, test_runner),
366            1 => Word::mutate_interesting_byte(word, size, test_runner),
367            2 => Word::mutate_interesting_word(word, size, test_runner),
368            3 => Word::mutate_interesting_dword(word, size, test_runner),
369            4 => None,
370            _ => unreachable!(),
371        }
372        .map(|word| DynSolValue::FixedBytes(word, size)),
373        DynSolValue::CustomStruct { name, prop_names, tuple: mut values } => {
374            if let DynSolType::CustomStruct { name: _, prop_names: _, tuple: tuple_types }
375            | DynSolType::Tuple(tuple_types) = param
376                && !values.is_empty()
377            {
378                // Mutate random struct element.
379                mutate_random_tuple_value(&mut values, tuple_types, test_runner, state);
380                Some(DynSolValue::CustomStruct { name, prop_names, tuple: values })
381            } else {
382                None
383            }
384        }
385        DynSolValue::Tuple(mut values) => {
386            if let DynSolType::Tuple(tuple_types) = param
387                && !values.is_empty()
388            {
389                // Mutate random tuple element.
390                mutate_random_tuple_value(&mut values, tuple_types, test_runner, state);
391                Some(DynSolValue::Tuple(values))
392            } else {
393                None
394            }
395        }
396        _ => None,
397    }
398    .unwrap_or_else(|| new_value(param, test_runner))
399}
400
401/// Mutates random value from given tuples.
402fn mutate_random_tuple_value(
403    tuple_values: &mut [DynSolValue],
404    tuple_types: &[DynSolType],
405    test_runner: &mut TestRunner,
406    state: &EvmFuzzState,
407) {
408    let id = test_runner.rng().random_range(0..tuple_values.len());
409    let param_type = &tuple_types[id];
410    let old_val = replace(&mut tuple_values[id], DynSolValue::Bool(false));
411    let new_val = mutate_param_value(param_type, old_val, test_runner, state);
412    tuple_values[id] = new_val;
413}
414
415/// Mutates random value from given array.
416fn mutate_random_array_value(
417    array_values: &mut [DynSolValue],
418    element_type: &DynSolType,
419    test_runner: &mut TestRunner,
420    state: &EvmFuzzState,
421) {
422    let elem = array_values.choose_mut(&mut test_runner.rng()).unwrap();
423    let old_val = replace(elem, DynSolValue::Bool(false));
424    let new_val = mutate_param_value(element_type, old_val, test_runner, state);
425    *elem = new_val;
426}
427
428#[cfg(test)]
429mod tests {
430    use crate::{
431        FuzzFixtures,
432        strategies::{EvmFuzzState, fuzz_calldata, fuzz_calldata_from_state},
433    };
434    use alloy_primitives::B256;
435    use foundry_common::abi::get_func;
436    use foundry_config::FuzzDictionaryConfig;
437    use revm::database::{CacheDB, EmptyDB};
438    use std::collections::HashSet;
439
440    #[test]
441    fn can_fuzz_array() {
442        let f = "testArray(uint64[2] calldata values)";
443        let func = get_func(f).unwrap();
444        let db = CacheDB::new(EmptyDB::default());
445        let state = EvmFuzzState::new(&db, FuzzDictionaryConfig::default(), &[], None, None);
446        let strategy = proptest::prop_oneof![
447            60 => fuzz_calldata(func.clone(), &FuzzFixtures::default()),
448            40 => fuzz_calldata_from_state(func, &state),
449        ];
450        let cfg = proptest::test_runner::Config { failure_persistence: None, ..Default::default() };
451        let mut runner = proptest::test_runner::TestRunner::new(cfg);
452        let _ = runner.run(&strategy, |_| Ok(()));
453    }
454
455    #[test]
456    fn can_fuzz_string_and_bytes_with_ast_literals_and_hashes() {
457        use super::fuzz_param_from_state;
458        use crate::strategies::LiteralMaps;
459        use alloy_dyn_abi::DynSolType;
460        use alloy_primitives::keccak256;
461        use proptest::strategy::Strategy;
462
463        // Seed dict with string values and their hashes --> mimic `CheatcodeAnalysis` behavior.
464        let mut literals = LiteralMaps::default();
465        literals.strings.insert("hello".to_string());
466        literals.strings.insert("world".to_string());
467        literals.words.entry(DynSolType::FixedBytes(32)).or_default().insert(keccak256("hello"));
468        literals.words.entry(DynSolType::FixedBytes(32)).or_default().insert(keccak256("world"));
469
470        let db = CacheDB::new(EmptyDB::default());
471        let state = EvmFuzzState::new(&db, FuzzDictionaryConfig::default(), &[], None, None);
472        state.seed_literals(literals);
473
474        let cfg = proptest::test_runner::Config { failure_persistence: None, ..Default::default() };
475        let mut runner = proptest::test_runner::TestRunner::new(cfg);
476
477        // Verify strategies generates the seeded AST literals
478        let mut generated_bytes = HashSet::new();
479        let mut generated_hashes = HashSet::new();
480        let mut generated_strings = HashSet::new();
481        let bytes_strategy = fuzz_param_from_state(&DynSolType::Bytes, &state);
482        let string_strategy = fuzz_param_from_state(&DynSolType::String, &state);
483        let bytes32_strategy = fuzz_param_from_state(&DynSolType::FixedBytes(32), &state);
484
485        for _ in 0..256 {
486            let tree = bytes_strategy.new_tree(&mut runner).unwrap();
487            if let Some(bytes) = tree.current().as_bytes()
488                && let Ok(s) = std::str::from_utf8(bytes)
489            {
490                generated_bytes.insert(s.to_string());
491            }
492
493            let tree = string_strategy.new_tree(&mut runner).unwrap();
494            if let Some(s) = tree.current().as_str() {
495                generated_strings.insert(s.to_string());
496            }
497
498            let tree = bytes32_strategy.new_tree(&mut runner).unwrap();
499            if let Some((bytes, size)) = tree.current().as_fixed_bytes()
500                && size == 32
501            {
502                generated_hashes.insert(B256::from_slice(bytes));
503            }
504        }
505
506        assert!(generated_bytes.contains("hello"));
507        assert!(generated_bytes.contains("world"));
508        assert!(generated_strings.contains("hello"));
509        assert!(generated_strings.contains("world"));
510        assert!(generated_hashes.contains(&keccak256("hello")));
511        assert!(generated_hashes.contains(&keccak256("world")));
512    }
513}