diff --git a/crates/common/src/traits.rs b/crates/common/src/traits.rs index 4232fb946dc3..fa8449f72798 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 invariant setup function. + fn is_invariant_target_setup(&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_invariant_target_setup(&self) -> bool { + self.name.is_invariant_target_setup() + } } impl TestFunctionExt for String { @@ -78,6 +85,10 @@ impl TestFunctionExt for String { fn is_setup(&self) -> bool { self.as_str().is_setup() } + + fn is_invariant_target_setup(&self) -> bool { + self.as_str().is_invariant_target_setup() + } } impl TestFunctionExt for str { @@ -100,6 +111,18 @@ impl TestFunctionExt for str { fn is_setup(&self) -> bool { self.eq_ignore_ascii_case("setup") } + + fn is_invariant_target_setup(&self) -> bool { + self.eq_ignore_ascii_case("excludeArtifacts") || + self.eq_ignore_ascii_case("excludeContracts") || + self.eq_ignore_ascii_case("excludeSenders") || + self.eq_ignore_ascii_case("targetArtifacts") || + self.eq_ignore_ascii_case("targetArtifactSelectors") || + self.eq_ignore_ascii_case("targetContracts") || + self.eq_ignore_ascii_case("targetSelectors") || + self.eq_ignore_ascii_case("targetSenders") || + self.eq_ignore_ascii_case("targetInterfaces") + } } /// An extension trait for `std::error::Error` for ABI encoding. diff --git a/crates/config/src/inline/conf_parser.rs b/crates/config/src/inline/conf_parser.rs index f3b81a987ac5..1f6fca6c7ac5 100644 --- a/crates/config/src/inline/conf_parser.rs +++ b/crates/config/src/inline/conf_parser.rs @@ -1,13 +1,10 @@ use super::{remove_whitespaces, InlineConfigParserError}; -use crate::{ - inline::{INLINE_CONFIG_FIXTURE_KEY, INLINE_CONFIG_PREFIX}, - InlineConfigError, NatSpec, -}; +use crate::{inline::INLINE_CONFIG_PREFIX, InlineConfigError, NatSpec}; use regex::Regex; /// This trait is intended to parse configurations from /// structured text. Foundry users can annotate Solidity test functions, -/// providing special configs and fixtures just for the execution of a specific test. +/// providing special configs just for the execution of a specific test. /// /// An example: /// @@ -21,10 +18,6 @@ use regex::Regex; /// /// forge-config: ci.fuzz.runs = 10000 /// function test_ImportantFuzzTest(uint256 x) public {...} /// } -/// -/// /// forge-config: fixture -/// function x() public returns (uint256[] memory) {...} -/// } /// ``` pub trait InlineConfigParser where @@ -110,16 +103,7 @@ where } } -/// Type of inline config. -pub enum InlineConfigType { - /// Profile inline config. - Profile, - /// Fixture inline config. - Fixture, -} - -/// Checks if all configuration lines specified in `natspec` use a valid profile -/// or are test fixture configurations. +/// Checks if all configuration lines specified in `natspec` use a valid profile. /// /// i.e. Given available profiles /// ```rust @@ -127,15 +111,8 @@ pub enum InlineConfigType { /// ``` /// A configuration like `forge-config: ciii.invariant.depth = 1` would result /// in an error. -/// A fixture can be set by using `forge-config: fixture` configuration. -pub fn validate_inline_config_type( - natspec: &NatSpec, - profiles: &[String], -) -> Result { +pub fn validate_profiles(natspec: &NatSpec, profiles: &[String]) -> Result<(), InlineConfigError> { for config in natspec.config_lines() { - if config.eq(&format!("{INLINE_CONFIG_PREFIX}:{INLINE_CONFIG_FIXTURE_KEY}")) { - return Ok(InlineConfigType::Fixture); - } if !profiles.iter().any(|p| config.starts_with(&format!("{INLINE_CONFIG_PREFIX}:{p}."))) { let err_line: String = natspec.debug_context(); let profiles = format!("{profiles:?}"); @@ -145,7 +122,7 @@ pub fn validate_inline_config_type( })? } } - Ok(InlineConfigType::Profile) + Ok(()) } /// Tries to parse a `u32` from `value`. The `key` argument is used to give details @@ -162,7 +139,7 @@ pub fn parse_config_bool(key: String, value: String) -> Result = Lazy::new(|| { @@ -41,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, @@ -52,28 +48,6 @@ impl InlineConfig { } } -/// Represents per-test fixtures, declared inline -/// as structured comments in Solidity test files. This allows -/// setting data sets for specific fuzzed parameters in a solidity test. -#[derive(Clone, Debug, Default)] -pub struct InlineFixturesConfig { - /// Maps a test-contract to a set of test-fixtures. - configs: HashMap>, -} - -impl InlineFixturesConfig { - /// Records a function to be used as fixture for given contract. - /// The name of function should be the same as the name of fuzzed parameter. - pub fn add_fixture(&mut self, contract: String, fixture: String) { - self.configs.entry(contract).or_default().insert(fixture); - } - - /// Returns functions to be used as fixtures for given contract. - pub fn get_fixtures(&mut self, contract: String) -> Option<&HashSet> { - self.configs.get(&contract) - } -} - pub(crate) fn remove_whitespaces(s: &str) -> String { s.chars().filter(|c| !c.is_whitespace()).collect() } diff --git a/crates/config/src/lib.rs b/crates/config/src/lib.rs index 9a0a22448631..51769ab314a0 100644 --- a/crates/config/src/lib.rs +++ b/crates/config/src/lib.rs @@ -95,10 +95,7 @@ use providers::remappings::RemappingsProvider; mod inline; use crate::etherscan::EtherscanEnvProvider; -pub use inline::{ - validate_inline_config_type, InlineConfig, InlineConfigError, InlineConfigParser, - InlineConfigType, InlineFixturesConfig, NatSpec, -}; +pub use inline::{validate_profiles, InlineConfig, InlineConfigError, InlineConfigParser, NatSpec}; /// Foundry configuration /// diff --git a/crates/forge/src/lib.rs b/crates/forge/src/lib.rs index 849420e4af53..98dc0aa4783e 100644 --- a/crates/forge/src/lib.rs +++ b/crates/forge/src/lib.rs @@ -3,8 +3,8 @@ extern crate tracing; use foundry_compilers::ProjectCompileOutput; use foundry_config::{ - validate_inline_config_type, Config, FuzzConfig, InlineConfig, InlineConfigError, - InlineConfigParser, InlineConfigType, InlineFixturesConfig, InvariantConfig, NatSpec, + validate_profiles, Config, FuzzConfig, InlineConfig, InlineConfigError, InlineConfigParser, + InvariantConfig, NatSpec, }; use proptest::test_runner::{ FailurePersistence, FileFailurePersistence, RngAlgorithm, TestRng, TestRunner, @@ -40,8 +40,6 @@ pub struct TestOptions { pub inline_fuzz: InlineConfig, /// Contains per-test specific "invariant" configurations. pub inline_invariant: InlineConfig, - /// Contains per-test specific "fixture" configurations. - pub inline_fixtures: InlineFixturesConfig, } impl TestOptions { @@ -57,45 +55,33 @@ impl TestOptions { let natspecs: Vec = NatSpec::parse(output, root); let mut inline_invariant = InlineConfig::::default(); let mut inline_fuzz = InlineConfig::::default(); - let mut inline_fixtures = InlineFixturesConfig::default(); for natspec in natspecs { - match validate_inline_config_type(&natspec, &profiles)? { - InlineConfigType::Fixture => { - inline_fixtures.add_fixture(natspec.contract, natspec.function); - } - InlineConfigType::Profile => { - FuzzConfig::validate_configs(&natspec)?; - InvariantConfig::validate_configs(&natspec)?; - - // Apply in-line configurations for the current profile - let configs: Vec = natspec.current_profile_configs().collect(); - let c: &str = &natspec.contract; - let f: &str = &natspec.function; - let line: String = natspec.debug_context(); - - match base_fuzz.try_merge(&configs) { - Ok(Some(conf)) => inline_fuzz.insert(c, f, conf), - Ok(None) => { /* No inline config found, do nothing */ } - Err(e) => Err(InlineConfigError { line: line.clone(), source: e })?, - } - - match base_invariant.try_merge(&configs) { - Ok(Some(conf)) => inline_invariant.insert(c, f, conf), - Ok(None) => { /* No inline config found, do nothing */ } - Err(e) => Err(InlineConfigError { line: line.clone(), source: e })?, - } - } + // Perform general validation + validate_profiles(&natspec, &profiles)?; + FuzzConfig::validate_configs(&natspec)?; + InvariantConfig::validate_configs(&natspec)?; + + // Apply in-line configurations for the current profile + let configs: Vec = natspec.current_profile_configs().collect(); + let c: &str = &natspec.contract; + let f: &str = &natspec.function; + let line: String = natspec.debug_context(); + + match base_fuzz.try_merge(&configs) { + Ok(Some(conf)) => inline_fuzz.insert(c, f, conf), + Ok(None) => { /* No inline config found, do nothing */ } + Err(e) => Err(InlineConfigError { line: line.clone(), source: e })?, + } + + match base_invariant.try_merge(&configs) { + Ok(Some(conf)) => inline_invariant.insert(c, f, conf), + Ok(None) => { /* No inline config found, do nothing */ } + Err(e) => Err(InlineConfigError { line: line.clone(), source: e })?, } } - Ok(Self { - fuzz: base_fuzz, - invariant: base_invariant, - inline_fuzz, - inline_invariant, - inline_fixtures, - }) + Ok(Self { fuzz: base_fuzz, invariant: base_invariant, inline_fuzz, inline_invariant }) } /// Returns a "fuzz" test runner instance. Parameters are used to select tight scoped fuzz diff --git a/crates/forge/src/runner.rs b/crates/forge/src/runner.rs index 1240a99fbf4c..17268c2b6f74 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; @@ -12,7 +13,7 @@ use foundry_common::{ contracts::{ContractsByAddress, ContractsByArtifact}, TestFunctionExt, }; -use foundry_config::{FuzzConfig, InlineFixturesConfig, InvariantConfig}; +use foundry_config::{FuzzConfig, InvariantConfig}; use foundry_evm::{ constants::CALLER, coverage::HitMaps, @@ -76,14 +77,14 @@ impl<'a> ContractRunner<'a> { impl<'a> ContractRunner<'a> { /// Deploys the test contract inside the runner from the sending account, and optionally runs /// the `setUp` function on the test contract. - pub fn setup(&mut self, setup: bool, fixtures: &InlineFixturesConfig) -> TestSetup { - match self._setup(setup, fixtures) { + pub fn setup(&mut self, setup: bool) -> TestSetup { + match self._setup(setup) { Ok(setup) => setup, Err(err) => TestSetup::failed(err.to_string()), } } - fn _setup(&mut self, setup: bool, fixtures: &InlineFixturesConfig) -> Result { + fn _setup(&mut self, setup: bool) -> Result { trace!(?setup, "Setting test contract"); // We max out their balance so that they can deploy and make calls. @@ -172,7 +173,7 @@ impl<'a> ContractRunner<'a> { labeled_addresses, reason, coverage, - fuzz_fixtures: self.fuzz_fixtures(address, fixtures), + fuzz_fixtures: self.fuzz_fixtures(address), } } else { TestSetup::success( @@ -181,35 +182,75 @@ impl<'a> ContractRunner<'a> { traces, Default::default(), None, - self.fuzz_fixtures(address, fixtures), + self.fuzz_fixtures(address), ) }; Ok(setup) } - /// Collect fixtures from test contract. Fixtures are functions prefixed with `fixture_` key - /// and followed by the name of the parameter. + /// Collect fixtures from test contract. /// - /// For example: - /// `fixture_test() returns (address[] memory)` function - /// define an array of addresses to be used for fuzzed `test` named parameter in scope of the + /// - as storage arrays defined in test contract + /// Fixtures can be defined: + /// - as functions by having inline fixture configuration and same name as the parameter to be + /// fuzzed + /// + /// For example, a storage variable declared as + /// `uint256[] public amount = [1, 2, 3];` + /// define an array of uint256 values to be used for fuzzing `amount` named parameter in scope + /// of the current test. + /// + /// For example, a function declared as + /// `function owner() public returns (address[] memory)` + /// define 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, fixtures: &InlineFixturesConfig) -> FuzzFixtures { - match fixtures.to_owned().get_fixtures(self.name.to_string()) { - Some(functions) => { - let mut fixtures = HashMap::with_capacity(functions.len()); - for func in self.contract.abi.functions().filter(|f| functions.contains(&f.name)) { + fn fuzz_fixtures(&mut self, address: Address) -> FuzzFixtures { + let mut fixtures = HashMap::new(); + self.contract + .abi + .functions() + .filter(|func| { + !func.is_setup() && + !func.is_invariant_test() && + !func.is_invariant_target_setup() && + !func.is_test() + }) + .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(func.name.clone(), decoded_result); } - } - FuzzFixtures::new(fixtures) - } - None => FuzzFixtures::default(), - } + } 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(func.name.clone(), DynSolValue::Array(vals)); + }; + }); + + FuzzFixtures::new(fixtures) } /// Runs all tests for a contract whose names match the provided regular expression @@ -256,7 +297,7 @@ impl<'a> ContractRunner<'a> { if tmp_tracing { self.executor.set_tracing(true); } - let setup = self.setup(needs_setup, &test_options.inline_fixtures); + let setup = self.setup(needs_setup); if tmp_tracing { self.executor.set_tracing(false); } diff --git a/crates/forge/tests/it/invariant.rs b/crates/forge/tests/it/invariant.rs index 4423e231211d..e03272ea80ca 100644 --- a/crates/forge/tests/it/invariant.rs +++ b/crates/forge/tests/it/invariant.rs @@ -477,6 +477,8 @@ async fn test_invariant_fuzzed_selected_targets() { 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, diff --git a/testdata/default/fuzz/invariant/common/InvariantCalldataDictionary.t.sol b/testdata/default/fuzz/invariant/common/InvariantCalldataDictionary.t.sol index b05baf49e0cd..fb484f2e829e 100644 --- a/testdata/default/fuzz/invariant/common/InvariantCalldataDictionary.t.sol +++ b/testdata/default/fuzz/invariant/common/InvariantCalldataDictionary.t.sol @@ -81,12 +81,10 @@ contract InvariantCalldataDictionary is DSTest { return targets; } - /// forge-config: fixture function sender() external returns (address[] memory) { return actors; } - /// forge-config: fixture function candidate() external returns (address[] memory) { return actors; } diff --git a/testdata/default/fuzz/invariant/common/InvariantFixtures.t.sol b/testdata/default/fuzz/invariant/common/InvariantFixtures.t.sol index 4a0f7b62a3ea..58fce7e5ed8b 100644 --- a/testdata/default/fuzz/invariant/common/InvariantFixtures.t.sol +++ b/testdata/default/fuzz/invariant/common/InvariantFixtures.t.sol @@ -19,7 +19,7 @@ contract Target { bytes memory backup, string memory extra ) external { - if (owner_ == 0x6B175474E89094C44Da98b954EedeAC495271d0F) { + if (owner_ == address(0x6B175474E89094C44Da98b954EedeAC495271d0F)) { ownerFound = true; } if (_amount == 1122334455) amountFound = true; @@ -39,47 +39,26 @@ contract Target { /// Try to compromise target contract by finding all accepted values using fixtures. contract InvariantFixtures is DSTest { Target target; + address[] public owner_ = [address(0x6B175474E89094C44Da98b954EedeAC495271d0F)]; + uint256[] public _amount = [1, 2, 1122334455]; + int32[] public magic = [-777, 777]; function setUp() public { target = new Target(); } - /// forge-config: fixture - function owner_() external pure returns (address[] memory) { - address[] memory addressFixture = new address[](1); - addressFixture[0] = 0x6B175474E89094C44Da98b954EedeAC495271d0F; - return addressFixture; - } - - /// forge-config: fixture - function _amount() external pure returns (uint256[] memory) { - uint256[] memory amountFixture = new uint256[](1); - amountFixture[0] = 1122334455; - return amountFixture; - } - - /// forge-config: fixture - function magic() external pure returns (int32[] memory) { - int32[] memory magicFixture = new int32[](1); - magicFixture[0] = -777; - return magicFixture; - } - - /// forge-config: fixture function key() external pure returns (bytes32[] memory) { bytes32[] memory keyFixture = new bytes32[](1); keyFixture[0] = "abcd1234"; return keyFixture; } - /// forge-config: fixture function backup() external pure returns (bytes[] memory) { bytes[] memory backupFixture = new bytes[](1); backupFixture[0] = "qwerty1234"; return backupFixture; } - /// forge-config: fixture function extra() external pure returns (string[] memory) { string[] memory extraFixture = new string[](1); extraFixture[0] = "112233aabbccdd";