Skip to main content

foundry_evm_fuzz/strategies/
param.rs

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