diff --git a/crates/common/src/traits.rs b/crates/common/src/traits.rs index 4232fb946dc3..8ed1edbec304 100644 --- a/crates/common/src/traits.rs +++ b/crates/common/src/traits.rs @@ -33,6 +33,9 @@ pub trait TestFunctionExt { /// Returns whether this function is a `setUp` function. fn is_setup(&self) -> bool; + + /// Returns whether this function is a fixture function. + fn is_fixture(&self) -> bool; } impl TestFunctionExt for Function { @@ -56,6 +59,10 @@ impl TestFunctionExt for Function { fn is_setup(&self) -> bool { self.name.is_setup() } + + fn is_fixture(&self) -> bool { + self.name.is_fixture() + } } impl TestFunctionExt for String { @@ -78,6 +85,10 @@ impl TestFunctionExt for String { fn is_setup(&self) -> bool { self.as_str().is_setup() } + + fn is_fixture(&self) -> bool { + self.as_str().is_fixture() + } } impl TestFunctionExt for str { @@ -100,6 +111,10 @@ impl TestFunctionExt for str { fn is_setup(&self) -> bool { self.eq_ignore_ascii_case("setup") } + + fn is_fixture(&self) -> bool { + self.starts_with("fixture") + } } /// An extension trait for `std::error::Error` for ABI encoding. diff --git a/crates/config/src/fuzz.rs b/crates/config/src/fuzz.rs index 3b0e13bcd509..7049a401a5f3 100644 --- a/crates/config/src/fuzz.rs +++ b/crates/config/src/fuzz.rs @@ -111,10 +111,6 @@ pub struct FuzzDictionaryConfig { /// Once the fuzzer exceeds this limit, it will start evicting random entries #[serde(deserialize_with = "crate::deserialize_usize_or_max")] pub max_fuzz_dictionary_values: usize, - /// How many random addresses to use and to recycle when fuzzing calldata. - /// If not specified then `max_fuzz_dictionary_addresses` value applies. - #[serde(deserialize_with = "crate::deserialize_usize_or_max")] - pub max_calldata_fuzz_dictionary_addresses: usize, } impl Default for FuzzDictionaryConfig { @@ -127,7 +123,6 @@ impl Default for FuzzDictionaryConfig { max_fuzz_dictionary_addresses: (300 * 1024 * 1024) / 20, // limit this to 200MB max_fuzz_dictionary_values: (200 * 1024 * 1024) / 32, - max_calldata_fuzz_dictionary_addresses: 0, } } } diff --git a/crates/config/src/inline/conf_parser.rs b/crates/config/src/inline/conf_parser.rs index acad057ae0d7..1f6fca6c7ac5 100644 --- a/crates/config/src/inline/conf_parser.rs +++ b/crates/config/src/inline/conf_parser.rs @@ -149,7 +149,7 @@ mod tests { function: Default::default(), line: Default::default(), docs: r" - forge-config: ciii.invariant.depth = 1 + forge-config: ciii.invariant.depth = 1 forge-config: default.invariant.depth = 1 " .into(), @@ -167,7 +167,7 @@ mod tests { function: Default::default(), line: Default::default(), docs: r" - forge-config: ci.invariant.depth = 1 + forge-config: ci.invariant.depth = 1 forge-config: default.invariant.depth = 1 " .into(), diff --git a/crates/config/src/inline/mod.rs b/crates/config/src/inline/mod.rs index 9989d5b76c55..f2222901f906 100644 --- a/crates/config/src/inline/mod.rs +++ b/crates/config/src/inline/mod.rs @@ -37,7 +37,7 @@ impl InlineConfig { } /// Inserts an inline configuration, for a test function. - /// Configuration is identified by the pair "contract", "function". + /// Configuration is identified by the pair "contract", "function". pub fn insert(&mut self, contract_id: C, fn_name: F, config: T) where C: Into, diff --git a/crates/evm/evm/src/executors/fuzz/mod.rs b/crates/evm/evm/src/executors/fuzz/mod.rs index ae63d2386bae..8e3a85bddfaa 100644 --- a/crates/evm/evm/src/executors/fuzz/mod.rs +++ b/crates/evm/evm/src/executors/fuzz/mod.rs @@ -11,7 +11,7 @@ use foundry_evm_core::{ use foundry_evm_coverage::HitMaps; use foundry_evm_fuzz::{ strategies::{build_initial_state, fuzz_calldata, fuzz_calldata_from_state, EvmFuzzState}, - BaseCounterExample, CounterExample, FuzzCase, FuzzError, FuzzTestResult, + BaseCounterExample, CounterExample, FuzzCase, FuzzError, FuzzFixtures, FuzzTestResult, }; use foundry_evm_traces::CallTraceArena; use proptest::test_runner::{TestCaseError, TestError, TestRunner}; @@ -55,6 +55,7 @@ impl FuzzedExecutor { pub fn fuzz( &self, func: &Function, + fuzz_fixtures: &FuzzFixtures, address: Address, should_fail: bool, rd: &RevertDecoder, @@ -80,10 +81,12 @@ impl FuzzedExecutor { let state = self.build_fuzz_state(); let dictionary_weight = self.config.dictionary.dictionary_weight.min(100); + let strat = proptest::prop_oneof![ - 100 - dictionary_weight => fuzz_calldata(func.clone()), + 100 - dictionary_weight => fuzz_calldata(func.clone(), fuzz_fixtures), dictionary_weight => fuzz_calldata_from_state(func.clone(), &state), ]; + debug!(func=?func.name, should_fail, "fuzzing"); let run_result = self.runner.clone().run(&strat, |calldata| { let fuzz_res = self.single_fuzz(address, should_fail, calldata)?; diff --git a/crates/evm/evm/src/executors/invariant/mod.rs b/crates/evm/evm/src/executors/invariant/mod.rs index 2a7000ec1a78..b5669cb7dddd 100644 --- a/crates/evm/evm/src/executors/invariant/mod.rs +++ b/crates/evm/evm/src/executors/invariant/mod.rs @@ -18,14 +18,14 @@ use foundry_evm_fuzz::{ }, strategies::{ build_initial_state, collect_created_contracts, invariant_strat, override_call_strat, - CalldataFuzzDictionary, EvmFuzzState, + EvmFuzzState, }, - FuzzCase, FuzzedCases, + FuzzCase, FuzzFixtures, FuzzedCases, }; use foundry_evm_traces::CallTraceArena; use parking_lot::RwLock; use proptest::{ - strategy::{BoxedStrategy, Strategy, ValueTree}, + strategy::{BoxedStrategy, Strategy}, test_runner::{TestCaseError, TestRunner}, }; use revm::{primitives::HashMap, DatabaseCommit}; @@ -88,12 +88,8 @@ sol! { } /// Alias for (Dictionary for fuzzing, initial contracts to fuzz and an InvariantStrategy). -type InvariantPreparation = ( - EvmFuzzState, - FuzzRunIdentifiedContracts, - BoxedStrategy, - CalldataFuzzDictionary, -); +type InvariantPreparation = + (EvmFuzzState, FuzzRunIdentifiedContracts, BoxedStrategy); /// Enriched results of an invariant run check. /// @@ -152,14 +148,15 @@ impl<'a> InvariantExecutor<'a> { pub fn invariant_fuzz( &mut self, invariant_contract: InvariantContract<'_>, + fuzz_fixtures: &FuzzFixtures, ) -> Result { // Throw an error to abort test run if the invariant function accepts input params if !invariant_contract.invariant_function.inputs.is_empty() { return Err(eyre!("Invariant test function should have no inputs")) } - let (fuzz_state, targeted_contracts, strat, calldata_fuzz_dictionary) = - self.prepare_fuzzing(&invariant_contract)?; + let (fuzz_state, targeted_contracts, strat) = + self.prepare_fuzzing(&invariant_contract, fuzz_fixtures)?; // Stores the consumed gas and calldata of every successful fuzz call. let fuzz_cases: RefCell> = RefCell::new(Default::default()); @@ -329,7 +326,7 @@ impl<'a> InvariantExecutor<'a> { Ok(()) }); - trace!(target: "forge::test::invariant::calldata_address_fuzz_dictionary", "{:?}", calldata_fuzz_dictionary.inner.addresses); + trace!(target: "forge::test::invariant::fuzz_fixtures", "{:?}", fuzz_fixtures); trace!(target: "forge::test::invariant::dictionary", "{:?}", fuzz_state.dictionary_read().values().iter().map(hex::encode).collect::>()); let (reverts, error) = failures.into_inner().into_inner(); @@ -350,6 +347,7 @@ impl<'a> InvariantExecutor<'a> { fn prepare_fuzzing( &mut self, invariant_contract: &InvariantContract<'_>, + fuzz_fixtures: &FuzzFixtures, ) -> eyre::Result { // Finds out the chosen deployed contracts and/or senders. self.select_contract_artifacts(invariant_contract.address)?; @@ -360,16 +358,13 @@ impl<'a> InvariantExecutor<'a> { let fuzz_state: EvmFuzzState = build_initial_state(self.executor.backend.mem_db(), self.config.dictionary); - let calldata_fuzz_config = - CalldataFuzzDictionary::new(&self.config.dictionary, &fuzz_state); - // Creates the invariant strategy. let strat = invariant_strat( fuzz_state.clone(), targeted_senders, targeted_contracts.clone(), self.config.dictionary.dictionary_weight, - calldata_fuzz_config.clone(), + fuzz_fixtures.clone(), ) .no_shrink() .boxed(); @@ -387,7 +382,7 @@ impl<'a> InvariantExecutor<'a> { fuzz_state.clone(), targeted_contracts.clone(), target_contract_ref.clone(), - calldata_fuzz_config.clone(), + fuzz_fixtures.clone(), ), target_contract_ref, )); @@ -396,7 +391,7 @@ impl<'a> InvariantExecutor<'a> { self.executor.inspector.fuzzer = Some(Fuzzer { call_generator, fuzz_state: fuzz_state.clone(), collect: true }); - Ok((fuzz_state, targeted_contracts, strat, calldata_fuzz_config)) + Ok((fuzz_state, targeted_contracts, strat)) } /// Fills the `InvariantExecutor` with the artifact identifier filters (in `path:name` string diff --git a/crates/evm/fuzz/src/lib.rs b/crates/evm/fuzz/src/lib.rs index f8928ea3e13f..b2a058e5bb02 100644 --- a/crates/evm/fuzz/src/lib.rs +++ b/crates/evm/fuzz/src/lib.rs @@ -14,7 +14,7 @@ use foundry_evm_coverage::HitMaps; use foundry_evm_traces::CallTraceArena; use itertools::Itertools; use serde::{Deserialize, Serialize}; -use std::{collections::HashMap, fmt}; +use std::{collections::HashMap, fmt, sync::Arc}; pub use proptest::test_runner::{Config as FuzzConfig, Reason}; @@ -272,3 +272,42 @@ impl FuzzedCases { self.lowest().map(|c| c.gas).unwrap_or_default() } } + +/// Fixtures to be used for fuzz tests. +/// The key represents name of the fuzzed parameter, value holds possible fuzzed values. +/// For example, for a fixture function declared as +/// `function fixture_sender() external returns (address[] memory senders)` +/// the fuzz fixtures will contain `sender` key with `senders` array as value +#[derive(Clone, Default, Debug)] +pub struct FuzzFixtures { + inner: Arc>, +} + +impl FuzzFixtures { + pub fn new(fixtures: HashMap) -> FuzzFixtures { + Self { inner: Arc::new(fixtures) } + } + + /// Returns configured fixtures for `param_name` fuzzed parameter. + pub fn param_fixtures(&self, param_name: &str) -> Option<&[DynSolValue]> { + if let Some(param_fixtures) = self.inner.get(&normalize_fixture(param_name)) { + match param_fixtures { + DynSolValue::FixedArray(_) => param_fixtures.as_fixed_array(), + _ => param_fixtures.as_array(), + } + } else { + None + } + } +} + +/// Extracts fixture name from a function name. +/// For example: fixtures defined in `fixture_Owner` function will be applied for `owner` parameter. +pub fn fixture_name(function_name: String) -> String { + normalize_fixture(function_name.strip_prefix("fixture").unwrap()) +} + +/// Normalize fixture parameter name, for example `_Owner` to `owner`. +fn normalize_fixture(param_name: &str) -> String { + param_name.trim_matches(&['_']).to_ascii_lowercase() +} diff --git a/crates/evm/fuzz/src/strategies/calldata.rs b/crates/evm/fuzz/src/strategies/calldata.rs index ff3bb5713446..760d66175481 100644 --- a/crates/evm/fuzz/src/strategies/calldata.rs +++ b/crates/evm/fuzz/src/strategies/calldata.rs @@ -1,71 +1,26 @@ -use crate::strategies::{fuzz_param, EvmFuzzState}; +use crate::{ + strategies::{fuzz_param, fuzz_param_from_state, EvmFuzzState}, + FuzzFixtures, +}; use alloy_dyn_abi::JsonAbiExt; use alloy_json_abi::Function; -use alloy_primitives::{Address, Bytes}; -use foundry_config::FuzzDictionaryConfig; -use proptest::prelude::Strategy; -use std::{collections::HashSet, sync::Arc}; - -/// Clonable wrapper around [CalldataFuzzDictionary]. -#[derive(Clone, Debug)] -pub struct CalldataFuzzDictionary { - pub inner: Arc, -} - -impl CalldataFuzzDictionary { - pub fn new(config: &FuzzDictionaryConfig, state: &EvmFuzzState) -> Self { - Self { inner: Arc::new(CalldataFuzzDictionaryConfig::new(config, state)) } - } -} - -#[derive(Clone, Debug)] -pub struct CalldataFuzzDictionaryConfig { - /// Addresses that can be used for fuzzing calldata. - pub addresses: Vec
, -} - -/// Represents custom configuration for invariant fuzzed calldata strategies. -/// -/// At the moment only the dictionary of addresses to be used for a fuzzed `function(address)` can -/// be configured, but support for other types can be added. -impl CalldataFuzzDictionaryConfig { - /// Creates config with the set of addresses that can be used for fuzzing invariant calldata (if - /// `max_calldata_fuzz_dictionary_addresses` configured). - /// The set of addresses contains a number of `max_calldata_fuzz_dictionary_addresses` random - /// addresses plus all addresses that already had their PUSH bytes collected (retrieved from - /// `EvmFuzzState`, if `include_push_bytes` config enabled). - pub fn new(config: &FuzzDictionaryConfig, state: &EvmFuzzState) -> Self { - let mut addresses = HashSet::
::new(); - - let dict_size = config.max_calldata_fuzz_dictionary_addresses; - if dict_size > 0 { - addresses.extend(std::iter::repeat_with(Address::random).take(dict_size)); - // Add all addresses that already had their PUSH bytes collected. - addresses.extend(state.dictionary_read().addresses()); - } - - Self { addresses: addresses.into_iter().collect() } - } -} +use alloy_primitives::Bytes; +use proptest::prelude::{BoxedStrategy, Strategy}; /// Given a function, it returns a strategy which generates valid calldata -/// for that function's input types. -pub fn fuzz_calldata(func: Function) -> impl Strategy { - fuzz_calldata_with_config(func, None) -} - -/// Given a function, it returns a strategy which generates valid calldata -/// for that function's input types, following custom configuration rules. -pub fn fuzz_calldata_with_config( - func: Function, - config: Option<&CalldataFuzzDictionary>, -) -> impl Strategy { +/// for that function's input types, following declared test fixtures. +pub fn fuzz_calldata(func: Function, fuzz_fixtures: &FuzzFixtures) -> impl Strategy { // We need to compose all the strategies generated for each parameter in all - // possible combinations + // possible combinations, accounting any parameter declared fixture let strats = func .inputs .iter() - .map(|input| fuzz_param(&input.selector_type().parse().unwrap(), config)) + .map(|input| { + fuzz_param( + &input.selector_type().parse().unwrap(), + fuzz_fixtures.param_fixtures(&input.name), + ) + }) .collect::>(); strats.prop_map(move |values| { func.abi_encode_input(&values) @@ -78,3 +33,54 @@ pub fn fuzz_calldata_with_config( .into() }) } + +/// Given a function and some state, it returns a strategy which generated valid calldata for the +/// given function's input types, based on state taken from the EVM. +pub fn fuzz_calldata_from_state(func: Function, state: &EvmFuzzState) -> BoxedStrategy { + let strats = func + .inputs + .iter() + .map(|input| fuzz_param_from_state(&input.selector_type().parse().unwrap(), state)) + .collect::>(); + strats + .prop_map(move |values| { + func.abi_encode_input(&values) + .unwrap_or_else(|_| { + panic!( + "Fuzzer generated invalid arguments for function `{}` with inputs {:?}: {:?}", + func.name, func.inputs, values + ) + }) + .into() + }) + .no_shrink() + .boxed() +} + +#[cfg(test)] +mod tests { + use crate::{strategies::fuzz_calldata, FuzzFixtures}; + use alloy_dyn_abi::{DynSolValue, JsonAbiExt}; + use alloy_json_abi::Function; + use alloy_primitives::Address; + use proptest::prelude::Strategy; + use std::collections::HashMap; + + #[test] + fn can_fuzz_with_fixtures() { + let function = Function::parse("test_fuzzed_address(address addressFixture)").unwrap(); + + let address_fixture = DynSolValue::Address(Address::random()); + let mut fixtures = HashMap::new(); + fixtures.insert( + "addressFixture".to_string(), + DynSolValue::Array(vec![address_fixture.clone()]), + ); + + let expected = function.abi_encode_input(&[address_fixture]).unwrap(); + let strategy = fuzz_calldata(function, &FuzzFixtures::new(fixtures)); + let _ = strategy.prop_map(move |fuzzed| { + assert_eq!(expected, fuzzed); + }); + } +} diff --git a/crates/evm/fuzz/src/strategies/int.rs b/crates/evm/fuzz/src/strategies/int.rs index f772d97c0c1e..b27f62f2854d 100644 --- a/crates/evm/fuzz/src/strategies/int.rs +++ b/crates/evm/fuzz/src/strategies/int.rs @@ -1,3 +1,4 @@ +use alloy_dyn_abi::{DynSolType, DynSolValue}; use alloy_primitives::{Sign, I256, U256}; use proptest::{ strategy::{NewTree, Strategy, ValueTree}, @@ -83,12 +84,20 @@ impl ValueTree for IntValueTree { /// param). Then generate a value for this bit size. /// 2. Generate a random value around the edges (+/- 3 around min, 0 and max possible value) /// 3. Generate a value from a predefined fixtures set +/// +/// To define int fixtures: +/// - return an array of possible values for a parameter named `amount` declare a function +/// `function fixture_amount() public returns (int32[] memory)`. +/// - use `amount` named parameter in fuzzed test in order to include fixtures in fuzzed values +/// `function testFuzz_int32(int32 amount)`. +/// +/// If fixture is not a valid int type then error is raised and random value generated. #[derive(Debug)] pub struct IntStrategy { /// Bit size of int (e.g. 256) bits: usize, /// A set of fixtures to be generated - fixtures: Vec, + fixtures: Vec, /// The weight for edge cases (+/- 3 around 0 and max possible value) edge_weight: usize, /// The weight for fixtures @@ -102,10 +111,10 @@ impl IntStrategy { /// #Arguments /// * `bits` - Size of uint in bits /// * `fixtures` - A set of fixed values to be generated (according to fixtures weight) - pub fn new(bits: usize, fixtures: Vec) -> Self { + pub fn new(bits: usize, fixtures: Option<&[DynSolValue]>) -> Self { Self { bits, - fixtures, + fixtures: Vec::from(fixtures.unwrap_or_default()), edge_weight: 10usize, fixtures_weight: 40usize, random_weight: 50usize, @@ -132,12 +141,22 @@ impl IntStrategy { } fn generate_fixtures_tree(&self, runner: &mut TestRunner) -> NewTree { - // generate edge cases if there's no fixtures + // generate random cases if there's no fixtures if self.fixtures.is_empty() { - return self.generate_edge_tree(runner) + return self.generate_random_tree(runner) } - let idx = runner.rng().gen_range(0..self.fixtures.len()); - Ok(IntValueTree::new(self.fixtures[idx], false)) + + // Generate value tree from fixture. + let fixture = &self.fixtures[runner.rng().gen_range(0..self.fixtures.len())]; + if let Some(int_fixture) = fixture.as_int() { + if int_fixture.1 == self.bits { + return Ok(IntValueTree::new(int_fixture.0, false)); + } + } + + // If fixture is not a valid type, raise error and generate random value. + error!("{:?} is not a valid {} fixture", fixture, DynSolType::Int(self.bits)); + self.generate_random_tree(runner) } fn generate_random_tree(&self, runner: &mut TestRunner) -> NewTree { diff --git a/crates/evm/fuzz/src/strategies/invariants.rs b/crates/evm/fuzz/src/strategies/invariants.rs index 137e70852366..08e53b2a0bf5 100644 --- a/crates/evm/fuzz/src/strategies/invariants.rs +++ b/crates/evm/fuzz/src/strategies/invariants.rs @@ -1,7 +1,8 @@ -use super::{fuzz_calldata_with_config, fuzz_param_from_state, CalldataFuzzDictionary}; +use super::{fuzz_calldata, fuzz_param_from_state}; use crate::{ invariant::{BasicTxDetails, FuzzRunIdentifiedContracts, SenderFilters}, strategies::{fuzz_calldata_from_state, fuzz_param, EvmFuzzState}, + FuzzFixtures, }; use alloy_json_abi::{Function, JsonAbi}; use alloy_primitives::{Address, Bytes}; @@ -14,7 +15,7 @@ pub fn override_call_strat( fuzz_state: EvmFuzzState, contracts: FuzzRunIdentifiedContracts, target: Arc>, - calldata_fuzz_config: CalldataFuzzDictionary, + fuzz_fixtures: FuzzFixtures, ) -> SBoxedStrategy<(Address, Bytes)> { let contracts_ref = contracts.targets.clone(); proptest::prop_oneof![ @@ -24,7 +25,7 @@ pub fn override_call_strat( ] .prop_flat_map(move |target_address| { let fuzz_state = fuzz_state.clone(); - let calldata_fuzz_config = calldata_fuzz_config.clone(); + let fuzz_fixtures = fuzz_fixtures.clone(); let func = { let contracts = contracts.targets.lock(); @@ -40,7 +41,7 @@ pub fn override_call_strat( }; func.prop_flat_map(move |func| { - fuzz_contract_with_calldata(&fuzz_state, &calldata_fuzz_config, target_address, func) + fuzz_contract_with_calldata(&fuzz_state, &fuzz_fixtures, target_address, func) }) }) .sboxed() @@ -61,11 +62,11 @@ pub fn invariant_strat( senders: SenderFilters, contracts: FuzzRunIdentifiedContracts, dictionary_weight: u32, - calldata_fuzz_config: CalldataFuzzDictionary, + fuzz_fixtures: FuzzFixtures, ) -> impl Strategy { // We only want to seed the first value, since we want to generate the rest as we mutate the // state - generate_call(fuzz_state, senders, contracts, dictionary_weight, calldata_fuzz_config) + generate_call(fuzz_state, senders, contracts, dictionary_weight, fuzz_fixtures) } /// Strategy to generate a transaction where the `sender`, `target` and `calldata` are all generated @@ -75,7 +76,7 @@ fn generate_call( senders: SenderFilters, contracts: FuzzRunIdentifiedContracts, dictionary_weight: u32, - calldata_fuzz_config: CalldataFuzzDictionary, + fuzz_fixtures: FuzzFixtures, ) -> BoxedStrategy { let senders = Rc::new(senders); any::() @@ -92,11 +93,11 @@ fn generate_call( let senders = senders.clone(); let fuzz_state = fuzz_state.clone(); - let calldata_fuzz_config = calldata_fuzz_config.clone(); + let fuzz_fixtures = fuzz_fixtures.clone(); func.prop_flat_map(move |func| { let sender = select_random_sender(&fuzz_state, senders.clone(), dictionary_weight); let contract = - fuzz_contract_with_calldata(&fuzz_state, &calldata_fuzz_config, contract, func); + fuzz_contract_with_calldata(&fuzz_state, &fuzz_fixtures, contract, func); (sender, contract) }) }) @@ -164,7 +165,7 @@ fn select_random_function( /// for that function's input types. pub fn fuzz_contract_with_calldata( fuzz_state: &EvmFuzzState, - calldata_fuzz_config: &CalldataFuzzDictionary, + fuzz_fixtures: &FuzzFixtures, contract: Address, func: Function, ) -> impl Strategy { @@ -173,7 +174,7 @@ pub fn fuzz_contract_with_calldata( // `prop_oneof!` / `TupleUnion` `Arc`s for cheap cloning. #[allow(clippy::arc_with_non_send_sync)] prop_oneof![ - 60 => fuzz_calldata_with_config(func.clone(), Some(calldata_fuzz_config)), + 60 => fuzz_calldata(func.clone(), fuzz_fixtures), 40 => fuzz_calldata_from_state(func, fuzz_state), ] .prop_map(move |calldata| { diff --git a/crates/evm/fuzz/src/strategies/mod.rs b/crates/evm/fuzz/src/strategies/mod.rs index 0e82a4d4b8d6..74cefca2ab37 100644 --- a/crates/evm/fuzz/src/strategies/mod.rs +++ b/crates/evm/fuzz/src/strategies/mod.rs @@ -8,14 +8,38 @@ mod param; pub use param::{fuzz_param, fuzz_param_from_state}; mod calldata; -pub use calldata::{ - fuzz_calldata, fuzz_calldata_with_config, CalldataFuzzDictionary, CalldataFuzzDictionaryConfig, -}; +pub use calldata::{fuzz_calldata, fuzz_calldata_from_state}; mod state; -pub use state::{ - build_initial_state, collect_created_contracts, fuzz_calldata_from_state, EvmFuzzState, -}; +pub use state::{build_initial_state, collect_created_contracts, EvmFuzzState}; mod invariants; pub use invariants::{fuzz_contract_with_calldata, invariant_strat, override_call_strat}; + +/// Macro to create strategy with fixtures. +/// 1. A default strategy if no fixture defined for current parameter. +/// 2. A weighted strategy that use fixtures and default strategy values for current parameter. +/// If fixture is not of the same type as fuzzed parameter then value is rejected and error raised. +macro_rules! fixture_strategy { + ($fixtures:ident, $strategy_value:expr, $default_strategy:expr) => { + if let Some(fixtures) = $fixtures { + proptest::prop_oneof![ + 50 => { + let custom_fixtures: Vec = + fixtures.iter().enumerate().map(|(_, value)| value.to_owned()).collect(); + let custom_fixtures_len = custom_fixtures.len(); + any::() + .prop_filter_map("invalid fixture", move |index| { + let index = index.index(custom_fixtures_len); + $strategy_value(custom_fixtures.get(index)) + }) + }, + 50 => $default_strategy + ].boxed() + } else { + $default_strategy.boxed() + } + }; +} + +pub(crate) use fixture_strategy; diff --git a/crates/evm/fuzz/src/strategies/param.rs b/crates/evm/fuzz/src/strategies/param.rs index 20e69a27e725..12904cb00569 100644 --- a/crates/evm/fuzz/src/strategies/param.rs +++ b/crates/evm/fuzz/src/strategies/param.rs @@ -1,5 +1,5 @@ use super::state::EvmFuzzState; -use crate::strategies::calldata::CalldataFuzzDictionary; +use crate::strategies::fixture_strategy; use alloy_dyn_abi::{DynSolType, DynSolValue}; use alloy_primitives::{Address, B256, I256, U256}; use proptest::prelude::*; @@ -7,68 +7,105 @@ use proptest::prelude::*; /// The max length of arrays we fuzz for is 256. const MAX_ARRAY_LEN: usize = 256; -/// Given a parameter type, returns a strategy for generating values for that type. +/// Given a parameter type and configured fixtures for param name, returns a strategy for generating +/// values for that type. Fixtures can be currently generated for uint, int, address, bytes and +/// string types and are defined for parameter name. +/// +/// For example, fixtures for parameter `owner` of type `address` can be defined in a function with +/// a `function fixture_owner() public returns (address[] memory)` signature. +/// +/// Fixtures are matched on parameter name, hence fixtures defined in +/// `fixture_owner` function can be used in a fuzzed test function with a signature like +/// `function testFuzz_ownerAddress(address owner, uint amount)`. +/// +/// Fuzzer will reject value and raise error if the fixture type is not of the same type as +/// parameter to fuzz. /// /// Works with ABI Encoder v2 tuples. pub fn fuzz_param( param: &DynSolType, - config: Option<&CalldataFuzzDictionary>, + fuzz_fixtures: Option<&[DynSolValue]>, ) -> BoxedStrategy { match *param { DynSolType::Address => { - if let Some(config) = config { - let len = config.inner.addresses.len(); - if len > 0 { - let dict = config.inner.clone(); - // Create strategy to return random address from configured dictionary. - return any::() - .prop_map(move |index| { - let index = index.index(len); - DynSolValue::Address(dict.addresses[index]) - }) - .boxed(); - } - } - - // If no config for addresses dictionary then create unbounded addresses strategy. - any::
().prop_map(DynSolValue::Address).boxed() - } - DynSolType::Int(n @ 8..=256) => { - super::IntStrategy::new(n, vec![]).prop_map(move |x| DynSolValue::Int(x, n)).boxed() + fixture_strategy!( + fuzz_fixtures, + |fixture: Option<&DynSolValue>| { + if let Some(val @ DynSolValue::Address(_)) = fixture { + Some(val.clone()) + } else { + error!("{:?} is not a valid address fixture", fixture.unwrap()); + None + } + }, + DynSolValue::type_strategy(&DynSolType::Address) + ) } - DynSolType::Uint(n @ 8..=256) => { - super::UintStrategy::new(n, vec![]).prop_map(move |x| DynSolValue::Uint(x, n)).boxed() - } - DynSolType::Function | DynSolType::Bool | DynSolType::Bytes => { - DynSolValue::type_strategy(param).boxed() - } - DynSolType::FixedBytes(size @ 1..=32) => any::() - .prop_map(move |mut v| { - v[size..].fill(0); - DynSolValue::FixedBytes(v, size) - }) + DynSolType::Int(n @ 8..=256) => super::IntStrategy::new(n, fuzz_fixtures) + .prop_map(move |x| DynSolValue::Int(x, n)) .boxed(), - DynSolType::String => DynSolValue::type_strategy(param) - .prop_map(move |value| { + DynSolType::Uint(n @ 8..=256) => super::UintStrategy::new(n, fuzz_fixtures) + .prop_map(move |x| DynSolValue::Uint(x, n)) + .boxed(), + DynSolType::Function | DynSolType::Bool => DynSolValue::type_strategy(param).boxed(), + DynSolType::Bytes => { + fixture_strategy!( + fuzz_fixtures, + |fixture: Option<&DynSolValue>| { + if let Some(val @ DynSolValue::Bytes(_)) = fixture { + Some(val.clone()) + } else { + error!("{:?} is not a valid bytes fixture", fixture.unwrap()); + None + } + }, + DynSolValue::type_strategy(&DynSolType::Bytes) + ) + } + DynSolType::FixedBytes(size @ 1..=32) => fixture_strategy!( + fuzz_fixtures, + |fixture: Option<&DynSolValue>| { + if let Some(val @ DynSolValue::FixedBytes(_, _)) = fixture { + if let Some(val) = val.as_fixed_bytes() { + if val.1 == size { + return Some(DynSolValue::FixedBytes(B256::from_slice(val.0), val.1)) + } + } + } + error!("{:?} is not a valid fixed bytes fixture", fixture.unwrap()); + None + }, + DynSolValue::type_strategy(&DynSolType::FixedBytes(size)) + ), + DynSolType::String => fixture_strategy!( + fuzz_fixtures, + |fixture: Option<&DynSolValue>| { + if let Some(val @ DynSolValue::String(_)) = fixture { + Some(val.clone()) + } else { + error!("{:?} is not a valid string fixture", fixture.unwrap()); + None + } + }, + DynSolValue::type_strategy(&DynSolType::String).prop_map(move |value| { DynSolValue::String( value.as_str().unwrap().trim().trim_end_matches('\0').to_string(), ) }) - .boxed(), - + ), DynSolType::Tuple(ref params) => params .iter() - .map(|p| fuzz_param(p, config)) + .map(|p| fuzz_param(p, None)) .collect::>() .prop_map(DynSolValue::Tuple) .boxed(), DynSolType::FixedArray(ref param, size) => { - proptest::collection::vec(fuzz_param(param, config), size) + proptest::collection::vec(fuzz_param(param, None), size) .prop_map(DynSolValue::FixedArray) .boxed() } DynSolType::Array(ref param) => { - proptest::collection::vec(fuzz_param(param, config), 0..MAX_ARRAY_LEN) + proptest::collection::vec(fuzz_param(param, None), 0..MAX_ARRAY_LEN) .prop_map(DynSolValue::Array) .boxed() } @@ -174,7 +211,10 @@ pub fn fuzz_param_from_state( #[cfg(test)] mod tests { - use crate::strategies::{build_initial_state, fuzz_calldata, fuzz_calldata_from_state}; + use crate::{ + strategies::{build_initial_state, fuzz_calldata, fuzz_calldata_from_state}, + FuzzFixtures, + }; use foundry_common::abi::get_func; use foundry_config::FuzzDictionaryConfig; use revm::db::{CacheDB, EmptyDB}; @@ -186,7 +226,7 @@ mod tests { let db = CacheDB::new(EmptyDB::default()); let state = build_initial_state(&db, FuzzDictionaryConfig::default()); let strat = proptest::prop_oneof![ - 60 => fuzz_calldata(func.clone()), + 60 => fuzz_calldata(func.clone(), &FuzzFixtures::default()), 40 => fuzz_calldata_from_state(func, &state), ]; let cfg = proptest::test_runner::Config { failure_persistence: None, ..Default::default() }; diff --git a/crates/evm/fuzz/src/strategies/state.rs b/crates/evm/fuzz/src/strategies/state.rs index 2c5e98d9c39a..2ee3f7fca181 100644 --- a/crates/evm/fuzz/src/strategies/state.rs +++ b/crates/evm/fuzz/src/strategies/state.rs @@ -1,14 +1,10 @@ -use super::fuzz_param_from_state; use crate::invariant::{ArtifactFilters, FuzzRunIdentifiedContracts}; -use alloy_dyn_abi::JsonAbiExt; -use alloy_json_abi::Function; -use alloy_primitives::{Address, Bytes, Log, B256, U256}; +use alloy_primitives::{Address, Log, B256, U256}; use foundry_common::contracts::{ContractsByAddress, ContractsByArtifact}; use foundry_config::FuzzDictionaryConfig; use foundry_evm_core::utils::StateChangeset; use indexmap::IndexSet; use parking_lot::{lock_api::RwLockReadGuard, RawRwLock, RwLock}; -use proptest::prelude::{BoxedStrategy, Strategy}; use revm::{ db::{CacheDB, DatabaseRef}, interpreter::opcode::{self, spec_opcode_gas}, @@ -181,29 +177,6 @@ impl FuzzDictionary { } } -/// Given a function and some state, it returns a strategy which generated valid calldata for the -/// given function's input types, based on state taken from the EVM. -pub fn fuzz_calldata_from_state(func: Function, state: &EvmFuzzState) -> BoxedStrategy { - let strats = func - .inputs - .iter() - .map(|input| fuzz_param_from_state(&input.selector_type().parse().unwrap(), state)) - .collect::>(); - strats - .prop_map(move |values| { - func.abi_encode_input(&values) - .unwrap_or_else(|_| { - panic!( - "Fuzzer generated invalid arguments for function `{}` with inputs {:?}: {:?}", - func.name, func.inputs, values - ) - }) - .into() - }) - .no_shrink() - .boxed() -} - /// Builds the initial [EvmFuzzState] from a database. pub fn build_initial_state( db: &CacheDB, diff --git a/crates/evm/fuzz/src/strategies/uint.rs b/crates/evm/fuzz/src/strategies/uint.rs index 7b9aac1d49ed..1b1eb2540499 100644 --- a/crates/evm/fuzz/src/strategies/uint.rs +++ b/crates/evm/fuzz/src/strategies/uint.rs @@ -1,3 +1,4 @@ +use alloy_dyn_abi::{DynSolType, DynSolValue}; use alloy_primitives::U256; use proptest::{ strategy::{NewTree, Strategy, ValueTree}, @@ -71,12 +72,20 @@ impl ValueTree for UintValueTree { /// param). Then generate a value for this bit size. /// 2. Generate a random value around the edges (+/- 3 around 0 and max possible value) /// 3. Generate a value from a predefined fixtures set +/// +/// To define uint fixtures: +/// - return an array of possible values for a parameter named `amount` declare a function +/// `function fixture_amount() public returns (uint32[] memory)`. +/// - use `amount` named parameter in fuzzed test in order to include fixtures in fuzzed values +/// `function testFuzz_uint32(uint32 amount)`. +/// +/// If fixture is not a valid uint type then error is raised and random value generated. #[derive(Debug)] pub struct UintStrategy { /// Bit size of uint (e.g. 256) bits: usize, /// A set of fixtures to be generated - fixtures: Vec, + fixtures: Vec, /// The weight for edge cases (+/- 3 around 0 and max possible value) edge_weight: usize, /// The weight for fixtures @@ -90,10 +99,10 @@ impl UintStrategy { /// #Arguments /// * `bits` - Size of uint in bits /// * `fixtures` - A set of fixed values to be generated (according to fixtures weight) - pub fn new(bits: usize, fixtures: Vec) -> Self { + pub fn new(bits: usize, fixtures: Option<&[DynSolValue]>) -> Self { Self { bits, - fixtures, + fixtures: Vec::from(fixtures.unwrap_or_default()), edge_weight: 10usize, fixtures_weight: 40usize, random_weight: 50usize, @@ -105,19 +114,27 @@ impl UintStrategy { // Choose if we want values around 0 or max let is_min = rng.gen_bool(0.5); let offset = U256::from(rng.gen_range(0..4)); - let max = - if self.bits < 256 { (U256::from(1) << self.bits) - U256::from(1) } else { U256::MAX }; - let start = if is_min { offset } else { max.saturating_sub(offset) }; + let start = if is_min { offset } else { self.type_max().saturating_sub(offset) }; Ok(UintValueTree::new(start, false)) } fn generate_fixtures_tree(&self, runner: &mut TestRunner) -> NewTree { - // generate edge cases if there's no fixtures + // generate random cases if there's no fixtures if self.fixtures.is_empty() { - return self.generate_edge_tree(runner) + return self.generate_random_tree(runner) } - let idx = runner.rng().gen_range(0..self.fixtures.len()); - Ok(UintValueTree::new(self.fixtures[idx], false)) + + // Generate value tree from fixture. + let fixture = &self.fixtures[runner.rng().gen_range(0..self.fixtures.len())]; + if let Some(uint_fixture) = fixture.as_uint() { + if uint_fixture.1 == self.bits { + return Ok(UintValueTree::new(uint_fixture.0, false)); + } + } + + // If fixture is not a valid type, raise error and generate random value. + error!("{:?} is not a valid {} fixture", fixture, DynSolType::Uint(self.bits)); + self.generate_random_tree(runner) } fn generate_random_tree(&self, runner: &mut TestRunner) -> NewTree { @@ -151,6 +168,14 @@ impl UintStrategy { Ok(UintValueTree::new(start, false)) } + + fn type_max(&self) -> U256 { + if self.bits < 256 { + (U256::from(1) << self.bits) - U256::from(1) + } else { + U256::MAX + } + } } impl Strategy for UintStrategy { diff --git a/crates/forge/bin/cmd/test/mod.rs b/crates/forge/bin/cmd/test/mod.rs index 4a120adc7355..b5ec658a38dc 100644 --- a/crates/forge/bin/cmd/test/mod.rs +++ b/crates/forge/bin/cmd/test/mod.rs @@ -254,7 +254,7 @@ impl TestArgs { let profiles = get_available_profiles(toml)?; let test_options: TestOptions = TestOptionsBuilder::default() - .fuzz(config.clone().fuzz) + .fuzz(config.fuzz.clone()) .invariant(config.invariant) .profiles(profiles) .build(&output, project_root)?; diff --git a/crates/forge/src/result.rs b/crates/forge/src/result.rs index 09c3661dc719..acb5deb421e5 100644 --- a/crates/forge/src/result.rs +++ b/crates/forge/src/result.rs @@ -9,7 +9,7 @@ use foundry_evm::{ coverage::HitMaps, debug::DebugArena, executors::EvmError, - fuzz::{CounterExample, FuzzCase}, + fuzz::{CounterExample, FuzzCase, FuzzFixtures}, traces::{CallTraceArena, CallTraceDecoder, TraceKind, Traces}, }; use serde::{Deserialize, Serialize}; @@ -533,6 +533,8 @@ pub struct TestSetup { pub reason: Option, /// Coverage info during setup pub coverage: Option, + /// Defined fuzz test fixtures + pub fuzz_fixtures: FuzzFixtures, } impl TestSetup { @@ -565,8 +567,9 @@ impl TestSetup { traces: Traces, labeled_addresses: HashMap, coverage: Option, + fuzz_fixtures: FuzzFixtures, ) -> Self { - Self { address, logs, traces, labeled_addresses, reason: None, coverage } + Self { address, logs, traces, labeled_addresses, reason: None, coverage, fuzz_fixtures } } pub fn failed_with( @@ -582,6 +585,7 @@ impl TestSetup { labeled_addresses, reason: Some(reason), coverage: None, + fuzz_fixtures: FuzzFixtures::default(), } } diff --git a/crates/forge/src/runner.rs b/crates/forge/src/runner.rs index ad335e767480..ed48bdc34494 100644 --- a/crates/forge/src/runner.rs +++ b/crates/forge/src/runner.rs @@ -5,6 +5,7 @@ use crate::{ result::{SuiteResult, TestKind, TestResult, TestSetup, TestStatus}, TestFilter, TestOptions, }; +use alloy_dyn_abi::DynSolValue; use alloy_json_abi::Function; use alloy_primitives::{Address, U256}; use eyre::Result; @@ -20,9 +21,9 @@ use foundry_evm::{ executors::{ fuzz::{CaseOutcome, CounterExampleOutcome, FuzzOutcome, FuzzedExecutor}, invariant::{replay_run, InvariantExecutor, InvariantFuzzError, InvariantFuzzTestResult}, - EvmError, ExecutionErr, Executor, RawCallResult, + CallResult, EvmError, ExecutionErr, Executor, RawCallResult, }, - fuzz::{invariant::InvariantContract, CounterExample}, + fuzz::{fixture_name, invariant::InvariantContract, CounterExample, FuzzFixtures}, traces::{load_contracts, TraceKind}, }; use proptest::test_runner::TestRunner; @@ -166,14 +167,84 @@ impl<'a> ContractRunner<'a> { traces.extend(setup_traces.map(|traces| (TraceKind::Setup, traces))); logs.extend(setup_logs); - TestSetup { address, logs, traces, labeled_addresses, reason, coverage } + TestSetup { + address, + logs, + traces, + labeled_addresses, + reason, + coverage, + fuzz_fixtures: self.fuzz_fixtures(address), + } } else { - TestSetup::success(address, logs, traces, Default::default(), None) + TestSetup::success( + address, + logs, + traces, + Default::default(), + None, + self.fuzz_fixtures(address), + ) }; Ok(setup) } + /// Collect fixtures from test contract. + /// + /// Fixtures can be defined: + /// - as storage arrays in test contract, prefixed with `fixture` + /// - as functions prefixed with `fixture` and followed by parameter name to be + /// fuzzed + /// + /// Storage array fixtures: + /// `uint256[] public fixture_amount = [1, 2, 3];` + /// define an array of uint256 values to be used for fuzzing `amount` named parameter in scope + /// of the current test. + /// + /// Function fixtures: + /// `function fixture_owner() public returns (address[] memory){}` + /// returns an array of addresses to be used for fuzzing `owner` named parameter in scope of the + /// current test. + fn fuzz_fixtures(&mut self, address: Address) -> FuzzFixtures { + let mut fixtures = HashMap::new(); + self.contract.abi.functions().filter(|func| func.is_fixture()).for_each(|func| { + if func.inputs.is_empty() { + // Read fixtures declared as functions. + if let Ok(CallResult { raw: _, decoded_result }) = + self.executor.call(CALLER, address, func, &[], U256::ZERO, None) + { + fixtures.insert(fixture_name(func.name.clone()), decoded_result); + } + } else { + // For reading fixtures from storage arrays we collect values by calling the + // function with incremented indexes until there's an error. + let mut vals = Vec::new(); + let mut index = 0; + loop { + if let Ok(CallResult { raw: _, decoded_result }) = self.executor.call( + CALLER, + address, + func, + &[DynSolValue::Uint(U256::from(index), 256)], + U256::ZERO, + None, + ) { + vals.push(decoded_result); + } else { + // No result returned for this index, we reached the end of storage + // array or the function is not a valid fixture. + break; + } + index += 1; + } + fixtures.insert(fixture_name(func.name.clone()), DynSolValue::Array(vals)); + }; + }); + + FuzzFixtures::new(fixtures) + } + /// Runs all tests for a contract whose names match the provided regular expression pub fn run_tests( mut self, @@ -442,7 +513,8 @@ impl<'a> ContractRunner<'a> { identified_contracts: &ContractsByAddress, ) -> TestResult { trace!(target: "forge::test::fuzz", "executing invariant test for {:?}", func.name); - let TestSetup { address, logs, traces, labeled_addresses, coverage, .. } = setup; + let TestSetup { address, logs, traces, labeled_addresses, coverage, fuzz_fixtures, .. } = + setup; // First, run the test normally to see if it needs to be skipped. let start = Instant::now(); @@ -479,7 +551,7 @@ impl<'a> ContractRunner<'a> { InvariantContract { address, invariant_function: func, abi: &self.contract.abi }; let InvariantFuzzTestResult { error, cases, reverts, last_run_inputs, gas_report_traces } = - match evm.invariant_fuzz(invariant_contract.clone()) { + match evm.invariant_fuzz(invariant_contract.clone(), &fuzz_fixtures) { Ok(x) => x, Err(e) => { return TestResult { @@ -587,7 +659,13 @@ impl<'a> ContractRunner<'a> { let _guard = span.enter(); let TestSetup { - address, mut logs, mut traces, mut labeled_addresses, mut coverage, .. + address, + mut logs, + mut traces, + mut labeled_addresses, + mut coverage, + fuzz_fixtures, + .. } = setup; // Run fuzz test @@ -598,7 +676,8 @@ impl<'a> ContractRunner<'a> { self.sender, fuzz_config.clone(), ); - let result = fuzzed_executor.fuzz(func, address, should_fail, self.revert_decoder); + let result = + fuzzed_executor.fuzz(func, &fuzz_fixtures, address, should_fail, self.revert_decoder); let mut debug = Default::default(); let mut breakpoints = Default::default(); diff --git a/crates/forge/tests/it/invariant.rs b/crates/forge/tests/it/invariant.rs index efbd95d7103e..e03272ea80ca 100644 --- a/crates/forge/tests/it/invariant.rs +++ b/crates/forge/tests/it/invariant.rs @@ -139,7 +139,13 @@ async fn test_invariant() { ), ( "default/fuzz/invariant/common/InvariantCalldataDictionary.t.sol:InvariantCalldataDictionary", - vec![("invariant_owner_never_changes()", true, None, None, None)], + vec![( + "invariant_owner_never_changes()", + false, + Some("".into()), + None, + None, + )], ), ( "default/fuzz/invariant/common/InvariantAssume.t.sol:InvariantAssume", @@ -157,6 +163,16 @@ async fn test_invariant() { "default/fuzz/invariant/target/FuzzedTargetContracts.t.sol:DynamicTargetContract", vec![("invariant_dynamic_targets()", true, None, None, None)], ), + ( + "default/fuzz/invariant/common/InvariantFixtures.t.sol:InvariantFixtures", + vec![( + "invariant_target_not_compromised()", + false, + Some("".into()), + None, + None, + )], + ), ]), ); } @@ -348,30 +364,13 @@ async fn test_invariant_preserve_state() { } #[tokio::test(flavor = "multi_thread")] -async fn test_invariant_calldata_fuzz_dictionary_addresses() { - // should not fail with default options (address dict not finite) +async fn test_invariant_with_address_fixture() { let mut runner = TEST_DATA_DEFAULT.runner(); let results = runner.test_collect(&Filter::new( ".*", ".*", ".*fuzz/invariant/common/InvariantCalldataDictionary.t.sol", )); - assert_multiple( - &results, - BTreeMap::from([( - "default/fuzz/invariant/common/InvariantCalldataDictionary.t.sol:InvariantCalldataDictionary", - vec![("invariant_owner_never_changes()", true, None, None, None)], - )]), - ); - - // same test should fail when calldata address dict is bounded - // set address dictionary to single entry to fail fast - runner.test_options.invariant.dictionary.max_calldata_fuzz_dictionary_addresses = 1; - let results = runner.test_collect(&Filter::new( - ".*", - ".*", - ".*fuzz/invariant/common/InvariantCalldataDictionary.t.sol", - )); assert_multiple( &results, BTreeMap::from([( @@ -407,6 +406,8 @@ async fn test_invariant_assume_does_not_revert() { async fn test_invariant_assume_respects_restrictions() { let filter = Filter::new(".*", ".*", ".*fuzz/invariant/common/InvariantAssume.t.sol"); let mut runner = TEST_DATA_DEFAULT.runner(); + runner.test_options.invariant.runs = 1; + runner.test_options.invariant.depth = 10; runner.test_options.invariant.max_assume_rejects = 1; let results = runner.test_collect(&filter); assert_multiple( @@ -471,3 +472,25 @@ async fn test_invariant_fuzzed_selected_targets() { ]), ); } + +#[tokio::test(flavor = "multi_thread")] +async fn test_invariant_fixtures() { + let filter = Filter::new(".*", ".*", ".*fuzz/invariant/common/InvariantFixtures.t.sol"); + let mut runner = TEST_DATA_DEFAULT.runner(); + runner.test_options.invariant.runs = 1; + runner.test_options.invariant.depth = 100; + let results = runner.test_collect(&filter); + assert_multiple( + &results, + BTreeMap::from([( + "default/fuzz/invariant/common/InvariantFixtures.t.sol:InvariantFixtures", + vec![( + "invariant_target_not_compromised()", + false, + Some("".into()), + None, + None, + )], + )]), + ); +} diff --git a/crates/forge/tests/it/test_helpers.rs b/crates/forge/tests/it/test_helpers.rs index f65a3c116222..27f5749904ff 100644 --- a/crates/forge/tests/it/test_helpers.rs +++ b/crates/forge/tests/it/test_helpers.rs @@ -87,7 +87,6 @@ impl ForgeTestProfile { dictionary_weight: 40, max_fuzz_dictionary_addresses: 10_000, max_fuzz_dictionary_values: 10_000, - max_calldata_fuzz_dictionary_addresses: 0, }, gas_report_samples: 256, failure_persist_dir: Some(tempfile::tempdir().unwrap().into_path()), @@ -104,7 +103,6 @@ impl ForgeTestProfile { include_push_bytes: true, max_fuzz_dictionary_addresses: 10_000, max_fuzz_dictionary_values: 10_000, - max_calldata_fuzz_dictionary_addresses: 0, }, shrink_sequence: true, shrink_run_limit: 2usize.pow(18u32), diff --git a/testdata/default/fuzz/FuzzInt.t.sol b/testdata/default/fuzz/FuzzInt.t.sol index aac0825dbb5d..071727f6da92 100644 --- a/testdata/default/fuzz/FuzzInt.t.sol +++ b/testdata/default/fuzz/FuzzInt.t.sol @@ -3,7 +3,8 @@ pragma solidity 0.8.18; import "ds-test/test.sol"; -// See https://github.com/foundry-rs/foundry/pull/735 for context +// https://github.com/foundry-rs/foundry/pull/735 behavior changed with https://github.com/foundry-rs/foundry/issues/3521 +// random values (instead edge cases) are generated if no fixtures defined contract FuzzNumbersTest is DSTest { function testPositive(int256) public { assertTrue(true); @@ -14,31 +15,31 @@ contract FuzzNumbersTest is DSTest { } function testNegative0(int256 val) public { - assertTrue(val != 0); + assertTrue(val == 0); } function testNegative1(int256 val) public { - assertTrue(val != -1); + assertTrue(val == -1); } function testNegative2(int128 val) public { - assertTrue(val != 1); + assertTrue(val == 1); } function testNegativeMax0(int256 val) public { - assertTrue(val != type(int256).max); + assertTrue(val == type(int256).max); } function testNegativeMax1(int256 val) public { - assertTrue(val != type(int256).max - 2); + assertTrue(val == type(int256).max - 2); } function testNegativeMin0(int256 val) public { - assertTrue(val != type(int256).min); + assertTrue(val == type(int256).min); } function testNegativeMin1(int256 val) public { - assertTrue(val != type(int256).min + 2); + assertTrue(val == type(int256).min + 2); } function testEquality(int256 x, int256 y) public { diff --git a/testdata/default/fuzz/FuzzUint.t.sol b/testdata/default/fuzz/FuzzUint.t.sol index 5ae90a57bba0..923c2980f2bc 100644 --- a/testdata/default/fuzz/FuzzUint.t.sol +++ b/testdata/default/fuzz/FuzzUint.t.sol @@ -3,7 +3,8 @@ pragma solidity 0.8.18; import "ds-test/test.sol"; -// See https://github.com/foundry-rs/foundry/pull/735 for context +// https://github.com/foundry-rs/foundry/pull/735 behavior changed with https://github.com/foundry-rs/foundry/issues/3521 +// random values (instead edge cases) are generated if no fixtures defined contract FuzzNumbersTest is DSTest { function testPositive(uint256) public { assertTrue(true); @@ -14,19 +15,19 @@ contract FuzzNumbersTest is DSTest { } function testNegative0(uint256 val) public { - assertTrue(val != 0); + assertTrue(val == 0); } function testNegative2(uint256 val) public { - assertTrue(val != 2); + assertTrue(val == 2); } function testNegative2Max(uint256 val) public { - assertTrue(val != type(uint256).max - 2); + assertTrue(val == type(uint256).max - 2); } function testNegativeMax(uint256 val) public { - assertTrue(val != type(uint256).max); + assertTrue(val == type(uint256).max); } function testEquality(uint256 x, uint256 y) public { diff --git a/testdata/default/fuzz/invariant/common/InvariantAssume.t.sol b/testdata/default/fuzz/invariant/common/InvariantAssume.t.sol index 4ac0d085c180..9808a870f722 100644 --- a/testdata/default/fuzz/invariant/common/InvariantAssume.t.sol +++ b/testdata/default/fuzz/invariant/common/InvariantAssume.t.sol @@ -8,7 +8,7 @@ contract Handler is DSTest { Vm constant vm = Vm(HEVM_ADDRESS); function doSomething(uint256 param) public { - vm.assume(param != 0); + vm.assume(param == 0); } } diff --git a/testdata/default/fuzz/invariant/common/InvariantCalldataDictionary.t.sol b/testdata/default/fuzz/invariant/common/InvariantCalldataDictionary.t.sol index e1486f9639e9..5387b020d37d 100644 --- a/testdata/default/fuzz/invariant/common/InvariantCalldataDictionary.t.sol +++ b/testdata/default/fuzz/invariant/common/InvariantCalldataDictionary.t.sol @@ -62,11 +62,14 @@ contract InvariantCalldataDictionary is DSTest { address owner; Owned owned; Handler handler; + address[] actors; function setUp() public { owner = address(this); owned = new Owned(); handler = new Handler(owned); + actors.push(owner); + actors.push(address(777)); } function targetSelectors() public returns (FuzzSelector[] memory) { @@ -78,6 +81,14 @@ contract InvariantCalldataDictionary is DSTest { return targets; } + function fixtureSender() external returns (address[] memory) { + return actors; + } + + function fixtureCandidate() external returns (address[] memory) { + return actors; + } + function invariant_owner_never_changes() public { assertEq(owned.owner(), owner); } diff --git a/testdata/default/fuzz/invariant/common/InvariantFixtures.t.sol b/testdata/default/fuzz/invariant/common/InvariantFixtures.t.sol new file mode 100644 index 000000000000..b3f1e17cb249 --- /dev/null +++ b/testdata/default/fuzz/invariant/common/InvariantFixtures.t.sol @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity ^0.8.0; + +import "ds-test/test.sol"; + +contract Target { + bool ownerFound; + bool amountFound; + bool magicFound; + bool keyFound; + bool backupFound; + bool extraStringFound; + + function fuzzWithFixtures( + address owner_, + uint256 _amount, + int32 magic, + bytes32 key, + bytes memory backup, + string memory extra + ) external { + if (owner_ == address(0x6B175474E89094C44Da98b954EedeAC495271d0F)) { + ownerFound = true; + } + if (_amount == 1122334455) amountFound = true; + if (magic == -777) magicFound = true; + if (key == "abcd1234") keyFound = true; + if (keccak256(backup) == keccak256("qwerty1234")) backupFound = true; + if (keccak256(abi.encodePacked(extra)) == keccak256(abi.encodePacked("112233aabbccdd"))) { + extraStringFound = true; + } + } + + function isCompromised() public view returns (bool) { + return ownerFound && amountFound && magicFound && keyFound && backupFound && extraStringFound; + } +} + +/// Try to compromise target contract by finding all accepted values using fixtures. +contract InvariantFixtures is DSTest { + Target target; + address[] public fixture_owner_ = [address(0x6B175474E89094C44Da98b954EedeAC495271d0F)]; + uint256[] public fixture_amount = [1, 2, 1122334455]; + + function setUp() public { + target = new Target(); + } + + function fixtureMagic() external returns (int32[2] memory) { + int32[2] memory magic; + magic[0] = -777; + magic[1] = 777; + return magic; + } + + function fixtureKey() external pure returns (bytes32[] memory) { + bytes32[] memory keyFixture = new bytes32[](1); + keyFixture[0] = "abcd1234"; + return keyFixture; + } + + function fixtureBackup() external pure returns (bytes[] memory) { + bytes[] memory backupFixture = new bytes[](1); + backupFixture[0] = "qwerty1234"; + return backupFixture; + } + + function fixtureExtra() external pure returns (string[] memory) { + string[] memory extraFixture = new string[](1); + extraFixture[0] = "112233aabbccdd"; + return extraFixture; + } + + function invariant_target_not_compromised() public { + assertEq(target.isCompromised(), false); + } +}