diff --git a/foundry.toml b/foundry.toml index 6fde36d9..4f93dbfb 100644 --- a/foundry.toml +++ b/foundry.toml @@ -1,6 +1,13 @@ [profile.default] fs_permissions = [{ access = "read-write", path = "./"}] +[rpc_endpoints] +# We intentionally use both dashes and underscores in the key names to ensure both are supported. +# The RPC URLs below match the StdChains URLs but append a trailing slash for testing. +mainnet = "https://api.mycryptoapi.com/eth/" +optimism_goerli = "https://goerli.optimism.io/" +arbitrum-one-goerli = "https://goerli-rollup.arbitrum.io/rpc/" + [fmt] # These are all the `forge fmt` defaults. line_length = 120 diff --git a/src/StdCheats.sol b/src/StdCheats.sol index bd9fa3c5..240db21d 100644 --- a/src/StdCheats.sol +++ b/src/StdCheats.sol @@ -9,6 +9,45 @@ import "./Vm.sol"; abstract contract StdCheatsSafe { VmSafe private constant vm = VmSafe(address(uint160(uint256(keccak256("hevm cheat code"))))); + /// @dev To hide constructor warnings across solc versions due to different constructor visibility requirements and + /// syntaxes, we put the constructor in a private method and assign an unused return value to a variable. This + /// forces the method to run during construction, but without declaring an explicit constructor. + uint256 private CONSTRUCTOR = _constructor(); + + struct Chain { + // The chain name, using underscores as the separator to match `foundry.toml` conventions. + string name; + // The chain's Chain ID. + uint256 chainId; + // A default RPC endpoint for this chain. + // NOTE: This default RPC URL is included for convenience to facilitate quick tests and + // experimentation. Do not use this RPC URL for production test suites, CI, or other heavy + // usage as you will be throttled and this is a disservice to others who need this endpoint. + string rpcUrl; + } + + struct Chains { + Chain Anvil; + Chain Hardhat; + Chain Mainnet; + Chain Goerli; + Chain Sepolia; + Chain Optimism; + Chain OptimismGoerli; + Chain ArbitrumOne; + Chain ArbitrumOneGoerli; + Chain ArbitrumNova; + Chain Polygon; + Chain PolygonMumbai; + Chain Avalanche; + Chain AvalancheFuji; + Chain BnbSmartChain; + Chain BnbSmartChainTestnet; + Chain GnosisChain; + } + + Chains stdChains; + // Data structures to parse Transaction objects from the broadcast artifact // that conform to EIP1559. The Raw structs is what is parsed from the JSON // and then converted to the one that is used by the user for better UX. @@ -186,6 +225,96 @@ abstract contract StdCheatsSafe { string value; } + function _constructor() private returns (uint256) { + // Initialize `stdChains` with the defaults. + stdChains = Chains({ + Anvil: Chain("Anvil", 31337, "http://127.0.0.1:8545"), + Hardhat: Chain("Hardhat", 31337, "http://127.0.0.1:8545"), + Mainnet: Chain("Mainnet", 1, "https://api.mycryptoapi.com/eth"), + Goerli: Chain("Goerli", 5, "https://goerli.infura.io/v3/84842078b09946638c03157f83405213"), // Default Infura key from ethers.js: https://github.com/ethers-io/ethers.js/blob/c80fcddf50a9023486e9f9acb1848aba4c19f7b6/packages/providers/src.ts/infura-provider.ts + Sepolia: Chain("Sepolia", 11155111, "https://rpc.sepolia.dev"), + Optimism: Chain("Optimism", 10, "https://mainnet.optimism.io"), + OptimismGoerli: Chain("OptimismGoerli", 420, "https://goerli.optimism.io"), + ArbitrumOne: Chain("ArbitrumOne", 42161, "https://arb1.arbitrum.io/rpc"), + ArbitrumOneGoerli: Chain("ArbitrumOneGoerli", 421613, "https://goerli-rollup.arbitrum.io/rpc"), + ArbitrumNova: Chain("ArbitrumNova", 42170, "https://nova.arbitrum.io/rpc"), + Polygon: Chain("Polygon", 137, "https://polygon-rpc.com"), + PolygonMumbai: Chain("PolygonMumbai", 80001, "https://rpc-mumbai.matic.today"), + Avalanche: Chain("Avalanche", 43114, "https://api.avax.network/ext/bc/C/rpc"), + AvalancheFuji: Chain("AvalancheFuji", 43113, "https://api.avax-test.network/ext/bc/C/rpc"), + BnbSmartChain: Chain("BnbSmartChain", 56, "https://bsc-dataseed1.binance.org"), + BnbSmartChainTestnet: Chain("BnbSmartChainTestnet", 97, "https://data-seed-prebsc-1-s1.binance.org:8545"), + GnosisChain: Chain("GnosisChain", 100, "https://rpc.gnosischain.com") + }); + + // Loop over RPC URLs in the config file to replace the default RPC URLs + (string[2][] memory rpcs) = vm.rpcUrls(); + for (uint256 i = 0; i < rpcs.length; i++) { + (string memory name, string memory rpcUrl) = (rpcs[i][0], rpcs[i][1]); + // forgefmt: disable-start + if (isEqual(name, "anvil")) stdChains.Anvil.rpcUrl = rpcUrl; + else if (isEqual(name, "hardhat")) stdChains.Hardhat.rpcUrl = rpcUrl; + else if (isEqual(name, "mainnet")) stdChains.Mainnet.rpcUrl = rpcUrl; + else if (isEqual(name, "goerli")) stdChains.Goerli.rpcUrl = rpcUrl; + else if (isEqual(name, "sepolia")) stdChains.Sepolia.rpcUrl = rpcUrl; + else if (isEqual(name, "optimism")) stdChains.Optimism.rpcUrl = rpcUrl; + else if (isEqual(name, "optimism_goerli", "optimism-goerli")) stdChains.OptimismGoerli.rpcUrl = rpcUrl; + else if (isEqual(name, "arbitrum_one", "arbitrum-one")) stdChains.ArbitrumOne.rpcUrl = rpcUrl; + else if (isEqual(name, "arbitrum_one_goerli", "arbitrum-one-goerli")) stdChains.ArbitrumOneGoerli.rpcUrl = rpcUrl; + else if (isEqual(name, "arbitrum_nova", "arbitrum-nova")) stdChains.ArbitrumNova.rpcUrl = rpcUrl; + else if (isEqual(name, "polygon")) stdChains.Polygon.rpcUrl = rpcUrl; + else if (isEqual(name, "polygon_mumbai", "polygon-mumbai")) stdChains.PolygonMumbai.rpcUrl = rpcUrl; + else if (isEqual(name, "avalanche")) stdChains.Avalanche.rpcUrl = rpcUrl; + else if (isEqual(name, "avalanche_fuji", "avalanche-fuji")) stdChains.AvalancheFuji.rpcUrl = rpcUrl; + else if (isEqual(name, "bnb_smart_chain", "bnb-smart-chain")) stdChains.BnbSmartChain.rpcUrl = rpcUrl; + else if (isEqual(name, "bnb_smart_chain_testnet", "bnb-smart-chain-testnet")) stdChains.BnbSmartChainTestnet.rpcUrl = rpcUrl; + else if (isEqual(name, "gnosis_chain", "gnosis-chain")) stdChains.GnosisChain.rpcUrl = rpcUrl; + // forgefmt: disable-end + } + return 0; + } + + function isEqual(string memory a, string memory b) private pure returns (bool) { + return keccak256(abi.encode(a)) == keccak256(abi.encode(b)); + } + + function isEqual(string memory a, string memory b, string memory c) private pure returns (bool) { + return keccak256(abi.encode(a)) == keccak256(abi.encode(b)) + || keccak256(abi.encode(a)) == keccak256(abi.encode(c)); + } + + function assumeNoPrecompiles(address addr) internal virtual { + // Assembly required since `block.chainid` was introduced in 0.8.0. + uint256 chainId; + assembly { + chainId := chainid() + } + assumeNoPrecompiles(addr, chainId); + } + + function assumeNoPrecompiles(address addr, uint256 chainId) internal virtual { + // Note: For some chains like Optimism these are technically predeploys (i.e. bytecode placed at a specific + // address), but the same rationale for excluding them applies so we include those too. + + // These should be present on all EVM-compatible chains. + vm.assume(addr < address(0x1) || addr > address(0x9)); + + // forgefmt: disable-start + if (chainId == stdChains.Optimism.chainId || chainId == stdChains.OptimismGoerli.chainId) { + // https://github.com/ethereum-optimism/optimism/blob/eaa371a0184b56b7ca6d9eb9cb0a2b78b2ccd864/op-bindings/predeploys/addresses.go#L6-L21 + vm.assume(addr < address(0x4200000000000000000000000000000000000000) || addr > address(0x4200000000000000000000000000000000000800)); + } else if (chainId == stdChains.ArbitrumOne.chainId || chainId == stdChains.ArbitrumOneGoerli.chainId) { + // https://developer.arbitrum.io/useful-addresses#arbitrum-precompiles-l2-same-on-all-arb-chains + vm.assume(addr < address(0x0000000000000000000000000000000000000064) || addr > address(0x0000000000000000000000000000000000000068)); + } else if (chainId == stdChains.Avalanche.chainId || chainId == stdChains.AvalancheFuji.chainId) { + // https://github.com/ava-labs/subnet-evm/blob/47c03fd007ecaa6de2c52ea081596e0a88401f58/precompile/params.go#L18-L59 + vm.assume(addr < address(0x0100000000000000000000000000000000000000) || addr > address(0x01000000000000000000000000000000000000ff)); + vm.assume(addr < address(0x0200000000000000000000000000000000000000) || addr > address(0x02000000000000000000000000000000000000FF)); + vm.assume(addr < address(0x0300000000000000000000000000000000000000) || addr > address(0x03000000000000000000000000000000000000Ff)); + } + // forgefmt: disable-end + } + function readEIP1559ScriptArtifact(string memory path) internal virtual returns (EIP1559ScriptArtifact memory) { string memory data = vm.readFile(path); bytes memory parsedData = vm.parseJson(data); diff --git a/src/Vm.sol b/src/Vm.sol index 798ac8fd..5a24d1cf 100644 --- a/src/Vm.sol +++ b/src/Vm.sol @@ -132,6 +132,14 @@ interface VmSafe { // struct json = { uint256 a; address b; } // If we defined a json struct with the opposite order, meaning placing the address b first, it would try to // decode the tuple in that order, and thus fail. + + // Returns the RPC url for the given alias + function rpcUrl(string calldata) external returns (string memory); + // Returns all rpc urls and their aliases `[alias, url][]` + function rpcUrls() external returns (string[2][] memory); + + // If the condition is false, discard this run's fuzz inputs and generate new ones. + function assume(bool) external; } interface Vm is VmSafe { @@ -187,8 +195,6 @@ interface Vm is VmSafe { function expectCall(address, bytes calldata) external; // Expects a call to an address with the specified msg.value and calldata function expectCall(address, uint256, bytes calldata) external; - // If the condition is false, discard this run's fuzz inputs and generate new ones - function assume(bool) external; // Sets block.coinbase (who) function coinbase(address) external; // Snapshot the current state of the evm. @@ -243,8 +249,4 @@ interface Vm is VmSafe { function transact(bytes32 txHash) external; // Fetches the given transaction from the given fork and executes it on the current state function transact(uint256 forkId, bytes32 txHash) external; - // Returns the RPC url for the given alias - function rpcUrl(string calldata) external returns (string memory); - // Returns all rpc urls and their aliases `[alias, url][]` - function rpcUrls() external returns (string[2][] memory); } diff --git a/src/console2.sol b/src/console2.sol index cdde3d71..0b487d61 100644 --- a/src/console2.sol +++ b/src/console2.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity >=0.4.22 <0.9.0; -/// @dev The orignal console.sol uses `int` and `uint` for computing function selectors, but it should +/// @dev The original console.sol uses `int` and `uint` for computing function selectors, but it should /// use `int256` and `uint256`. This modified version fixes that. This version is recommended /// over `console.sol` if you don't need compatibility with Hardhat as the logs will show up in /// forge stack traces. If you do need compatibility with Hardhat, you must use `console.sol`. diff --git a/test/StdAssertions.t.sol b/test/StdAssertions.t.sol index 6ebd3a4a..4d5827d6 100644 --- a/test/StdAssertions.t.sol +++ b/test/StdAssertions.t.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity >=0.7.0 <0.9.0; -import "src/Test.sol"; +import "../src/Test.sol"; contract StdAssertionsTest is Test { string constant CUSTOM_ERROR = "guh!"; diff --git a/test/StdCheats.t.sol b/test/StdCheats.t.sol index 28270c02..c8a68370 100644 --- a/test/StdCheats.t.sol +++ b/test/StdCheats.t.sol @@ -1,9 +1,9 @@ // SPDX-License-Identifier: MIT pragma solidity >=0.7.0 <0.9.0; -import "src/StdCheats.sol"; -import "src/Test.sol"; -import "src/StdJson.sol"; +import "../src/StdCheats.sol"; +import "../src/Test.sol"; +import "../src/StdJson.sol"; contract StdCheatsTest is Test { Bar test; @@ -223,6 +223,27 @@ contract StdCheatsTest is Test { } return number; } + + function testChainRpcInitialization() public { + // RPCs specified in `foundry.toml` should be updated. + assertEq(stdChains.Mainnet.rpcUrl, "https://api.mycryptoapi.com/eth/"); + assertEq(stdChains.OptimismGoerli.rpcUrl, "https://goerli.optimism.io/"); + assertEq(stdChains.ArbitrumOneGoerli.rpcUrl, "https://goerli-rollup.arbitrum.io/rpc/"); + + // Other RPCs should remain unchanged. + assertEq(stdChains.Anvil.rpcUrl, "http://127.0.0.1:8545"); + assertEq(stdChains.Hardhat.rpcUrl, "http://127.0.0.1:8545"); + assertEq(stdChains.Sepolia.rpcUrl, "https://rpc.sepolia.dev"); + } + + // Ensure we can connect to the default RPC URL for each chain. + function testRpcs() public { + (string[2][] memory rpcs) = vm.rpcUrls(); + for (uint256 i = 0; i < rpcs.length; i++) { + ( /* string memory name */ , string memory rpcUrl) = (rpcs[i][0], rpcs[i][1]); + vm.createSelectFork(rpcUrl); + } + } } contract Bar { diff --git a/test/StdError.t.sol b/test/StdError.t.sol index d9abc495..ccd3efac 100644 --- a/test/StdError.t.sol +++ b/test/StdError.t.sol @@ -1,8 +1,8 @@ // SPDX-License-Identifier: MIT pragma solidity >=0.8.0 <0.9.0; -import "src/StdError.sol"; -import "src/Test.sol"; +import "../src/StdError.sol"; +import "../src/Test.sol"; contract StdErrorsTest is Test { ErrorsTest test; diff --git a/test/StdMath.t.sol b/test/StdMath.t.sol index 19f753c0..95037ea5 100644 --- a/test/StdMath.t.sol +++ b/test/StdMath.t.sol @@ -1,8 +1,8 @@ // SPDX-License-Identifier: MIT pragma solidity >=0.8.0 <0.9.0; -import "src/StdMath.sol"; -import "src/Test.sol"; +import "../src/StdMath.sol"; +import "../src/Test.sol"; contract StdMathTest is Test { function testGetAbs() external { diff --git a/test/StdStorage.t.sol b/test/StdStorage.t.sol index 96752cb5..d4c563a0 100644 --- a/test/StdStorage.t.sol +++ b/test/StdStorage.t.sol @@ -1,8 +1,8 @@ // SPDX-License-Identifier: MIT pragma solidity >=0.7.0 <0.9.0; -import "src/StdStorage.sol"; -import "src/Test.sol"; +import "../src/StdStorage.sol"; +import "../src/Test.sol"; contract StdStorageTest is Test { using stdStorage for StdStorage; diff --git a/test/StdUtils.t.sol b/test/StdUtils.t.sol index 17c77aee..18b6bad7 100644 --- a/test/StdUtils.t.sol +++ b/test/StdUtils.t.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity >=0.7.0 <0.9.0; -import "src/Test.sol"; +import "../src/Test.sol"; contract StdUtilsTest is Test { function testBound() public { @@ -72,4 +72,9 @@ contract StdUtilsTest is Test { address create2Address = computeCreate2Address(salt, initcodeHash, deployer); assertEq(create2Address, 0xB147a5d25748fda14b463EB04B111027C290f4d3); } + + function testAssumeNoPrecompilesL1(address addr) external { + assumeNoPrecompiles(addr, stdChains.Mainnet.chainId); + assertTrue(addr < address(1) || addr > address(9)); + } }