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                        let mut rng = StdRng::seed_from_u64(0x1337); // use deterministic rng
140
141                        // Do not use addresses of deployed libraries as fuzz input, instead return
142                        // a deterministically random address. We cannot filter out this value (via
143                        // `prop_filter_map`) as proptest can invoke this closure after test
144                        // execution, and returning a `None` will cause it to panic.
145                        // See <https://github.com/foundry-rs/foundry/issues/9764> and <https://github.com/foundry-rs/foundry/issues/8639>.
146                        loop {
147                            fuzzed_addr.randomize_with(&mut rng);
148                            if !deployed_libs.contains(&fuzzed_addr) {
149                                break;
150                            }
151                        }
152                    }
153                    DynSolValue::Address(fuzzed_addr)
154                })
155                .boxed()
156        }
157        DynSolType::Function => value()
158            .prop_map(move |value| {
159                DynSolValue::Function(alloy_primitives::Function::from_word(value))
160            })
161            .boxed(),
162        DynSolType::FixedBytes(size @ 1..=32) => value()
163            .prop_map(move |mut v| {
164                v[size..].fill(0);
165                DynSolValue::FixedBytes(B256::from(v), size)
166            })
167            .boxed(),
168        DynSolType::Bool => DynSolValue::type_strategy(param).boxed(),
169        DynSolType::String => DynSolValue::type_strategy(param)
170            .prop_map(move |value| {
171                DynSolValue::String(
172                    value.as_str().unwrap().trim().trim_end_matches('\0').to_string(),
173                )
174            })
175            .boxed(),
176        DynSolType::Bytes => {
177            value().prop_map(move |value| DynSolValue::Bytes(value.0.into())).boxed()
178        }
179        DynSolType::Int(n @ 8..=256) => match n / 8 {
180            32 => value()
181                .prop_map(move |value| DynSolValue::Int(I256::from_raw(value.into()), 256))
182                .boxed(),
183            1..=31 => value()
184                .prop_map(move |value| {
185                    // Generate a uintN in the correct range, then shift it to the range of intN
186                    // by subtracting 2^(N-1)
187                    let uint = U256::from_be_bytes(value.0) % U256::from(1).wrapping_shl(n);
188                    let max_int_plus1 = U256::from(1).wrapping_shl(n - 1);
189                    let num = I256::from_raw(uint.wrapping_sub(max_int_plus1));
190                    DynSolValue::Int(num, n)
191                })
192                .boxed(),
193            _ => unreachable!(),
194        },
195        DynSolType::Uint(n @ 8..=256) => match n / 8 {
196            32 => value()
197                .prop_map(move |value| DynSolValue::Uint(U256::from_be_bytes(value.0), 256))
198                .boxed(),
199            1..=31 => value()
200                .prop_map(move |value| {
201                    let uint = U256::from_be_bytes(value.0) % U256::from(1).wrapping_shl(n);
202                    DynSolValue::Uint(uint, n)
203                })
204                .boxed(),
205            _ => unreachable!(),
206        },
207        DynSolType::Tuple(ref params) => params
208            .iter()
209            .map(|p| fuzz_param_from_state(p, state))
210            .collect::<Vec<_>>()
211            .prop_map(DynSolValue::Tuple)
212            .boxed(),
213        DynSolType::FixedArray(ref param, size) => {
214            proptest::collection::vec(fuzz_param_from_state(param, state), size)
215                .prop_map(DynSolValue::FixedArray)
216                .boxed()
217        }
218        DynSolType::Array(ref param) => {
219            proptest::collection::vec(fuzz_param_from_state(param, state), 0..MAX_ARRAY_LEN)
220                .prop_map(DynSolValue::Array)
221                .boxed()
222        }
223        _ => panic!("unsupported fuzz param type: {param}"),
224    }
225}
226
227#[cfg(test)]
228mod tests {
229    use crate::{
230        strategies::{fuzz_calldata, fuzz_calldata_from_state, EvmFuzzState},
231        FuzzFixtures,
232    };
233    use foundry_common::abi::get_func;
234    use foundry_config::FuzzDictionaryConfig;
235    use revm::database::{CacheDB, EmptyDB};
236
237    #[test]
238    fn can_fuzz_array() {
239        let f = "testArray(uint64[2] calldata values)";
240        let func = get_func(f).unwrap();
241        let db = CacheDB::new(EmptyDB::default());
242        let state = EvmFuzzState::new(&db, FuzzDictionaryConfig::default(), &[]);
243        let strategy = proptest::prop_oneof![
244            60 => fuzz_calldata(func.clone(), &FuzzFixtures::default()),
245            40 => fuzz_calldata_from_state(func, &state),
246        ];
247        let cfg = proptest::test_runner::Config { failure_persistence: None, ..Default::default() };
248        let mut runner = proptest::test_runner::TestRunner::new(cfg);
249        let _ = runner.run(&strategy, |_| Ok(()));
250    }
251}