foundry_evm_fuzz/strategies/
param.rs1use 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
11const MAX_ARRAY_LEN: usize = 256;
13
14pub fn fuzz_param(param: &DynSolType) -> BoxedStrategy<DynSolValue> {
18 fuzz_param_inner(param, None)
19}
20
21pub 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
111pub fn fuzz_param_from_state(
116 param: &DynSolType,
117 state: &EvmFuzzState,
118) -> BoxedStrategy<DynSolValue> {
119 let value = || {
121 let state = state.clone();
122 let param = param.clone();
123 any::<(bool, prop::sample::Index)>().prop_map(move |(bias, index)| {
127 let state = state.dictionary_read();
128 let values = if bias { state.samples(¶m) } else { None }
129 .unwrap_or_else(|| state.values())
130 .as_slice();
131 values[index.index(values.len())]
132 })
133 };
134
135 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); 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 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
231pub 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 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 0 => {
289 values.remove(test_runner.rng().random_range(0..values.len()));
290 }
291 1 => values.push(new_value(param_type, test_runner)),
293 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_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_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
349fn 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
363fn 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}