diff --git a/crates/evm/fuzz/src/strategies/invariants.rs b/crates/evm/fuzz/src/strategies/invariants.rs index c7d04dd1ab02..4dc47eeada95 100644 --- a/crates/evm/fuzz/src/strategies/invariants.rs +++ b/crates/evm/fuzz/src/strategies/invariants.rs @@ -1,7 +1,7 @@ use super::{fuzz_calldata, fuzz_param_from_state}; use crate::{ invariant::{BasicTxDetails, CallDetails, FuzzRunIdentifiedContracts, SenderFilters}, - strategies::{fuzz_calldata_from_state, fuzz_param, EvmFuzzState}, + strategies::{fuzz_calldata_from_state, fuzz_param, param::FuzzConfig, EvmFuzzState}, FuzzFixtures, }; use alloy_json_abi::Function; @@ -94,7 +94,7 @@ fn select_random_sender( } else { assert!(dictionary_weight <= 100, "dictionary_weight must be <= 100"); proptest::prop_oneof![ - 100 - dictionary_weight => fuzz_param(&alloy_dyn_abi::DynSolType::Address), + 100 - dictionary_weight => fuzz_param(&alloy_dyn_abi::DynSolType::Address, &FuzzConfig::new()), dictionary_weight => fuzz_param_from_state(&alloy_dyn_abi::DynSolType::Address, fuzz_state), ] .prop_map(move |addr| addr.as_address().unwrap()) diff --git a/crates/evm/fuzz/src/strategies/param.rs b/crates/evm/fuzz/src/strategies/param.rs index 0c08a7776466..dd81d2772ca3 100644 --- a/crates/evm/fuzz/src/strategies/param.rs +++ b/crates/evm/fuzz/src/strategies/param.rs @@ -1,3 +1,4 @@ +use std::collections::HashMap; use super::state::EvmFuzzState; use alloy_dyn_abi::{DynSolType, DynSolValue}; use alloy_primitives::{Address, B256, I256, U256}; @@ -6,11 +7,31 @@ use proptest::prelude::*; /// The max length of arrays we fuzz for is 256. const MAX_ARRAY_LEN: usize = 256; +/// Struct to hold range configuration +#[derive(Default, Clone)] +pub struct FuzzConfig { + ranges: HashMap +} + +impl FuzzConfig { + /// Initiates a new range configuration + pub fn new() -> Self { + Self { ranges: HashMap::new() } + } + + /// Adds a range + pub fn with_range(mut self, param_name: &str, min: U256, max: U256) -> Self { + self.ranges.insert(param_name.to_string(), (min, max)); + self + } +} + + /// Given a parameter type, returns a strategy for generating values for that type. /// /// See [`fuzz_param_with_fixtures`] for more information. -pub fn fuzz_param(param: &DynSolType) -> BoxedStrategy { - fuzz_param_inner(param, None) +pub fn fuzz_param(param: &DynSolType, config: &FuzzConfig) -> BoxedStrategy { + fuzz_param_inner(param,config, None) } /// Given a parameter type and configured fixtures for param name, returns a strategy for generating @@ -33,13 +54,16 @@ pub fn fuzz_param_with_fixtures( fixtures: Option<&[DynSolValue]>, name: &str, ) -> BoxedStrategy { - fuzz_param_inner(param, fixtures.map(|f| (f, name))) + fuzz_param_inner(param, &FuzzConfig::new(), fixtures.map(|f| (f, name))) } fn fuzz_param_inner( param: &DynSolType, + config: &FuzzConfig, mut fuzz_fixtures: Option<(&[DynSolValue], &str)>, ) -> BoxedStrategy { + let param_name = fuzz_fixtures.as_ref().map(|(_, name)| *name); + if let Some((fixtures, name)) = fuzz_fixtures { if !fixtures.iter().all(|f| f.matches(param)) { error!("fixtures for {name:?} do not match type {param}"); @@ -71,7 +95,15 @@ fn fuzz_param_inner( .prop_map(move |x| DynSolValue::Int(x, n)) .boxed(), DynSolType::Uint(n @ 8..=256) => { - super::UintStrategy::new(n, fuzz_fixtures, None, None, false) + let (min, max) = if let Some(name) = param_name { + config.ranges.get(name) + .map(|(min, max)| (Some(*min), Some(*max))) + .unwrap_or((None, None)) + } else { + (None, None) + }; + + super::UintStrategy::new(n, fuzz_fixtures, min, max, false) .prop_map(move |x| DynSolValue::Uint(x, n)) .boxed() } @@ -87,17 +119,17 @@ fn fuzz_param_inner( .boxed(), DynSolType::Tuple(ref params) => params .iter() - .map(|param| fuzz_param_inner(param, None)) + .map(|param| fuzz_param_inner(param,&FuzzConfig::new(), None)) .collect::>() .prop_map(DynSolValue::Tuple) .boxed(), DynSolType::FixedArray(ref param, size) => { - proptest::collection::vec(fuzz_param_inner(param, None), size) + proptest::collection::vec(fuzz_param_inner(param, &FuzzConfig::new(), None), size) .prop_map(DynSolValue::FixedArray) .boxed() } DynSolType::Array(ref param) => { - proptest::collection::vec(fuzz_param_inner(param, None), 0..MAX_ARRAY_LEN) + proptest::collection::vec(fuzz_param_inner(param, &FuzzConfig::new(), None), 0..MAX_ARRAY_LEN) .prop_map(DynSolValue::Array) .boxed() } @@ -210,10 +242,15 @@ mod tests { strategies::{fuzz_calldata, fuzz_calldata_from_state, EvmFuzzState}, FuzzFixtures, }; + use alloy_dyn_abi::{DynSolType, DynSolValue}; + use alloy_primitives::U256; use foundry_common::abi::get_func; use foundry_config::FuzzDictionaryConfig; + use proptest::{prelude::Strategy, test_runner::TestRunner}; use revm::db::{CacheDB, EmptyDB}; + use super::{fuzz_param_inner, FuzzConfig}; + #[test] fn can_fuzz_array() { let f = "testArray(uint64[2] calldata values)"; @@ -228,4 +265,156 @@ mod tests { let mut runner = proptest::test_runner::TestRunner::new(cfg); let _ = runner.run(&strategy, |_| Ok(())); } + + #[test] + fn test_uint_param_with_range() { + let mut config = FuzzConfig::new(); + let min = U256::from(100u64); + let max = U256::from(1000u64); + config = config.with_range("amount", min, max); + + let param = DynSolType::Uint(256); + let strategy = fuzz_param_inner(¶m, &config, Some((&[], "amount"))); + + let mut runner = TestRunner::default(); + for _ in 0..1000 { + let value = strategy.new_tree(&mut runner).unwrap().current(); + if let DynSolValue::Uint(value, _) = value { + assert!( + value >= min && value <= max, + "Generated value {} outside configured range [{}, {}]", + value, + min, + max + ); + } else { + panic!("Expected Uint value"); + } + } + } + + #[test] + fn test_uint_param_without_range() { + let config = FuzzConfig::new(); + let param = DynSolType::Uint(8); + let strategy = fuzz_param_inner(¶m, &config, None); + + let mut runner = TestRunner::default(); + for _ in 0..1000 { + let value = strategy.new_tree(&mut runner).unwrap().current(); + if let DynSolValue::Uint(value, bits) = value { + assert!( + value <= U256::from(u8::MAX), + "Generated value {} exceeds uint8 max", + value + ); + assert_eq!(bits, 8, "Incorrect bit size"); + } else { + panic!("Expected Uint value"); + } + } + } + + #[test] + fn test_uint_param_with_fixtures() { + let config = FuzzConfig::new(); + let fixtures = vec![ + DynSolValue::Uint(U256::from(500u64), 256), + DynSolValue::Uint(U256::from(600u64), 256), + ]; + + let param = DynSolType::Uint(256); + let strategy = fuzz_param_inner(¶m, &config, Some((&fixtures, "test"))); + + let mut runner = TestRunner::default(); + let mut found_fixture = false; + + for _ in 0..1000 { + let value = strategy.new_tree(&mut runner).unwrap().current(); + if let DynSolValue::Uint(value, _) = value { + if value == U256::from(500u64) || value == U256::from(600u64) { + found_fixture = true; + break; + } + } + } + assert!(found_fixture, "Never generated fixture value"); + } + + + #[test] + fn test_uint_param_with_range_and_fixtures() { + let mut config = FuzzConfig::new(); + let min = U256::from(100u64); + let max = U256::from(1000u64); + config = config.with_range("test", min, max); + + let fixtures = vec![ + DynSolValue::Uint(U256::from(50u64), 256), + DynSolValue::Uint(U256::from(500u64), 256), + DynSolValue::Uint(U256::from(1500u64), 256), + ]; + + let param = DynSolType::Uint(256); + let strategy = fuzz_param_inner(¶m, &config, Some((&fixtures, "test"))); + + let mut runner = TestRunner::default(); + for _ in 0..1000 { + let value = strategy.new_tree(&mut runner).unwrap().current(); + if let DynSolValue::Uint(value, _) = value { + assert!( + value >= min && value <= max, + "Generated value {} outside configured range [{}, {}]", + value, + min, + max + ); + } + } + } + + #[test] + fn test_param_range_matching() { + let mut config = FuzzConfig::new(); + config = config + .with_range("amount", U256::from(100u64), U256::from(1000u64)) + .with_range("other", U256::from(2000u64), U256::from(3000u64)); + + let param = DynSolType::Uint(256); + let mut runner = TestRunner::default(); + + let strategy1 = fuzz_param_inner(¶m, &config, Some((&[], "amount"))); + for _ in 0..100 { + let value = strategy1.new_tree(&mut runner).unwrap().current(); + match value { + DynSolValue::Uint(value, bits) => { + assert_eq!(bits, 256, "Incorrect bit size"); + assert!( + value >= U256::from(100u64) && value <= U256::from(1000u64), + "Generated value {} outside 'amount' range [100, 1000]", + value + ); + } + _ => panic!("Expected Uint value"), + } + } + + let strategy2 = fuzz_param_inner(¶m, &config, Some((&[], "nonexistent"))); + for _ in 0..100 { + let value = strategy2.new_tree(&mut runner).unwrap().current(); + match value { + DynSolValue::Uint(value, bits) => { + assert_eq!(bits, 256, "Incorrect bit size"); + assert!( + value <= (U256::from(1) << 256) - U256::from(1), + "Generated value {} exceeds maximum uint256 value", + value + ); + } + _ => panic!("Expected Uint value"), + } + } + } + + } diff --git a/crates/evm/fuzz/src/strategies/uint.rs b/crates/evm/fuzz/src/strategies/uint.rs index 57ee7f12ab43..c2dc16d33a2f 100644 --- a/crates/evm/fuzz/src/strategies/uint.rs +++ b/crates/evm/fuzz/src/strategies/uint.rs @@ -201,8 +201,12 @@ impl UintStrategy { self.generate_log_uniform(runner) } else if self.max_bound > self.min_bound { let range = self.max_bound - self.min_bound + U256::from(1); - let random = self.generate_random_values_uniformly(runner) % range; - self.min_bound + random + if range == U256::ZERO { + self.min_bound + } else { + let random = self.generate_random_values_uniformly(runner) % range; + self.min_bound + random + } } else { self.min_bound }; @@ -254,6 +258,7 @@ impl Strategy for UintStrategy { #[cfg(test)] mod tests { use crate::strategies::uint::UintValueTree; + use alloy_dyn_abi::DynSolValue; use alloy_primitives::U256; use proptest::{prelude::Strategy, strategy::ValueTree, test_runner::TestRunner}; @@ -281,4 +286,108 @@ mod tests { } } + #[test] + fn test_uint_value_tree_bounds() { + let min = U256::from(100u64); + let max = U256::from(200u64); + let start = U256::from(150u64); + + let mut tree = UintValueTree::new(start, false, min, max); + + assert_eq!(tree.current(), start); + + while tree.simplify() { + let curr = tree.current(); + assert!(curr >= min && curr <= max, + "Simplify produced out of bounds value: {}", curr); + } + + tree = UintValueTree::new(start, false, min, max); + + while tree.complicate() { + let curr = tree.current(); + assert!(curr >= min && curr <= max, + "Complicate produced out of bounds value: {}", curr); + } + } + + #[test] + fn test_edge_case_generation() { + let min = U256::from(100u64); + let max = U256::from(1000u64); + let strategy = UintStrategy::new(64, None, Some(min), Some(max), false); + let mut runner = TestRunner::default(); + + let mut found_min_area = false; + let mut found_max_area = false; + + for _ in 0..1000 { + let tree = strategy.generate_edge_tree(&mut runner).unwrap(); + let value = tree.current(); + + assert!(value >= min && value <= max, + "Edge case {} outside bounds [{}, {}]", value, min, max); + + if value <= min + U256::from(3) { + found_min_area = true; + } + if value >= max - U256::from(3) { + found_max_area = true; + } + } + + assert!(found_min_area, "Never generated values near minimum"); + assert!(found_max_area, "Never generated values near maximum"); + } + + + #[test] + fn test_fixture_generation() { + let min = U256::from(100u64); + let max = U256::from(1000u64); + let valid_fixture = U256::from(500u64); + let fixtures = vec![DynSolValue::Uint(valid_fixture, 64)]; + + let strategy = UintStrategy::new(64, Some(&fixtures), Some(min), Some(max), false); + let mut runner = TestRunner::default(); + + for _ in 0..100 { + let tree = strategy.generate_fixtures_tree(&mut runner).unwrap(); + let value = tree.current(); + assert!(value >= min && value <= max, + "Fixture value {} outside bounds [{}, {}]", value, min, max); + } + } + + #[test] + fn test_log_uniform_sampling() { + let strategy = UintStrategy::new(256, None, None, None, true); + let mut runner = TestRunner::default(); + let mut log2_buckets = vec![0; 256]; + let iterations = 100000; + + for _ in 0..iterations { + let tree = strategy.generate_random_tree(&mut runner).unwrap(); + let value = tree.current(); + + // Find the highest set bit (log2 bucket) + let mut highest_bit = 0; + for i in 0..256 { + if value >= (U256::from(1) << i) { + highest_bit = i; + } + } + log2_buckets[highest_bit] += 1; + } + + let mut populated_buckets = 0; + for &count in &log2_buckets { + if count > 0 { + populated_buckets += 1; + } + } + assert!(populated_buckets > 200, + "Log-uniform sampling didn't cover enough orders of magnitude"); + } + }