diff --git a/packages/contracts-bedrock/interfaces/universal/ICrossDomainMessenger.sol b/packages/contracts-bedrock/interfaces/universal/ICrossDomainMessenger.sol index 256b09fa56efe..3f0b777400294 100644 --- a/packages/contracts-bedrock/interfaces/universal/ICrossDomainMessenger.sol +++ b/packages/contracts-bedrock/interfaces/universal/ICrossDomainMessenger.sol @@ -17,6 +17,9 @@ interface ICrossDomainMessenger { function RELAY_CONSTANT_OVERHEAD() external view returns (uint64); function RELAY_GAS_CHECK_BUFFER() external view returns (uint64); function RELAY_RESERVED_GAS() external view returns (uint64); + function TX_BASE_GAS() external view returns (uint64); + function FLOOR_CALLDATA_OVERHEAD() external view returns (uint64); + function ENCODING_OVERHEAD() external view returns (uint64); function baseGas(bytes memory _message, uint32 _minGasLimit) external pure returns (uint64); function failedMessages(bytes32) external view returns (bool); function messageNonce() external view returns (uint256); diff --git a/packages/contracts-bedrock/scripts/checks/check-frozen-files.sh b/packages/contracts-bedrock/scripts/checks/check-frozen-files.sh index f400ab5cab555..77eec5660b855 100755 --- a/packages/contracts-bedrock/scripts/checks/check-frozen-files.sh +++ b/packages/contracts-bedrock/scripts/checks/check-frozen-files.sh @@ -54,6 +54,7 @@ ALLOWED_FILES=( "src/L1/L1StandardBridge.sol" "src/L1/OPContractsManager.sol" "src/L1/OPContractsManagerInterop.sol" + "src/L1/OPPrestateUpdater.sol" "src/L1/OptimismPortal2.sol" "src/L1/OptimismPortalInterop.sol" "src/L1/ProtocolVersions.sol" diff --git a/packages/contracts-bedrock/snapshots/abi/L1CrossDomainMessenger.json b/packages/contracts-bedrock/snapshots/abi/L1CrossDomainMessenger.json index ac33f91e24c75..6cce51733d3c5 100644 --- a/packages/contracts-bedrock/snapshots/abi/L1CrossDomainMessenger.json +++ b/packages/contracts-bedrock/snapshots/abi/L1CrossDomainMessenger.json @@ -4,6 +4,32 @@ "stateMutability": "nonpayable", "type": "constructor" }, + { + "inputs": [], + "name": "ENCODING_OVERHEAD", + "outputs": [ + { + "internalType": "uint64", + "name": "", + "type": "uint64" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "FLOOR_CALLDATA_OVERHEAD", + "outputs": [ + { + "internalType": "uint64", + "name": "", + "type": "uint64" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [], "name": "MESSAGE_VERSION", @@ -134,6 +160,19 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [], + "name": "TX_BASE_GAS", + "outputs": [ + { + "internalType": "uint64", + "name": "", + "type": "uint64" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [ { diff --git a/packages/contracts-bedrock/snapshots/abi/L2CrossDomainMessenger.json b/packages/contracts-bedrock/snapshots/abi/L2CrossDomainMessenger.json index 717bbb6eb6f4d..b9f04f127fbb1 100644 --- a/packages/contracts-bedrock/snapshots/abi/L2CrossDomainMessenger.json +++ b/packages/contracts-bedrock/snapshots/abi/L2CrossDomainMessenger.json @@ -4,6 +4,32 @@ "stateMutability": "nonpayable", "type": "constructor" }, + { + "inputs": [], + "name": "ENCODING_OVERHEAD", + "outputs": [ + { + "internalType": "uint64", + "name": "", + "type": "uint64" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "FLOOR_CALLDATA_OVERHEAD", + "outputs": [ + { + "internalType": "uint64", + "name": "", + "type": "uint64" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [], "name": "MESSAGE_VERSION", @@ -121,6 +147,19 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [], + "name": "TX_BASE_GAS", + "outputs": [ + { + "internalType": "uint64", + "name": "", + "type": "uint64" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [ { diff --git a/packages/contracts-bedrock/snapshots/semver-lock.json b/packages/contracts-bedrock/snapshots/semver-lock.json index 71146521dad2b..68d3e79e9fb90 100644 --- a/packages/contracts-bedrock/snapshots/semver-lock.json +++ b/packages/contracts-bedrock/snapshots/semver-lock.json @@ -4,28 +4,28 @@ "sourceCodeHash": "0xe772f7db8033e4a738850cb28ac4849d3a454c93732135a8a10d4f7cb498088e" }, "src/L1/L1CrossDomainMessenger.sol": { - "initCodeHash": "0x03a3c0eb2418aba9f93bb89723ba2ee7cb9e1988ca00f380503c960149c85b7a", - "sourceCodeHash": "0x5907cdb82ec5f6bef2184558a511d049ab3ee65388cf44d0c20d0f234ef8ca44" + "initCodeHash": "0x5a6272f6bd4346da460b7ff2ebc426d158d7ffc65dc0f2c0651a9e6d545bfd03", + "sourceCodeHash": "0xf11fa72481dbe43dad4e7a48645fcf92d0feeefa0b98b282042bdda568508372" }, "src/L1/L1ERC721Bridge.sol": { - "initCodeHash": "0xa54dbc482f557966a636158bc3a566c351cb04ee8b1cb13aa2de61b9a0ddb064", - "sourceCodeHash": "0xe9c85de57005fe964acb9c62f5009aa5a4ba0eacc24bec67a0e4a043d2db5757" + "initCodeHash": "0x819fcccb74bef53241d7d651f73949115e5706b61c7cb8cdb9b6d9d34ef39e89", + "sourceCodeHash": "0x5036f59ae3414d1106f148765bba9d1759844c9c2d3e18ab5c81bb49cf59eab8" }, "src/L1/L1StandardBridge.sol": { - "initCodeHash": "0xe95886a6408f77fb433687049d85dd3c2200dfacc2dd6c5fd7f9554f6fc5b11b", - "sourceCodeHash": "0x7f614d0c5251365580c02dd898900a583ece2aa28579adea74a3808b575949b4" + "initCodeHash": "0xbd4a03a3611a0de9669c4c8556f90c5aaef666b899d9ded1c07abc60263da8d6", + "sourceCodeHash": "0x44797707aea8c63dec049a02d69ea056662a06e5cf320028ab8b388634bf1c67" }, "src/L1/OPContractsManager.sol": { - "initCodeHash": "0x7ad29e286feabd0ef333f8969be8e766b3c68e97fe7b75e4c8a9699dd49e87f8", - "sourceCodeHash": "0xa412132a08809d03088690b7275c8118327d69d1e42b9d033c34cc283dea77a0" + "initCodeHash": "0xc50bce08abbfc66d7f902b5b1f5504c99cbdca17ca4ff98f67801e40c0751f6b", + "sourceCodeHash": "0xff40fd58a78fdfa90e8ac84bed278e6af8329fd826b478fb4fcd6641f62ae1b6" }, "src/L1/OptimismPortal2.sol": { - "initCodeHash": "0x93c5e8ff52ff8b1cedd985b4a8890c12b56d5959832405c7622615c3541908f5", - "sourceCodeHash": "0x753465f9317d89f8ea64bcd3fc3fb1164d040b9be2d5ba3d401d951f2ceff023" + "initCodeHash": "0xd1651b8a6f4d25611a0105d5cc7c1da3921417bd44da870ec63bf5ccd1bc7c63", + "sourceCodeHash": "0xce7373d8c7df47caa8b090f3afb3d2539677f12cb3eff7fc0ab1fd85638f05c1" }, "src/L1/OptimismPortalInterop.sol": { - "initCodeHash": "0x12ecac882d3a45901993f93661a5e8ada1fb964d25e7843cfb9f56b2a30aa7b4", - "sourceCodeHash": "0xa031a40aa6f722466a76a36871989942f34351a327227a62266bb9a26c439fe2" + "initCodeHash": "0xd59854648bf205dfbea96b483b2937441c32e9ef66b002468c2c14c0d6661728", + "sourceCodeHash": "0xd00b267dcf125e77c10b28c088be4378ec779927e3bcfeb6aa9a7f3d51370490" }, "src/L1/ProtocolVersions.sol": { "initCodeHash": "0x5a76c8530cb24cf23d3baacc6eefaac226382af13f1e2a35535d2ec2b0573b29", @@ -72,20 +72,20 @@ "sourceCodeHash": "0xd0471c328c1d17c5863261322bf8d5aff2e7e9e3a1135631a993aa75667621df" }, "src/L2/L2CrossDomainMessenger.sol": { - "initCodeHash": "0x6117d2ca80029c802b1e5cc36341f03ec48efd07df0251121d3faf5e93aa5901", - "sourceCodeHash": "0x48001529220d274c5cd2e84787239b6d2244780d23894e0a8e96550a40be18fe" + "initCodeHash": "0xe160be403df12709c371c33195d1b9c3b5e9499e902e86bdabc8eed749c3fd61", + "sourceCodeHash": "0x12ea125038b87e259a0d203e119faa6e9726ab2bdbc30430f820ccd48fe87e14" }, "src/L2/L2ERC721Bridge.sol": { - "initCodeHash": "0xe0c4646e3372d53c294028ecbeff97b1ecb14cf7e44361b112c62b0c0d956ea2", - "sourceCodeHash": "0xc3c7fe397f91baa0b89567b8ecb2c0aff522000fd4889346e1dfec2a281486fe" + "initCodeHash": "0x863f0f5b410983f3e51cd97c60a3a42915141b7452864d0e176571d640002b81", + "sourceCodeHash": "0xc05bfcfadfd09a56cfea68e7c1853faa36d114d9a54cd307348be143e442c35a" }, "src/L2/L2StandardBridge.sol": { - "initCodeHash": "0x0b5ec1511a7059f8eed938e68a2258485a3e24ac9ebef1f386e8dd6725b1a7c4", - "sourceCodeHash": "0xf59e693939236d00dbc5e21a5ba3e94eb442967e6a4533973d805c391e554465" + "initCodeHash": "0xba5b288a396b34488ba7be68473305529c7da7c43e5f1cfc48d6a4aecd014103", + "sourceCodeHash": "0x9dd26676cd1276c807ffd4747236783c5170d0919c70693e70b7e4c4c2675429" }, "src/L2/L2StandardBridgeInterop.sol": { - "initCodeHash": "0xa34db426a412a36c06b7a172a107af4b075d1df6997e9bb5619e14f8e52de49c", - "sourceCodeHash": "0xc2c5c34bfe7bf2833c2b4dec73f403b4c3a57d26ef0b19a9b6e4958dc0908090" + "initCodeHash": "0xa7a2e7efe8116ebb21f47ee06c1e62d3b2f5a046478094611a2ab4b714154030", + "sourceCodeHash": "0xde724da82ecf3c96b330c2876a7285b6e2b933ac599241eaa3174c443ebbe33a" }, "src/L2/L2ToL1MessagePasser.sol": { "initCodeHash": "0xf9d82084dcef31a3737a76d8ee4e5842ea190d0f77ed4678adb3bbb95217050f", diff --git a/packages/contracts-bedrock/src/L1/L1CrossDomainMessenger.sol b/packages/contracts-bedrock/src/L1/L1CrossDomainMessenger.sol index 5e475c715c8be..c1cd2efc0cea3 100644 --- a/packages/contracts-bedrock/src/L1/L1CrossDomainMessenger.sol +++ b/packages/contracts-bedrock/src/L1/L1CrossDomainMessenger.sol @@ -31,8 +31,8 @@ contract L1CrossDomainMessenger is CrossDomainMessenger, ISemver { address private spacer_253_0_20; /// @notice Semantic version. - /// @custom:semver 2.5.0 - string public constant version = "2.5.0"; + /// @custom:semver 2.6.0 + string public constant version = "2.6.0"; /// @notice Constructs the L1CrossDomainMessenger contract. constructor() { diff --git a/packages/contracts-bedrock/src/L1/L1ERC721Bridge.sol b/packages/contracts-bedrock/src/L1/L1ERC721Bridge.sol index 78afeef759cf3..51f51c8ea1047 100644 --- a/packages/contracts-bedrock/src/L1/L1ERC721Bridge.sol +++ b/packages/contracts-bedrock/src/L1/L1ERC721Bridge.sol @@ -28,8 +28,8 @@ contract L1ERC721Bridge is ERC721Bridge, ISemver { ISuperchainConfig public superchainConfig; /// @notice Semantic version. - /// @custom:semver 2.3.1 - string public constant version = "2.3.1"; + /// @custom:semver 2.4.0 + string public constant version = "2.4.0"; /// @notice Constructs the L1ERC721Bridge contract. constructor() ERC721Bridge() { diff --git a/packages/contracts-bedrock/src/L1/L1StandardBridge.sol b/packages/contracts-bedrock/src/L1/L1StandardBridge.sol index 3a22ef24adce4..d893370a022c3 100644 --- a/packages/contracts-bedrock/src/L1/L1StandardBridge.sol +++ b/packages/contracts-bedrock/src/L1/L1StandardBridge.sol @@ -74,8 +74,8 @@ contract L1StandardBridge is StandardBridge, ISemver { ); /// @notice Semantic version. - /// @custom:semver 2.2.2 - string public constant version = "2.2.2"; + /// @custom:semver 2.3.0 + string public constant version = "2.3.0"; /// @notice Address of the SuperchainConfig contract. ISuperchainConfig public superchainConfig; diff --git a/packages/contracts-bedrock/src/L1/OPContractsManager.sol b/packages/contracts-bedrock/src/L1/OPContractsManager.sol index b70b4179a0977..eb858c767098b 100644 --- a/packages/contracts-bedrock/src/L1/OPContractsManager.sol +++ b/packages/contracts-bedrock/src/L1/OPContractsManager.sol @@ -532,6 +532,20 @@ contract OPContractsManagerUpgrader is OPContractsManagerBase { assertValidOpChainConfig(_opChainConfigs[i]); ISystemConfig.Addresses memory opChainAddrs = _opChainConfigs[i].systemConfigProxy.getAddresses(); + // -------- Upgrade Contracts Stored in SystemConfig -------- + + // OptimismPortal and L1CrossDomainMessenger are being upgraded to include the fixes + // for EIP-7623 (minimum gas limits for L1 -> L2 messages). + upgradeTo(_opChainConfigs[i].proxyAdmin, opChainAddrs.optimismPortal, impls.optimismPortalImpl); + upgradeTo( + _opChainConfigs[i].proxyAdmin, opChainAddrs.l1CrossDomainMessenger, impls.l1CrossDomainMessengerImpl + ); + + // L1ERC721Bridge and L1StandardBridge are being upgraded to include the tweaks to the + // EOA checking code for EIP-7702 (code length == 23). + upgradeTo(_opChainConfigs[i].proxyAdmin, opChainAddrs.l1ERC721Bridge, impls.l1ERC721BridgeImpl); + upgradeTo(_opChainConfigs[i].proxyAdmin, opChainAddrs.l1StandardBridge, impls.l1StandardBridgeImpl); + // -------- Discover and Upgrade Proofs Contracts -------- // All chains have the Permissioned Dispute Game. @@ -1216,9 +1230,9 @@ contract OPContractsManager is ISemver { // -------- Constants and Variables -------- - /// @custom:semver 1.7.0 + /// @custom:semver 1.8.0 function version() public pure virtual returns (string memory) { - return "1.7.0"; + return "1.8.0"; } OPContractsManagerGameTypeAdder public immutable opcmGameTypeAdder; diff --git a/packages/contracts-bedrock/src/L1/OptimismPortal2.sol b/packages/contracts-bedrock/src/L1/OptimismPortal2.sol index 0573afdf46f3b..0b43241ea4f9f 100644 --- a/packages/contracts-bedrock/src/L1/OptimismPortal2.sol +++ b/packages/contracts-bedrock/src/L1/OptimismPortal2.sol @@ -178,9 +178,9 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ISemver { } /// @notice Semantic version. - /// @custom:semver 3.13.0 + /// @custom:semver 3.14.0 function version() public pure virtual returns (string memory) { - return "3.13.0"; + return "3.14.0"; } /// @notice Constructs the OptimismPortal contract. @@ -255,7 +255,7 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ISemver { /// @param _byteCount Number of bytes in the calldata. /// @return The minimum gas limit for a deposit. function minimumGasLimit(uint64 _byteCount) public pure returns (uint64) { - return _byteCount * 16 + 21000; + return _byteCount * 40 + 21000; } /// @notice Accepts value so that users can send ETH directly to this contract and have the diff --git a/packages/contracts-bedrock/src/L1/OptimismPortalInterop.sol b/packages/contracts-bedrock/src/L1/OptimismPortalInterop.sol index 5993b7f27dc5b..7920a4931b165 100644 --- a/packages/contracts-bedrock/src/L1/OptimismPortalInterop.sol +++ b/packages/contracts-bedrock/src/L1/OptimismPortalInterop.sol @@ -25,9 +25,9 @@ contract OptimismPortalInterop is OptimismPortal2 { OptimismPortal2(_proofMaturityDelaySeconds, _disputeGameFinalityDelaySeconds) { } - /// @custom:semver +interop.2 + /// @custom:semver +interop.3 function version() public pure override returns (string memory) { - return string.concat(super.version(), "+interop.2"); + return string.concat(super.version(), "+interop.3"); } /// @notice Sets static configuration options for the L2 system. diff --git a/packages/contracts-bedrock/src/L2/L2CrossDomainMessenger.sol b/packages/contracts-bedrock/src/L2/L2CrossDomainMessenger.sol index 8c5af32f016ea..3e33c55dd0c19 100644 --- a/packages/contracts-bedrock/src/L2/L2CrossDomainMessenger.sol +++ b/packages/contracts-bedrock/src/L2/L2CrossDomainMessenger.sol @@ -19,8 +19,8 @@ import { IL2ToL1MessagePasser } from "interfaces/L2/IL2ToL1MessagePasser.sol"; /// L2 on the L2 side. Users are generally encouraged to use this contract instead of lower /// level message passing contracts. contract L2CrossDomainMessenger is CrossDomainMessenger, ISemver { - /// @custom:semver 2.1.1-beta.8 - string public constant version = "2.1.1-beta.8"; + /// @custom:semver 2.2.0 + string public constant version = "2.2.0"; /// @notice Constructs the L2CrossDomainMessenger contract. constructor() { diff --git a/packages/contracts-bedrock/src/L2/L2ERC721Bridge.sol b/packages/contracts-bedrock/src/L2/L2ERC721Bridge.sol index 5f56ca1bf1f69..e080f694751f9 100644 --- a/packages/contracts-bedrock/src/L2/L2ERC721Bridge.sol +++ b/packages/contracts-bedrock/src/L2/L2ERC721Bridge.sol @@ -24,8 +24,8 @@ import { ISemver } from "interfaces/universal/ISemver.sol"; /// **WARNING**: Do not bridge an ERC721 that was originally deployed on Optimism. This /// bridge ONLY supports ERC721s originally deployed on Ethereum. contract L2ERC721Bridge is ERC721Bridge, ISemver { - /// @custom:semver 1.9.0 - string public constant version = "1.9.0"; + /// @custom:semver 1.10.0 + string public constant version = "1.10.0"; /// @notice Constructs the L2ERC721Bridge contract. constructor() ERC721Bridge() { diff --git a/packages/contracts-bedrock/src/L2/L2StandardBridge.sol b/packages/contracts-bedrock/src/L2/L2StandardBridge.sol index 53418eec97386..1e9c2ac15f315 100644 --- a/packages/contracts-bedrock/src/L2/L2StandardBridge.sol +++ b/packages/contracts-bedrock/src/L2/L2StandardBridge.sol @@ -57,9 +57,9 @@ contract L2StandardBridge is StandardBridge, ISemver { ); /// @notice Semantic version. - /// @custom:semver 1.12.0 + /// @custom:semver 1.13.0 function version() public pure virtual returns (string memory) { - return "1.12.0"; + return "1.13.0"; } /// @notice Constructs the L2StandardBridge contract. diff --git a/packages/contracts-bedrock/src/L2/L2StandardBridgeInterop.sol b/packages/contracts-bedrock/src/L2/L2StandardBridgeInterop.sol index 485d862a29313..8dd1230e12e39 100644 --- a/packages/contracts-bedrock/src/L2/L2StandardBridgeInterop.sol +++ b/packages/contracts-bedrock/src/L2/L2StandardBridgeInterop.sol @@ -39,9 +39,9 @@ contract L2StandardBridgeInterop is L2StandardBridge { event Converted(address indexed from, address indexed to, address indexed caller, uint256 amount); /// @notice Semantic version. - /// @custom:semver +interop.9 + /// @custom:semver +interop.10 function version() public pure override returns (string memory) { - return string.concat(super.version(), "+interop.9"); + return string.concat(super.version(), "+interop.10"); } /// @notice Converts `amount` of `from` token to `to` token. diff --git a/packages/contracts-bedrock/src/libraries/EOA.sol b/packages/contracts-bedrock/src/libraries/EOA.sol index 7d9d5cb719733..254cf6eabe822 100644 --- a/packages/contracts-bedrock/src/libraries/EOA.sol +++ b/packages/contracts-bedrock/src/libraries/EOA.sol @@ -9,7 +9,7 @@ library EOA { function isSenderEOA() internal view returns (bool isEOA_) { if (msg.sender == tx.origin) { isEOA_ = true; - } else { + } else if (address(msg.sender).code.length == 23) { // If the sender is not the origin, check for 7702 delegated EOAs. assembly { let ptr := mload(0x40) @@ -17,6 +17,9 @@ library EOA { extcodecopy(caller(), ptr, 0, 0x20) isEOA_ := eq(shr(232, mload(ptr)), 0xEF0100) } + } else { + // If more or less than 23 bytes of code, not a 7702 delegated EOA. + isEOA_ = false; } } } diff --git a/packages/contracts-bedrock/src/universal/CrossDomainMessenger.sol b/packages/contracts-bedrock/src/universal/CrossDomainMessenger.sol index 65b781707febe..ba21a0c7c51ea 100644 --- a/packages/contracts-bedrock/src/universal/CrossDomainMessenger.sol +++ b/packages/contracts-bedrock/src/universal/CrossDomainMessenger.sol @@ -3,6 +3,7 @@ pragma solidity 0.8.15; // Libraries import { Initializable } from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import { Math } from "@openzeppelin/contracts/utils/math/Math.sol"; import { SafeCall } from "src/libraries/SafeCall.sol"; import { Hashing } from "src/libraries/Hashing.sol"; import { Encoding } from "src/libraries/Encoding.sol"; @@ -115,6 +116,19 @@ abstract contract CrossDomainMessenger is /// call in `relayMessage`. uint64 public constant RELAY_GAS_CHECK_BUFFER = 5_000; + /// @notice Base gas required for any transaction in the EVM. + uint64 public constant TX_BASE_GAS = 21_000; + + /// @notice Floor overhead per byte of non-zero calldata in a message. Calldata floor was + /// introduced in EIP-7623. + uint64 public constant FLOOR_CALLDATA_OVERHEAD = 40; + + /// @notice Overhead added to the internal message data when the full call to relayMessage is + /// ABI encoded. This is a constant value that is specific to the V1 message encoding + /// scheme. 260 is an upper bound, actual overhead can be as low as 228 bytes for an + /// empty message. + uint64 public constant ENCODING_OVERHEAD = 260; + /// @notice Mapping of message hashes to boolean receipt values. Note that a message will only /// be present in this mapping if it has successfully been relayed on this chain, and /// can therefore not be relayed again. @@ -340,23 +354,45 @@ abstract contract CrossDomainMessenger is /// @param _message Message to compute the amount of required gas for. /// @param _minGasLimit Minimum desired gas limit when message goes to target. /// @return Amount of gas required to guarantee message receipt. - function baseGas(bytes calldata _message, uint32 _minGasLimit) public pure returns (uint64) { - return - // Constant overhead - RELAY_CONSTANT_OVERHEAD - // Calldata overhead - + (uint64(_message.length) * MIN_GAS_CALLDATA_OVERHEAD) - // Dynamic overhead (EIP-150) - + ((_minGasLimit * MIN_GAS_DYNAMIC_OVERHEAD_NUMERATOR) / MIN_GAS_DYNAMIC_OVERHEAD_DENOMINATOR) - // Gas reserved for the worst-case cost of 3/5 of the `CALL` opcode's dynamic gas - // factors. (Conservative) - + RELAY_CALL_OVERHEAD - // Relay reserved gas (to ensure execution of `relayMessage` completes after the - // subcontext finishes executing) (Conservative) - + RELAY_RESERVED_GAS - // Gas reserved for the execution between the `hasMinGas` check and the `CALL` - // opcode. (Conservative) - + RELAY_GAS_CHECK_BUFFER; + function baseGas(bytes memory _message, uint32 _minGasLimit) public pure returns (uint64) { + // Base gas should really be computed on the fully encoded message but that would break the + // expected API, so we instead just add the encoding overhead to the message length inside + // of this function. + + // We need a minimum amount of execution gas to ensure that the message will be received on + // the other side without running out of gas (stored within the failedMessages mapping). + // If we get beyond the hasMinGas check, then we *must* supply more than minGasLimit to + // the external call. + uint64 executionGas = uint64( + // Constant costs for relayMessage + RELAY_CONSTANT_OVERHEAD + // Covers dynamic parts of the CALL opcode + + RELAY_CALL_OVERHEAD + // Ensures execution of relayMessage completes after call + + RELAY_RESERVED_GAS + // Buffer between hasMinGas check and the CALL + + RELAY_GAS_CHECK_BUFFER + // Minimum gas limit, multiplied by 64/63 to account for EIP-150. + + ((_minGasLimit * MIN_GAS_DYNAMIC_OVERHEAD_NUMERATOR) / MIN_GAS_DYNAMIC_OVERHEAD_DENOMINATOR) + ); + + // Total message size is the result of properly ABI encoding the call to relayMessage. + // Since we only get the message data and not the rest of the calldata, we use the + // ENCODING_OVERHEAD constant to conservatively account for the remaining bytes. + uint64 totalMessageSize = uint64(_message.length + ENCODING_OVERHEAD); + + // Finally, replicate the transaction cost formula as defined after EIP-7623. This is + // mostly relevant in the L1 -> L2 case because we need to be able to cover the intrinsic + // cost of the message but it doesn't hurt in the L2 -> L1 case. After EIP-7623, the cost + // of a transaction is floored by its calldata size. We don't need to account for the + // contract creation case because this is always a call to relayMessage. + return TX_BASE_GAS + + uint64( + Math.max( + executionGas + (totalMessageSize * MIN_GAS_CALLDATA_OVERHEAD), + (totalMessageSize * FLOOR_CALLDATA_OVERHEAD) + ) + ); } /// @notice Initializer. diff --git a/packages/contracts-bedrock/test/L1/L1CrossDomainMessenger.t.sol b/packages/contracts-bedrock/test/L1/L1CrossDomainMessenger.t.sol index 762ac3ed19063..39d8e8c86fa97 100644 --- a/packages/contracts-bedrock/test/L1/L1CrossDomainMessenger.t.sol +++ b/packages/contracts-bedrock/test/L1/L1CrossDomainMessenger.t.sol @@ -3,7 +3,7 @@ pragma solidity 0.8.15; // Testing utilities import { CommonTest } from "test/setup/CommonTest.sol"; -import { Reverter } from "test/mocks/Callers.sol"; +import { Reverter, GasBurner } from "test/mocks/Callers.sol"; import { stdError } from "forge-std/StdError.sol"; // Libraries @@ -18,6 +18,23 @@ import { IL1CrossDomainMessenger } from "interfaces/L1/IL1CrossDomainMessenger.s import { IOptimismPortal2 } from "interfaces/L1/IOptimismPortal2.sol"; import { ISuperchainConfig } from "interfaces/L1/ISuperchainConfig.sol"; +contract Encoding_Harness { + function encodeCrossDomainMessage( + uint256 nonce, + address sender, + address target, + uint256 value, + uint256 gasLimit, + bytes memory data + ) + external + pure + returns (bytes memory) + { + return Encoding.encodeCrossDomainMessage(nonce, sender, target, value, gasLimit, data); + } +} + contract L1CrossDomainMessenger_Test is CommonTest { /// @dev The receiver address address recipient = address(0xabbaacdc); @@ -25,9 +42,13 @@ contract L1CrossDomainMessenger_Test is CommonTest { /// @dev The storage slot of the l2Sender uint256 senderSlotIndex; + /// @dev Encoding library harness. + Encoding_Harness encoding; + function setUp() public override { super.setUp(); senderSlotIndex = ForgeArtifacts.getSlot("OptimismPortal2", "l2Sender").slot; + encoding = new Encoding_Harness(); } /// @dev Tests that the implementation is initialized correctly. @@ -275,6 +296,95 @@ contract L1CrossDomainMessenger_Test is CommonTest { ); } + /// @notice Tests that the relayMessage function on L2 will always succeed for any potential + /// message, regardless of the size of the message, the minimum gas limit, or the + /// amount of gas used by the target contract. + function testFuzz_relayMessage_baseGasSufficient_succeeds( + uint24 _messageLength, + uint32 _minGasLimit, + uint32 _gasToUse + ) + external + { + // TODO(#14609): Update this test to use default.isolate = true once a new stable Foundry + // release is available that includes #9904. That will allow us to use this test to check + // for changes to the EVM itself that might cause our gas formula to be incorrect. + + // Skip if this is a fork test, won't work. + skipIfForkTest("L2CrossDomainMessenger doesn't exist on L1 in forked test"); + + // Using smaller uint so the fuzzer doesn't give as many massive values that get bounded. + // TODO: Known issue, messages above 34k can actually OOG on the receiving side if the + // target uses all available gas. Can be resolved by capping data sizes on the XDM or by + // increasing the amount of available relay gas to ~100k. If increasing relay gas, should + // have the relay gas only increase when the calldata size is large to avoid disrupting + // in-flight L2 -> L1 messages. + _messageLength = uint24(bound(_messageLength, 0, 34_000)); + + // Need more than 500 since GasBurner requires it. + // It's ok to try to use more than minGasLimit since the L2CrossDomainMessenger should + // catch the revert and store the message hash in the failedMessages mapping. + _gasToUse = uint32(bound(_gasToUse, 500, type(uint32).max)); + + // Generate random bytes, more useful than having a _message parameter. + bytes memory _message = vm.randomBytes(_messageLength); + + // Compute the base gas. + // Base gas should really be computed on the fully encoded message but that would break the + // expected API, so we instead just add the encoding overhead to the message length inside + // of the baseGas function. + uint64 baseGas = l1CrossDomainMessenger.baseGas(_message, _minGasLimit); + + // Deploy a gas burner. + address target = address(new GasBurner(_gasToUse)); + + // Encode the message. + bytes memory encoded = Encoding.encodeCrossDomainMessage( + Encoding.encodeVersionedNonce(0, 1), // nonce + alice, // Sender doesn't matter + target, + 0, // Value doesn't matter + _minGasLimit, + _message + ); + + // Count the number of non-zero bytes in the message. + uint256 zeroBytesInCalldata = 0; + uint256 nonzeroBytesInCalldata = 0; + for (uint256 i = 0; i < encoded.length; i++) { + if (encoded[i] != bytes1(0)) { + nonzeroBytesInCalldata++; + } else { + zeroBytesInCalldata++; + } + } + + // Base gas must always be sufficient to cover the floor cost from EIP-7623. + assertGt(baseGas, 21000 + ((zeroBytesInCalldata + nonzeroBytesInCalldata * 4) * 10)); + + // Actual gas on L2 will be the base gas minus the intrinsic gas cost. Note that even after + // EIP-7623, we still deduct 21k + 16 gas per calldata token from the gas limit before + // execution happens. After execution, if the message didn't spend enough in execution gas, + // the EVM will floor the cost of the transaction to 21k + 40 gas per calldata token. + uint256 gasSupplied = baseGas - (21000 + ((zeroBytesInCalldata + nonzeroBytesInCalldata * 4) * 4)); + + // We'll trigger the L2CrossDomainMessenger as if we're the L1CrossDomainMessenger + address caller = AddressAliasHelper.applyL1ToL2Alias(address(l1CrossDomainMessenger)); + vm.prank(caller); + + // Trigger the L2CrossDomainMessenger. + // Should NOT fail. + (bool success,) = address(l2CrossDomainMessenger).call{ gas: gasSupplied }(encoded); + assertTrue(success, "L2CrossDomainMessenger call should not fail"); + + // Message should either be in the failed or successful messages mapping. + bool inFailedMessages = l2CrossDomainMessenger.failedMessages(keccak256(encoded)); + bool inSuccessfulMessages = l2CrossDomainMessenger.successfulMessages(keccak256(encoded)); + assertTrue( + inFailedMessages || inSuccessfulMessages, "message should be in either failed or successful messages" + ); + } + /// @dev Tests that the relayMessage function reverts if eth is /// sent from a contract other than the standard bridge. function test_replayMessage_withValue_reverts() external { @@ -288,6 +398,61 @@ contract L1CrossDomainMessenger_Test is CommonTest { ); } + /// @notice Tests that the ENCODING_OVERHEAD is always greater than or equal to the number of + /// bytes in an encoded message OTHER than the size of the data field. + /// ENCODING_OVERHEAD needs to account for these other bytes so that the total message + /// size used in the baseGas function is accurate. This test ensures that if the + /// encoding ever changes, this test will fail and the developer will need to update + /// the ENCODING_OVERHEAD constant. + /// @param _nonce The nonce to encode into the message. + /// @param _version The version to encode into the message. + /// @param _sender The sender to encode into the message. + /// @param _target The target to encode into the message. + /// @param _value The value to encode into the message. + /// @param _minGasLimit The minimum gas limit to encode into the message. + /// @param _message The message to encode into the message. + function testFuzz_encodingOverhead_sufficient_succeeds( + uint240 _nonce, + uint16 _version, + address _sender, + address _target, + uint256 _value, + uint256 _minGasLimit, + bytes memory _message + ) + external + { + // Make sure that unexpected nonces aren't being used right now. + // Prevents people from forgetting to update this test if a new version is ever used. + if (_version > 2) { + vm.expectRevert("Encoding: unknown cross domain message version"); + encoding.encodeCrossDomainMessage( + Encoding.encodeVersionedNonce({ _nonce: 0, _version: _version }), + _sender, + _target, + _value, + _minGasLimit, + _message + ); + } + + // Clamp the version to 0 or 1. + _version = _version % 2; + + // Encode the message. + bytes memory encoded = encoding.encodeCrossDomainMessage( + Encoding.encodeVersionedNonce({ _nonce: _nonce, _version: _version }), + _sender, + _target, + _value, + _minGasLimit, + _message + ); + + // Total size should always be greater than or equal to the overhead bytes. + assertGe(l1CrossDomainMessenger.ENCODING_OVERHEAD(), encoded.length - _message.length); + } + /// @dev Tests that the xDomainMessageSender is reset to the original value /// after a message is relayed. function test_xDomainMessageSender_reset_succeeds() external { diff --git a/packages/contracts-bedrock/test/libraries/EOA.t.sol b/packages/contracts-bedrock/test/libraries/EOA.t.sol index ddead83ffe392..845fb540e6c8f 100644 --- a/packages/contracts-bedrock/test/libraries/EOA.t.sol +++ b/packages/contracts-bedrock/test/libraries/EOA.t.sol @@ -81,4 +81,27 @@ contract EOA_isEOA_Test is Test { vm.prank(sender); assertEq(harness.isSenderEOA(), false); } + + /// @notice Tests that a contract with 23 bytes of code is not detected as an EOA. + /// @param _privateKey The private key of the sender. + function testFuzz_isEOA_isContract23BytesNot7702_succeeds(uint256 _privateKey) external { + // Make sure that the private key is in the range of a valid secp256k1 private key. + _privateKey = boundPrivateKey(_privateKey); + + // Generate a random 23 byte code. + bytes memory code = vm.randomBytes(23); + + // If the code happens to be EOF code, change it! + if (code[0] == 0xEF) { + code[0] = 0xFE; // Anything but EF! + } + + // Make sure that the sender is a contract. + address sender = vm.addr(_privateKey); + vm.etch(sender, code); + + // Should not be considered an EOA. + vm.prank(sender); + assertEq(harness.isSenderEOA(), false); + } } diff --git a/packages/contracts-bedrock/test/mocks/Callers.sol b/packages/contracts-bedrock/test/mocks/Callers.sol index c64eb42352079..965d1eaa1e470 100644 --- a/packages/contracts-bedrock/test/mocks/Callers.sol +++ b/packages/contracts-bedrock/test/mocks/Callers.sol @@ -1,6 +1,9 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; +// Libraries +import { Burn } from "src/libraries/Burn.sol"; + contract CallRecorder { struct CallInfo { address sender; @@ -50,3 +53,32 @@ contract DelegateCaller { } } } + +/// @title GasBurner +/// @notice Contract that burns a specified amount of gas on receive or fallback. +contract GasBurner { + /// @notice The amount of gas to burn on receive or fallback. + uint256 immutable GAS_TO_BURN; + + /// @notice Constructor. + /// @param _gas The amount of gas to burn on receive or fallback. + constructor(uint256 _gas) { + // 500 gas buffer for Solidity overhead. + GAS_TO_BURN = _gas - 500; + } + + /// @notice Receive function that burns the specified amount of gas. + receive() external payable { + _burn(); + } + + /// @notice Fallback function that burns the specified amount of gas. + fallback() external payable { + _burn(); + } + + /// @notice Internal function that burns the specified amount of gas. + function _burn() internal view { + Burn.gas(GAS_TO_BURN); + } +} diff --git a/packages/contracts-bedrock/test/universal/CrossDomainMessenger.t.sol b/packages/contracts-bedrock/test/universal/CrossDomainMessenger.t.sol index 88c48dfed2c60..01a7ccdb9ba36 100644 --- a/packages/contracts-bedrock/test/universal/CrossDomainMessenger.t.sol +++ b/packages/contracts-bedrock/test/universal/CrossDomainMessenger.t.sol @@ -6,6 +6,7 @@ import { Test } from "forge-std/Test.sol"; import { CommonTest } from "test/setup/CommonTest.sol"; // Libraries +import { Math } from "@openzeppelin/contracts/utils/math/Math.sol"; import { Predeploys } from "src/libraries/Predeploys.sol"; import { Hashing } from "src/libraries/Hashing.sol"; import { Encoding } from "src/libraries/Encoding.sol"; @@ -39,6 +40,100 @@ contract CrossDomainMessenger_BaseGas_Test is CommonTest { uint64 minGasLimit = optimismPortal2.minimumGasLimit(uint64(_data.length)); assertTrue(baseGas >= minGasLimit); } + + /// @notice Test that baseGas returns at least the floor cost for calldata + function test_baseGas_floor_succeeds() external view { + // Create a message large enough that the floor cost would be higher than the execution gas + bytes memory largeMessage = new bytes(100_000); + + uint64 baseGasResult = l1CrossDomainMessenger.baseGas(largeMessage, 0); + + // Calculate the expected floor cost + uint64 expectedFloorCost = l1CrossDomainMessenger.TX_BASE_GAS() + + ( + uint64(largeMessage.length + l1CrossDomainMessenger.ENCODING_OVERHEAD()) + * l1CrossDomainMessenger.FLOOR_CALLDATA_OVERHEAD() + ); + + // Verify that the result is at least the floor cost + assertTrue(baseGasResult >= expectedFloorCost, "baseGas should return at least the floor cost"); + } + + /// @notice Test that baseGas returns the execution gas when it's higher than the floor cost + function test_baseGas_executionGas_succeeds() external view { + // Create a small message where execution gas would be higher than floor cost + bytes memory smallMessage = new bytes(10); + uint32 highGasLimit = 1_000_000; + + uint64 baseGasResult = l1CrossDomainMessenger.baseGas(smallMessage, highGasLimit); + + // Calculate the expected floor cost + uint64 floorCost = l1CrossDomainMessenger.TX_BASE_GAS() + + ( + uint64(smallMessage.length + l1CrossDomainMessenger.ENCODING_OVERHEAD()) + * l1CrossDomainMessenger.FLOOR_CALLDATA_OVERHEAD() + ); + + // Calculate the expected execution gas (simplified version of what's in the contract) + uint64 executionGas = l1CrossDomainMessenger.RELAY_CONSTANT_OVERHEAD() + + l1CrossDomainMessenger.RELAY_CALL_OVERHEAD() + l1CrossDomainMessenger.RELAY_RESERVED_GAS() + + l1CrossDomainMessenger.RELAY_GAS_CHECK_BUFFER() + + ( + (highGasLimit * l1CrossDomainMessenger.MIN_GAS_DYNAMIC_OVERHEAD_NUMERATOR()) + / l1CrossDomainMessenger.MIN_GAS_DYNAMIC_OVERHEAD_DENOMINATOR() + ); + + uint64 expectedExecutionGasWithOverhead = l1CrossDomainMessenger.TX_BASE_GAS() + executionGas + + ( + uint64(smallMessage.length + l1CrossDomainMessenger.ENCODING_OVERHEAD()) + * l1CrossDomainMessenger.MIN_GAS_CALLDATA_OVERHEAD() + ); + + // Verify that the result is the execution gas (which should be higher than floor cost) + assertTrue( + baseGasResult >= expectedExecutionGasWithOverhead, "baseGas should return at least the execution gas" + ); + assertTrue( + expectedExecutionGasWithOverhead > floorCost, "Execution gas should be higher than floor cost for this test" + ); + } + + /// @notice Fuzz test to verify the baseGas function correctly implements the Math.max logic + /// @param _message The message to test + /// @param _minGasLimit The minimum gas limit to test + function testFuzz_baseGas_maxLogic_succeeds(bytes calldata _message, uint32 _minGasLimit) external view { + uint64 baseGasResult = l1CrossDomainMessenger.baseGas(_message, _minGasLimit); + + // Calculate the expected execution gas + uint64 executionGas = l1CrossDomainMessenger.RELAY_CONSTANT_OVERHEAD() + + l1CrossDomainMessenger.RELAY_CALL_OVERHEAD() + l1CrossDomainMessenger.RELAY_RESERVED_GAS() + + l1CrossDomainMessenger.RELAY_GAS_CHECK_BUFFER() + + ( + (_minGasLimit * l1CrossDomainMessenger.MIN_GAS_DYNAMIC_OVERHEAD_NUMERATOR()) + / l1CrossDomainMessenger.MIN_GAS_DYNAMIC_OVERHEAD_DENOMINATOR() + ); + + uint64 executionGasWithOverhead = executionGas + + ( + uint64(_message.length + l1CrossDomainMessenger.ENCODING_OVERHEAD()) + * l1CrossDomainMessenger.MIN_GAS_CALLDATA_OVERHEAD() + ); + + // The result should be at least the maximum of the two calculations + uint64 expectedMinimum = uint64( + Math.max( + executionGasWithOverhead, + uint64(_message.length + l1CrossDomainMessenger.ENCODING_OVERHEAD()) + * l1CrossDomainMessenger.FLOOR_CALLDATA_OVERHEAD() + ) + ); + expectedMinimum += l1CrossDomainMessenger.TX_BASE_GAS(); + + assertTrue( + baseGasResult >= expectedMinimum, + "baseGas should return at least the maximum of execution gas and floor cost" + ); + } } /// @title ExternalRelay diff --git a/packages/contracts-bedrock/test/universal/Specs.t.sol b/packages/contracts-bedrock/test/universal/Specs.t.sol index 63a0b733c0e07..90ac8ae53735c 100644 --- a/packages/contracts-bedrock/test/universal/Specs.t.sol +++ b/packages/contracts-bedrock/test/universal/Specs.t.sol @@ -116,6 +116,9 @@ contract Specification_Test is CommonTest { _addSpec({ _name: "L1CrossDomainMessenger", _sel: _getSel("RELAY_CONSTANT_OVERHEAD()") }); _addSpec({ _name: "L1CrossDomainMessenger", _sel: _getSel("RELAY_GAS_CHECK_BUFFER()") }); _addSpec({ _name: "L1CrossDomainMessenger", _sel: _getSel("RELAY_RESERVED_GAS()") }); + _addSpec({ _name: "L1CrossDomainMessenger", _sel: _getSel("TX_BASE_GAS()") }); + _addSpec({ _name: "L1CrossDomainMessenger", _sel: _getSel("FLOOR_CALLDATA_OVERHEAD()") }); + _addSpec({ _name: "L1CrossDomainMessenger", _sel: _getSel("ENCODING_OVERHEAD()") }); _addSpec({ _name: "L1CrossDomainMessenger", _sel: _getSel("baseGas(bytes,uint32)") }); _addSpec({ _name: "L1CrossDomainMessenger", _sel: _getSel("failedMessages(bytes32)") }); _addSpec({ _name: "L1CrossDomainMessenger", _sel: _getSel("initialize(address,address)") });