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 => DynSolValue::type_strategy(param)
174            .prop_map(move |value| {
175                DynSolValue::String(
176                    value.as_str().unwrap().trim().trim_end_matches('\0').to_string(),
177                )
178            })
179            .boxed(),
180        DynSolType::Bytes => {
181            value().prop_map(move |value| DynSolValue::Bytes(value.0.into())).boxed()
182        }
183        DynSolType::Int(n @ 8..=256) => match n / 8 {
184            32 => value()
185                .prop_map(move |value| DynSolValue::Int(I256::from_raw(value.into()), 256))
186                .boxed(),
187            1..=31 => value()
188                .prop_map(move |value| {
189                    // Generate a uintN in the correct range, then shift it to the range of intN
190                    // by subtracting 2^(N-1)
191                    let uint = U256::from_be_bytes(value.0) % U256::from(1).wrapping_shl(n);
192                    let max_int_plus1 = U256::from(1).wrapping_shl(n - 1);
193                    let num = I256::from_raw(uint.wrapping_sub(max_int_plus1));
194                    DynSolValue::Int(num, n)
195                })
196                .boxed(),
197            _ => unreachable!(),
198        },
199        DynSolType::Uint(n @ 8..=256) => match n / 8 {
200            32 => value()
201                .prop_map(move |value| DynSolValue::Uint(U256::from_be_bytes(value.0), 256))
202                .boxed(),
203            1..=31 => value()
204                .prop_map(move |value| {
205                    let uint = U256::from_be_bytes(value.0) % U256::from(1).wrapping_shl(n);
206                    DynSolValue::Uint(uint, n)
207                })
208                .boxed(),
209            _ => unreachable!(),
210        },
211        DynSolType::Tuple(ref params) => params
212            .iter()
213            .map(|p| fuzz_param_from_state(p, state))
214            .collect::<Vec<_>>()
215            .prop_map(DynSolValue::Tuple)
216            .boxed(),
217        DynSolType::FixedArray(ref param, size) => {
218            proptest::collection::vec(fuzz_param_from_state(param, state), size)
219                .prop_map(DynSolValue::FixedArray)
220                .boxed()
221        }
222        DynSolType::Array(ref param) => {
223            proptest::collection::vec(fuzz_param_from_state(param, state), 0..MAX_ARRAY_LEN)
224                .prop_map(DynSolValue::Array)
225                .boxed()
226        }
227        _ => panic!("unsupported fuzz param type: {param}"),
228    }
229}
230
231/// Mutates the current value of the given parameter type and value.
232pub fn mutate_param_value(
233    param: &DynSolType,
234    value: DynSolValue,
235    test_runner: &mut TestRunner,
236    state: &EvmFuzzState,
237) -> DynSolValue {
238    let new_value = |param: &DynSolType, test_runner: &mut TestRunner| {
239        fuzz_param_from_state(param, state)
240            .new_tree(test_runner)
241            .expect("Could not generate case")
242            .current()
243    };
244
245    match value {
246        DynSolValue::Bool(val) => {
247            // flip boolean value
248            trace!(target: "mutator", "Bool flip {val}");
249            Some(DynSolValue::Bool(!val))
250        }
251        DynSolValue::Uint(val, size) => match test_runner.rng().random_range(0..=6) {
252            0 => U256::increment_decrement(val, size, test_runner),
253            1 => U256::flip_random_bit(val, size, test_runner),
254            2 => U256::mutate_interesting_byte(val, size, test_runner),
255            3 => U256::mutate_interesting_word(val, size, test_runner),
256            4 => U256::mutate_interesting_dword(val, size, test_runner),
257            5 => U256::mutate_with_gaussian_noise(val, size, test_runner),
258            6 => None,
259            _ => unreachable!(),
260        }
261        .map(|v| DynSolValue::Uint(v, size)),
262        DynSolValue::Int(val, size) => match test_runner.rng().random_range(0..=6) {
263            0 => I256::increment_decrement(val, size, test_runner),
264            1 => I256::flip_random_bit(val, size, test_runner),
265            2 => I256::mutate_interesting_byte(val, size, test_runner),
266            3 => I256::mutate_interesting_word(val, size, test_runner),
267            4 => I256::mutate_interesting_dword(val, size, test_runner),
268            5 => I256::mutate_with_gaussian_noise(val, size, test_runner),
269            6 => None,
270            _ => unreachable!(),
271        }
272        .map(|v| DynSolValue::Int(v, size)),
273        DynSolValue::Address(val) => match test_runner.rng().random_range(0..=4) {
274            0 => Address::flip_random_bit(val, 20, test_runner),
275            1 => Address::mutate_interesting_byte(val, 20, test_runner),
276            2 => Address::mutate_interesting_word(val, 20, test_runner),
277            3 => Address::mutate_interesting_dword(val, 20, test_runner),
278            4 => None,
279            _ => unreachable!(),
280        }
281        .map(DynSolValue::Address),
282        DynSolValue::Array(mut values) => {
283            if let DynSolType::Array(param_type) = param
284                && !values.is_empty()
285            {
286                match test_runner.rng().random_range(0..=2) {
287                    // Decrease array size by removing a random element.
288                    0 => {
289                        values.remove(test_runner.rng().random_range(0..values.len()));
290                    }
291                    // Increase array size.
292                    1 => values.push(new_value(param_type, test_runner)),
293                    // Mutate random array element.
294                    2 => mutate_random_array_value(&mut values, param_type, test_runner, state),
295                    _ => unreachable!(),
296                }
297                Some(DynSolValue::Array(values))
298            } else {
299                None
300            }
301        }
302        DynSolValue::FixedArray(mut values) => {
303            if let DynSolType::FixedArray(param_type, _size) = param
304                && !values.is_empty()
305            {
306                mutate_random_array_value(&mut values, param_type, test_runner, state);
307                Some(DynSolValue::FixedArray(values))
308            } else {
309                None
310            }
311        }
312        DynSolValue::FixedBytes(word, size) => match test_runner.rng().random_range(0..=4) {
313            0 => Word::flip_random_bit(word, size, test_runner),
314            1 => Word::mutate_interesting_byte(word, size, test_runner),
315            2 => Word::mutate_interesting_word(word, size, test_runner),
316            3 => Word::mutate_interesting_dword(word, size, test_runner),
317            4 => None,
318            _ => unreachable!(),
319        }
320        .map(|word| DynSolValue::FixedBytes(word, size)),
321        DynSolValue::CustomStruct { name, prop_names, tuple: mut values } => {
322            if let DynSolType::CustomStruct { name: _, prop_names: _, tuple: tuple_types }
323            | DynSolType::Tuple(tuple_types) = param
324                && !values.is_empty()
325            {
326                // Mutate random struct element.
327                mutate_random_tuple_value(&mut values, tuple_types, test_runner, state);
328                Some(DynSolValue::CustomStruct { name, prop_names, tuple: values })
329            } else {
330                None
331            }
332        }
333        DynSolValue::Tuple(mut values) => {
334            if let DynSolType::Tuple(tuple_types) = param
335                && !values.is_empty()
336            {
337                // Mutate random tuple element.
338                mutate_random_tuple_value(&mut values, tuple_types, test_runner, state);
339                Some(DynSolValue::Tuple(values))
340            } else {
341                None
342            }
343        }
344        _ => None,
345    }
346    .unwrap_or_else(|| new_value(param, test_runner))
347}
348
349/// Mutates random value from given tuples.
350fn mutate_random_tuple_value(
351    tuple_values: &mut [DynSolValue],
352    tuple_types: &[DynSolType],
353    test_runner: &mut TestRunner,
354    state: &EvmFuzzState,
355) {
356    let id = test_runner.rng().random_range(0..tuple_values.len());
357    let param_type = &tuple_types[id];
358    let old_val = replace(&mut tuple_values[id], DynSolValue::Bool(false));
359    let new_val = mutate_param_value(param_type, old_val, test_runner, state);
360    tuple_values[id] = new_val;
361}
362
363/// Mutates random value from given array.
364fn mutate_random_array_value(
365    array_values: &mut [DynSolValue],
366    element_type: &DynSolType,
367    test_runner: &mut TestRunner,
368    state: &EvmFuzzState,
369) {
370    let elem = array_values.choose_mut(&mut test_runner.rng()).unwrap();
371    let old_val = replace(elem, DynSolValue::Bool(false));
372    let new_val = mutate_param_value(element_type, old_val, test_runner, state);
373    *elem = new_val;
374}
375
376#[cfg(test)]
377mod tests {
378    use crate::{
379        FuzzFixtures,
380        strategies::{EvmFuzzState, fuzz_calldata, fuzz_calldata_from_state},
381    };
382    use foundry_common::abi::get_func;
383    use foundry_config::FuzzDictionaryConfig;
384    use revm::database::{CacheDB, EmptyDB};
385
386    #[test]
387    fn can_fuzz_array() {
388        let f = "testArray(uint64[2] calldata values)";
389        let func = get_func(f).unwrap();
390        let db = CacheDB::new(EmptyDB::default());
391        let state = EvmFuzzState::new(&db, FuzzDictionaryConfig::default(), &[]);
392        let strategy = proptest::prop_oneof![
393            60 => fuzz_calldata(func.clone(), &FuzzFixtures::default()),
394            40 => fuzz_calldata_from_state(func, &state),
395        ];
396        let cfg = proptest::test_runner::Config { failure_persistence: None, ..Default::default() };
397        let mut runner = proptest::test_runner::TestRunner::new(cfg);
398        let _ = runner.run(&strategy, |_| Ok(()));
399    }
400}