diff --git a/packages/contracts-bedrock/interfaces/L1/IOptimismPortalInterop.sol b/packages/contracts-bedrock/interfaces/L1/IOptimismPortalInterop.sol index 6dec13a8335c9..91ee7f7d0d7e5 100644 --- a/packages/contracts-bedrock/interfaces/L1/IOptimismPortalInterop.sol +++ b/packages/contracts-bedrock/interfaces/L1/IOptimismPortalInterop.sol @@ -10,6 +10,7 @@ import { ISuperchainConfig } from "interfaces/L1/ISuperchainConfig.sol"; import { ConfigType } from "interfaces/L2/IL1BlockInterop.sol"; interface IOptimismPortalInterop { + error CustomGasTokenNotSupported(); error AlreadyFinalized(); error BadTarget(); error Blacklisted(); diff --git a/packages/contracts-bedrock/interfaces/L1/ISystemConfig.sol b/packages/contracts-bedrock/interfaces/L1/ISystemConfig.sol index e687a17e048bb..f34385d1ed248 100644 --- a/packages/contracts-bedrock/interfaces/L1/ISystemConfig.sol +++ b/packages/contracts-bedrock/interfaces/L1/ISystemConfig.sol @@ -3,6 +3,7 @@ pragma solidity ^0.8.0; import { IResourceMetering } from "interfaces/L1/IResourceMetering.sol"; +/// @notice This interface corresponds to the Custom Gas Token version of the SystemConfig contract. interface ISystemConfig { enum UpdateType { BATCHER, diff --git a/packages/contracts-bedrock/interfaces/L2/IETHLiquidity.sol b/packages/contracts-bedrock/interfaces/L2/IETHLiquidity.sol index cd87ad5c1d448..77c1c0b3caf25 100644 --- a/packages/contracts-bedrock/interfaces/L2/IETHLiquidity.sol +++ b/packages/contracts-bedrock/interfaces/L2/IETHLiquidity.sol @@ -2,6 +2,7 @@ pragma solidity ^0.8.0; interface IETHLiquidity { + error NotCustomGasToken(); error Unauthorized(); event LiquidityBurned(address indexed caller, uint256 value); diff --git a/packages/contracts-bedrock/interfaces/L2/IL1Block.sol b/packages/contracts-bedrock/interfaces/L2/IL1Block.sol index dba4a09cabaca..1e7cb415002f4 100644 --- a/packages/contracts-bedrock/interfaces/L2/IL1Block.sol +++ b/packages/contracts-bedrock/interfaces/L2/IL1Block.sol @@ -2,21 +2,26 @@ pragma solidity ^0.8.0; interface IL1Block { + error NotDepositor(); + + event GasPayingTokenSet(address indexed token, uint8 indexed decimals, bytes32 name, bytes32 symbol); + function DEPOSITOR_ACCOUNT() external pure returns (address addr_); function baseFeeScalar() external view returns (uint32); function basefee() external view returns (uint256); function batcherHash() external view returns (bytes32); function blobBaseFee() external view returns (uint256); function blobBaseFeeScalar() external view returns (uint32); - function gasPayingToken() external pure returns (address addr_, uint8 decimals_); - function gasPayingTokenName() external pure returns (string memory name_); - function gasPayingTokenSymbol() external pure returns (string memory symbol_); + function gasPayingToken() external view returns (address addr_, uint8 decimals_); + function gasPayingTokenName() external view returns (string memory name_); + function gasPayingTokenSymbol() external view returns (string memory symbol_); function hash() external view returns (bytes32); - function isCustomGasToken() external pure returns (bool is_); + function isCustomGasToken() external view returns (bool); function l1FeeOverhead() external view returns (uint256); function l1FeeScalar() external view returns (uint256); function number() external view returns (uint64); function sequenceNumber() external view returns (uint64); + function setGasPayingToken(address _token, uint8 _decimals, bytes32 _name, bytes32 _symbol) external; function setL1BlockValues( uint64 _number, uint64 _timestamp, diff --git a/packages/contracts-bedrock/interfaces/L2/IL1BlockInterop.sol b/packages/contracts-bedrock/interfaces/L2/IL1BlockInterop.sol index 8781d9eef95f6..b9fa78733e788 100644 --- a/packages/contracts-bedrock/interfaces/L2/IL1BlockInterop.sol +++ b/packages/contracts-bedrock/interfaces/L2/IL1BlockInterop.sol @@ -2,6 +2,7 @@ pragma solidity ^0.8.0; enum ConfigType { + SET_GAS_PAYING_TOKEN, ADD_DEPENDENCY, REMOVE_DEPENDENCY } @@ -16,6 +17,7 @@ interface IL1BlockInterop { event DependencyAdded(uint256 indexed chainId); event DependencyRemoved(uint256 indexed chainId); + event GasPayingTokenSet(address indexed token, uint8 indexed decimals, bytes32 name, bytes32 symbol); function DEPOSITOR_ACCOUNT() external pure returns (address addr_); function baseFeeScalar() external view returns (uint32); @@ -25,11 +27,11 @@ interface IL1BlockInterop { function blobBaseFeeScalar() external view returns (uint32); function dependencySetSize() external view returns (uint8); function depositsComplete() external; - function gasPayingToken() external pure returns (address addr_, uint8 decimals_); - function gasPayingTokenName() external pure returns (string memory name_); - function gasPayingTokenSymbol() external pure returns (string memory symbol_); + function gasPayingToken() external view returns (address addr_, uint8 decimals_); + function gasPayingTokenName() external view returns (string memory name_); + function gasPayingTokenSymbol() external view returns (string memory symbol_); function hash() external view returns (bytes32); - function isCustomGasToken() external pure returns (bool is_); + function isCustomGasToken() external view returns (bool); function isDeposit() external view returns (bool isDeposit_); function isInDependencySet(uint256 _chainId) external view returns (bool); function l1FeeOverhead() external view returns (uint256); @@ -37,6 +39,7 @@ interface IL1BlockInterop { function number() external view returns (uint64); function sequenceNumber() external view returns (uint64); function setConfig(ConfigType _type, bytes memory _value) external; + function setGasPayingToken(address _token, uint8 _decimals, bytes32 _name, bytes32 _symbol) external; function setL1BlockValues( uint64 _number, uint64 _timestamp, diff --git a/packages/contracts-bedrock/interfaces/L2/ISuperchainWETH.sol b/packages/contracts-bedrock/interfaces/L2/ISuperchainWETH.sol index e646807b8d158..a6b6ef3ce7bf7 100644 --- a/packages/contracts-bedrock/interfaces/L2/ISuperchainWETH.sol +++ b/packages/contracts-bedrock/interfaces/L2/ISuperchainWETH.sol @@ -7,6 +7,7 @@ import { ISemver } from "interfaces/universal/ISemver.sol"; interface ISuperchainWETH is IWETH98, IERC7802, ISemver { error Unauthorized(); + error NotCustomGasToken(); error InvalidCrossDomainSender(); error ZeroAddress(); @@ -15,7 +16,7 @@ interface ISuperchainWETH is IWETH98, IERC7802, ISemver { event RelayETH(address indexed from, address indexed to, uint256 amount, uint256 source); function balanceOf(address src) external view returns (uint256); - function withdraw(uint256 wad) external; + function withdraw(uint256 _amount) external; function supportsInterface(bytes4 _interfaceId) external view returns (bool); function sendETH(address _to, uint256 _chainId) external payable returns (bytes32 msgHash_); function relayETH(address _from, address _to, uint256 _amount) external; diff --git a/packages/contracts-bedrock/scripts/deploy/Deploy.s.sol b/packages/contracts-bedrock/scripts/deploy/Deploy.s.sol index 7e6e22533109b..8207f81d445ca 100644 --- a/packages/contracts-bedrock/scripts/deploy/Deploy.s.sol +++ b/packages/contracts-bedrock/scripts/deploy/Deploy.s.sol @@ -210,6 +210,12 @@ contract Deploy is Deployer { ); vm.stopPrank(); + if (cfg.useCustomGasToken()) { + // Reset the systemconfig then reinitialize it with the custom gas token + resetInitializedProxy("SystemConfig"); + initializeSystemConfig(); + } + if (cfg.useAltDA()) { bytes32 typeHash = keccak256(bytes(cfg.daCommitmentType())); bytes32 keccakHash = keccak256(bytes("KeccakCommitment")); @@ -499,6 +505,11 @@ contract Deploy is Deployer { bytes32 batcherHash = bytes32(uint256(uint160(cfg.batchSenderAddress()))); + address customGasTokenAddress = Constants.ETHER; + if (cfg.useCustomGasToken()) { + customGasTokenAddress = cfg.customGasTokenAddress(); + } + IProxyAdmin proxyAdmin = IProxyAdmin(payable(artifacts.mustGetAddress("ProxyAdmin"))); proxyAdmin.upgradeAndCall({ _proxy: payable(systemConfigProxy), diff --git a/packages/contracts-bedrock/scripts/deploy/DeployConfig.s.sol b/packages/contracts-bedrock/scripts/deploy/DeployConfig.s.sol index 0b1c8c8e0a39d..13ccedab62ef7 100644 --- a/packages/contracts-bedrock/scripts/deploy/DeployConfig.s.sol +++ b/packages/contracts-bedrock/scripts/deploy/DeployConfig.s.sol @@ -85,6 +85,9 @@ contract DeployConfig is Script { uint256 public daBondSize; uint256 public daResolverRefundPercentage; + bool public useCustomGasToken; + address public customGasTokenAddress; + bool public useInterop; bool public useSoulGasToken; bool public isSoulBackedByNative; @@ -170,6 +173,9 @@ contract DeployConfig is Script { daBondSize = _readOr(_json, "$.daBondSize", 1000000000); daResolverRefundPercentage = _readOr(_json, "$.daResolverRefundPercentage", 0); + useCustomGasToken = _readOr(_json, "$.useCustomGasToken", false); + customGasTokenAddress = _readOr(_json, "$.customGasTokenAddress", address(0)); + useInterop = _readOr(_json, "$.useInterop", false); useSoulGasToken = _readOr(_json, "$.useSoulGasToken", false); isSoulBackedByNative = _readOr(_json, "$.isSoulBackedByNative", false); @@ -240,6 +246,12 @@ contract DeployConfig is Script { fundDevAccounts = _fundDevAccounts; } + /// @notice Allow the `useCustomGasToken` config to be overridden in testing environments + function setUseCustomGasToken(address _token) public { + useCustomGasToken = true; + customGasTokenAddress = _token; + } + /// @notice Allow the `useUpgradedFork` config to be overridden in testing environments /// @dev When true, the forked system WILL be upgraded in setUp(). /// When false, the forked system WILL NOT be upgraded in setUp(). diff --git a/packages/contracts-bedrock/snapshots/.gas-snapshot b/packages/contracts-bedrock/snapshots/.gas-snapshot new file mode 100644 index 0000000000000..87a5af9334469 --- /dev/null +++ b/packages/contracts-bedrock/snapshots/.gas-snapshot @@ -0,0 +1,13 @@ +GasBenchMark_L1BlockInterop_DepositsComplete:test_depositsComplete_benchmark() (gas: 7589) +GasBenchMark_L1BlockInterop_DepositsComplete_Warm:test_depositsComplete_benchmark() (gas: 5589) +GasBenchMark_L1BlockInterop_SetValuesInterop:test_setL1BlockValuesInterop_benchmark() (gas: 175722) +GasBenchMark_L1BlockInterop_SetValuesInterop_Warm:test_setL1BlockValuesInterop_benchmark() (gas: 5144) +GasBenchMark_L1Block_SetValuesEcotone:test_setL1BlockValuesEcotone_benchmark() (gas: 158553) +GasBenchMark_L1Block_SetValuesEcotone_Warm:test_setL1BlockValuesEcotone_benchmark() (gas: 7619) +GasBenchMark_L1CrossDomainMessenger:test_sendMessage_benchmark_0() (gas: 356487) +GasBenchMark_L1CrossDomainMessenger:test_sendMessage_benchmark_1() (gas: 2954716) +GasBenchMark_L1StandardBridge_Deposit:test_depositERC20_benchmark_0() (gas: 551627) +GasBenchMark_L1StandardBridge_Deposit:test_depositERC20_benchmark_1() (gas: 4063775) +GasBenchMark_L1StandardBridge_Deposit:test_depositETH_benchmark_0() (gas: 450267) +GasBenchMark_L1StandardBridge_Deposit:test_depositETH_benchmark_1() (gas: 3496188) +GasBenchMark_L1StandardBridge_Finalize:test_finalizeETHWithdrawal_benchmark() (gas: 59798) \ No newline at end of file diff --git a/packages/contracts-bedrock/snapshots/abi/ETHLiquidity.json b/packages/contracts-bedrock/snapshots/abi/ETHLiquidity.json index 72385798e5f89..5fd386c52e5ae 100644 --- a/packages/contracts-bedrock/snapshots/abi/ETHLiquidity.json +++ b/packages/contracts-bedrock/snapshots/abi/ETHLiquidity.json @@ -70,6 +70,11 @@ "name": "LiquidityMinted", "type": "event" }, + { + "inputs": [], + "name": "NotCustomGasToken", + "type": "error" + }, { "inputs": [], "name": "Unauthorized", diff --git a/packages/contracts-bedrock/snapshots/abi/L1Block.json b/packages/contracts-bedrock/snapshots/abi/L1Block.json index cab59f5c3f784..a32eff778d6f2 100644 --- a/packages/contracts-bedrock/snapshots/abi/L1Block.json +++ b/packages/contracts-bedrock/snapshots/abi/L1Block.json @@ -111,7 +111,7 @@ "type": "uint8" } ], - "stateMutability": "pure", + "stateMutability": "view", "type": "function" }, { @@ -124,7 +124,7 @@ "type": "string" } ], - "stateMutability": "pure", + "stateMutability": "view", "type": "function" }, { @@ -137,7 +137,7 @@ "type": "string" } ], - "stateMutability": "pure", + "stateMutability": "view", "type": "function" }, { @@ -172,11 +172,11 @@ "outputs": [ { "internalType": "bool", - "name": "is_", + "name": "", "type": "bool" } ], - "stateMutability": "pure", + "stateMutability": "view", "type": "function" }, { @@ -231,6 +231,34 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [ + { + "internalType": "address", + "name": "_token", + "type": "address" + }, + { + "internalType": "uint8", + "name": "_decimals", + "type": "uint8" + }, + { + "internalType": "bytes32", + "name": "_name", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "_symbol", + "type": "bytes32" + } + ], + "name": "setGasPayingToken", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [ { @@ -311,5 +339,41 @@ ], "stateMutability": "pure", "type": "function" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "indexed": true, + "internalType": "uint8", + "name": "decimals", + "type": "uint8" + }, + { + "indexed": false, + "internalType": "bytes32", + "name": "name", + "type": "bytes32" + }, + { + "indexed": false, + "internalType": "bytes32", + "name": "symbol", + "type": "bytes32" + } + ], + "name": "GasPayingTokenSet", + "type": "event" + }, + { + "inputs": [], + "name": "NotDepositor", + "type": "error" } ] \ No newline at end of file diff --git a/packages/contracts-bedrock/snapshots/abi/L1BlockInterop.json b/packages/contracts-bedrock/snapshots/abi/L1BlockInterop.json index a34befff9274f..1d54b5af228ca 100644 --- a/packages/contracts-bedrock/snapshots/abi/L1BlockInterop.json +++ b/packages/contracts-bedrock/snapshots/abi/L1BlockInterop.json @@ -131,7 +131,7 @@ "type": "uint8" } ], - "stateMutability": "pure", + "stateMutability": "view", "type": "function" }, { @@ -144,7 +144,7 @@ "type": "string" } ], - "stateMutability": "pure", + "stateMutability": "view", "type": "function" }, { @@ -157,7 +157,7 @@ "type": "string" } ], - "stateMutability": "pure", + "stateMutability": "view", "type": "function" }, { @@ -192,11 +192,11 @@ "outputs": [ { "internalType": "bool", - "name": "is_", + "name": "", "type": "bool" } ], - "stateMutability": "pure", + "stateMutability": "view", "type": "function" }, { @@ -301,6 +301,34 @@ "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [ + { + "internalType": "address", + "name": "_token", + "type": "address" + }, + { + "internalType": "uint8", + "name": "_decimals", + "type": "uint8" + }, + { + "internalType": "bytes32", + "name": "_name", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "_symbol", + "type": "bytes32" + } + ], + "name": "setGasPayingToken", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [ { @@ -415,6 +443,37 @@ "name": "DependencyRemoved", "type": "event" }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "indexed": true, + "internalType": "uint8", + "name": "decimals", + "type": "uint8" + }, + { + "indexed": false, + "internalType": "bytes32", + "name": "name", + "type": "bytes32" + }, + { + "indexed": false, + "internalType": "bytes32", + "name": "symbol", + "type": "bytes32" + } + ], + "name": "GasPayingTokenSet", + "type": "event" + }, { "inputs": [], "name": "AlreadyDependency", diff --git a/packages/contracts-bedrock/snapshots/abi/OptimismPortalInterop.json b/packages/contracts-bedrock/snapshots/abi/OptimismPortalInterop.json index 2dd27e68eaff9..b99b23ad029ff 100644 --- a/packages/contracts-bedrock/snapshots/abi/OptimismPortalInterop.json +++ b/packages/contracts-bedrock/snapshots/abi/OptimismPortalInterop.json @@ -785,6 +785,11 @@ "name": "ContentLengthMismatch", "type": "error" }, + { + "inputs": [], + "name": "CustomGasTokenNotSupported", + "type": "error" + }, { "inputs": [], "name": "EmptyItem", diff --git a/packages/contracts-bedrock/snapshots/abi/SuperchainWETH.json b/packages/contracts-bedrock/snapshots/abi/SuperchainWETH.json index 24b27063f3423..e8df09a806406 100644 --- a/packages/contracts-bedrock/snapshots/abi/SuperchainWETH.json +++ b/packages/contracts-bedrock/snapshots/abi/SuperchainWETH.json @@ -305,7 +305,7 @@ "inputs": [ { "internalType": "uint256", - "name": "wad", + "name": "_amount", "type": "uint256" } ], @@ -519,6 +519,11 @@ "name": "InvalidCrossDomainSender", "type": "error" }, + { + "inputs": [], + "name": "NotCustomGasToken", + "type": "error" + }, { "inputs": [], "name": "Unauthorized", diff --git a/packages/contracts-bedrock/snapshots/abi/WETH.json b/packages/contracts-bedrock/snapshots/abi/WETH.json index 0f97edfd828d3..03cf2880ccce0 100644 --- a/packages/contracts-bedrock/snapshots/abi/WETH.json +++ b/packages/contracts-bedrock/snapshots/abi/WETH.json @@ -104,7 +104,7 @@ "type": "string" } ], - "stateMutability": "pure", + "stateMutability": "view", "type": "function" }, { @@ -117,7 +117,7 @@ "type": "string" } ], - "stateMutability": "pure", + "stateMutability": "view", "type": "function" }, { diff --git a/packages/contracts-bedrock/snapshots/semver-lock.json b/packages/contracts-bedrock/snapshots/semver-lock.json index 0dacf1d7956b6..2874e19199a96 100644 --- a/packages/contracts-bedrock/snapshots/semver-lock.json +++ b/packages/contracts-bedrock/snapshots/semver-lock.json @@ -4,8 +4,8 @@ "sourceCodeHash": "0xe772f7db8033e4a738850cb28ac4849d3a454c93732135a8a10d4f7cb498088e" }, "src/L1/L1CrossDomainMessenger.sol": { - "initCodeHash": "0x03a3c0eb2418aba9f93bb89723ba2ee7cb9e1988ca00f380503c960149c85b7a", - "sourceCodeHash": "0x5907cdb82ec5f6bef2184558a511d049ab3ee65388cf44d0c20d0f234ef8ca44" + "initCodeHash": "0x2ea7673ba4d64378590ea026c7cdd8af7ac9baf0a43194ee92521e0ee3fff3b1", + "sourceCodeHash": "0x6033b2929fb629c60cf96735cec8a1e3cefb223831083de02bf9b78296002de2" }, "src/L1/L1ERC721Bridge.sol": { "initCodeHash": "0x86ff2f104ae7aa24e34abac9b5467b4c803183d507ba8dae6e156ae2d7c4aca7", @@ -13,7 +13,7 @@ }, "src/L1/L1StandardBridge.sol": { "initCodeHash": "0x6aca5250f7c6248e455ccc7a689a818815610449a5fb8762bb1902646694f6ac", - "sourceCodeHash": "0xf9ba98657dc235355146e381b654fe3ed766feb7cd87636ec0c9d4c6dd3e1973" + "sourceCodeHash": "0x41c556b7eb53b96e30f913a7ca61a2e857adfa39c638e8d5c240c7666c581a6e" }, "src/L1/OPContractsManager.sol": { "initCodeHash": "0x792cdc28c9fb20ea9afa840cc62f928195bb1e9e6aef8738e7a289a43b91ef80", @@ -28,8 +28,8 @@ "sourceCodeHash": "0x09d877d68eb2439759e0a912a5d86d85b4742dfd205bdc1ac9dfbb32f8ccb7b0" }, "src/L1/OptimismPortalInterop.sol": { - "initCodeHash": "0x1d97b348b70a43a4dd96f8285252593931f4e0b2ff4e6d549169e82c8ddea4f9", - "sourceCodeHash": "0x1610886629862a69b043f45fa8a187341f5a9ab05fd2c2d84f0add56e2be4439" + "initCodeHash": "0x7c0a2f5bcf9e75573d481f680c58404d00ed08b52d8dc53ede73f26d37597c73", + "sourceCodeHash": "0x455a2cc3efa3f39b488ee56bbe7b6b29a6b4b29782b320b0fad04bb24f145252" }, "src/L1/ProtocolVersions.sol": { "initCodeHash": "0x5a76c8530cb24cf23d3baacc6eefaac226382af13f1e2a35535d2ec2b0573b29", @@ -44,7 +44,7 @@ "sourceCodeHash": "0x9016b1979c2f1def83a849389543708d857cf0430756815737dadda8e63047c5" }, "src/L1/SystemConfigInterop.sol": { - "initCodeHash": "0x36fd6c64d81c83fd97e039ab77fb6dfd3a76f12faba923bf04491bb124b590e8", + "initCodeHash": "0xede91fbecb59329ef5c7c05790a52ebc25436e1067eac27d39257df274d97f71", "sourceCodeHash": "0x609a10f2f85a2b1cc60a5accd795f65c84edc09b0e98124011bd9e7caeb905d9" }, "src/L2/BaseFeeVault.sol": { @@ -56,40 +56,40 @@ "sourceCodeHash": "0x661d7659f09b7f909e8bd5e6c41e8c98f2091036ed2123b7e18a1a74120bd849" }, "src/L2/ETHLiquidity.sol": { - "initCodeHash": "0x776ece4a1bb24d97287806769470327641da240b083898a90943e2844957cc46", - "sourceCodeHash": "0xe5c08ce62327113e4bbaf29f47e5f1ddfad6fbd63c07132eedfba5af5325f331" + "initCodeHash": "0xdc7075deb7e7c407e6ec3be33ce352ee4a9ca0e82f4bffeb360c93b395a2452f", + "sourceCodeHash": "0x92bf6a7a13d1331cbfed35e22130b54b00576bd31de5feb309dfadef2a0b7dd9" }, "src/L2/GasPriceOracle.sol": { "initCodeHash": "0x83d50e3b34cd1b4de32f1cced28796b07aefc526cc17ceb1903ad55f4abc90b7", "sourceCodeHash": "0x305c72d7be9149fce7095bd4641a1a19acada3126fbc43599f674cadbf6e7d6c" }, "src/L2/L1Block.sol": { - "initCodeHash": "0x935992f6d9675ea6deb8477632e0876996c013743f9ccf64ba0019d4bb0c41a7", - "sourceCodeHash": "0x603894eec774b098b74e0caf653763bd1f6cf840f32ae27210ca69eafb78f22d" + "initCodeHash": "0x33f35f0b23e1c76afa86a10eeb50e4a312ac2dda175202480de68c9d7c60abbe", + "sourceCodeHash": "0xc9012dfd79ed68e448942aa75522f4f4dbca955c73f877584becbf8155bd4a75" }, "src/L2/L1BlockInterop.sol": { - "initCodeHash": "0x3528adb272a1c209044493c722668ec250a9f5860ec78ed50e063322724fbd94", - "sourceCodeHash": "0x01017177d32f567df0273acb1561043d97cf0a36308a95917e0f402682ae5209" + "initCodeHash": "0xb96ef2536087b67b919d592f619999244652390825f79626f690bc1381093992", + "sourceCodeHash": "0x9493f90136917fc95d2ac942f061c1b9cffeff6d327afb46fe4e69784e7f2100" }, "src/L2/L1FeeVault.sol": { "initCodeHash": "0x6745b7be3895a5e8d373df0066d931bae29c47672ac46c2f5829bd0052cc6d9e", "sourceCodeHash": "0xd0471c328c1d17c5863261322bf8d5aff2e7e9e3a1135631a993aa75667621df" }, "src/L2/L2CrossDomainMessenger.sol": { - "initCodeHash": "0x6117d2ca80029c802b1e5cc36341f03ec48efd07df0251121d3faf5e93aa5901", - "sourceCodeHash": "0x48001529220d274c5cd2e84787239b6d2244780d23894e0a8e96550a40be18fe" + "initCodeHash": "0xa6adb9d117af95f1a8be066b67a56ed8a029dd692ca363d636ad251ec9179945", + "sourceCodeHash": "0x74882c3cca14807219789b5b2b264ebd17479b409042c09db2da52736f9de36e" }, "src/L2/L2ERC721Bridge.sol": { "initCodeHash": "0xea899e672803634b98d619174bf85dc8b3f7e6407bb7306eb72ed4c8eefce0c0", "sourceCodeHash": "0xea896e18eceb9ba6e8125e9f3371549787e082db4b26d642b279b5697651d473" }, "src/L2/L2StandardBridge.sol": { - "initCodeHash": "0xbc702854c3b6da0e5c476fabc29b1a2464bee3a1ebda7175232a5c41d7ace5e6", - "sourceCodeHash": "0x0a2ea11f3114fd1c62fae90feec5032930afc898ac75af52a67a5266eeeedf9b" + "initCodeHash": "0xde3b3a1aa0056d25ab3f4ad6e29b9d56c4740b29cd9bef36588d849ffc0178f6", + "sourceCodeHash": "0x6c32dba4550b9c82d80666796e89c77799c21001691a116663970508121be03b" }, "src/L2/L2StandardBridgeInterop.sol": { - "initCodeHash": "0x38977293cf77c047b1c3f0df5848ee9d52f742ddc7916b67c4f714c103a5a2a5", - "sourceCodeHash": "0xd0f5eaf7c8f1d14f2f997f634e279d0c4fd6843cb20b0e7eb9b9c757da591d38" + "initCodeHash": "0xb8df14bf93beb53be269c641c80b50d6ad872894da49d5f60d0b976e0ba8839e", + "sourceCodeHash": "0x3b35ee19099fc4ffb111a4cd774434c414e56d33017cce47fdba3f712e2429b1" }, "src/L2/L2ToL1MessagePasser.sol": { "initCodeHash": "0xf9d82084dcef31a3737a76d8ee4e5842ea190d0f77ed4678adb3bbb95217050f", @@ -132,12 +132,12 @@ "sourceCodeHash": "0xcd2b49cb7cf6d18616ee8bec9183fe5b5b460941875bc0b4158c4d5390ec3b0c" }, "src/L2/SuperchainWETH.sol": { - "initCodeHash": "0x545686820e440d72529c815b7406844272d5ec33b741b2be6ebbe3a3db1ca8ad", - "sourceCodeHash": "0x6145e61cc0a0c95db882a76ecffea15c358c2b574d5157e53b85a69908701613" + "initCodeHash": "0x6ded8aeea6edf7e0ead7b0d2a12ef236f1fb7d21980a1dd564cbe86affca7927", + "sourceCodeHash": "0x11d711704a5afcae6076d017ee001b25bc705728973b1ad2e6a32274a8475f50" }, "src/L2/WETH.sol": { - "initCodeHash": "0x38b396fc35d72e8013bad2fe8d7dea5285499406d4c4b62e27c54252e1e0f00a", - "sourceCodeHash": "0xf4f83ca89d2519045a2916c670bda66f39b431a13921e639a5342bfc6157b178" + "initCodeHash": "0x480d4f8dbec1b0d3211bccbbdfb69796f3e90c784f724b1bbfd4703b0aafdeba", + "sourceCodeHash": "0xe9964aa66db1dfc86772958b4c9276697e67f7055529a43e6a49a055009bc995" }, "src/cannon/MIPS.sol": { "initCodeHash": "0x9d8a3c089fb84919159403a961fe0514d8be00f07b3a8be1a13a9289cc0ad11a", diff --git a/packages/contracts-bedrock/src/L1/L1CrossDomainMessenger.sol b/packages/contracts-bedrock/src/L1/L1CrossDomainMessenger.sol index 5e475c715c8be..5ed58061d60b9 100644 --- a/packages/contracts-bedrock/src/L1/L1CrossDomainMessenger.sol +++ b/packages/contracts-bedrock/src/L1/L1CrossDomainMessenger.sol @@ -6,6 +6,7 @@ import { CrossDomainMessenger } from "src/universal/CrossDomainMessenger.sol"; // Libraries import { Predeploys } from "src/libraries/Predeploys.sol"; +import { Constants } from "src/libraries/Constants.sol"; // Interfaces import { ISemver } from "interfaces/universal/ISemver.sol"; @@ -31,6 +32,7 @@ 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"; @@ -48,6 +50,13 @@ contract L1CrossDomainMessenger is CrossDomainMessenger, ISemver { __CrossDomainMessenger_init({ _otherMessenger: CrossDomainMessenger(Predeploys.L2_CROSS_DOMAIN_MESSENGER) }); } + /// @inheritdoc CrossDomainMessenger + /// @dev This is added to maintain compatibility with the CrossDomainMessenger abstract contract and should always + /// return the ether address and 18 decimals. + function gasPayingToken() internal pure override returns (address addr_, uint8 decimals_) { + return (Constants.ETHER, 18); + } + /// @notice Getter function for the OptimismPortal contract on this chain. /// Public getter is legacy and will be removed in the future. Use `portal()` instead. /// @return Contract of the OptimismPortal on this chain. diff --git a/packages/contracts-bedrock/src/L1/L1StandardBridge.sol b/packages/contracts-bedrock/src/L1/L1StandardBridge.sol index bb5737716a230..2ac676f507f6b 100644 --- a/packages/contracts-bedrock/src/L1/L1StandardBridge.sol +++ b/packages/contracts-bedrock/src/L1/L1StandardBridge.sol @@ -6,6 +6,7 @@ import { StandardBridge } from "src/universal/StandardBridge.sol"; // Libraries import { Predeploys } from "src/libraries/Predeploys.sol"; +import { Constants } from "src/libraries/Constants.sol"; // Interfaces import { ISemver } from "interfaces/universal/ISemver.sol"; @@ -74,6 +75,7 @@ contract L1StandardBridge is StandardBridge, ISemver { ); /// @notice Semantic version. + /// @custom:semver 2.2.1 string public constant version = "2.2.1"; @@ -111,6 +113,13 @@ contract L1StandardBridge is StandardBridge, ISemver { _initiateETHDeposit(msg.sender, msg.sender, RECEIVE_DEFAULT_GAS_LIMIT, bytes("")); } + /// @inheritdoc StandardBridge + /// @dev This is added to maintain compatibility with the CrossDomainMessenger abstract contract and should always + /// return the ether address and 18 decimals. + function gasPayingToken() internal pure override returns (address addr_, uint8 decimals_) { + return (Constants.ETHER, 18); + } + /// @custom:legacy /// @notice Deposits some amount of ETH into the sender's account on L2. /// @param _minGasLimit Minimum gas limit for the deposit message on L2. diff --git a/packages/contracts-bedrock/src/L1/OptimismPortalInterop.sol b/packages/contracts-bedrock/src/L1/OptimismPortalInterop.sol index 9155db4eb72f5..a5ae31b46caa1 100644 --- a/packages/contracts-bedrock/src/L1/OptimismPortalInterop.sol +++ b/packages/contracts-bedrock/src/L1/OptimismPortalInterop.sol @@ -12,6 +12,9 @@ import { Unauthorized } from "src/libraries/PortalErrors.sol"; // Interfaces import { IL1BlockInterop, ConfigType } from "interfaces/L2/IL1BlockInterop.sol"; +/// @notice Error thrown when attempting to use custom gas token specific actions. +error CustomGasTokenNotSupported(); + /// @custom:proxied true /// @title OptimismPortalInterop /// @notice The OptimismPortal is a low-level contract responsible for passing messages between L1 @@ -35,6 +38,7 @@ contract OptimismPortalInterop is OptimismPortal2 { /// @param _value Encoded value of the configuration. function setConfig(ConfigType _type, bytes memory _value) external { if (msg.sender != address(systemConfig)) revert Unauthorized(); + if (_type == ConfigType.SET_GAS_PAYING_TOKEN) revert CustomGasTokenNotSupported(); // Set L2 deposit gas as used without paying burning gas. Ensures that deposits cannot use too much L2 gas. // This value must be large enough to cover the cost of calling `L1Block.setConfig`. diff --git a/packages/contracts-bedrock/src/L2/ETHLiquidity.sol b/packages/contracts-bedrock/src/L2/ETHLiquidity.sol index 5a8c97f819b8e..2b876d1e52877 100644 --- a/packages/contracts-bedrock/src/L2/ETHLiquidity.sol +++ b/packages/contracts-bedrock/src/L2/ETHLiquidity.sol @@ -5,11 +5,12 @@ pragma solidity 0.8.15; import { SafeSend } from "src/universal/SafeSend.sol"; // Libraries -import { Unauthorized } from "src/libraries/errors/CommonErrors.sol"; +import { Unauthorized, NotCustomGasToken } from "src/libraries/errors/CommonErrors.sol"; import { Predeploys } from "src/libraries/Predeploys.sol"; // Interfaces import { ISemver } from "interfaces/universal/ISemver.sol"; +import { IL1Block } from "interfaces/L2/IL1Block.sol"; /// @custom:proxied true /// @custom:predeploy 0x4200000000000000000000000000000000000025 @@ -24,12 +25,13 @@ contract ETHLiquidity is ISemver { event LiquidityMinted(address indexed caller, uint256 value); /// @notice Semantic version. - /// @custom:semver 1.0.0-beta.6 - string public constant version = "1.0.0-beta.6"; + /// @custom:semver 1.0.0-beta.5 + string public constant version = "1.0.0-beta.5"; /// @notice Allows an address to lock ETH liquidity into this contract. function burn() external payable { if (msg.sender != Predeploys.SUPERCHAIN_WETH) revert Unauthorized(); + if (IL1Block(Predeploys.L1_BLOCK_ATTRIBUTES).isCustomGasToken()) revert NotCustomGasToken(); emit LiquidityBurned(msg.sender, msg.value); } @@ -37,6 +39,7 @@ contract ETHLiquidity is ISemver { /// @param _amount The amount of liquidity to unlock. function mint(uint256 _amount) external { if (msg.sender != Predeploys.SUPERCHAIN_WETH) revert Unauthorized(); + if (IL1Block(Predeploys.L1_BLOCK_ATTRIBUTES).isCustomGasToken()) revert NotCustomGasToken(); new SafeSend{ value: _amount }(payable(msg.sender)); emit LiquidityMinted(msg.sender, _amount); } diff --git a/packages/contracts-bedrock/src/L2/L1Block.sol b/packages/contracts-bedrock/src/L2/L1Block.sol index 9814adbf843a7..48d6e70e9c504 100644 --- a/packages/contracts-bedrock/src/L2/L1Block.sol +++ b/packages/contracts-bedrock/src/L2/L1Block.sol @@ -3,6 +3,7 @@ pragma solidity 0.8.15; // Libraries import { Constants } from "src/libraries/Constants.sol"; +import { GasPayingToken, IGasToken } from "src/libraries/GasPayingToken.sol"; import { NotDepositor } from "src/libraries/L1BlockErrors.sol"; // Interfaces @@ -15,7 +16,10 @@ import { ISemver } from "interfaces/universal/ISemver.sol"; /// Values within this contract are updated once per epoch (every L1 block) and can only be /// set by the "depositor" account, a special system address. Depositor account transactions /// are created by the protocol whenever we move to a new epoch. -contract L1Block is ISemver { +contract L1Block is ISemver, IGasToken { + /// @notice Event emitted when the gas paying token is set. + event GasPayingTokenSet(address indexed token, uint8 indexed decimals, bytes32 name, bytes32 symbol); + /// @notice Address of the special depositor account. function DEPOSITOR_ACCOUNT() public pure returns (address addr_) { addr_ = Constants.DEPOSITOR_ACCOUNT; @@ -56,35 +60,34 @@ contract L1Block is ISemver { /// @notice The latest L1 blob base fee. uint256 public blobBaseFee; - /// @custom:semver 1.5.1-beta.6 + /// @custom:semver 1.5.1-beta.5 function version() public pure virtual returns (string memory) { - return "1.5.1-beta.6"; + return "1.5.1-beta.5"; } /// @notice Returns the gas paying token, its decimals, name and symbol. - function gasPayingToken() public pure returns (address addr_, uint8 decimals_) { - addr_ = Constants.ETHER; - decimals_ = 18; + /// If nothing is set in state, then it means ether is used. + function gasPayingToken() public view returns (address addr_, uint8 decimals_) { + (addr_, decimals_) = GasPayingToken.getToken(); } /// @notice Returns the gas paying token name. /// If nothing is set in state, then it means ether is used. - /// This function cannot be removed because WETH depends on it. - function gasPayingTokenName() public pure returns (string memory name_) { - name_ = "Ether"; + function gasPayingTokenName() public view returns (string memory name_) { + name_ = GasPayingToken.getName(); } /// @notice Returns the gas paying token symbol. /// If nothing is set in state, then it means ether is used. - /// This function cannot be removed because WETH depends on it. - function gasPayingTokenSymbol() public pure returns (string memory symbol_) { - symbol_ = "ETH"; + function gasPayingTokenSymbol() public view returns (string memory symbol_) { + symbol_ = GasPayingToken.getSymbol(); } /// @notice Getter for custom gas token paying networks. Returns true if the /// network uses a custom gas token. - function isCustomGasToken() public pure returns (bool is_) { - is_ = false; + function isCustomGasToken() public view returns (bool) { + (address token,) = gasPayingToken(); + return token != Constants.ETHER; } /// @notice size of historyHashes. @@ -201,4 +204,15 @@ contract L1Block is ISemver { function historySize() external pure returns (uint256) { return HISTORY_SIZE; } + + /// @notice Sets the gas paying token for the L2 system. Can only be called by the special + /// depositor account. This function is not called on every L2 block but instead + /// only called by specially crafted L1 deposit transactions. + function setGasPayingToken(address _token, uint8 _decimals, bytes32 _name, bytes32 _symbol) external { + if (msg.sender != DEPOSITOR_ACCOUNT()) revert NotDepositor(); + + GasPayingToken.set({ _token: _token, _decimals: _decimals, _name: _name, _symbol: _symbol }); + + emit GasPayingTokenSet({ token: _token, decimals: _decimals, name: _name, symbol: _symbol }); + } } diff --git a/packages/contracts-bedrock/src/L2/L1BlockInterop.sol b/packages/contracts-bedrock/src/L2/L1BlockInterop.sol index 3cc03c1ef5349..7b92b202a052d 100644 --- a/packages/contracts-bedrock/src/L2/L1BlockInterop.sol +++ b/packages/contracts-bedrock/src/L2/L1BlockInterop.sol @@ -6,6 +6,7 @@ import { L1Block } from "src/L2/L1Block.sol"; // Libraries import { EnumerableSet } from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; +import { GasPayingToken } from "src/libraries/GasPayingToken.sol"; import { StaticConfig } from "src/libraries/StaticConfig.sol"; import { Predeploys } from "src/libraries/Predeploys.sol"; import { @@ -18,9 +19,11 @@ import { } from "src/libraries/L1BlockErrors.sol"; /// @notice Enum representing different types of configurations that can be set on L1BlockInterop. +/// @custom:value SET_GAS_PAYING_TOKEN Represents the config type for setting the gas paying token. /// @custom:value ADD_DEPENDENCY Represents the config type for adding a chain to the interop dependency set. /// @custom:value REMOVE_DEPENDENCY Represents the config type for removing a chain from the interop dependency set. enum ConfigType { + SET_GAS_PAYING_TOKEN, ADD_DEPENDENCY, REMOVE_DEPENDENCY } @@ -46,9 +49,9 @@ contract L1BlockInterop is L1Block { /// keccak256(abi.encode(uint256(keccak256("l1Block.identifier.isDeposit")) - 1)) & ~bytes32(uint256(0xff)) uint256 internal constant IS_DEPOSIT_SLOT = 0x921bd3a089295c6e5540e8fba8195448d253efd6f2e3e495b499b627dc36a300; - /// @custom:semver +interop-beta.4 + /// @custom:semver +interop-beta.3 function version() public pure override returns (string memory) { - return string.concat(super.version(), "+interop-beta.4"); + return string.concat(super.version(), "+interop-beta.3"); } /// @notice Returns whether the call was triggered from a a deposit or not. @@ -104,13 +107,25 @@ contract L1BlockInterop is L1Block { function setConfig(ConfigType _type, bytes calldata _value) external { if (msg.sender != DEPOSITOR_ACCOUNT()) revert NotDepositor(); - if (_type == ConfigType.ADD_DEPENDENCY) { + if (_type == ConfigType.SET_GAS_PAYING_TOKEN) { + _setGasPayingToken(_value); + } else if (_type == ConfigType.ADD_DEPENDENCY) { _addDependency(_value); } else if (_type == ConfigType.REMOVE_DEPENDENCY) { _removeDependency(_value); } } + /// @notice Internal method to set the gas paying token. + /// @param _value The encoded value with which to set the gas paying token. + function _setGasPayingToken(bytes calldata _value) internal { + (address token, uint8 decimals, bytes32 name, bytes32 symbol) = StaticConfig.decodeSetGasPayingToken(_value); + + GasPayingToken.set({ _token: token, _decimals: decimals, _name: name, _symbol: symbol }); + + emit GasPayingTokenSet({ token: token, decimals: decimals, name: name, symbol: symbol }); + } + /// @notice Internal method to add a dependency to the interop dependency set. /// @param _value The encoded value with which to add the dependency. function _addDependency(bytes calldata _value) internal { diff --git a/packages/contracts-bedrock/src/L2/L2CrossDomainMessenger.sol b/packages/contracts-bedrock/src/L2/L2CrossDomainMessenger.sol index 8c5af32f016ea..58143132d713b 100644 --- a/packages/contracts-bedrock/src/L2/L2CrossDomainMessenger.sol +++ b/packages/contracts-bedrock/src/L2/L2CrossDomainMessenger.sol @@ -11,6 +11,7 @@ import { Predeploys } from "src/libraries/Predeploys.sol"; // Interfaces import { ISemver } from "interfaces/universal/ISemver.sol"; import { IL2ToL1MessagePasser } from "interfaces/L2/IL2ToL1MessagePasser.sol"; +import { IL1Block } from "interfaces/L2/IL1Block.sol"; /// @custom:proxied true /// @custom:predeploy 0x4200000000000000000000000000000000000007 @@ -19,8 +20,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.1.1-beta.7 + string public constant version = "2.1.1-beta.7"; /// @notice Constructs the L2CrossDomainMessenger contract. constructor() { @@ -48,6 +49,11 @@ contract L2CrossDomainMessenger is CrossDomainMessenger, ISemver { ); } + /// @inheritdoc CrossDomainMessenger + function gasPayingToken() internal view override returns (address addr_, uint8 decimals_) { + (addr_, decimals_) = IL1Block(Predeploys.L1_BLOCK_ATTRIBUTES).gasPayingToken(); + } + /// @inheritdoc CrossDomainMessenger function _isOtherMessenger() internal view override returns (bool) { return AddressAliasHelper.undoL1ToL2Alias(msg.sender) == address(otherMessenger); diff --git a/packages/contracts-bedrock/src/L2/L2StandardBridge.sol b/packages/contracts-bedrock/src/L2/L2StandardBridge.sol index 7fe46d67b7fa9..6acff3790a8ef 100644 --- a/packages/contracts-bedrock/src/L2/L2StandardBridge.sol +++ b/packages/contracts-bedrock/src/L2/L2StandardBridge.sol @@ -11,6 +11,7 @@ import { Predeploys } from "src/libraries/Predeploys.sol"; import { ISemver } from "interfaces/universal/ISemver.sol"; import { ICrossDomainMessenger } from "interfaces/universal/ICrossDomainMessenger.sol"; import { OptimismMintableERC20 } from "src/universal/OptimismMintableERC20.sol"; +import { IL1Block } from "interfaces/L2/IL1Block.sol"; /// @custom:proxied true /// @custom:predeploy 0x4200000000000000000000000000000000000010 @@ -57,9 +58,9 @@ contract L2StandardBridge is StandardBridge, ISemver { ); /// @notice Semantic version. - /// @custom:semver 1.11.1-beta.8 + /// @custom:semver 1.11.1-beta.7 function version() public pure virtual returns (string memory) { - return "1.11.1-beta.8"; + return "1.11.1-beta.7"; } /// @notice Constructs the L2StandardBridge contract. @@ -83,6 +84,11 @@ contract L2StandardBridge is StandardBridge, ISemver { ); } + /// @inheritdoc StandardBridge + function gasPayingToken() internal view override returns (address addr_, uint8 decimals_) { + (addr_, decimals_) = IL1Block(Predeploys.L1_BLOCK_ATTRIBUTES).gasPayingToken(); + } + /// @custom:legacy /// @notice Initiates a withdrawal from L2 to L1. /// This function only works with OptimismMintableERC20 tokens or ether. Use the @@ -103,6 +109,7 @@ contract L2StandardBridge is StandardBridge, ISemver { virtual onlyEOA { + require(isCustomGasToken() == false, "L2StandardBridge: not supported with custom gas token"); _initiateWithdrawal(_l2Token, msg.sender, msg.sender, _amount, _minGasLimit, _extraData); } @@ -131,6 +138,7 @@ contract L2StandardBridge is StandardBridge, ISemver { payable virtual { + require(isCustomGasToken() == false, "L2StandardBridge: not supported with custom gas token"); _initiateWithdrawal(_l2Token, msg.sender, _to, _amount, _minGasLimit, _extraData); } diff --git a/packages/contracts-bedrock/src/L2/L2StandardBridgeInterop.sol b/packages/contracts-bedrock/src/L2/L2StandardBridgeInterop.sol index 0ac13b27f8bb2..589454b724723 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-beta.8 + /// @custom:semver +interop-beta.7 function version() public pure override returns (string memory) { - return string.concat(super.version(), "+interop-beta.8"); + return string.concat(super.version(), "+interop-beta.7"); } /// @notice Converts `amount` of `from` token to `to` token. diff --git a/packages/contracts-bedrock/src/L2/SuperchainWETH.sol b/packages/contracts-bedrock/src/L2/SuperchainWETH.sol index 989d6d55ca25a..ab6ff44a33aba 100644 --- a/packages/contracts-bedrock/src/L2/SuperchainWETH.sol +++ b/packages/contracts-bedrock/src/L2/SuperchainWETH.sol @@ -5,7 +5,7 @@ pragma solidity 0.8.15; import { WETH98 } from "src/universal/WETH98.sol"; // Libraries -import { Unauthorized, ZeroAddress } from "src/libraries/errors/CommonErrors.sol"; +import { NotCustomGasToken, Unauthorized, ZeroAddress } from "src/libraries/errors/CommonErrors.sol"; import { Predeploys } from "src/libraries/Predeploys.sol"; import { Preinstalls } from "src/libraries/Preinstalls.sol"; import { SafeSend } from "src/universal/SafeSend.sol"; @@ -13,6 +13,7 @@ import { SafeSend } from "src/universal/SafeSend.sol"; // Interfaces import { ISemver } from "interfaces/universal/ISemver.sol"; import { IL2ToL2CrossDomainMessenger } from "interfaces/L2/IL2ToL2CrossDomainMessenger.sol"; +import { IL1Block } from "interfaces/L2/IL1Block.sol"; import { IETHLiquidity } from "interfaces/L2/IETHLiquidity.sol"; import { IERC7802, IERC165 } from "interfaces/L2/IERC7802.sol"; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; @@ -42,8 +43,20 @@ contract SuperchainWETH is WETH98, IERC7802, ISemver { event RelayETH(address indexed from, address indexed to, uint256 amount, uint256 source); /// @notice Semantic version. - /// @custom:semver 1.0.0-beta.14 - string public constant version = "1.0.0-beta.14"; + /// @custom:semver 1.0.0-beta.13 + string public constant version = "1.0.0-beta.13"; + + /// @inheritdoc WETH98 + function deposit() public payable override { + if (IL1Block(Predeploys.L1_BLOCK_ATTRIBUTES).isCustomGasToken()) revert NotCustomGasToken(); + super.deposit(); + } + + /// @inheritdoc WETH98 + function withdraw(uint256 _amount) public override { + if (IL1Block(Predeploys.L1_BLOCK_ATTRIBUTES).isCustomGasToken()) revert NotCustomGasToken(); + super.withdraw(_amount); + } /// @inheritdoc WETH98 function allowance(address owner, address spender) public view override returns (uint256) { @@ -76,8 +89,10 @@ contract SuperchainWETH is WETH98, IERC7802, ISemver { _mint(_to, _amount); // Withdraw from ETHLiquidity contract. - // NOTE: 'mint' will soon change to 'withdraw'. - IETHLiquidity(Predeploys.ETH_LIQUIDITY).mint(_amount); + if (!IL1Block(Predeploys.L1_BLOCK_ATTRIBUTES).isCustomGasToken()) { + // NOTE: 'mint' will soon change to 'withdraw'. + IETHLiquidity(Predeploys.ETH_LIQUIDITY).mint(_amount); + } emit CrosschainMint(_to, _amount, msg.sender); } @@ -91,8 +106,10 @@ contract SuperchainWETH is WETH98, IERC7802, ISemver { _burn(_from, _amount); // Deposit to ETHLiquidity contract. - // NOTE: 'burn' will soon change to 'deposit'. - IETHLiquidity(Predeploys.ETH_LIQUIDITY).burn{ value: _amount }(); + if (!IL1Block(Predeploys.L1_BLOCK_ATTRIBUTES).isCustomGasToken()) { + // NOTE: 'burn' will soon change to 'deposit'. + IETHLiquidity(Predeploys.ETH_LIQUIDITY).burn{ value: _amount }(); + } emit CrosschainBurn(_from, _amount, msg.sender); } @@ -110,6 +127,10 @@ contract SuperchainWETH is WETH98, IERC7802, ISemver { function sendETH(address _to, uint256 _chainId) external payable returns (bytes32 msgHash_) { if (_to == address(0)) revert ZeroAddress(); + if (IL1Block(Predeploys.L1_BLOCK_ATTRIBUTES).isCustomGasToken()) { + revert NotCustomGasToken(); + } + // NOTE: 'burn' will soon change to 'deposit'. IETHLiquidity(Predeploys.ETH_LIQUIDITY).burn{ value: msg.value }(); @@ -134,11 +155,16 @@ contract SuperchainWETH is WETH98, IERC7802, ISemver { if (crossDomainMessageSender != address(this)) revert InvalidCrossDomainSender(); - // NOTE: 'mint' will soon change to 'withdraw'. - IETHLiquidity(Predeploys.ETH_LIQUIDITY).mint(_amount); + if (IL1Block(Predeploys.L1_BLOCK_ATTRIBUTES).isCustomGasToken()) { + // Since ETH is not the native asset on custom gas token chains, send SuperchainWETH to the recipient. + _mint(_to, _amount); + } else { + // NOTE: 'mint' will soon change to 'withdraw'. + IETHLiquidity(Predeploys.ETH_LIQUIDITY).mint(_amount); - // This is a forced ETH send to the recipient, the recipient should NOT expect to be called. - new SafeSend{ value: _amount }(payable(_to)); + // This is a forced ETH send to the recipient, the recipient should NOT expect to be called. + new SafeSend{ value: _amount }(payable(_to)); + } emit RelayETH(_from, _to, _amount, source); } diff --git a/packages/contracts-bedrock/src/L2/WETH.sol b/packages/contracts-bedrock/src/L2/WETH.sol index 558beaaa8d890..5dc716fca569b 100644 --- a/packages/contracts-bedrock/src/L2/WETH.sol +++ b/packages/contracts-bedrock/src/L2/WETH.sol @@ -13,20 +13,19 @@ import { IL1Block } from "interfaces/L2/IL1Block.sol"; /// @title WETH contract that reads the name and symbol from the L1Block contract. /// Allows for nice rendering of token names for chains using custom gas token. -/// This contract is not proxied and contains calls to the custom gas token methods. contract WETH is WETH98, ISemver { - /// @custom:semver 1.1.0-beta.5 - string public constant version = "1.1.0-beta.5"; + /// @custom:semver 1.1.0-beta.4 + string public constant version = "1.1.0-beta.4"; /// @notice Returns the name of the wrapped native asset. Will be "Wrapped Ether" /// if the native asset is Ether. - function name() external pure override returns (string memory name_) { + function name() external view override returns (string memory name_) { name_ = string.concat("Wrapped ", IL1Block(Predeploys.L1_BLOCK_ATTRIBUTES).gasPayingTokenName()); } /// @notice Returns the symbol of the wrapped native asset. Will be "WETH" if the /// native asset is Ether. - function symbol() external pure override returns (string memory symbol_) { + function symbol() external view override returns (string memory symbol_) { symbol_ = string.concat("W", IL1Block(Predeploys.L1_BLOCK_ATTRIBUTES).gasPayingTokenSymbol()); } } diff --git a/packages/contracts-bedrock/src/libraries/PortalErrors.sol b/packages/contracts-bedrock/src/libraries/PortalErrors.sol index 9096a2938fdc1..6004066b397ee 100644 --- a/packages/contracts-bedrock/src/libraries/PortalErrors.sol +++ b/packages/contracts-bedrock/src/libraries/PortalErrors.sol @@ -9,6 +9,8 @@ error LargeCalldata(); error SmallGasLimit(); /// @notice Error for when a withdrawal transfer fails. error TransferFailed(); +/// @notice Error for when a method is called that only works when using a custom gas token. +error OnlyCustomGasToken(); /// @notice Error for when a method cannot be called with non zero CALLVALUE. error NoValue(); /// @notice Error for an unauthorized CALLER. diff --git a/packages/contracts-bedrock/src/libraries/errors/CommonErrors.sol b/packages/contracts-bedrock/src/libraries/errors/CommonErrors.sol index c04d915bd2326..30ce96972a191 100644 --- a/packages/contracts-bedrock/src/libraries/errors/CommonErrors.sol +++ b/packages/contracts-bedrock/src/libraries/errors/CommonErrors.sol @@ -4,6 +4,12 @@ pragma solidity ^0.8.0; /// @notice Error for an unauthorized CALLER. error Unauthorized(); +/// @notice Error for when a method is called that only works when using a custom gas token. +error OnlyCustomGasToken(); + +/// @notice Error for when a method is called that only works when NOT using a custom gas token. +error NotCustomGasToken(); + /// @notice Error for when a transfer via call fails. error TransferFailed(); diff --git a/packages/contracts-bedrock/src/universal/CrossDomainMessenger.sol b/packages/contracts-bedrock/src/universal/CrossDomainMessenger.sol index 65b781707febe..85d801f0a1a6c 100644 --- a/packages/contracts-bedrock/src/universal/CrossDomainMessenger.sol +++ b/packages/contracts-bedrock/src/universal/CrossDomainMessenger.sol @@ -175,6 +175,10 @@ abstract contract CrossDomainMessenger is /// @param _message Message to trigger the target address with. /// @param _minGasLimit Minimum gas limit that the message can be executed with. function sendMessage(address _target, bytes calldata _message, uint32 _minGasLimit) external payable { + if (isCustomGasToken()) { + require(msg.value == 0, "CrossDomainMessenger: cannot send value with custom gas token"); + } + // Triggers a message to the other messenger. Note that the amount of gas provided to the // message is the amount of gas requested by the user PLUS the base gas value. We want to // guarantee the property that the call to the target contract will always have at least @@ -359,6 +363,15 @@ abstract contract CrossDomainMessenger is + RELAY_GAS_CHECK_BUFFER; } + /// @notice Returns the address of the gas token and the token's decimals. + function gasPayingToken() internal view virtual returns (address, uint8); + + /// @notice Returns whether the chain uses a custom gas token or not. + function isCustomGasToken() internal view returns (bool) { + (address token,) = gasPayingToken(); + return token != Constants.ETHER; + } + /// @notice Initializer. /// @param _otherMessenger CrossDomainMessenger contract on the other chain. function __CrossDomainMessenger_init(CrossDomainMessenger _otherMessenger) internal onlyInitializing { diff --git a/packages/contracts-bedrock/src/universal/StandardBridge.sol b/packages/contracts-bedrock/src/universal/StandardBridge.sol index 0bfa5698ce8df..51316b82dac5a 100644 --- a/packages/contracts-bedrock/src/universal/StandardBridge.sol +++ b/packages/contracts-bedrock/src/universal/StandardBridge.sol @@ -9,6 +9,7 @@ import { Address } from "@openzeppelin/contracts/utils/Address.sol"; import { ERC165Checker } from "@openzeppelin/contracts/utils/introspection/ERC165Checker.sol"; import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import { SafeCall } from "src/libraries/SafeCall.sol"; +import { Constants } from "src/libraries/Constants.sol"; // Interfaces import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; @@ -134,6 +135,15 @@ abstract contract StandardBridge is Initializable { /// Must be implemented by contracts that inherit. receive() external payable virtual; + /// @notice Returns the address of the custom gas token and the token's decimals. + function gasPayingToken() internal view virtual returns (address, uint8); + + /// @notice Returns whether the chain uses a custom gas token or not. + function isCustomGasToken() internal view returns (bool) { + (address token,) = gasPayingToken(); + return token != Constants.ETHER; + } + /// @notice Getter for messenger contract. /// Public getter is legacy and will be removed in the future. Use `messenger` instead. /// @return Contract of the messenger on this domain. @@ -247,6 +257,7 @@ abstract contract StandardBridge is Initializable { onlyOtherBridge { require(paused() == false, "StandardBridge: paused"); + require(isCustomGasToken() == false, "StandardBridge: cannot bridge ETH with custom gas token"); require(msg.value == _amount, "StandardBridge: amount sent does not match amount required"); require(_to != address(this), "StandardBridge: cannot send to self"); require(_to != address(messenger), "StandardBridge: cannot send to messenger"); @@ -315,6 +326,7 @@ abstract contract StandardBridge is Initializable { ) internal { + require(isCustomGasToken() == false, "StandardBridge: cannot bridge ETH with custom gas token"); require(msg.value == _amount, "StandardBridge: bridging ETH must include sufficient ETH value"); // Emit the correct events. By default this will be _amount, but child diff --git a/packages/contracts-bedrock/test/L1/OptimismPortalInterop.t.sol b/packages/contracts-bedrock/test/L1/OptimismPortalInterop.t.sol index df9100ce8817f..07ca34969cd2f 100644 --- a/packages/contracts-bedrock/test/L1/OptimismPortalInterop.t.sol +++ b/packages/contracts-bedrock/test/L1/OptimismPortalInterop.t.sol @@ -27,6 +27,19 @@ contract OptimismPortalInterop_Test is CommonTest { assert(bytes(_optimismPortalInterop().version()).length > 0); } + /// @dev Tests that the config for the gas paying token cannot be set. + function testFuzz_setConfig_gasPayingToken_reverts(bytes calldata _value) public { + vm.prank(address(_optimismPortalInterop().systemConfig())); + vm.expectRevert(IOptimismPortalInterop.CustomGasTokenNotSupported.selector); + _optimismPortalInterop().setConfig(ConfigType.SET_GAS_PAYING_TOKEN, _value); + } + + /// @dev Tests that setting the gas paying token config as not the system config reverts. + function testFuzz_setConfig_gasPayingTokenButNotSystemConfig_reverts(bytes calldata _value) public { + vm.expectRevert(Unauthorized.selector); + _optimismPortalInterop().setConfig(ConfigType.SET_GAS_PAYING_TOKEN, _value); + } + /// @dev Tests that the config for adding a dependency can be set. function testFuzz_setConfig_addDependency_succeeds(bytes calldata _value) public { vm.expectEmit(address(optimismPortal2)); diff --git a/packages/contracts-bedrock/test/L2/ETHLiquidity.t.sol b/packages/contracts-bedrock/test/L2/ETHLiquidity.t.sol index bae3601b5105e..9814e776b2e5e 100644 --- a/packages/contracts-bedrock/test/L2/ETHLiquidity.t.sol +++ b/packages/contracts-bedrock/test/L2/ETHLiquidity.t.sol @@ -5,7 +5,7 @@ pragma solidity 0.8.15; import { CommonTest } from "test/setup/CommonTest.sol"; // Error imports -import { Unauthorized } from "src/libraries/errors/CommonErrors.sol"; +import { Unauthorized, NotCustomGasToken } from "src/libraries/errors/CommonErrors.sol"; /// @title ETHLiquidity_Test /// @notice Contract for testing the ETHLiquidity contract. @@ -73,6 +73,26 @@ contract ETHLiquidity_Test is CommonTest { assertEq(address(ethLiquidity).balance, STARTING_LIQUIDITY_BALANCE); } + /// @notice Tests that the burn function reverts when called on a custom gas token chain. + /// @param _amount Amount of ETH (in wei) to call the burn function with. + function testFuzz_burn_fromCustomGasTokenChain_fails(uint256 _amount) public { + // Assume + _amount = bound(_amount, 0, type(uint248).max - 1); + + // Arrange + vm.deal(address(superchainWeth), _amount); + vm.mockCall(address(l1Block), abi.encodeCall(l1Block.isCustomGasToken, ()), abi.encode(true)); + + // Act + vm.prank(address(superchainWeth)); + vm.expectRevert(NotCustomGasToken.selector); + ethLiquidity.burn{ value: _amount }(); + + // Assert + assertEq(address(superchainWeth).balance, _amount); + assertEq(address(ethLiquidity).balance, STARTING_LIQUIDITY_BALANCE); + } + /// @notice Tests that the mint function fails when the amount requested is greater than the /// available balance. In practice this should never happen because the starting /// balance is expected to be uint248 wei, the total ETH supply is far less than that @@ -137,4 +157,24 @@ contract ETHLiquidity_Test is CommonTest { assertEq(address(ethLiquidity).balance, STARTING_LIQUIDITY_BALANCE); assertEq(superchainWeth.balanceOf(address(ethLiquidity)), 0); } + + /// @notice Tests that the mint function reverts when called on a custom gas token chain. + /// @param _amount Amount of ETH (in wei) to call the mint function with. + function testFuzz_mint_fromCustomGasTokenChain_fails(uint256 _amount) public { + // Assume + _amount = bound(_amount, 0, type(uint248).max - 1); + + // Arrange + vm.mockCall(address(l1Block), abi.encodeCall(l1Block.isCustomGasToken, ()), abi.encode(true)); + + // Act + vm.prank(address(superchainWeth)); + vm.expectRevert(NotCustomGasToken.selector); + ethLiquidity.mint(_amount); + + // Assert + assertEq(address(superchainWeth).balance, 0); + assertEq(address(ethLiquidity).balance, STARTING_LIQUIDITY_BALANCE); + assertEq(superchainWeth.balanceOf(address(ethLiquidity)), 0); + } } diff --git a/packages/contracts-bedrock/test/L2/L1Block.t.sol b/packages/contracts-bedrock/test/L2/L1Block.t.sol index 2d320cca053ba..245331da4c2a7 100644 --- a/packages/contracts-bedrock/test/L2/L1Block.t.sol +++ b/packages/contracts-bedrock/test/L2/L1Block.t.sol @@ -12,29 +12,13 @@ import "src/libraries/L1BlockErrors.sol"; contract L1BlockTest is CommonTest { address depositor; + event GasPayingTokenSet(address indexed token, uint8 indexed decimals, bytes32 name, bytes32 symbol); + /// @dev Sets up the test suite. function setUp() public virtual override { super.setUp(); depositor = l1Block.DEPOSITOR_ACCOUNT(); } - - function test_isCustomGasToken_succeeds() external view { - assertFalse(l1Block.isCustomGasToken()); - } - - function test_gasPayingToken_succeeds() external view { - (address token, uint8 decimals) = l1Block.gasPayingToken(); - assertEq(token, Constants.ETHER); - assertEq(uint256(decimals), uint256(18)); - } - - function test_gasPayingTokenName_succeeds() external view { - assertEq("Ether", l1Block.gasPayingTokenName()); - } - - function test_gasPayingTokenSymbol_succeeds() external view { - assertEq("ETH", l1Block.gasPayingTokenSymbol()); - } } contract L1BlockBedrock_Test is L1BlockTest { @@ -236,3 +220,50 @@ contract L1BlockEcotone_Test is L1BlockTest { } } } + +contract L1BlockCustomGasToken_Test is L1BlockTest { + function testFuzz_setGasPayingToken_succeeds( + address _token, + uint8 _decimals, + string calldata _name, + string calldata _symbol + ) + external + { + vm.assume(_token != address(0)); + vm.assume(_token != Constants.ETHER); + + // Using vm.assume() would cause too many test rejections. + string memory name = _name; + if (bytes(_name).length > 32) { + name = _name[:32]; + } + bytes32 b32name = bytes32(abi.encodePacked(name)); + + // Using vm.assume() would cause too many test rejections. + string memory symbol = _symbol; + if (bytes(_symbol).length > 32) { + symbol = _symbol[:32]; + } + bytes32 b32symbol = bytes32(abi.encodePacked(symbol)); + + vm.expectEmit(address(l1Block)); + emit GasPayingTokenSet({ token: _token, decimals: _decimals, name: b32name, symbol: b32symbol }); + + vm.prank(depositor); + l1Block.setGasPayingToken({ _token: _token, _decimals: _decimals, _name: b32name, _symbol: b32symbol }); + + (address token, uint8 decimals) = l1Block.gasPayingToken(); + assertEq(token, _token); + assertEq(decimals, _decimals); + + assertEq(name, l1Block.gasPayingTokenName()); + assertEq(symbol, l1Block.gasPayingTokenSymbol()); + assertTrue(l1Block.isCustomGasToken()); + } + + function test_setGasPayingToken_isDepositor_reverts() external { + vm.expectRevert(NotDepositor.selector); + l1Block.setGasPayingToken(address(this), 18, "Test", "TST"); + } +} diff --git a/packages/contracts-bedrock/test/L2/L1BlockInterop.t.sol b/packages/contracts-bedrock/test/L2/L1BlockInterop.t.sol index c569f42025f36..40dfa459e16d8 100644 --- a/packages/contracts-bedrock/test/L2/L1BlockInterop.t.sol +++ b/packages/contracts-bedrock/test/L2/L1BlockInterop.t.sol @@ -80,6 +80,45 @@ contract L1BlockInteropTest is CommonTest { assertEq(_l1BlockInterop().dependencySetSize(), 0); } + /// @dev Tests that the config for setting the gas paying token succeeds. + function testFuzz_setConfig_gasPayingToken_succeeds( + address _token, + uint8 _decimals, + bytes32 _name, + bytes32 _symbol + ) + public + prankDepositor + { + vm.assume(_token != address(vm)); + + vm.expectEmit(address(l1Block)); + emit GasPayingTokenSet({ token: _token, decimals: _decimals, name: _name, symbol: _symbol }); + + _l1BlockInterop().setConfig( + ConfigType.SET_GAS_PAYING_TOKEN, + StaticConfig.encodeSetGasPayingToken({ _token: _token, _decimals: _decimals, _name: _name, _symbol: _symbol }) + ); + } + + /// @dev Tests that setting the gas paying token config as not the depositor reverts. + function testFuzz_setConfig_gasPayingTokenButNotDepositor_reverts( + address _token, + uint8 _decimals, + bytes32 _name, + bytes32 _symbol + ) + public + { + vm.assume(_token != address(vm)); + + vm.expectRevert(NotDepositor.selector); + _l1BlockInterop().setConfig( + ConfigType.SET_GAS_PAYING_TOKEN, + StaticConfig.encodeSetGasPayingToken({ _token: _token, _decimals: _decimals, _name: _name, _symbol: _symbol }) + ); + } + /// @dev Tests that the config for adding a dependency can be set. function testFuzz_setConfig_addDependency_succeeds(uint256 _chainId) public prankDepositor { vm.assume(_chainId != block.chainid); diff --git a/packages/contracts-bedrock/test/L2/L2CrossDomainMessenger.t.sol b/packages/contracts-bedrock/test/L2/L2CrossDomainMessenger.t.sol index 470c6d82d8599..ca1fd3c2d2d55 100644 --- a/packages/contracts-bedrock/test/L2/L2CrossDomainMessenger.t.sol +++ b/packages/contracts-bedrock/test/L2/L2CrossDomainMessenger.t.sol @@ -16,6 +16,7 @@ import { AddressAliasHelper } from "src/vendor/AddressAliasHelper.sol"; // Interfaces import { IL2CrossDomainMessenger } from "interfaces/L2/IL2CrossDomainMessenger.sol"; import { IL2ToL1MessagePasser } from "interfaces/L2/IL2ToL1MessagePasser.sol"; +import { IGasToken } from "src/libraries/GasPayingToken.sol"; contract L2CrossDomainMessenger_Test is CommonTest { /// @dev Receiver address for testing @@ -311,4 +312,105 @@ contract L2CrossDomainMessenger_Test is CommonTest { assertEq(l2CrossDomainMessenger.successfulMessages(hash), true); assertEq(l2CrossDomainMessenger.failedMessages(hash), true); } + + /// @dev Tests that sendMessage succeeds with a custom gas token when the call value is zero. + function test_sendMessage_customGasTokenButNoValue_succeeds() external { + // Mock the gasPayingToken function to return a custom gas token + vm.mockCall(address(l1Block), abi.encodeCall(IGasToken.gasPayingToken, ()), abi.encode(address(1), uint8(2))); + + bytes memory xDomainCallData = + Encoding.encodeCrossDomainMessage(l2CrossDomainMessenger.messageNonce(), alice, recipient, 0, 100, hex"ff"); + vm.expectCall( + address(l2ToL1MessagePasser), + abi.encodeCall( + IL2ToL1MessagePasser.initiateWithdrawal, + (address(l1CrossDomainMessenger), l2CrossDomainMessenger.baseGas(hex"ff", 100), xDomainCallData) + ) + ); + + // MessagePassed event + vm.expectEmit(true, true, true, true); + emit MessagePassed( + l2ToL1MessagePasser.messageNonce(), + address(l2CrossDomainMessenger), + address(l1CrossDomainMessenger), + 0, + l2CrossDomainMessenger.baseGas(hex"ff", 100), + xDomainCallData, + Hashing.hashWithdrawal( + Types.WithdrawalTransaction({ + nonce: l2ToL1MessagePasser.messageNonce(), + sender: address(l2CrossDomainMessenger), + target: address(l1CrossDomainMessenger), + value: 0, + gasLimit: l2CrossDomainMessenger.baseGas(hex"ff", 100), + data: xDomainCallData + }) + ) + ); + + vm.prank(alice); + l2CrossDomainMessenger.sendMessage(recipient, hex"ff", uint32(100)); + } + + /// @dev Tests that the sendMessage reverts when call value is non-zero with custom gas token. + function test_sendMessage_customGasTokenWithValue_reverts() external { + // Mock the gasPayingToken function to return a custom gas token + vm.mockCall(address(l1Block), abi.encodeCall(IGasToken.gasPayingToken, ()), abi.encode(address(1), uint8(2))); + + vm.expectRevert("CrossDomainMessenger: cannot send value with custom gas token"); + l2CrossDomainMessenger.sendMessage{ value: 1 }(recipient, hex"ff", uint32(100)); + } + + /// @dev Tests that the relayMessage succeeds with a custom gas token when the call value is zero. + function test_relayMessage_customGasTokenAndNoValue_succeeds() external { + // Mock the gasPayingToken function to return a custom gas token + vm.mockCall(address(l1Block), abi.encodeCall(IGasToken.gasPayingToken, ()), abi.encode(address(1), uint8(2))); + + address target = address(0xabcd); + address sender = address(l1CrossDomainMessenger); + address caller = AddressAliasHelper.applyL1ToL2Alias(address(l1CrossDomainMessenger)); + + vm.expectCall(target, hex"1111"); + + vm.prank(caller); + + vm.expectEmit(true, true, true, true); + + bytes32 hash = + Hashing.hashCrossDomainMessage(Encoding.encodeVersionedNonce(0, 1), sender, target, 0, 0, hex"1111"); + + emit RelayedMessage(hash); + + l2CrossDomainMessenger.relayMessage( + Encoding.encodeVersionedNonce(0, 1), // nonce + sender, + target, + 0, // value + 0, + hex"1111" + ); + + // the message hash is in the successfulMessages mapping + assert(l2CrossDomainMessenger.successfulMessages(hash)); + // it is not in the received messages mapping + assertEq(l2CrossDomainMessenger.failedMessages(hash), false); + } + + /// @dev Tests that the relayMessage reverts when call value is non-zero with custom gas token. + /// The L1CrossDomainMessenger `sendMessage` function cannot send value with a custom gas token. + function test_relayMessage_customGasTokenWithValue_reverts() external virtual { + // Mock the gasPayingToken function to return a custom gas token + vm.mockCall(address(l1Block), abi.encodeCall(IGasToken.gasPayingToken, ()), abi.encode(address(1), uint8(2))); + vm.expectRevert("CrossDomainMessenger: value must be zero unless message is from a system address"); + + l2CrossDomainMessenger.relayMessage{ value: 1 }( + Encoding.encodeVersionedNonce({ _nonce: 0, _version: 1 }), + address(0xabcd), + address(0xabcd), + 1, // value + 0, + hex"1111" + ); + } } diff --git a/packages/contracts-bedrock/test/L2/L2StandardBridge.t.sol b/packages/contracts-bedrock/test/L2/L2StandardBridge.t.sol index 48d0f978d359b..70ccc5122e7d3 100644 --- a/packages/contracts-bedrock/test/L2/L2StandardBridge.t.sol +++ b/packages/contracts-bedrock/test/L2/L2StandardBridge.t.sol @@ -20,6 +20,7 @@ import { ICrossDomainMessenger } from "interfaces/universal/ICrossDomainMessenge import { IStandardBridge } from "interfaces/universal/IStandardBridge.sol"; import { IL2ToL1MessagePasser } from "interfaces/L2/IL2ToL1MessagePasser.sol"; import { IL2StandardBridge } from "interfaces/L2/IL2StandardBridge.sol"; +import { IGasToken } from "src/libraries/GasPayingToken.sol"; contract L2StandardBridge_Test is CommonTest { using stdStorage for StdStorage; @@ -127,6 +128,19 @@ contract L2StandardBridge_Test is CommonTest { assertEq(address(l2ToL1MessagePasser).balance, 100); } + /// @dev Tests that the receive function reverts with custom gas token. + function testFuzz_receive_customGasToken_reverts(uint256 _value) external { + vm.prank(alice, alice); + vm.mockCall(address(l1Block), abi.encodeCall(IGasToken.gasPayingToken, ()), abi.encode(address(1), uint8(2))); + vm.deal(alice, _value); + (bool success, bytes memory data) = address(l2StandardBridge).call{ value: _value }(hex""); + assertFalse(success); + assembly { + data := add(data, 0x04) + } + assertEq(abi.decode(data, (string)), "StandardBridge: cannot bridge ETH with custom gas token"); + } + /// @dev Tests that `withdraw` reverts if the amount is not equal to the value sent. function test_withdraw_insufficientValue_reverts() external { assertEq(address(l2ToL1MessagePasser).balance, 0); @@ -154,6 +168,74 @@ contract L2StandardBridge_Test is CommonTest { l2StandardBridge.withdrawTo{ value: 100 }(address(L2Token), alice, 100, 1, hex""); } + /// @dev Tests that `withdraw` reverts with custom gas token. + function test_withdraw_customGasToken_reverts() external { + vm.mockCall(address(l1Block), abi.encodeCall(IGasToken.gasPayingToken, ()), abi.encode(address(1), uint8(18))); + vm.expectRevert("L2StandardBridge: not supported with custom gas token"); + vm.prank(alice, alice); + l2StandardBridge.withdraw(address(Predeploys.LEGACY_ERC20_ETH), 1, 1, hex""); + } + + /// @dev Tests that `withdraw` reverts with custom gas token. + function test_withdrawERC20_customGasToken_reverts() external { + vm.mockCall(address(l1Block), abi.encodeCall(IGasToken.gasPayingToken, ()), abi.encode(address(1), uint8(18))); + vm.expectRevert("L2StandardBridge: not supported with custom gas token"); + vm.prank(alice, alice); + l2StandardBridge.withdraw(address(L1Token), 1, 1, hex""); + } + + /// @dev Tests that `withdraw` reverts with custom gas token. + function test_withdrawERC20WithValue_customGasToken_reverts() external { + vm.deal(alice, 1 ether); + vm.mockCall(address(l1Block), abi.encodeCall(IGasToken.gasPayingToken, ()), abi.encode(address(1), uint8(18))); + vm.expectRevert("L2StandardBridge: not supported with custom gas token"); + vm.prank(alice, alice); + l2StandardBridge.withdraw{ value: 1 ether }(address(L1Token), 1, 1, hex""); + } + + /// @dev Tests that `withdraw` with value reverts with custom gas token. + function test_withdraw_customGasTokenWithValue_reverts() external { + vm.deal(alice, 1 ether); + vm.mockCall(address(l1Block), abi.encodeCall(IGasToken.gasPayingToken, ()), abi.encode(address(1), uint8(18))); + vm.expectRevert("L2StandardBridge: not supported with custom gas token"); + vm.prank(alice, alice); + l2StandardBridge.withdraw{ value: 1 ether }(address(Predeploys.LEGACY_ERC20_ETH), 1, 1, hex""); + } + + /// @dev Tests that `withdrawTo` reverts with custom gas token. + function test_withdrawTo_customGasToken_reverts() external { + vm.mockCall(address(l1Block), abi.encodeCall(IGasToken.gasPayingToken, ()), abi.encode(address(1), uint8(18))); + vm.expectRevert("L2StandardBridge: not supported with custom gas token"); + vm.prank(alice, alice); + l2StandardBridge.withdrawTo(address(Predeploys.LEGACY_ERC20_ETH), bob, 1, 1, hex""); + } + + /// @dev Tests that `withdrawTo` reverts with custom gas token. + function test_withdrawToERC20_customGasToken_reverts() external { + vm.mockCall(address(l1Block), abi.encodeCall(IGasToken.gasPayingToken, ()), abi.encode(address(1), uint8(18))); + vm.expectRevert("L2StandardBridge: not supported with custom gas token"); + vm.prank(alice, alice); + l2StandardBridge.withdrawTo(address(L2Token), bob, 1, 1, hex""); + } + + /// @dev Tests that `withdrawTo` reverts with custom gas token. + function test_withdrawToERC20WithValue_customGasToken_reverts() external { + vm.deal(alice, 1 ether); + vm.mockCall(address(l1Block), abi.encodeCall(IGasToken.gasPayingToken, ()), abi.encode(address(1), uint8(18))); + vm.expectRevert("L2StandardBridge: not supported with custom gas token"); + vm.prank(alice, alice); + l2StandardBridge.withdrawTo{ value: 1 ether }(address(L2Token), bob, 1, 1, hex""); + } + + /// @dev Tests that `withdrawTo` with value reverts with custom gas token. + function test_withdrawTo_customGasTokenWithValue_reverts() external { + vm.deal(alice, 1 ether); + vm.mockCall(address(l1Block), abi.encodeCall(IGasToken.gasPayingToken, ()), abi.encode(address(1), uint8(18))); + vm.expectRevert("L2StandardBridge: not supported with custom gas token"); + vm.prank(alice, alice); + l2StandardBridge.withdrawTo{ value: 1 ether }(address(Predeploys.LEGACY_ERC20_ETH), bob, 1, 1, hex""); + } + /// @dev Tests that the legacy `withdraw` interface on the L2StandardBridge /// successfully initiates a withdrawal. function test_withdraw_ether_succeeds() external { @@ -477,6 +559,15 @@ contract L2StandardBridge_Bridge_Test is CommonTest { l2StandardBridge.bridgeETH{ value: _value }(_minGasLimit, _extraData); } + /// @dev Tests that bridging reverts with custom gas token. + function test_bridgeETH_customGasToken_reverts() external { + vm.prank(alice, alice); + vm.mockCall(address(l1Block), abi.encodeCall(IGasToken.gasPayingToken, ()), abi.encode(address(1), uint8(2))); + vm.expectRevert("StandardBridge: cannot bridge ETH with custom gas token"); + + l2StandardBridge.bridgeETH(50000, hex"dead"); + } + /// @dev Tests that bridging ETH to a different address succeeds. function testFuzz_bridgeETHTo_succeeds(uint256 _value, uint32 _minGasLimit, bytes calldata _extraData) external { uint256 nonce = l2CrossDomainMessenger.messageNonce(); @@ -513,6 +604,20 @@ contract L2StandardBridge_Bridge_Test is CommonTest { l2StandardBridge.bridgeETHTo{ value: _value }(bob, _minGasLimit, _extraData); } + + /// @dev Tests that bridging reverts with custom gas token. + function testFuzz_bridgeETHTo_customGasToken_reverts( + uint256 _value, + uint32 _minGasLimit, + bytes calldata _extraData + ) + external + { + vm.mockCall(address(l1Block), abi.encodeCall(IGasToken.gasPayingToken, ()), abi.encode(address(1), uint8(2))); + vm.expectRevert("StandardBridge: cannot bridge ETH with custom gas token"); + vm.deal(address(this), _value); + l2StandardBridge.bridgeETHTo{ value: _value }(bob, _minGasLimit, _extraData); + } } contract L2StandardBridge_FinalizeBridgeETH_Test is CommonTest { diff --git a/packages/contracts-bedrock/test/L2/SuperchainWETH.t.sol b/packages/contracts-bedrock/test/L2/SuperchainWETH.t.sol index e122e79794ae1..bc59c76c116fc 100644 --- a/packages/contracts-bedrock/test/L2/SuperchainWETH.t.sol +++ b/packages/contracts-bedrock/test/L2/SuperchainWETH.t.sol @@ -6,7 +6,7 @@ import { CommonTest } from "test/setup/CommonTest.sol"; // Libraries import { Predeploys } from "src/libraries/Predeploys.sol"; -import { Unauthorized, ZeroAddress } from "src/libraries/errors/CommonErrors.sol"; +import { NotCustomGasToken, Unauthorized, ZeroAddress } from "src/libraries/errors/CommonErrors.sol"; import { Preinstalls } from "src/libraries/Preinstalls.sol"; // Interfaces @@ -54,12 +54,13 @@ contract SuperchainWETH_Test is CommonTest { /// @notice Tests that the deposit function can be called on a non-custom gas token chain. /// @param _amount The amount of WETH to send. - function testFuzz_deposit_succeeds(uint256 _amount) public { + function testFuzz_deposit_fromNonCustomGasTokenChain_succeeds(uint256 _amount) public { // Assume _amount = bound(_amount, 0, type(uint248).max - 1); // Arrange vm.deal(alice, _amount); + _mockAndExpect(address(l1Block), abi.encodeCall(l1Block.isCustomGasToken, ()), abi.encode(false)); // Act vm.expectEmit(address(superchainWeth)); @@ -72,9 +73,29 @@ contract SuperchainWETH_Test is CommonTest { assertEq(superchainWeth.balanceOf(alice), _amount); } + /// @notice Tests that the deposit function reverts when called on a custom gas token chain. + /// @param _amount The amount of WETH to send. + function testFuzz_deposit_fromCustomGasTokenChain_fails(uint256 _amount) public { + // Assume + _amount = bound(_amount, 0, type(uint248).max - 1); + + // Arrange + vm.deal(address(alice), _amount); + _mockAndExpect(address(l1Block), abi.encodeCall(l1Block.isCustomGasToken, ()), abi.encode(true)); + + // Act + vm.prank(alice); + vm.expectRevert(NotCustomGasToken.selector); + superchainWeth.deposit{ value: _amount }(); + + // Assert + assertEq(alice.balance, _amount); + assertEq(superchainWeth.balanceOf(alice), 0); + } + /// @notice Tests that the withdraw function can be called on a non-custom gas token chain. /// @param _amount The amount of WETH to send. - function testFuzz_withdraw_succeeds(uint256 _amount) public { + function testFuzz_withdraw_fromNonCustomGasTokenChain_succeeds(uint256 _amount) public { // Assume _amount = bound(_amount, 0, type(uint248).max - 1); @@ -82,6 +103,7 @@ contract SuperchainWETH_Test is CommonTest { vm.deal(alice, _amount); vm.prank(alice); superchainWeth.deposit{ value: _amount }(); + _mockAndExpect(address(l1Block), abi.encodeCall(l1Block.isCustomGasToken, ()), abi.encode(false)); // Act vm.expectEmit(address(superchainWeth)); @@ -94,6 +116,28 @@ contract SuperchainWETH_Test is CommonTest { assertEq(superchainWeth.balanceOf(alice), 0); } + /// @notice Tests that the withdraw function reverts when called on a custom gas token chain. + /// @param _amount The amount of WETH to send. + function testFuzz_withdraw_fromCustomGasTokenChain_fails(uint256 _amount) public { + // Assume + _amount = bound(_amount, 0, type(uint248).max - 1); + + // Arrange + vm.deal(alice, _amount); + vm.prank(alice); + superchainWeth.deposit{ value: _amount }(); + _mockAndExpect(address(l1Block), abi.encodeCall(l1Block.isCustomGasToken, ()), abi.encode(true)); + + // Act + vm.prank(alice); + vm.expectRevert(NotCustomGasToken.selector); + superchainWeth.withdraw(_amount); + + // Assert + assertEq(alice.balance, 0); + assertEq(superchainWeth.balanceOf(alice), _amount); + } + /// @notice Tests the `crosschainMint` function reverts when the caller is not the `SuperchainTokenBridge`. function testFuzz_crosschainMint_callerNotBridge_reverts(address _caller, address _to, uint256 _amount) public { // Ensure the caller is not the bridge @@ -108,7 +152,7 @@ contract SuperchainWETH_Test is CommonTest { } /// @notice Tests the `crosschainMint` with non custom gas token succeeds and emits the `CrosschainMint` event. - function testFuzz_crosschainMint_fromBridge_succeeds(address _to, uint256 _amount) public { + function testFuzz_crosschainMint_fromBridgeNonCustomGasTokenChain_succeeds(address _to, uint256 _amount) public { // Ensure `_to` is not the zero address vm.assume(_to != ZERO_ADDRESS); _amount = bound(_amount, 0, type(uint248).max - 1); @@ -125,6 +169,9 @@ contract SuperchainWETH_Test is CommonTest { vm.expectEmit(address(superchainWeth)); emit CrosschainMint(_to, _amount, Predeploys.SUPERCHAIN_TOKEN_BRIDGE); + // Mock the `isCustomGasToken` function to return false + _mockAndExpect(address(l1Block), abi.encodeCall(l1Block.isCustomGasToken, ()), abi.encode(false)); + // Expect the call to the `mint` function in the `ETHLiquidity` contract vm.expectCall(Predeploys.ETH_LIQUIDITY, abi.encodeCall(IETHLiquidity.mint, (_amount)), 1); @@ -138,6 +185,39 @@ contract SuperchainWETH_Test is CommonTest { assertEq(address(superchainWeth).balance, _amount); } + /// @notice Tests the `crosschainMint` with custom gas token succeeds and emits the `CrosschainMint` event. + function testFuzz_crosschainMint_fromBridgeCustomGasTokenChain_succeeds(address _to, uint256 _amount) public { + // Ensure `_to` is not the zero address + vm.assume(_to != ZERO_ADDRESS); + _amount = bound(_amount, 0, type(uint248).max - 1); + + // Get the balance of `_to` before the mint to compare later on the assertions + uint256 _toBalanceBefore = superchainWeth.balanceOf(_to); + + // Look for the emit of the `Transfer` event + vm.expectEmit(address(superchainWeth)); + emit Transfer(ZERO_ADDRESS, _to, _amount); + + // Look for the emit of the `CrosschainMint` event + vm.expectEmit(address(superchainWeth)); + emit CrosschainMint(_to, _amount, Predeploys.SUPERCHAIN_TOKEN_BRIDGE); + + // Mock the `isCustomGasToken` function to return false + _mockAndExpect(address(l1Block), abi.encodeCall(l1Block.isCustomGasToken, ()), abi.encode(true)); + + // Expect to not call the `mint` function in the `ETHLiquidity` contract + vm.expectCall(Predeploys.ETH_LIQUIDITY, abi.encodeCall(IETHLiquidity.mint, (_amount)), 0); + + // Call the `mint` function with the bridge caller + vm.prank(Predeploys.SUPERCHAIN_TOKEN_BRIDGE); + superchainWeth.crosschainMint(_to, _amount); + + // Check the total supply and balance of `_to` after the mint were updated correctly + assertEq(superchainWeth.balanceOf(_to), _toBalanceBefore + _amount); + assertEq(superchainWeth.totalSupply(), 0); + assertEq(address(superchainWeth).balance, 0); + } + /// @notice Tests the `crosschainBurn` function reverts when the caller is not the `SuperchainTokenBridge`. function testFuzz_crosschainBurn_callerNotBridge_reverts(address _caller, address _from, uint256 _amount) public { // Ensure the caller is not the bridge @@ -153,7 +233,7 @@ contract SuperchainWETH_Test is CommonTest { /// @notice Tests the `crosschainBurn` with non custom gas token burns the amount and emits the `CrosschainBurn` /// event. - function testFuzz_crosschainBurn_succeeds(address _from, uint256 _amount) public { + function testFuzz_crosschainBurn_fromBridgeNonCustomGasTokenChain_succeeds(address _from, uint256 _amount) public { // Ensure `_from` is not the zero address vm.assume(_from != ZERO_ADDRESS); _amount = bound(_amount, 0, type(uint248).max - 1); @@ -175,6 +255,9 @@ contract SuperchainWETH_Test is CommonTest { vm.expectEmit(address(superchainWeth)); emit CrosschainBurn(_from, _amount, Predeploys.SUPERCHAIN_TOKEN_BRIDGE); + // Mock the `isCustomGasToken` function to return false + _mockAndExpect(address(l1Block), abi.encodeCall(l1Block.isCustomGasToken, ()), abi.encode(false)); + // Expect the call to the `burn` function in the `ETHLiquidity` contract vm.expectCall(Predeploys.ETH_LIQUIDITY, abi.encodeCall(IETHLiquidity.burn, ()), 1); @@ -188,6 +271,45 @@ contract SuperchainWETH_Test is CommonTest { assertEq(address(superchainWeth).balance, 0); } + /// @notice Tests the `crosschainBurn` with custom gas token burns the amount and emits the `CrosschainBurn` + /// event. + function testFuzz_crosschainBurn_fromBridgeCustomGasTokenChain_succeeds(address _from, uint256 _amount) public { + // Ensure `_from` is not the zero address + vm.assume(_from != ZERO_ADDRESS); + _amount = bound(_amount, 0, type(uint248).max - 1); + + // Mock the `isCustomGasToken` function to return false + _mockAndExpect(address(l1Block), abi.encodeCall(l1Block.isCustomGasToken, ()), abi.encode(true)); + + // Mint some tokens to `_from` so then they can be burned + vm.prank(Predeploys.SUPERCHAIN_TOKEN_BRIDGE); + superchainWeth.crosschainMint(_from, _amount); + + // Get the total supply and balance of `_from` before the burn to compare later on the assertions + uint256 _totalSupplyBefore = superchainWeth.totalSupply(); + uint256 _fromBalanceBefore = superchainWeth.balanceOf(_from); + + // Look for the emit of the `Transfer` event + vm.expectEmit(address(superchainWeth)); + emit Transfer(_from, ZERO_ADDRESS, _amount); + + // Look for the emit of the `CrosschainBurn` event + vm.expectEmit(address(superchainWeth)); + emit CrosschainBurn(_from, _amount, Predeploys.SUPERCHAIN_TOKEN_BRIDGE); + + // Expect to not call the `burn` function in the `ETHLiquidity` contract + vm.expectCall(Predeploys.ETH_LIQUIDITY, abi.encodeCall(IETHLiquidity.burn, ()), 0); + + // Call the `burn` function with the bridge caller + vm.prank(Predeploys.SUPERCHAIN_TOKEN_BRIDGE); + superchainWeth.crosschainBurn(_from, _amount); + + // Check the total supply and balance of `_from` after the burn were updated correctly + assertEq(superchainWeth.balanceOf(_from), _fromBalanceBefore - _amount); + assertEq(superchainWeth.totalSupply(), _totalSupplyBefore); + assertEq(address(superchainWeth).balance, 0); + } + /// @notice Tests that the `crosschainBurn` function reverts when called with insufficient balance. function testFuzz_crosschainBurn_insufficientBalance_fails(address _from, uint256 _amount) public { // Assume @@ -250,7 +372,7 @@ contract SuperchainWETH_Test is CommonTest { } /// @notice Test that the burn function reverts to protect against accidentally changing the visibility. - function testFuzz_calling_burnFunction_reverts(address _caller, address _from, uint256 _amount) public { + function testFuzz_calling_burnFuunction_reverts(address _caller, address _from, uint256 _amount) public { // Arrange // nosemgrep: sol-style-use-abi-encodecall bytes memory _calldata = abi.encodeWithSignature("burn(address,uint256)", _from, _amount); @@ -372,7 +494,7 @@ contract SuperchainWETH_Test is CommonTest { /// @notice Tests the `sendETH` function burns the sender ETH, sends the message, and emits the `SendETH` /// event. - function testFuzz_sendETH_succeeds( + function testFuzz_sendETH_fromNonCustomGasTokenChain_succeeds( address _sender, address _to, uint256 _amount, @@ -389,6 +511,7 @@ contract SuperchainWETH_Test is CommonTest { // Arrange vm.deal(_sender, _amount); + _mockAndExpect(address(l1Block), abi.encodeCall(l1Block.isCustomGasToken, ()), abi.encode(false)); // Get the total balance of `_sender` before the send to compare later on the assertions uint256 _senderBalanceBefore = _sender.balance; @@ -419,6 +542,30 @@ contract SuperchainWETH_Test is CommonTest { assertEq(_sender.balance, _senderBalanceBefore - _amount); } + /// @notice Tests the `sendETH` function reverts when called on a custom gas token chain. + function testFuzz_sendETH_fromCustomGasTokenChain_fails( + address _sender, + address _to, + uint256 _amount, + uint256 _chainId + ) + external + { + // Assume + vm.assume(_sender != ZERO_ADDRESS); + vm.assume(_to != ZERO_ADDRESS); + _amount = bound(_amount, 0, type(uint248).max - 1); + + // Arrange + vm.deal(_sender, _amount); + _mockAndExpect(address(l1Block), abi.encodeCall(l1Block.isCustomGasToken, ()), abi.encode(true)); + + // Call the `sendETH` function + vm.prank(_sender); + vm.expectRevert(NotCustomGasToken.selector); + superchainWeth.sendETH{ value: _amount }(_to, _chainId); + } + /// @notice Tests the `relayETH` function reverts when the caller is not the L2ToL2CrossDomainMessenger. function testFuzz_relayETH_notMessenger_reverts(address _caller, address _to, uint256 _amount) public { // Ensure the caller is not the messenger @@ -459,6 +606,51 @@ contract SuperchainWETH_Test is CommonTest { superchainWeth.relayETH(_crossDomainMessageSender, _to, _amount); } + /// @notice Tests the `relayETH` function succeeds and sends SuperchainWETH to the recipient on a custom gas token + /// chain. + function testFuzz_relayETH_fromCustomGasTokenChain_succeeds( + address _from, + address _to, + uint256 _amount, + uint256 _source + ) + public + { + // Assume + vm.assume(_to != ZERO_ADDRESS); + _amount = bound(_amount, 0, type(uint248).max - 1); + + // Get the balance of `_to` before the mint to compare later on the assertions + uint256 _toBalanceBefore = superchainWeth.balanceOf(_to); + + // Look for the emit of the `Transfer` event + vm.expectEmit(address(superchainWeth)); + emit Transfer(ZERO_ADDRESS, _to, _amount); + + // Look for the emit of the `RelayETH` event + vm.expectEmit(address(superchainWeth)); + emit RelayETH(_from, _to, _amount, _source); + + // Arrange + _mockAndExpect(address(l1Block), abi.encodeCall(l1Block.isCustomGasToken, ()), abi.encode(true)); + _mockAndExpect( + Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER, + abi.encodeCall(IL2ToL2CrossDomainMessenger.crossDomainMessageContext, ()), + abi.encode(address(superchainWeth), _source) + ); + // Expect to not call the `mint` function in the `ETHLiquidity` contract + vm.expectCall(Predeploys.ETH_LIQUIDITY, abi.encodeCall(IETHLiquidity.mint, (_amount)), 0); + + // Act + vm.prank(Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER); + superchainWeth.relayETH(_from, _to, _amount); + + // Check the total supply and balance of `_to` after the mint were updated correctly + assertEq(superchainWeth.balanceOf(_to), _toBalanceBefore + _amount); + assertEq(superchainWeth.totalSupply(), 0); + assertEq(address(superchainWeth).balance, 0); + } + /// @notice Tests the `relayETH` function relays the proper amount of ETH and emits the `RelayETH` event. function testFuzz_relayETH_succeeds(address _from, address _to, uint256 _amount, uint256 _source) public { // Assume @@ -469,6 +661,7 @@ contract SuperchainWETH_Test is CommonTest { // Arrange vm.deal(address(superchainWeth), _amount); vm.deal(Predeploys.ETH_LIQUIDITY, _amount); + _mockAndExpect(address(l1Block), abi.encodeCall(l1Block.isCustomGasToken, ()), abi.encode(false)); _mockAndExpect( Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER, abi.encodeCall(IL2ToL2CrossDomainMessenger.crossDomainMessageContext, ()), diff --git a/packages/contracts-bedrock/test/L2/WETH.t.sol b/packages/contracts-bedrock/test/L2/WETH.t.sol index 0a4985ca874ff..84bb138d74fba 100644 --- a/packages/contracts-bedrock/test/L2/WETH.t.sol +++ b/packages/contracts-bedrock/test/L2/WETH.t.sol @@ -26,11 +26,13 @@ contract WETH_Test is CommonTest { /// @dev Tests that the name function returns the correct value. function test_name_ether_succeeds() external view { + assertFalse(l1Block.isCustomGasToken()); assertEq("Wrapped Ether", weth.name()); } /// @dev Tests that the symbol function returns the correct value. function test_symbol_ether_succeeds() external view { + assertFalse(l1Block.isCustomGasToken()); assertEq("WETH", weth.symbol()); } } diff --git a/packages/contracts-bedrock/test/setup/CommonTest.sol b/packages/contracts-bedrock/test/setup/CommonTest.sol index 55f7abedb9738..d9612c7938a64 100644 --- a/packages/contracts-bedrock/test/setup/CommonTest.sol +++ b/packages/contracts-bedrock/test/setup/CommonTest.sol @@ -16,6 +16,7 @@ import { DeployUtils } from "scripts/libraries/DeployUtils.sol"; import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; // Libraries +import { Constants } from "src/libraries/Constants.sol"; import { console } from "forge-std/console.sol"; // Interfaces @@ -33,6 +34,7 @@ contract CommonTest is Test, Setup, Events { FFIInterface constant ffi = FFIInterface(address(uint160(uint256(keccak256(abi.encode("optimism.ffi")))))); bool useAltDAOverride; + address customGasToken; bool useInteropOverride; bool useSoulGasToken; bool isSoulBackedByNative; @@ -65,6 +67,9 @@ contract CommonTest is Test, Setup, Events { if (useAltDAOverride) { deploy.cfg().setUseAltDA(true); } + if (customGasToken != address(0)) { + deploy.cfg().setUseCustomGasToken(customGasToken); + } if (useInteropOverride) { deploy.cfg().setUseInterop(true); } @@ -82,7 +87,7 @@ contract CommonTest is Test, Setup, Events { if (isForkTest()) { // Skip any test suite which uses a nonstandard configuration. - if (useAltDAOverride || useInteropOverride) { + if (useAltDAOverride || customGasToken != address(0) || useInteropOverride) { vm.skip(true); } } else { @@ -199,6 +204,13 @@ contract CommonTest is Test, Setup, Events { useAltDAOverride = true; } + /// @dev Sets a custom gas token for testing. Cannot be ETH. + function enableCustomGasToken(address _token) public { + _checkNotDeployed("custom gas token"); + require(_token != Constants.ETHER, "CommonTest: Cannot set gas token to ETHER"); + customGasToken = _token; + } + /// @dev Enables interoperability mode for testing function enableInterop() public { _checkNotDeployed("interop"); diff --git a/packages/contracts-bedrock/test/setup/DeployVariations.t.sol b/packages/contracts-bedrock/test/setup/DeployVariations.t.sol index 6e08a10b1968d..c0a6293b25f08 100644 --- a/packages/contracts-bedrock/test/setup/DeployVariations.t.sol +++ b/packages/contracts-bedrock/test/setup/DeployVariations.t.sol @@ -3,6 +3,9 @@ pragma solidity 0.8.15; // Testing utilities import { CommonTest } from "test/setup/CommonTest.sol"; +import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; + +error CustomGasTokenNotSupported(); contract DeployVariations_Test is CommonTest { function setUp() public override { @@ -11,22 +14,34 @@ contract DeployVariations_Test is CommonTest { } // Enable features which should be possible to enable or disable regardless of other options. - function enableAddOns(bool _enableAltDa) public { + function enableAddOns(bool _enableCGT, bool _enableAltDa) public { + if (_enableCGT) { + if (true) revert CustomGasTokenNotSupported(); + + ERC20 token = new ERC20("Silly", "SIL"); + super.enableCustomGasToken(address(token)); + } if (_enableAltDa) { super.enableAltDA(); } } - /// @dev It should be possible to enable Fault Proofs with Alt-DA. - function testFuzz_enableFaultProofs_succeeds(bool _enableAltDa) public virtual { - enableAddOns(_enableAltDa); + /// @dev It should be possible to enable Fault Proofs with any mix of CGT and Alt-DA. + function testFuzz_enableFaultProofs_succeeds(bool _enableCGT, bool _enableAltDa) public virtual { + // We don't support CGT yet, so we need to set it to false + _enableCGT = false; + + enableAddOns(_enableCGT, _enableAltDa); super.setUp(); } - /// @dev It should be possible to enable Fault Proofs and Interop with Alt-DA. - function test_enableInteropAndFaultProofs_succeeds(bool _enableAltDa) public virtual { - enableAddOns(_enableAltDa); + /// @dev It should be possible to enable Fault Proofs and Interop with any mix of CGT and Alt-DA. + function test_enableInteropAndFaultProofs_succeeds(bool _enableCGT, bool _enableAltDa) public virtual { + // We don't support CGT yet, so we need to set it to false + _enableCGT = false; + + enableAddOns(_enableCGT, _enableAltDa); super.enableInterop(); super.setUp(); diff --git a/packages/contracts-bedrock/test/universal/StandardBridge.t.sol b/packages/contracts-bedrock/test/universal/StandardBridge.t.sol index d268e649ddab0..be7f8a51107c7 100644 --- a/packages/contracts-bedrock/test/universal/StandardBridge.t.sol +++ b/packages/contracts-bedrock/test/universal/StandardBridge.t.sol @@ -5,6 +5,7 @@ import { StandardBridge } from "src/universal/StandardBridge.sol"; import { CommonTest } from "test/setup/CommonTest.sol"; import { OptimismMintableERC20, ILegacyMintableERC20 } from "src/universal/OptimismMintableERC20.sol"; import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import { Constants } from "src/libraries/Constants.sol"; /// @title StandardBridgeTester /// @notice Simple wrapper around the StandardBridge contract that exposes @@ -20,6 +21,10 @@ contract StandardBridgeTester is StandardBridge { return _isCorrectTokenPair(_mintableToken, _otherToken); } + function gasPayingToken() internal pure override returns (address, uint8) { + return (Constants.ETHER, 18); + } + receive() external payable override { } }