diff --git a/Cargo.lock b/Cargo.lock index 779b8bec7..f3baa6833 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2306,6 +2306,7 @@ dependencies = [ "globset", "humantime-serde", "itertools 0.12.1", + "log", "mockall", "num_cpus", "once_cell", diff --git a/crates/edr_napi/src/solidity_tests/config.rs b/crates/edr_napi/src/solidity_tests/config.rs index 7f7ebfd36..dbde4a2aa 100644 --- a/crates/edr_napi/src/solidity_tests/config.rs +++ b/crates/edr_napi/src/solidity_tests/config.rs @@ -294,6 +294,8 @@ impl TryFrom for SolidityTestRunnerConfig { evm_opts, fuzz, invariant, + // Solidity fuzz fixtures are not supported by the JS backend + solidity_fuzz_fixtures: false, }) } } diff --git a/crates/foundry/forge/Cargo.toml b/crates/foundry/forge/Cargo.toml index 6cf7ddfc6..85cfcd0ac 100644 --- a/crates/foundry/forge/Cargo.toml +++ b/crates/foundry/forge/Cargo.toml @@ -35,6 +35,7 @@ alloy-primitives = { workspace = true, features = ["serde"] } tokio = { workspace = true, features = ["time"] } evm-disassembler.workspace = true thiserror = "1.0.61" +log = "0.4.22" [dev-dependencies] edr_test_utils.workspace = true diff --git a/crates/foundry/forge/src/config.rs b/crates/foundry/forge/src/config.rs index e9394bc08..fbec23970 100644 --- a/crates/foundry/forge/src/config.rs +++ b/crates/foundry/forge/src/config.rs @@ -36,6 +36,8 @@ pub struct SolidityTestRunnerConfig { pub coverage: bool, /// Whether to support the `testFail` prefix pub test_fail: bool, + /// Whether to enable solidity fuzz fixtures support + pub solidity_fuzz_fixtures: bool, /// Cheats configuration options pub cheats_config_options: CheatsConfigOptions, /// EVM options diff --git a/crates/foundry/forge/src/multi_runner.rs b/crates/foundry/forge/src/multi_runner.rs index fc6dca173..c29844141 100644 --- a/crates/foundry/forge/src/multi_runner.rs +++ b/crates/foundry/forge/src/multi_runner.rs @@ -74,6 +74,8 @@ pub struct MultiContractRunner { debug: bool, /// Whether to support the `testFail` prefix test_fail: bool, + /// Whether to enable solidity fuzz fixtures support + solidity_fuzz_fixtures: bool, /// Settings related to fuzz and/or invariant tests test_options: TestOptions, } @@ -104,6 +106,7 @@ impl MultiContractRunner { cheats_config_options, fuzz, invariant, + solidity_fuzz_fixtures, } = config; // Do canonicalization in blocking context. @@ -130,6 +133,7 @@ impl MultiContractRunner { trace, debug, test_fail, + solidity_fuzz_fixtures, test_options, }) } @@ -280,6 +284,7 @@ impl MultiContractRunner { sender: self.evm_opts.sender, debug: self.debug, test_fail: self.test_fail, + solidity_fuzz_fixtures: self.solidity_fuzz_fixtures, }, ); let r = runner.run_tests( diff --git a/crates/foundry/forge/src/runner.rs b/crates/foundry/forge/src/runner.rs index 3f4e51468..1ab525cd8 100644 --- a/crates/foundry/forge/src/runner.rs +++ b/crates/foundry/forge/src/runner.rs @@ -65,6 +65,8 @@ pub struct ContractRunner<'a> { pub debug: bool, /// Whether to support the `testFail` prefix pub test_fail: bool, + /// Whether to enable solidity fuzz fixtures support + pub solidity_fuzz_fixtures: bool, } /// Options for [`ContractRunner`]. @@ -78,6 +80,8 @@ pub struct ContractRunnerOptions { pub debug: bool, /// Whether to support the `testFail` prefix pub test_fail: bool, + /// whether to enable solidity fuzz fixtures support + pub solidity_fuzz_fixtures: bool, } impl<'a> ContractRunner<'a> { @@ -93,6 +97,7 @@ impl<'a> ContractRunner<'a> { sender, debug, test_fail, + solidity_fuzz_fixtures, } = options; Self { @@ -104,6 +109,7 @@ impl<'a> ContractRunner<'a> { sender, debug, test_fail, + solidity_fuzz_fixtures, } } } @@ -278,51 +284,62 @@ impl<'a> ContractRunner<'a> { /// 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 + let fixture_funcs = self + .contract .abi .functions() - .filter(|func| func.is_fixture()) - .for_each(|func| { - if func.inputs.is_empty() { - // Read fixtures declared as functions. + .filter(|func| func.is_fixture()); + + // No-op if the feature is disabled + if !self.solidity_fuzz_fixtures { + fixture_funcs.for_each(|func| { + log::warn!("Possible fuzz fixture usage detected: '{}', but solidity fuzz fixtures are disabled.", &func.name); + }); + + return FuzzFixtures::default(); + }; + + let mut fixtures = HashMap::new(); + fixture_funcs.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, &[], 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; + }) = 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; } - fixtures.insert(fixture_name(func.name.clone()), DynSolValue::Array(vals)); - }; - }); + index += 1; + } + fixtures.insert(fixture_name(func.name.clone()), DynSolValue::Array(vals)); + }; + }); FuzzFixtures::new(fixtures) } diff --git a/crates/foundry/forge/tests/it/test_helpers.rs b/crates/foundry/forge/tests/it/test_helpers.rs index 3696a0c69..c00239839 100644 --- a/crates/foundry/forge/tests/it/test_helpers.rs +++ b/crates/foundry/forge/tests/it/test_helpers.rs @@ -92,6 +92,7 @@ impl ForgeTestProfile { invariant: TestInvariantConfig::default().into(), coverage: false, test_fail: true, + solidity_fuzz_fixtures: true, } } diff --git a/js/integration-tests/solidity-tests/contracts/FuzzFixture.t.sol b/js/integration-tests/solidity-tests/contracts/FuzzFixture.t.sol new file mode 100644 index 000000000..09de661a1 --- /dev/null +++ b/js/integration-tests/solidity-tests/contracts/FuzzFixture.t.sol @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import "./test.sol"; +import "./Vm.sol"; + +// Contract to be tested with overflow vulnerability +contract IdentityContract { + function identity(uint256 amount) view public returns(uint256) { + require(amount != 7191815684697958081204101901807852913954269296144377099693178655035380638910, "Got value from fixture"); + return amount; + } +} + +// Test that fuzz fixtures specified in Solidity are not supported. +contract FuzzFixtureTest is DSTest { + Vm constant vm = Vm(HEVM_ADDRESS); + + IdentityContract testDummy; + + uint256[] public fixtureAmount = [ + // This is a random value + 7191815684697958081204101901807852913954269296144377099693178655035380638910 + ]; + + function setUp() public { + testDummy = new IdentityContract(); + } + + function testFuzzDummy(uint256 amount) public { + assertEq(testDummy.identity(amount), amount); + } +} diff --git a/js/integration-tests/solidity-tests/test/fuzz.ts b/js/integration-tests/solidity-tests/test/fuzz.ts index d12bc88f5..160d9ddf9 100644 --- a/js/integration-tests/solidity-tests/test/fuzz.ts +++ b/js/integration-tests/solidity-tests/test/fuzz.ts @@ -40,6 +40,20 @@ describe("Fuzz and invariant testing", function () { assert.isTrue(existsSync(failureDir)); }); + it("FuzzFixture is not supported", async function () { + const result = await testContext.runTestsWithStats("FuzzFixtureTest", { + fuzz: { + runs: 256, + dictionaryWeight: 1, + includeStorage: false, + includePushBytes: false, + seed: "0x7bb9ee74aaa2abe5e2ca8a116382a9f2ed70b651e70b430e1052eff52a74ffe3", + }, + }); + assert.equal(result.failedTests, 0); + assert.equal(result.totalTests, 1); + }); + // One test as steps should be sequential it("FailingInvariant", async function () { const failureDir = testContext.invariantFailuresPersistDir;