foundry_evm_fuzz/strategies/
param.rs

1use super::state::EvmFuzzState;
2use alloy_dyn_abi::{DynSolType, DynSolValue};
3use alloy_primitives::{Address, B256, I256, U256};
4use proptest::prelude::*;
5use rand::{rngs::StdRng, SeedableRng};
6
7/// The max length of arrays we fuzz for is 256.
8const MAX_ARRAY_LEN: usize = 256;
9
10/// Given a parameter type, returns a strategy for generating values for that type.
11///
12/// See [`fuzz_param_with_fixtures`] for more information.
13pub fn fuzz_param(param: &DynSolType) -> BoxedStrategy<DynSolValue> {
14    fuzz_param_inner(param, None)
15}
16
17/// Given a parameter type and configured fixtures for param name, returns a strategy for generating
18/// values for that type.
19///
20/// Fixtures can be currently generated for uint, int, address, bytes and
21/// string types and are defined for parameter name.
22/// For example, fixtures for parameter `owner` of type `address` can be defined in a function with
23/// a `function fixture_owner() public returns (address[] memory)` signature.
24///
25/// Fixtures are matched on parameter name, hence fixtures defined in
26/// `fixture_owner` function can be used in a fuzzed test function with a signature like
27/// `function testFuzz_ownerAddress(address owner, uint amount)`.
28///
29/// Raises an error if all the fixture types are not of the same type as the input parameter.
30///
31/// Works with ABI Encoder v2 tuples.
32pub fn fuzz_param_with_fixtures(
33    param: &DynSolType,
34    fixtures: Option<&[DynSolValue]>,
35    name: &str,
36) -> BoxedStrategy<DynSolValue> {
37    fuzz_param_inner(param, fixtures.map(|f| (f, name)))
38}
39
40fn fuzz_param_inner(
41    param: &DynSolType,
42    mut fuzz_fixtures: Option<(&[DynSolValue], &str)>,
43) -> BoxedStrategy<DynSolValue> {
44    if let Some((fixtures, name)) = fuzz_fixtures {
45        if !fixtures.iter().all(|f| f.matches(param)) {
46            error!("fixtures for {name:?} do not match type {param}");
47            fuzz_fixtures = None;
48        }
49    }
50    let fuzz_fixtures = fuzz_fixtures.map(|(f, _)| f);
51
52    let value = || {
53        let default_strategy = DynSolValue::type_strategy(param);
54        if let Some(fixtures) = fuzz_fixtures {
55            proptest::prop_oneof![
56                50 => {
57                    let fixtures = fixtures.to_vec();
58                    any::<prop::sample::Index>()
59                        .prop_map(move |index| index.get(&fixtures).clone())
60                },
61                50 => default_strategy,
62            ]
63            .boxed()
64        } else {
65            default_strategy.boxed()
66        }
67    };
68
69    match *param {
70        DynSolType::Address => value(),
71        DynSolType::Int(n @ 8..=256) => super::IntStrategy::new(n, fuzz_fixtures)
72            .prop_map(move |x| DynSolValue::Int(x, n))
73            .boxed(),
74        DynSolType::Uint(n @ 8..=256) => super::UintStrategy::new(n, fuzz_fixtures)
75            .prop_map(move |x| DynSolValue::Uint(x, n))
76            .boxed(),
77        DynSolType::Function | DynSolType::Bool => DynSolValue::type_strategy(param).boxed(),
78        DynSolType::Bytes => value(),
79        DynSolType::FixedBytes(_size @ 1..=32) => value(),
80        DynSolType::String => value()
81            .prop_map(move |value| {
82                DynSolValue::String(
83                    value.as_str().unwrap().trim().trim_end_matches('\0').to_string(),
84                )
85            })
86            .boxed(),
87        DynSolType::Tuple(ref params) => params
88            .iter()
89            .map(|param| fuzz_param_inner(param, None))
90            .collect::<Vec<_>>()
91            .prop_map(DynSolValue::Tuple)
92            .boxed(),
93        DynSolType::FixedArray(ref param, size) => {
94            proptest::collection::vec(fuzz_param_inner(param, None), size)
95                .prop_map(DynSolValue::FixedArray)
96                .boxed()
97        }
98        DynSolType::Array(ref param) => {
99            proptest::collection::vec(fuzz_param_inner(param, None), 0..MAX_ARRAY_LEN)
100                .prop_map(DynSolValue::Array)
101                .boxed()
102        }
103        _ => panic!("unsupported fuzz param type: {param}"),
104    }
105}
106
107/// Given a parameter type, returns a strategy for generating values for that type, given some EVM
108/// fuzz state.
109///
110/// Works with ABI Encoder v2 tuples.
111pub fn fuzz_param_from_state(
112    param: &DynSolType,
113    state: &EvmFuzzState,
114) -> BoxedStrategy<DynSolValue> {
115    // Value strategy that uses the state.
116    let value = || {
117        let state = state.clone();
118        let param = param.clone();
119        // Generate a bias and use it to pick samples or non-persistent values (50 / 50).
120        // Use `Index` instead of `Selector` when selecting a value to avoid iterating over the
121        // entire dictionary.
122        any::<(bool, prop::sample::Index)>().prop_map(move |(bias, index)| {
123            let state = state.dictionary_read();
124            let values = if bias { state.samples(&param) } else { None }
125                .unwrap_or_else(|| state.values())
126                .as_slice();
127            values[index.index(values.len())]
128        })
129    };
130
131    // Convert the value based on the parameter type
132    match *param {
133        DynSolType::Address => {
134            let deployed_libs = state.deployed_libs.clone();
135            value()
136                .prop_map(move |value| {
137                    let mut fuzzed_addr = Address::from_word(value);
138                    if !deployed_libs.contains(&fuzzed_addr) {
139                        DynSolValue::Address(fuzzed_addr)
140                    } else {
141                        let mut rng = StdRng::seed_from_u64(0x1337); // use deterministic rng
142
143                        // Do not use addresses of deployed libraries as fuzz input, instead return
144                        // a deterministically random address. We cannot filter out this value (via
145                        // `prop_filter_map`) as proptest can invoke this closure after test
146                        // execution, and returning a `None` will cause it to panic.
147                        // See <https://github.com/foundry-rs/foundry/issues/9764> and <https://github.com/foundry-rs/foundry/issues/8639>.
148                        loop {
149                            fuzzed_addr.randomize_with(&mut rng);
150                            if !deployed_libs.contains(&fuzzed_addr) {
151                                break;
152                            }
153                        }
154
155                        DynSolValue::Address(fuzzed_addr)
156                    }
157                })
158                .boxed()
159        }
160        DynSolType::Function => value()
161            .prop_map(move |value| {
162                DynSolValue::Function(alloy_primitives::Function::from_word(value))
163            })
164            .boxed(),
165        DynSolType::FixedBytes(size @ 1..=32) => value()
166            .prop_map(move |mut v| {
167                v[size..].fill(0);
168                DynSolValue::FixedBytes(B256::from(v), size)
169            })
170            .boxed(),
171        DynSolType::Bool => DynSolValue::type_strategy(param).boxed(),
172        DynSolType::String => DynSolValue::type_strategy(param)
173            .prop_map(move |value| {
174                DynSolValue::String(
175                    value.as_str().unwrap().trim().trim_end_matches('\0').to_string(),
176                )
177            })
178            .boxed(),
179        DynSolType::Bytes => {
180            value().prop_map(move |value| DynSolValue::Bytes(value.0.into())).boxed()
181        }
182        DynSolType::Int(n @ 8..=256) => match n / 8 {
183            32 => value()
184                .prop_map(move |value| DynSolValue::Int(I256::from_raw(value.into()), 256))
185                .boxed(),
186            1..=31 => value()
187                .prop_map(move |value| {
188                    // Generate a uintN in the correct range, then shift it to the range of intN
189                    // by subtracting 2^(N-1)
190                    let uint = U256::from_be_bytes(value.0) % U256::from(1).wrapping_shl(n);
191                    let max_int_plus1 = U256::from(1).wrapping_shl(n - 1);
192                    let num = I256::from_raw(uint.wrapping_sub(max_int_plus1));
193                    DynSolValue::Int(num, n)
194                })
195                .boxed(),
196            _ => unreachable!(),
197        },
198        DynSolType::Uint(n @ 8..=256) => match n / 8 {
199            32 => value()
200                .prop_map(move |value| DynSolValue::Uint(U256::from_be_bytes(value.0), 256))
201                .boxed(),
202            1..=31 => value()
203                .prop_map(move |value| {
204                    let uint = U256::from_be_bytes(value.0) % U256::from(1).wrapping_shl(n);
205                    DynSolValue::Uint(uint, n)
206                })
207                .boxed(),
208            _ => unreachable!(),
209        },
210        DynSolType::Tuple(ref params) => params
211            .iter()
212            .map(|p| fuzz_param_from_state(p, state))
213            .collect::<Vec<_>>()
214            .prop_map(DynSolValue::Tuple)
215            .boxed(),
216        DynSolType::FixedArray(ref param, size) => {
217            proptest::collection::vec(fuzz_param_from_state(param, state), size)
218                .prop_map(DynSolValue::FixedArray)
219                .boxed()
220        }
221        DynSolType::Array(ref param) => {
222            proptest::collection::vec(fuzz_param_from_state(param, state), 0..MAX_ARRAY_LEN)
223                .prop_map(DynSolValue::Array)
224                .boxed()
225        }
226        _ => panic!("unsupported fuzz param type: {param}"),
227    }
228}
229
230#[cfg(test)]
231mod tests {
232    use crate::{
233        strategies::{fuzz_calldata, fuzz_calldata_from_state, EvmFuzzState},
234        FuzzFixtures,
235    };
236    use foundry_common::abi::get_func;
237    use foundry_config::FuzzDictionaryConfig;
238    use revm::db::{CacheDB, EmptyDB};
239
240    #[test]
241    fn can_fuzz_array() {
242        let f = "testArray(uint64[2] calldata values)";
243        let func = get_func(f).unwrap();
244        let db = CacheDB::new(EmptyDB::default());
245        let state = EvmFuzzState::new(&db, FuzzDictionaryConfig::default(), &[]);
246        let strategy = proptest::prop_oneof![
247            60 => fuzz_calldata(func.clone(), &FuzzFixtures::default()),
248            40 => fuzz_calldata_from_state(func, &state),
249        ];
250        let cfg = proptest::test_runner::Config { failure_persistence: None, ..Default::default() };
251        let mut runner = proptest::test_runner::TestRunner::new(cfg);
252        let _ = runner.run(&strategy, |_| Ok(()));
253    }
254}