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
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 => {
174 let state = state.clone();
175 (proptest::bool::weighted(0.3), any::<prop::sample::Index>())
176 .prop_flat_map(move |(use_ast, select_index)| {
177 let dict = state.dictionary_read();
178
179 let ast_strings = dict.ast_strings();
181 if use_ast && !ast_strings.is_empty() {
182 let s = &ast_strings.as_slice()[select_index.index(ast_strings.len())];
183 return Just(DynSolValue::String(s.clone())).boxed();
184 }
185
186 DynSolValue::type_strategy(&DynSolType::String)
188 .prop_map(|value| {
189 DynSolValue::String(
190 value.as_str().unwrap().trim().trim_end_matches('\0').to_string(),
191 )
192 })
193 .boxed()
194 })
195 .boxed()
196 }
197 DynSolType::Bytes => {
198 let state_clone = state.clone();
199 (
200 value(),
201 proptest::bool::weighted(0.1),
202 proptest::bool::weighted(0.2),
203 any::<prop::sample::Index>(),
204 )
205 .prop_map(move |(word, use_ast_string, use_ast_bytes, select_index)| {
206 let dict = state_clone.dictionary_read();
207
208 let ast_strings = dict.ast_strings();
210 if use_ast_string && !ast_strings.is_empty() {
211 let s = &ast_strings.as_slice()[select_index.index(ast_strings.len())];
212 return DynSolValue::Bytes(s.as_bytes().to_vec());
213 }
214
215 let ast_bytes = dict.ast_bytes();
217 if use_ast_bytes && !ast_bytes.is_empty() {
218 let bytes = &ast_bytes.as_slice()[select_index.index(ast_bytes.len())];
219 return DynSolValue::Bytes(bytes.to_vec());
220 }
221
222 DynSolValue::Bytes(word.0.into())
224 })
225 .boxed()
226 }
227 DynSolType::Int(n @ 8..=256) => match n / 8 {
228 32 => value()
229 .prop_map(move |value| DynSolValue::Int(I256::from_raw(value.into()), 256))
230 .boxed(),
231 1..=31 => value()
232 .prop_map(move |value| {
233 let uint_n = U256::from_be_bytes(value.0) % U256::from(1).wrapping_shl(n);
235 let sign_bit = U256::from(1) << (n - 1);
237 let num = if uint_n >= sign_bit {
238 let modulus = U256::from(1) << n;
240 I256::from_raw(uint_n.wrapping_sub(modulus))
241 } else {
242 I256::from_raw(uint_n)
244 };
245
246 DynSolValue::Int(num, n)
247 })
248 .boxed(),
249 _ => unreachable!(),
250 },
251 DynSolType::Uint(n @ 8..=256) => match n / 8 {
252 32 => value()
253 .prop_map(move |value| DynSolValue::Uint(U256::from_be_bytes(value.0), 256))
254 .boxed(),
255 1..=31 => value()
256 .prop_map(move |value| {
257 let uint = U256::from_be_bytes(value.0) % U256::from(1).wrapping_shl(n);
258 DynSolValue::Uint(uint, n)
259 })
260 .boxed(),
261 _ => unreachable!(),
262 },
263 DynSolType::Tuple(ref params) => params
264 .iter()
265 .map(|p| fuzz_param_from_state(p, state))
266 .collect::<Vec<_>>()
267 .prop_map(DynSolValue::Tuple)
268 .boxed(),
269 DynSolType::FixedArray(ref param, size) => {
270 proptest::collection::vec(fuzz_param_from_state(param, state), size)
271 .prop_map(DynSolValue::FixedArray)
272 .boxed()
273 }
274 DynSolType::Array(ref param) => {
275 proptest::collection::vec(fuzz_param_from_state(param, state), 0..MAX_ARRAY_LEN)
276 .prop_map(DynSolValue::Array)
277 .boxed()
278 }
279 _ => panic!("unsupported fuzz param type: {param}"),
280 }
281}
282
283pub fn mutate_param_value(
285 param: &DynSolType,
286 value: DynSolValue,
287 test_runner: &mut TestRunner,
288 state: &EvmFuzzState,
289) -> DynSolValue {
290 let new_value = |param: &DynSolType, test_runner: &mut TestRunner| {
291 fuzz_param_from_state(param, state)
292 .new_tree(test_runner)
293 .expect("Could not generate case")
294 .current()
295 };
296
297 match value {
298 DynSolValue::Bool(val) => {
299 trace!(target: "mutator", "Bool flip {val}");
301 Some(DynSolValue::Bool(!val))
302 }
303 DynSolValue::Uint(val, size) => match test_runner.rng().random_range(0..=6) {
304 0 => U256::increment_decrement(val, size, test_runner),
305 1 => U256::flip_random_bit(val, size, test_runner),
306 2 => U256::mutate_interesting_byte(val, size, test_runner),
307 3 => U256::mutate_interesting_word(val, size, test_runner),
308 4 => U256::mutate_interesting_dword(val, size, test_runner),
309 5 => U256::mutate_with_gaussian_noise(val, size, test_runner),
310 6 => None,
311 _ => unreachable!(),
312 }
313 .map(|v| DynSolValue::Uint(v, size)),
314 DynSolValue::Int(val, size) => match test_runner.rng().random_range(0..=6) {
315 0 => I256::increment_decrement(val, size, test_runner),
316 1 => I256::flip_random_bit(val, size, test_runner),
317 2 => I256::mutate_interesting_byte(val, size, test_runner),
318 3 => I256::mutate_interesting_word(val, size, test_runner),
319 4 => I256::mutate_interesting_dword(val, size, test_runner),
320 5 => I256::mutate_with_gaussian_noise(val, size, test_runner),
321 6 => None,
322 _ => unreachable!(),
323 }
324 .map(|v| DynSolValue::Int(v, size)),
325 DynSolValue::Address(val) => match test_runner.rng().random_range(0..=4) {
326 0 => Address::flip_random_bit(val, 20, test_runner),
327 1 => Address::mutate_interesting_byte(val, 20, test_runner),
328 2 => Address::mutate_interesting_word(val, 20, test_runner),
329 3 => Address::mutate_interesting_dword(val, 20, test_runner),
330 4 => None,
331 _ => unreachable!(),
332 }
333 .map(DynSolValue::Address),
334 DynSolValue::Array(mut values) => {
335 if let DynSolType::Array(param_type) = param
336 && !values.is_empty()
337 {
338 match test_runner.rng().random_range(0..=2) {
339 0 => {
341 values.remove(test_runner.rng().random_range(0..values.len()));
342 }
343 1 => values.push(new_value(param_type, test_runner)),
345 2 => mutate_random_array_value(&mut values, param_type, test_runner, state),
347 _ => unreachable!(),
348 }
349 Some(DynSolValue::Array(values))
350 } else {
351 None
352 }
353 }
354 DynSolValue::FixedArray(mut values) => {
355 if let DynSolType::FixedArray(param_type, _size) = param
356 && !values.is_empty()
357 {
358 mutate_random_array_value(&mut values, param_type, test_runner, state);
359 Some(DynSolValue::FixedArray(values))
360 } else {
361 None
362 }
363 }
364 DynSolValue::FixedBytes(word, size) => match test_runner.rng().random_range(0..=4) {
365 0 => Word::flip_random_bit(word, size, test_runner),
366 1 => Word::mutate_interesting_byte(word, size, test_runner),
367 2 => Word::mutate_interesting_word(word, size, test_runner),
368 3 => Word::mutate_interesting_dword(word, size, test_runner),
369 4 => None,
370 _ => unreachable!(),
371 }
372 .map(|word| DynSolValue::FixedBytes(word, size)),
373 DynSolValue::CustomStruct { name, prop_names, tuple: mut values } => {
374 if let DynSolType::CustomStruct { name: _, prop_names: _, tuple: tuple_types }
375 | DynSolType::Tuple(tuple_types) = param
376 && !values.is_empty()
377 {
378 mutate_random_tuple_value(&mut values, tuple_types, test_runner, state);
380 Some(DynSolValue::CustomStruct { name, prop_names, tuple: values })
381 } else {
382 None
383 }
384 }
385 DynSolValue::Tuple(mut values) => {
386 if let DynSolType::Tuple(tuple_types) = param
387 && !values.is_empty()
388 {
389 mutate_random_tuple_value(&mut values, tuple_types, test_runner, state);
391 Some(DynSolValue::Tuple(values))
392 } else {
393 None
394 }
395 }
396 _ => None,
397 }
398 .unwrap_or_else(|| new_value(param, test_runner))
399}
400
401fn mutate_random_tuple_value(
403 tuple_values: &mut [DynSolValue],
404 tuple_types: &[DynSolType],
405 test_runner: &mut TestRunner,
406 state: &EvmFuzzState,
407) {
408 let id = test_runner.rng().random_range(0..tuple_values.len());
409 let param_type = &tuple_types[id];
410 let old_val = replace(&mut tuple_values[id], DynSolValue::Bool(false));
411 let new_val = mutate_param_value(param_type, old_val, test_runner, state);
412 tuple_values[id] = new_val;
413}
414
415fn mutate_random_array_value(
417 array_values: &mut [DynSolValue],
418 element_type: &DynSolType,
419 test_runner: &mut TestRunner,
420 state: &EvmFuzzState,
421) {
422 let elem = array_values.choose_mut(&mut test_runner.rng()).unwrap();
423 let old_val = replace(elem, DynSolValue::Bool(false));
424 let new_val = mutate_param_value(element_type, old_val, test_runner, state);
425 *elem = new_val;
426}
427
428#[cfg(test)]
429mod tests {
430 use crate::{
431 FuzzFixtures,
432 strategies::{EvmFuzzState, fuzz_calldata, fuzz_calldata_from_state},
433 };
434 use alloy_primitives::B256;
435 use foundry_common::abi::get_func;
436 use foundry_config::FuzzDictionaryConfig;
437 use revm::database::{CacheDB, EmptyDB};
438 use std::collections::HashSet;
439
440 #[test]
441 fn can_fuzz_array() {
442 let f = "testArray(uint64[2] calldata values)";
443 let func = get_func(f).unwrap();
444 let db = CacheDB::new(EmptyDB::default());
445 let state = EvmFuzzState::new(&db, FuzzDictionaryConfig::default(), &[], None, None);
446 let strategy = proptest::prop_oneof![
447 60 => fuzz_calldata(func.clone(), &FuzzFixtures::default()),
448 40 => fuzz_calldata_from_state(func, &state),
449 ];
450 let cfg = proptest::test_runner::Config { failure_persistence: None, ..Default::default() };
451 let mut runner = proptest::test_runner::TestRunner::new(cfg);
452 let _ = runner.run(&strategy, |_| Ok(()));
453 }
454
455 #[test]
456 fn can_fuzz_string_and_bytes_with_ast_literals_and_hashes() {
457 use super::fuzz_param_from_state;
458 use crate::strategies::LiteralMaps;
459 use alloy_dyn_abi::DynSolType;
460 use alloy_primitives::keccak256;
461 use proptest::strategy::Strategy;
462
463 let mut literals = LiteralMaps::default();
465 literals.strings.insert("hello".to_string());
466 literals.strings.insert("world".to_string());
467 literals.words.entry(DynSolType::FixedBytes(32)).or_default().insert(keccak256("hello"));
468 literals.words.entry(DynSolType::FixedBytes(32)).or_default().insert(keccak256("world"));
469
470 let db = CacheDB::new(EmptyDB::default());
471 let state = EvmFuzzState::new(&db, FuzzDictionaryConfig::default(), &[], None, None);
472 state.seed_literals(literals);
473
474 let cfg = proptest::test_runner::Config { failure_persistence: None, ..Default::default() };
475 let mut runner = proptest::test_runner::TestRunner::new(cfg);
476
477 let mut generated_bytes = HashSet::new();
479 let mut generated_hashes = HashSet::new();
480 let mut generated_strings = HashSet::new();
481 let bytes_strategy = fuzz_param_from_state(&DynSolType::Bytes, &state);
482 let string_strategy = fuzz_param_from_state(&DynSolType::String, &state);
483 let bytes32_strategy = fuzz_param_from_state(&DynSolType::FixedBytes(32), &state);
484
485 for _ in 0..256 {
486 let tree = bytes_strategy.new_tree(&mut runner).unwrap();
487 if let Some(bytes) = tree.current().as_bytes()
488 && let Ok(s) = std::str::from_utf8(bytes)
489 {
490 generated_bytes.insert(s.to_string());
491 }
492
493 let tree = string_strategy.new_tree(&mut runner).unwrap();
494 if let Some(s) = tree.current().as_str() {
495 generated_strings.insert(s.to_string());
496 }
497
498 let tree = bytes32_strategy.new_tree(&mut runner).unwrap();
499 if let Some((bytes, size)) = tree.current().as_fixed_bytes()
500 && size == 32
501 {
502 generated_hashes.insert(B256::from_slice(bytes));
503 }
504 }
505
506 assert!(generated_bytes.contains("hello"));
507 assert!(generated_bytes.contains("world"));
508 assert!(generated_strings.contains("hello"));
509 assert!(generated_strings.contains("world"));
510 assert!(generated_hashes.contains(&keccak256("hello")));
511 assert!(generated_hashes.contains(&keccak256("world")));
512 }
513}