From f8799a4f0fa2268a885051c2af44db8a152879cc Mon Sep 17 00:00:00 2001 From: Hex <165055168+hexshire@users.noreply.github.com> Date: Thu, 7 Aug 2025 17:55:17 -0300 Subject: [PATCH 01/23] poc(cgt): custom gas token l1 * feat: custom gas token l1 * fix: comments * fix: comments * fix: error * fix: comments * fix: natspec and tests --------- Co-authored-by: Ashitaka --- .../deploy-config/hardhat.json | 3 ++- .../deploy-config/internal-devnet.json | 3 ++- .../deploy-config/mainnet.json | 3 ++- .../deploy-config/sepolia-devnet-0.json | 3 ++- .../deploy-config/sepolia.json | 3 ++- .../interfaces/L1/IL1CrossDomainMessenger.sol | 7 +---- .../interfaces/L1/IL1StandardBridge.sol | 7 +---- .../interfaces/L1/IOPContractsManager.sol | 1 + .../interfaces/L1/IOptimismPortal2.sol | 8 +++++- .../interfaces/L1/ISystemConfig.sol | 4 ++- .../scripts/deploy/Deploy.s.sol | 1 + .../scripts/deploy/DeployConfig.s.sol | 3 ++- .../scripts/deploy/DeployOPChain.s.sol | 24 +++++++++++++---- .../src/L1/OPContractsManager.sol | 19 ++++++++++++- .../src/L1/OptimismPortal2.sol | 12 +++++++-- .../contracts-bedrock/src/L1/SystemConfig.sol | 8 +++++- .../test/L1/L1StandardBridge.t.sol | 5 ++-- .../test/L1/OPContractsManager.t.sol | 6 +++-- .../test/L1/OptimismPortal2.t.sol | 27 +++++++++++++++++++ .../test/L1/SystemConfig.t.sol | 16 +++++++---- .../test/invariants/SystemConfig.t.sol | 3 ++- .../test/opcm/SetDisputeGameImpl.t.sol | 3 ++- .../test/vendor/Initializable.t.sol | 6 +++-- 23 files changed, 133 insertions(+), 42 deletions(-) diff --git a/packages/contracts-bedrock/deploy-config/hardhat.json b/packages/contracts-bedrock/deploy-config/hardhat.json index 339a98da80ded..e81d125314e0f 100644 --- a/packages/contracts-bedrock/deploy-config/hardhat.json +++ b/packages/contracts-bedrock/deploy-config/hardhat.json @@ -62,5 +62,6 @@ "daChallengeWindow": 100, "daResolveWindow": 100, "daBondSize": 1000, - "daResolverRefundPercentage": 50 + "daResolverRefundPercentage": 50, + "isCustomGasToken": false } diff --git a/packages/contracts-bedrock/deploy-config/internal-devnet.json b/packages/contracts-bedrock/deploy-config/internal-devnet.json index 643c507dc86d5..ba86edaee7d2c 100644 --- a/packages/contracts-bedrock/deploy-config/internal-devnet.json +++ b/packages/contracts-bedrock/deploy-config/internal-devnet.json @@ -38,5 +38,6 @@ "eip1559Elasticity": 10, "systemConfigStartBlock": 8364212, "requiredProtocolVersion": "0x0000000000000000000000000000000000000000000000000000000000000000", - "recommendedProtocolVersion": "0x0000000000000000000000000000000000000000000000000000000000000000" + "recommendedProtocolVersion": "0x0000000000000000000000000000000000000000000000000000000000000000", + "isCustomGasToken": false } diff --git a/packages/contracts-bedrock/deploy-config/mainnet.json b/packages/contracts-bedrock/deploy-config/mainnet.json index cd217ba111538..f81c4d131cb9a 100644 --- a/packages/contracts-bedrock/deploy-config/mainnet.json +++ b/packages/contracts-bedrock/deploy-config/mainnet.json @@ -55,5 +55,6 @@ "proofMaturityDelaySeconds": 604800, "disputeGameFinalityDelaySeconds": 302400, "respectedGameType": 0, - "useFaultProofs": true + "useFaultProofs": true, + "isCustomGasToken": false } diff --git a/packages/contracts-bedrock/deploy-config/sepolia-devnet-0.json b/packages/contracts-bedrock/deploy-config/sepolia-devnet-0.json index 2392dcb9281a5..5f97a743f17f4 100644 --- a/packages/contracts-bedrock/deploy-config/sepolia-devnet-0.json +++ b/packages/contracts-bedrock/deploy-config/sepolia-devnet-0.json @@ -79,5 +79,6 @@ "useFaultProofs": true, "fundDevAccounts": false, "requiredProtocolVersion": "0x0000000000000000000000000000000000000005000000000000000000000000", - "recommendedProtocolVersion": "0x0000000000000000000000000000000000000005000000000000000000000000" + "recommendedProtocolVersion": "0x0000000000000000000000000000000000000005000000000000000000000000", + "isCustomGasToken": false } diff --git a/packages/contracts-bedrock/deploy-config/sepolia.json b/packages/contracts-bedrock/deploy-config/sepolia.json index 12a1cc392c803..31d0867938a8c 100644 --- a/packages/contracts-bedrock/deploy-config/sepolia.json +++ b/packages/contracts-bedrock/deploy-config/sepolia.json @@ -54,5 +54,6 @@ "proofMaturityDelaySeconds": 604800, "disputeGameFinalityDelaySeconds": 302400, "respectedGameType": 0, - "useFaultProofs": true + "useFaultProofs": true, + "isCustomGasToken": false } diff --git a/packages/contracts-bedrock/interfaces/L1/IL1CrossDomainMessenger.sol b/packages/contracts-bedrock/interfaces/L1/IL1CrossDomainMessenger.sol index 81d7bcd22abb1..df5054860bdc2 100644 --- a/packages/contracts-bedrock/interfaces/L1/IL1CrossDomainMessenger.sol +++ b/packages/contracts-bedrock/interfaces/L1/IL1CrossDomainMessenger.sol @@ -8,15 +8,10 @@ import { ISuperchainConfig } from "interfaces/L1/ISuperchainConfig.sol"; import { IProxyAdminOwnedBase } from "interfaces/L1/IProxyAdminOwnedBase.sol"; interface IL1CrossDomainMessenger is ICrossDomainMessenger, IProxyAdminOwnedBase { - error ReinitializableBase_ZeroInitVersion(); function PORTAL() external view returns (IOptimismPortal); - function initialize( - ISystemConfig _systemConfig, - IOptimismPortal _portal - ) - external; + function initialize(ISystemConfig _systemConfig, IOptimismPortal _portal) external; function initVersion() external view returns (uint8); function portal() external view returns (IOptimismPortal); function systemConfig() external view returns (ISystemConfig); diff --git a/packages/contracts-bedrock/interfaces/L1/IL1StandardBridge.sol b/packages/contracts-bedrock/interfaces/L1/IL1StandardBridge.sol index 4ea5e42f1edfe..0e11d6a2ceca0 100644 --- a/packages/contracts-bedrock/interfaces/L1/IL1StandardBridge.sol +++ b/packages/contracts-bedrock/interfaces/L1/IL1StandardBridge.sol @@ -8,7 +8,6 @@ import { ISuperchainConfig } from "interfaces/L1/ISuperchainConfig.sol"; import { IProxyAdminOwnedBase } from "interfaces/L1/IProxyAdminOwnedBase.sol"; interface IL1StandardBridge is IStandardBridge, IProxyAdminOwnedBase { - error ReinitializableBase_ZeroInitVersion(); event ERC20DepositInitiated( @@ -68,11 +67,7 @@ interface IL1StandardBridge is IStandardBridge, IProxyAdminOwnedBase { ) external payable; - function initialize( - ICrossDomainMessenger _messenger, - ISystemConfig _systemConfig - ) - external; + function initialize(ICrossDomainMessenger _messenger, ISystemConfig _systemConfig) external; function l2TokenBridge() external view returns (address); function systemConfig() external view returns (ISystemConfig); function version() external view returns (string memory); diff --git a/packages/contracts-bedrock/interfaces/L1/IOPContractsManager.sol b/packages/contracts-bedrock/interfaces/L1/IOPContractsManager.sol index 3dead92e931dc..d309f04c7d174 100644 --- a/packages/contracts-bedrock/interfaces/L1/IOPContractsManager.sol +++ b/packages/contracts-bedrock/interfaces/L1/IOPContractsManager.sol @@ -139,6 +139,7 @@ interface IOPContractsManager { uint32 basefeeScalar; uint32 blobBasefeeScalar; uint256 l2ChainId; + bool isCustomGasToken; // The correct type is OutputRoot memory but OP Deployer does not yet support structs. bytes startingAnchorRoot; // The salt mixer is used as part of making the resulting salt unique. diff --git a/packages/contracts-bedrock/interfaces/L1/IOptimismPortal2.sol b/packages/contracts-bedrock/interfaces/L1/IOptimismPortal2.sol index eb73b2956cc24..c549fb6266607 100644 --- a/packages/contracts-bedrock/interfaces/L1/IOptimismPortal2.sol +++ b/packages/contracts-bedrock/interfaces/L1/IOptimismPortal2.sol @@ -22,6 +22,7 @@ interface IOptimismPortal2 is IProxyAdminOwnedBase { error OptimismPortal_BadTarget(); error OptimismPortal_CallPaused(); error OptimismPortal_CalldataTooLarge(); + error OptimismPortal_NotAllowedOnCGTMode(); error OptimismPortal_GasEstimation(); error OptimismPortal_GasLimitTooLow(); error OptimismPortal_ImproperDisputeGame(); @@ -50,7 +51,12 @@ interface IOptimismPortal2 is IProxyAdminOwnedBase { event WithdrawalProven(bytes32 indexed withdrawalHash, address indexed from, address indexed to); event WithdrawalProvenExtension1(bytes32 indexed withdrawalHash, address indexed proofSubmitter); event ETHMigrated(address indexed lockbox, uint256 ethBalance); - event PortalMigrated(IETHLockbox oldLockbox, IETHLockbox newLockbox, IAnchorStateRegistry oldAnchorStateRegistry, IAnchorStateRegistry newAnchorStateRegistry); + event PortalMigrated( + IETHLockbox oldLockbox, + IETHLockbox newLockbox, + IAnchorStateRegistry oldAnchorStateRegistry, + IAnchorStateRegistry newAnchorStateRegistry + ); receive() external payable; diff --git a/packages/contracts-bedrock/interfaces/L1/ISystemConfig.sol b/packages/contracts-bedrock/interfaces/L1/ISystemConfig.sol index ca2c3ebe1444e..8665abc5a7228 100644 --- a/packages/contracts-bedrock/interfaces/L1/ISystemConfig.sol +++ b/packages/contracts-bedrock/interfaces/L1/ISystemConfig.sol @@ -58,7 +58,8 @@ interface ISystemConfig is IProxyAdminOwnedBase { address _batchInbox, Addresses memory _addresses, uint256 _l2ChainId, - ISuperchainConfig _superchainConfig + ISuperchainConfig _superchainConfig, + bool _isCustomGasToken ) external; function initVersion() external view returns (uint8); @@ -66,6 +67,7 @@ interface ISystemConfig is IProxyAdminOwnedBase { function l1ERC721Bridge() external view returns (address addr_); function l1StandardBridge() external view returns (address addr_); function l2ChainId() external view returns (uint256); + function isCustomGasToken() external view returns (bool); function maximumGasLimit() external pure returns (uint64); function minimumGasLimit() external view returns (uint64); function operatorFeeConstant() external view returns (uint64); diff --git a/packages/contracts-bedrock/scripts/deploy/Deploy.s.sol b/packages/contracts-bedrock/scripts/deploy/Deploy.s.sol index 841878845bb3d..574d3fe7cd168 100644 --- a/packages/contracts-bedrock/scripts/deploy/Deploy.s.sol +++ b/packages/contracts-bedrock/scripts/deploy/Deploy.s.sol @@ -423,6 +423,7 @@ contract Deploy is Deployer { basefeeScalar: cfg.basefeeScalar(), blobBasefeeScalar: cfg.blobbasefeeScalar(), l2ChainId: cfg.l2ChainID(), + isCustomGasToken: cfg.isCustomGasToken(), startingAnchorRoot: abi.encode( Proposal({ root: Hash.wrap(cfg.faultGameGenesisOutputRoot()), l2SequenceNumber: cfg.faultGameGenesisBlock() }) ), diff --git a/packages/contracts-bedrock/scripts/deploy/DeployConfig.s.sol b/packages/contracts-bedrock/scripts/deploy/DeployConfig.s.sol index 041dadd898268..91f67aee1c869 100644 --- a/packages/contracts-bedrock/scripts/deploy/DeployConfig.s.sol +++ b/packages/contracts-bedrock/scripts/deploy/DeployConfig.s.sol @@ -74,7 +74,7 @@ contract DeployConfig is Script { uint256 public daResolveWindow; uint256 public daBondSize; uint256 public daResolverRefundPercentage; - + bool public isCustomGasToken; bool public useInterop; bool public useUpgradedFork; @@ -119,6 +119,7 @@ contract DeployConfig is Script { l2GenesisBlockGasLimit = stdJson.readUint(_json, "$.l2GenesisBlockGasLimit"); basefeeScalar = uint32(_readOr(_json, "$.gasPriceOracleBaseFeeScalar", 1368)); blobbasefeeScalar = uint32(_readOr(_json, "$.gasPriceOracleBlobBaseFeeScalar", 810949)); + isCustomGasToken = _readOr(_json, "$.isCustomGasToken", false); enableGovernance = _readOr(_json, "$.enableGovernance", false); systemConfigStartBlock = stdJson.readUint(_json, "$.systemConfigStartBlock"); diff --git a/packages/contracts-bedrock/scripts/deploy/DeployOPChain.s.sol b/packages/contracts-bedrock/scripts/deploy/DeployOPChain.s.sol index 050dfed7695b9..90fbf6173de03 100644 --- a/packages/contracts-bedrock/scripts/deploy/DeployOPChain.s.sol +++ b/packages/contracts-bedrock/scripts/deploy/DeployOPChain.s.sol @@ -55,6 +55,7 @@ contract DeployOPChainInput is BaseDeployIO { Duration internal _disputeClockExtension; Duration internal _disputeMaxClockDuration; bool internal _allowCustomDisputeParameters; + bool internal _isCustomGasToken; uint32 internal _operatorFeeScalar; uint64 internal _operatorFeeConstant; @@ -107,13 +108,21 @@ contract DeployOPChainInput is BaseDeployIO { } function set(bytes4 _sel, bytes32 _value) public { - if (_sel == this.disputeAbsolutePrestate.selector) _disputeAbsolutePrestate = Claim.wrap(_value); - else revert("DeployImplementationsInput: unknown selector"); + if (_sel == this.disputeAbsolutePrestate.selector) { + _disputeAbsolutePrestate = Claim.wrap(_value); + } else { + revert("DeployImplementationsInput: unknown selector"); + } } function set(bytes4 _sel, bool _value) public { - if (_sel == this.allowCustomDisputeParameters.selector) _allowCustomDisputeParameters = _value; - else revert("DeployOPChainInput: unknown selector"); + if (_sel == this.allowCustomDisputeParameters.selector) { + _allowCustomDisputeParameters = _value; + } else if (_sel == this.isCustomGasToken.selector) { + _isCustomGasToken = _value; + } else { + revert("DeployOPChainInput: unknown selector"); + } } function opChainProxyAdminOwner() public view returns (address) { @@ -162,6 +171,10 @@ contract DeployOPChainInput is BaseDeployIO { return _l2ChainId; } + function isCustomGasToken() public view returns (bool) { + return _isCustomGasToken; + } + function startingAnchorRoot() public pure returns (bytes memory) { // WARNING: For now always hardcode the starting permissioned game anchor root to 0xdead, // and we do not set anything for the permissioned game. This is because we currently only @@ -381,7 +394,8 @@ contract DeployOPChain is Script { disputeMaxGameDepth: _doi.disputeMaxGameDepth(), disputeSplitDepth: _doi.disputeSplitDepth(), disputeClockExtension: _doi.disputeClockExtension(), - disputeMaxClockDuration: _doi.disputeMaxClockDuration() + disputeMaxClockDuration: _doi.disputeMaxClockDuration(), + isCustomGasToken: _doi.isCustomGasToken() }); vm.broadcast(msg.sender); diff --git a/packages/contracts-bedrock/src/L1/OPContractsManager.sol b/packages/contracts-bedrock/src/L1/OPContractsManager.sol index ee4ca4922df34..3500e82bd94ff 100644 --- a/packages/contracts-bedrock/src/L1/OPContractsManager.sol +++ b/packages/contracts-bedrock/src/L1/OPContractsManager.sol @@ -1268,6 +1268,21 @@ contract OPContractsManagerDeployer is OPContractsManagerBase { (IResourceMetering.ResourceConfig memory referenceResourceConfig, ISystemConfig.Addresses memory opChainAddrs) = defaultSystemConfigParams(_input, _output); + return systemConfigInitializerData(_input, _superchainConfig, referenceResourceConfig, opChainAddrs); + } + + /// @notice Helper method for encoding the call data for the SystemConfig initializer. + function systemConfigInitializerData( + OPContractsManager.DeployInput memory _input, + ISuperchainConfig _superchainConfig, + IResourceMetering.ResourceConfig memory referenceResourceConfig, + ISystemConfig.Addresses memory opChainAddrs + ) + internal + view + virtual + returns (bytes memory) + { return abi.encodeCall( ISystemConfig.initialize, ( @@ -1281,7 +1296,8 @@ contract OPContractsManagerDeployer is OPContractsManagerBase { chainIdToBatchInboxAddress(_input.l2ChainId), opChainAddrs, _input.l2ChainId, - _superchainConfig + _superchainConfig, + _input.isCustomGasToken ) ); } @@ -1663,6 +1679,7 @@ contract OPContractsManager is ISemver { uint32 basefeeScalar; uint32 blobBasefeeScalar; uint256 l2ChainId; + bool isCustomGasToken; // The correct type is Proposal memory but OP Deployer does not yet support structs. bytes startingAnchorRoot; // The salt mixer is used as part of making the resulting salt unique. diff --git a/packages/contracts-bedrock/src/L1/OptimismPortal2.sol b/packages/contracts-bedrock/src/L1/OptimismPortal2.sol index ef2f8c8cb0e07..c2bf33dae08b3 100644 --- a/packages/contracts-bedrock/src/L1/OptimismPortal2.sol +++ b/packages/contracts-bedrock/src/L1/OptimismPortal2.sol @@ -180,6 +180,9 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ReinitializableBase /// @notice Thrown when the portal is paused. error OptimismPortal_CallPaused(); + /// @notice Thrown when a CGT withdrawal is not allowed. + error OptimismPortal_NotAllowedOnCGTMode(); + /// @notice Thrown when a gas estimation transaction is being executed. error OptimismPortal_GasEstimation(); @@ -748,8 +751,13 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ReinitializableBase payable metered(_gasLimit) { - // Lock the ETH in the ETHLockbox. - if (msg.value > 0) ethLockbox.lockETH{ value: msg.value }(); + // Handle ETH deposits: prevent when custom gas token is active, otherwise lock in ETHLockbox. + if (msg.value > 0) { + if (systemConfig.isCustomGasToken()) { + revert OptimismPortal_NotAllowedOnCGTMode(); + } + ethLockbox.lockETH{ value: msg.value }(); + } // Just to be safe, make sure that people specify address(0) as the target when doing // contract creations. diff --git a/packages/contracts-bedrock/src/L1/SystemConfig.sol b/packages/contracts-bedrock/src/L1/SystemConfig.sol index 604a9936d3aef..048382afa7ec8 100644 --- a/packages/contracts-bedrock/src/L1/SystemConfig.sol +++ b/packages/contracts-bedrock/src/L1/SystemConfig.sol @@ -135,6 +135,9 @@ contract SystemConfig is ProxyAdminOwnedBase, OwnableUpgradeable, Reinitializabl /// @notice The SuperchainConfig contract that manages the pause state. ISuperchainConfig public superchainConfig; + /// @notice Whether the gas token is custom. + bool public isCustomGasToken; + /// @notice Emitted when configuration is updated. /// @param version SystemConfig version. /// @param updateType Type of update. @@ -180,7 +183,8 @@ contract SystemConfig is ProxyAdminOwnedBase, OwnableUpgradeable, Reinitializabl address _batchInbox, SystemConfig.Addresses memory _addresses, uint256 _l2ChainId, - ISuperchainConfig _superchainConfig + ISuperchainConfig _superchainConfig, + bool _isCustomGasToken ) public reinitializer(initVersion()) @@ -211,6 +215,8 @@ contract SystemConfig is ProxyAdminOwnedBase, OwnableUpgradeable, Reinitializabl l2ChainId = _l2ChainId; superchainConfig = _superchainConfig; + + isCustomGasToken = _isCustomGasToken; } /// @notice Upgrades the SystemConfig by adding a reference to the SuperchainConfig. diff --git a/packages/contracts-bedrock/test/L1/L1StandardBridge.t.sol b/packages/contracts-bedrock/test/L1/L1StandardBridge.t.sol index a67aeb6f7148f..508b238ed7e25 100644 --- a/packages/contracts-bedrock/test/L1/L1StandardBridge.t.sol +++ b/packages/contracts-bedrock/test/L1/L1StandardBridge.t.sol @@ -430,8 +430,9 @@ contract L1StandardBridge_Receive_Test is CommonTest { vm.etch(alice, hex"ffff"); vm.deal(alice, 100); vm.prank(alice); - vm.expectRevert("StandardBridge: function can only be called from an EOA"); - l1StandardBridge.depositETH{ value: 100 }(50000, hex""); + vm.expectRevert(bytes("StandardBridge: function can only be called from an EOA")); + (bool revertsAsExpected,) = address(l1StandardBridge).call{ value: 100 }(hex""); + assertTrue(revertsAsExpected, "expectRevert: call did not revert"); } } diff --git a/packages/contracts-bedrock/test/L1/OPContractsManager.t.sol b/packages/contracts-bedrock/test/L1/OPContractsManager.t.sol index b116c9a695e0f..2f0ff25737087 100644 --- a/packages/contracts-bedrock/test/L1/OPContractsManager.t.sol +++ b/packages/contracts-bedrock/test/L1/OPContractsManager.t.sol @@ -744,7 +744,8 @@ contract OPContractsManager_TestInit is Test { disputeMaxGameDepth: 73, disputeSplitDepth: 30, disputeClockExtension: Duration.wrap(10800), - disputeMaxClockDuration: Duration.wrap(302400) + disputeMaxClockDuration: Duration.wrap(302400), + isCustomGasToken: false }) ); } @@ -1980,7 +1981,8 @@ contract OPContractsManager_Deploy_Test is DeployOPChain_TestBase { disputeMaxGameDepth: _doi.disputeMaxGameDepth(), disputeSplitDepth: _doi.disputeSplitDepth(), disputeClockExtension: _doi.disputeClockExtension(), - disputeMaxClockDuration: _doi.disputeMaxClockDuration() + disputeMaxClockDuration: _doi.disputeMaxClockDuration(), + isCustomGasToken: _doi.isCustomGasToken() }); } diff --git a/packages/contracts-bedrock/test/L1/OptimismPortal2.t.sol b/packages/contracts-bedrock/test/L1/OptimismPortal2.t.sol index a99bdde4fe2f1..11c4c94ffc54b 100644 --- a/packages/contracts-bedrock/test/L1/OptimismPortal2.t.sol +++ b/packages/contracts-bedrock/test/L1/OptimismPortal2.t.sol @@ -2062,6 +2062,33 @@ contract OptimismPortal2_DepositTransaction_Test is OptimismPortal2_TestInit { optimismPortal2.depositTransaction({ _to: address(1), _value: 0, _gasLimit: 0, _isCreation: false, _data: hex"" }); } + /// @notice Tests that `depositTransaction` reverts when the value is greater than 0 and the + /// custom gas token is active. + function test_depositTransaction_customGasToken_reverts( + bytes memory _data, + uint64 _gasLimit, + uint256 _value + ) + external + { + // Prevent overflow on an upgrade context + _value = bound(_value, 1, type(uint256).max - address(ethLockbox).balance); + // Set the custom gas token to true. + vm.mockCall(address(systemConfig), abi.encodeCall(systemConfig.isCustomGasToken, ()), abi.encode(true)); + uint64 gasLimit = optimismPortal2.minimumGasLimit(uint64(_data.length)); + + vm.deal(depositor, _value); + vm.prank(depositor); + vm.expectRevert(IOptimismPortal.OptimismPortal_NotAllowedOnCGTMode.selector); + optimismPortal2.depositTransaction{ value: _value }({ + _to: address(0x40), + _value: _value, + _gasLimit: gasLimit, + _isCreation: false, + _data: _data + }); + } + /// @notice Tests that `depositTransaction` succeeds for small, but sufficient, gas limits. function testFuzz_depositTransaction_smallGasLimit_succeeds(bytes memory _data, bool _shouldFail) external { uint64 gasLimit = optimismPortal2.minimumGasLimit(uint64(_data.length)); diff --git a/packages/contracts-bedrock/test/L1/SystemConfig.t.sol b/packages/contracts-bedrock/test/L1/SystemConfig.t.sol index 9b1e1c3d813e5..b0eef32443f2c 100644 --- a/packages/contracts-bedrock/test/L1/SystemConfig.t.sol +++ b/packages/contracts-bedrock/test/L1/SystemConfig.t.sol @@ -80,6 +80,7 @@ contract SystemConfig_Constructor_Test is SystemConfig_TestInit { assertEq(actual.maximumBaseFee, 0); assertEq(impl.startBlock(), type(uint256).max); assertEq(address(impl.batchInbox()), address(0)); + assertEq(impl.isCustomGasToken(), false); // Check addresses assertEq(address(impl.l1CrossDomainMessenger()), address(0)); assertEq(address(impl.l1ERC721Bridge()), address(0)); @@ -159,7 +160,8 @@ contract SystemConfig_Initialize_Test is SystemConfig_TestInit { optimismMintableERC20Factory: address(0) }), _l2ChainId: 1234, - _superchainConfig: ISuperchainConfig(address(0)) + _superchainConfig: ISuperchainConfig(address(0)), + _isCustomGasToken: false }); } @@ -215,7 +217,8 @@ contract SystemConfig_Initialize_Test is SystemConfig_TestInit { optimismMintableERC20Factory: address(0) }), _l2ChainId: 1234, - _superchainConfig: ISuperchainConfig(address(0)) + _superchainConfig: ISuperchainConfig(address(0)), + _isCustomGasToken: false }); } } @@ -342,7 +345,8 @@ contract SystemConfig_StartBlock_Test is SystemConfig_TestInit { optimismMintableERC20Factory: address(0) }), _l2ChainId: 1234, - _superchainConfig: ISuperchainConfig(address(0)) + _superchainConfig: ISuperchainConfig(address(0)), + _isCustomGasToken: false }); assertEq(systemConfig.startBlock(), block.number); } @@ -373,7 +377,8 @@ contract SystemConfig_StartBlock_Test is SystemConfig_TestInit { optimismMintableERC20Factory: address(0) }), _l2ChainId: 1234, - _superchainConfig: ISuperchainConfig(address(0)) + _superchainConfig: ISuperchainConfig(address(0)), + _isCustomGasToken: false }); assertEq(systemConfig.startBlock(), 1); } @@ -670,7 +675,8 @@ contract SystemConfig_SetResourceConfig_Test is SystemConfig_TestInit { optimismMintableERC20Factory: address(0) }), _l2ChainId: 1234, - _superchainConfig: ISuperchainConfig(address(0)) + _superchainConfig: ISuperchainConfig(address(0)), + _isCustomGasToken: false }); } } diff --git a/packages/contracts-bedrock/test/invariants/SystemConfig.t.sol b/packages/contracts-bedrock/test/invariants/SystemConfig.t.sol index ebf75b3280ef6..a4a600717ad65 100644 --- a/packages/contracts-bedrock/test/invariants/SystemConfig.t.sol +++ b/packages/contracts-bedrock/test/invariants/SystemConfig.t.sol @@ -47,7 +47,8 @@ contract SystemConfig_GasLimitBoundaries_Invariant is Test { optimismMintableERC20Factory: address(0) }), 1234, // _l2ChainId - ISuperchainConfig(address(0)) // _superchainConfig + ISuperchainConfig(address(0)), // _superchainConfig + false // _isCustomGasToken ) ) ); diff --git a/packages/contracts-bedrock/test/opcm/SetDisputeGameImpl.t.sol b/packages/contracts-bedrock/test/opcm/SetDisputeGameImpl.t.sol index d6e1d08523ca9..a0c7d171406c4 100644 --- a/packages/contracts-bedrock/test/opcm/SetDisputeGameImpl.t.sol +++ b/packages/contracts-bedrock/test/opcm/SetDisputeGameImpl.t.sol @@ -195,7 +195,8 @@ contract SetDisputeGameImpl_Test is Test { optimismMintableERC20Factory: address(7) }), 10, - ISuperchainConfig(address(supConfigProxy)) + ISuperchainConfig(address(supConfigProxy)), + false ) ); } diff --git a/packages/contracts-bedrock/test/vendor/Initializable.t.sol b/packages/contracts-bedrock/test/vendor/Initializable.t.sol index e7460729bc6f4..f86d989bb7a40 100644 --- a/packages/contracts-bedrock/test/vendor/Initializable.t.sol +++ b/packages/contracts-bedrock/test/vendor/Initializable.t.sol @@ -166,7 +166,8 @@ contract Initializer_Test is CommonTest { optimismMintableERC20Factory: address(0) }), 0, - ISuperchainConfig(address(0)) + ISuperchainConfig(address(0)), + false ) ) }) @@ -202,7 +203,8 @@ contract Initializer_Test is CommonTest { optimismMintableERC20Factory: address(0) }), 0, - ISuperchainConfig(address(0)) + ISuperchainConfig(address(0)), + false ) ) }) From cdb2efd5ec6e75a76c8c9f6cc7a872cc7d810423 Mon Sep 17 00:00:00 2001 From: Hex <165055168+hexshire@users.noreply.github.com> Date: Thu, 7 Aug 2025 17:55:35 -0300 Subject: [PATCH 02/23] poc(cgt): custom gas token l2 * feat: add NativeAssetLiquidity and LiquidityController predeploys initial version * fix: set fixed contract version and update interfaces * test: add NA & LC predeploys to L2Genesis script and test * test: add predeploys to setup * feat: add semver to NA & LC predeploys * test: add enableCustomGasToken setup * test: add native asset liquidity unit tests * test: add liquidity controller unit tests * fix: unused imports on test * refactor: custom gas token setup in L2Genesis * refactor: replace onlyMinter modifier in LiquidityController * refactor: add liquidity controller events and remove gasTokenDecimals * fix: remove unused stdStorage * refactor: make liquidity controller initializable * refactor: fix setLiquidityController safety invariant and remove unnecessary payable casts * docs: add natspec to liquidity controller event arguments * chore: fix predeploys version * feat: add burn function on NativeAssetLiquidity * test: assert burner balance is 0 * fix: replaced ownable with proxy admin owner * refactor: removed isCustomGasToken unnecessary check * fix: predeploys addresses * feat(cgt): poc custom gas token l2 l1block upgrade * feat: update l1block and weth for cgt * feat: add cgt check on initiateWithdrawal * test: add l1block unit tests * test: add l2ToL1MessagePasser unit tests * docs: add comments on L1Block test * refactor: leave cgt tests with uncategorized tag * refactor: rename error and fix unused import * test: move cgt related tests to separate files --------- Co-authored-by: niha <205694301+0xniha@users.noreply.github.com> --- .../interfaces/L2/IL1Block.sol | 11 +- .../interfaces/L2/IL2ToL1MessagePasser.sol | 2 + .../interfaces/L2/ILiquidityController.sol | 24 ++ .../interfaces/L2/INativeAssetLiquidity.sol | 16 + .../contracts-bedrock/scripts/L2Genesis.s.sol | 50 ++- .../scripts/checks/test-validation/main.go | 26 +- .../scripts/deploy/DeployConfig.s.sol | 6 + .../snapshots/abi/L1Block.json | 30 +- .../snapshots/abi/L2ToL1MessagePasser.json | 5 + .../snapshots/abi/LiquidityController.json | 196 +++++++++++ .../snapshots/abi/NativeAssetLiquidity.json | 110 ++++++ .../contracts-bedrock/snapshots/abi/WETH.json | 4 +- .../snapshots/semver-lock.json | 20 +- .../storageLayout/LiquidityController.json | 37 ++ .../storageLayout/NativeAssetLiquidity.json | 1 + packages/contracts-bedrock/src/L2/L1Block.sol | 30 +- .../src/L2/L2ToL1MessagePasser.sol | 13 +- .../src/L2/LiquidityController.sol | 91 +++++ .../src/L2/NativeAssetLiquidity.sol | 61 ++++ packages/contracts-bedrock/src/L2/WETH.sol | 8 +- .../src/libraries/Predeploys.sol | 15 +- .../test/L2/L1Block_CGT.t.sol | 325 ++++++++++++++++++ .../test/L2/L2ToL1MessagePasser_CGT.t.sol | 76 ++++ .../test/L2/LiquidityController.t.sol | 209 +++++++++++ .../test/L2/NativeAssetLiquidity.t.sol | 182 ++++++++++ .../test/libraries/Predeploys.t.sol | 27 +- .../test/scripts/L2Genesis.t.sol | 61 +++- .../test/setup/CommonTest.sol | 9 + .../contracts-bedrock/test/setup/Setup.sol | 16 +- 29 files changed, 1603 insertions(+), 58 deletions(-) create mode 100644 packages/contracts-bedrock/interfaces/L2/ILiquidityController.sol create mode 100644 packages/contracts-bedrock/interfaces/L2/INativeAssetLiquidity.sol create mode 100644 packages/contracts-bedrock/snapshots/abi/LiquidityController.json create mode 100644 packages/contracts-bedrock/snapshots/abi/NativeAssetLiquidity.json create mode 100644 packages/contracts-bedrock/snapshots/storageLayout/LiquidityController.json create mode 100644 packages/contracts-bedrock/snapshots/storageLayout/NativeAssetLiquidity.json create mode 100644 packages/contracts-bedrock/src/L2/LiquidityController.sol create mode 100644 packages/contracts-bedrock/src/L2/NativeAssetLiquidity.sol create mode 100644 packages/contracts-bedrock/test/L2/L1Block_CGT.t.sol create mode 100644 packages/contracts-bedrock/test/L2/L2ToL1MessagePasser_CGT.t.sol create mode 100644 packages/contracts-bedrock/test/L2/LiquidityController.t.sol create mode 100644 packages/contracts-bedrock/test/L2/NativeAssetLiquidity.t.sol diff --git a/packages/contracts-bedrock/interfaces/L2/IL1Block.sol b/packages/contracts-bedrock/interfaces/L2/IL1Block.sol index 30c42275adf9a..bd9cf80589293 100644 --- a/packages/contracts-bedrock/interfaces/L2/IL1Block.sol +++ b/packages/contracts-bedrock/interfaces/L2/IL1Block.sol @@ -2,17 +2,18 @@ pragma solidity ^0.8.0; interface IL1Block { + function IS_CUSTOM_GAS_TOKEN() external view returns (bool); 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 is_); function l1FeeOverhead() external view returns (uint256); function l1FeeScalar() external view returns (uint256); function number() external view returns (uint64); @@ -35,5 +36,5 @@ interface IL1Block { function timestamp() external view returns (uint64); function version() external pure returns (string memory); - function __constructor__() external; + function __constructor__(bool _isCustomGasToken) external; } diff --git a/packages/contracts-bedrock/interfaces/L2/IL2ToL1MessagePasser.sol b/packages/contracts-bedrock/interfaces/L2/IL2ToL1MessagePasser.sol index 4629dbaba8d09..912e2e7be02cf 100644 --- a/packages/contracts-bedrock/interfaces/L2/IL2ToL1MessagePasser.sol +++ b/packages/contracts-bedrock/interfaces/L2/IL2ToL1MessagePasser.sol @@ -2,6 +2,8 @@ pragma solidity ^0.8.0; interface IL2ToL1MessagePasser { + error NotAllowedOnCGTMode(); + event MessagePassed( uint256 indexed nonce, address indexed sender, diff --git a/packages/contracts-bedrock/interfaces/L2/ILiquidityController.sol b/packages/contracts-bedrock/interfaces/L2/ILiquidityController.sol new file mode 100644 index 0000000000000..4e06c94431eba --- /dev/null +++ b/packages/contracts-bedrock/interfaces/L2/ILiquidityController.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import { ISemver } from "interfaces/universal/ISemver.sol"; + +interface ILiquidityController is ISemver { + error Unauthorized(); + + event Initialized(uint8 version); + + event MinterAuthorized(address indexed minter); + event LiquidityMinted(address indexed minter, address indexed to, uint256 amount); + event LiquidityBurned(address indexed minter, uint256 amount); + + function authorizeMinter(address _minter) external; + function mint(address _to, uint256 _amount) external; + function burn() external payable; + function minters(address) external view returns (bool); + function gasPayingTokenName() external view returns (string memory); + function gasPayingTokenSymbol() external view returns (string memory); + function initialize(string memory _gasPayingTokenName, string memory _gasPayingTokenSymbol) external; + + function __constructor__() external; +} diff --git a/packages/contracts-bedrock/interfaces/L2/INativeAssetLiquidity.sol b/packages/contracts-bedrock/interfaces/L2/INativeAssetLiquidity.sol new file mode 100644 index 0000000000000..794450c2ccbc0 --- /dev/null +++ b/packages/contracts-bedrock/interfaces/L2/INativeAssetLiquidity.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import { ISemver } from "interfaces/universal/ISemver.sol"; + +interface INativeAssetLiquidity is ISemver { + error Unauthorized(); + + event LiquidityDeposited(address indexed caller, uint256 value); + event LiquidityWithdrawn(address indexed caller, uint256 value); + event LiquidityBurned(address indexed caller, uint256 value); + + function deposit() external payable; + function withdraw(uint256 _amount) external; + function burn(uint256 _amount) external; +} diff --git a/packages/contracts-bedrock/scripts/L2Genesis.s.sol b/packages/contracts-bedrock/scripts/L2Genesis.s.sol index de75c6b99cf1e..ef18c52dcb065 100644 --- a/packages/contracts-bedrock/scripts/L2Genesis.s.sol +++ b/packages/contracts-bedrock/scripts/L2Genesis.s.sol @@ -30,6 +30,7 @@ import { ICrossDomainMessenger } from "interfaces/universal/ICrossDomainMessenge import { IL2CrossDomainMessenger } from "interfaces/L2/IL2CrossDomainMessenger.sol"; import { IGasPriceOracle } from "interfaces/L2/IGasPriceOracle.sol"; import { IL1Block } from "interfaces/L2/IL1Block.sol"; +import { ILiquidityController } from "interfaces/L2/ILiquidityController.sol"; /// @title L2Genesis /// @notice Generates the genesis state for the L2 network. @@ -60,6 +61,9 @@ contract L2Genesis is Script { bool deployCrossL2Inbox; bool enableGovernance; bool fundDevAccounts; + bool isCustomGasToken; + string gasPayingTokenName; + string gasPayingTokenSymbol; } using ForkUtils for Fork; @@ -195,7 +199,8 @@ contract L2Genesis is Script { vm.etch(addr, code); EIP1967Helper.setAdmin(addr, Predeploys.PROXY_ADMIN); - if (Predeploys.isSupportedPredeploy(addr, _input.fork, _input.deployCrossL2Inbox)) { + if (Predeploys.isSupportedPredeploy(addr, _input.fork, _input.deployCrossL2Inbox, _input.isCustomGasToken)) + { address implementation = Predeploys.predeployToCodeNamespace(addr); EIP1967Helper.setImplementation(addr, implementation); } @@ -219,7 +224,7 @@ contract L2Genesis is Script { setOptimismMintableERC20Factory(); // 12 setL1BlockNumber(); // 13 setL2ERC721Bridge(_input.l1ERC721BridgeProxy); // 14 - setL1Block(); // 15 + setL1Block(_input.isCustomGasToken); // 15 setL2ToL1MessagePasser(); // 16 setOptimismMintableERC721Factory(_input); // 17 setProxyAdmin(_input); // 18 @@ -236,6 +241,10 @@ contract L2Genesis is Script { } setL2ToL2CrossDomainMessenger(); // 23 } + if (_input.isCustomGasToken) { + setLiquidityController(_input); // 29 + setNativeAssetLiquidity(); // 30 + } } function setInteropPredeployProxies() internal { } @@ -346,10 +355,22 @@ contract L2Genesis is Script { } /// @notice This predeploy is following the safety invariant #1. - function setL1Block() internal { + function setL1Block(bool _isCustomGasToken) internal { + IL1Block l1Block = IL1Block( + DeployUtils.create1({ + _name: "L1Block", + _args: DeployUtils.encodeConstructor(abi.encodeCall(IL1Block.__constructor__, (_isCustomGasToken))) + }) + ); + // Note: L1 block attributes are set to 0. // Before the first user-tx the state is overwritten with actual L1 attributes. - _setImplementationCode(Predeploys.L1_BLOCK_ATTRIBUTES); + address impl = Predeploys.predeployToCodeNamespace(Predeploys.L1_BLOCK_ATTRIBUTES); + vm.etch(impl, address(l1Block).code); + + /// Reset so its not included state dump + vm.etch(address(l1Block), ""); + vm.resetNonce(address(l1Block)); } /// @notice This predeploy is following the safety invariant #1. @@ -546,6 +567,27 @@ contract L2Genesis is Script { _setImplementationCode(Predeploys.SUPERCHAIN_TOKEN_BRIDGE); } + /// @notice This predeploy is following the safety invariant #1. + function setLiquidityController(Input memory _input) internal { + address impl = _setImplementationCode(Predeploys.LIQUIDITY_CONTROLLER); + + ILiquidityController(impl).initialize({ _gasPayingTokenName: "", _gasPayingTokenSymbol: "" }); + + ILiquidityController(Predeploys.LIQUIDITY_CONTROLLER).initialize({ + _gasPayingTokenName: _input.gasPayingTokenName, + _gasPayingTokenSymbol: _input.gasPayingTokenSymbol + }); + } + + /// @notice This predeploy is following the safety invariant #1. + /// This contract has no initializer. + function setNativeAssetLiquidity() internal { + _setImplementationCode(Predeploys.NATIVE_ASSET_LIQUIDITY); + + // Pre-fund the liquidity contract with the specified amount + vm.deal(Predeploys.NATIVE_ASSET_LIQUIDITY, type(uint248).max); + } + /// @notice Sets all the preinstalls. function setPreinstalls() internal { address tmpSetPreinstalls = address(uint160(uint256(keccak256("SetPreinstalls")))); diff --git a/packages/contracts-bedrock/scripts/checks/test-validation/main.go b/packages/contracts-bedrock/scripts/checks/test-validation/main.go index 4b32827ab3dfc..02199eea88e3f 100644 --- a/packages/contracts-bedrock/scripts/checks/test-validation/main.go +++ b/packages/contracts-bedrock/scripts/checks/test-validation/main.go @@ -323,18 +323,20 @@ var excludedPaths = []string{ // Resolving these naming inconsistencies is outside the script's scope, but they are // documented here to avoid false validation failures while maintaining the validation rules // for standard contract tests. - "test/invariants/", // Invariant testing framework - no direct src counterpart - "test/opcm/", // OP Chain Manager tests - may have different structure - "test/scripts/", // Script tests - test deployment/utility scripts, not contracts - "test/integration/", // Integration tests - test multiple contracts together - "test/cannon/MIPS64Memory.t.sol", // Tests external MIPS implementation - "test/dispute/lib/LibClock.t.sol", // Tests library utilities - "test/dispute/lib/LibGameId.t.sol", // Tests library utilities - "test/setup/DeployVariations.t.sol", // Tests deployment variations - "test/universal/BenchmarkTest.t.sol", // Performance benchmarking tests - "test/universal/ExtendedPause.t.sol", // Tests extended functionality - "test/vendor/Initializable.t.sol", // Tests external vendor code - "test/vendor/InitializableOZv5.t.sol", // Tests external vendor code + "test/invariants/", // Invariant testing framework - no direct src counterpart + "test/opcm/", // OP Chain Manager tests - may have different structure + "test/scripts/", // Script tests - test deployment/utility scripts, not contracts + "test/integration/", // Integration tests - test multiple contracts together + "test/cannon/MIPS64Memory.t.sol", // Tests external MIPS implementation + "test/dispute/lib/LibClock.t.sol", // Tests library utilities + "test/dispute/lib/LibGameId.t.sol", // Tests library utilities + "test/setup/DeployVariations.t.sol", // Tests deployment variations + "test/universal/BenchmarkTest.t.sol", // Performance benchmarking tests + "test/universal/ExtendedPause.t.sol", // Tests extended functionality + "test/vendor/Initializable.t.sol", // Tests external vendor code + "test/vendor/InitializableOZv5.t.sol", // Tests external vendor code + "test/L2/L1Block_CGT.t.sol", // Tests L1Block with custom gas token + "test/L2/L2ToL1MessagePasser_CGT.t.sol", // Tests L2ToL1MessagePasser with custom gas token // PATHS EXCLUDED FROM CONTRACT NAME FILE PATH VALIDATION: // These paths are excluded because they don't follow the standard naming convention where the diff --git a/packages/contracts-bedrock/scripts/deploy/DeployConfig.s.sol b/packages/contracts-bedrock/scripts/deploy/DeployConfig.s.sol index 91f67aee1c869..f116c9975c41d 100644 --- a/packages/contracts-bedrock/scripts/deploy/DeployConfig.s.sol +++ b/packages/contracts-bedrock/scripts/deploy/DeployConfig.s.sol @@ -77,6 +77,7 @@ contract DeployConfig is Script { bool public isCustomGasToken; bool public useInterop; bool public useUpgradedFork; + bool public isCustomGasToken; function read(string memory _path) public { console.log("DeployConfig: reading file %s", _path); @@ -218,6 +219,11 @@ contract DeployConfig is Script { useUpgradedFork = _useUpgradedFork; } + /// @notice Allow the `isCustomGasToken` config to be overridden in testing environments + function setIsCustomGasToken(bool _isCustomGasToken) public { + isCustomGasToken = _isCustomGasToken; + } + function latestGenesisFork() internal view returns (Fork) { if (l2GenesisHoloceneTimeOffset == 0) { return Fork.HOLOCENE; diff --git a/packages/contracts-bedrock/snapshots/abi/L1Block.json b/packages/contracts-bedrock/snapshots/abi/L1Block.json index 153d2676cf5bb..9fbc6933fb0bf 100644 --- a/packages/contracts-bedrock/snapshots/abi/L1Block.json +++ b/packages/contracts-bedrock/snapshots/abi/L1Block.json @@ -1,4 +1,15 @@ [ + { + "inputs": [ + { + "internalType": "bool", + "name": "_isCustomGasToken", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, { "inputs": [], "name": "DEPOSITOR_ACCOUNT", @@ -12,6 +23,19 @@ "stateMutability": "pure", "type": "function" }, + { + "inputs": [], + "name": "IS_CUSTOM_GAS_TOKEN", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [], "name": "baseFeeScalar", @@ -105,7 +129,7 @@ "type": "string" } ], - "stateMutability": "pure", + "stateMutability": "view", "type": "function" }, { @@ -118,7 +142,7 @@ "type": "string" } ], - "stateMutability": "pure", + "stateMutability": "view", "type": "function" }, { @@ -144,7 +168,7 @@ "type": "bool" } ], - "stateMutability": "pure", + "stateMutability": "view", "type": "function" }, { diff --git a/packages/contracts-bedrock/snapshots/abi/L2ToL1MessagePasser.json b/packages/contracts-bedrock/snapshots/abi/L2ToL1MessagePasser.json index 77e1cf7596b35..6b7cd4244752a 100644 --- a/packages/contracts-bedrock/snapshots/abi/L2ToL1MessagePasser.json +++ b/packages/contracts-bedrock/snapshots/abi/L2ToL1MessagePasser.json @@ -152,5 +152,10 @@ ], "name": "WithdrawerBalanceBurnt", "type": "event" + }, + { + "inputs": [], + "name": "NotAllowedOnCGTMode", + "type": "error" } ] \ No newline at end of file diff --git a/packages/contracts-bedrock/snapshots/abi/LiquidityController.json b/packages/contracts-bedrock/snapshots/abi/LiquidityController.json new file mode 100644 index 0000000000000..120c8f8bbac16 --- /dev/null +++ b/packages/contracts-bedrock/snapshots/abi/LiquidityController.json @@ -0,0 +1,196 @@ +[ + { + "inputs": [], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_minter", + "type": "address" + } + ], + "name": "authorizeMinter", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "burn", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [], + "name": "gasPayingTokenName", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "gasPayingTokenSymbol", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "string", + "name": "_gasPayingTokenName", + "type": "string" + }, + { + "internalType": "string", + "name": "_gasPayingTokenSymbol", + "type": "string" + } + ], + "name": "initialize", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_amount", + "type": "uint256" + } + ], + "name": "mint", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "minters", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "version", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint8", + "name": "version", + "type": "uint8" + } + ], + "name": "Initialized", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "minter", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "LiquidityBurned", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "minter", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "LiquidityMinted", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "minter", + "type": "address" + } + ], + "name": "MinterAuthorized", + "type": "event" + }, + { + "inputs": [], + "name": "Unauthorized", + "type": "error" + } +] \ No newline at end of file diff --git a/packages/contracts-bedrock/snapshots/abi/NativeAssetLiquidity.json b/packages/contracts-bedrock/snapshots/abi/NativeAssetLiquidity.json new file mode 100644 index 0000000000000..7dec0f3f6bfca --- /dev/null +++ b/packages/contracts-bedrock/snapshots/abi/NativeAssetLiquidity.json @@ -0,0 +1,110 @@ +[ + { + "inputs": [ + { + "internalType": "uint256", + "name": "_amount", + "type": "uint256" + } + ], + "name": "burn", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "deposit", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [], + "name": "version", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_amount", + "type": "uint256" + } + ], + "name": "withdraw", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "caller", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "LiquidityBurned", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "caller", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "LiquidityDeposited", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "caller", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "LiquidityWithdrawn", + "type": "event" + }, + { + "inputs": [], + "name": "Unauthorized", + "type": "error" + } +] \ No newline at end of file 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 e7faca31901e0..f659b520547da 100644 --- a/packages/contracts-bedrock/snapshots/semver-lock.json +++ b/packages/contracts-bedrock/snapshots/semver-lock.json @@ -60,8 +60,8 @@ "sourceCodeHash": "0x4351fe2ac1106c8c220b8cfe7839bc107c24d8084deb21259ac954f5a362725d" }, "src/L2/L1Block.sol:L1Block": { - "initCodeHash": "0xc35734387887a95f611888f3944546c6bcf82fd4c05dcdaa1e019779b628ad68", - "sourceCodeHash": "0x6e5349fd781d5f0127ff29ccea4d86a80240550cfa322364183a0f629abcb43e" + "initCodeHash": "0x88a88621f893d0a96097c3480171055b69e57afc645b103aab86c188bdb9c2ef", + "sourceCodeHash": "0xa3f3cb02723ba452cc9b378bd5e012e771918fbf3c2db5506551682826677270" }, "src/L2/L1FeeVault.sol:L1FeeVault": { "initCodeHash": "0x9b664e3d84ad510091337b4aacaa494b142512e2f6f7fbcdb6210ed62ca9b885", @@ -84,13 +84,21 @@ "sourceCodeHash": "0xde724da82ecf3c96b330c2876a7285b6e2b933ac599241eaa3174c443ebbe33a" }, "src/L2/L2ToL1MessagePasser.sol:L2ToL1MessagePasser": { - "initCodeHash": "0x88f7b25f956eceeab9ad84c17e66cded6a1acbb933054ac2c8b336641f70f875", - "sourceCodeHash": "0x83396cbd12a0c5c02e09a4d99c4b62ab4e9d9eb762745e63283e2e818a78a39c" + "initCodeHash": "0xdad71cc062cab0edf07d7bcc9c10b0522f0ad5af4ac2a1b2d3830e5884468404", + "sourceCodeHash": "0xd3c6fc68785e2342be06a35efbadd17510acb780c6d11dac55567cb9d4698e26" }, "src/L2/L2ToL2CrossDomainMessenger.sol:L2ToL2CrossDomainMessenger": { "initCodeHash": "0x975fd33a3a386310d54dbb01b56f3a6a8350f55a3b6bd7781e5ccc2166ddf2e6", "sourceCodeHash": "0xbea4229c5c6988243dbc7cf5a086ddd412fe1f2903b8e20d56699fec8de0c2c9" }, + "src/L2/LiquidityController.sol:LiquidityController": { + "initCodeHash": "0x1692f8b5a14c14d579afc56964f9e2b48f6b32b2443ac0ded9d5c050866664de", + "sourceCodeHash": "0x753c1d9c2462393595f57c9a765440f649753e6ab74bf297f5d5d15541304e28" + }, + "src/L2/NativeAssetLiquidity.sol:NativeAssetLiquidity": { + "initCodeHash": "0xc532da05d70300ce98d32dec956b72d3ad476be7da61bb7937e8393579e920e4", + "sourceCodeHash": "0x796b1670108f32545ead802b1e839bdf67828e0c7ea12fd4b02ff67c125346af" + }, "src/L2/OperatorFeeVault.sol:OperatorFeeVault": { "initCodeHash": "0x3d8c0d7736e8767f2f797da1c20c5fe30bd7f48a4cf75f376290481ad7c0f91f", "sourceCodeHash": "0x2022fdb4e32769eb9446dab4aed4b8abb5261fd866f381cccfa7869df1a2adff" @@ -132,8 +140,8 @@ "sourceCodeHash": "0x0ff7c1f0264d784fac5d69b792c6bc9d064d4a09701c1bafa808388685c8c4f1" }, "src/L2/WETH.sol:WETH": { - "initCodeHash": "0xbc2cd025153720943e51b79822c2dc374d270a78b92cf47d49548c468e218e46", - "sourceCodeHash": "0x734a6b2aa6406bc145d848ad6071d3af1d40852aeb8f4b2f6f51beaad476e2d3" + "initCodeHash": "0x0d3777864ed564365ec6aee93ab11816424f2e435bf51a58fcbcc99ac5975361", + "sourceCodeHash": "0xb7bccbe1cb0ac6ac7c4a82eeb5e3175d46617cd579e8506e037d42f97502155d" }, "src/cannon/MIPS64.sol:MIPS64": { "initCodeHash": "0xbc7c3c50e8c3679576f87d79c2dae05dd1174e64bdaa4c1e0857314618e415a3", diff --git a/packages/contracts-bedrock/snapshots/storageLayout/LiquidityController.json b/packages/contracts-bedrock/snapshots/storageLayout/LiquidityController.json new file mode 100644 index 0000000000000..d817ac7953d16 --- /dev/null +++ b/packages/contracts-bedrock/snapshots/storageLayout/LiquidityController.json @@ -0,0 +1,37 @@ +[ + { + "bytes": "1", + "label": "_initialized", + "offset": 0, + "slot": "0", + "type": "uint8" + }, + { + "bytes": "1", + "label": "_initializing", + "offset": 1, + "slot": "0", + "type": "bool" + }, + { + "bytes": "32", + "label": "minters", + "offset": 0, + "slot": "1", + "type": "mapping(address => bool)" + }, + { + "bytes": "32", + "label": "gasPayingTokenName", + "offset": 0, + "slot": "2", + "type": "string" + }, + { + "bytes": "32", + "label": "gasPayingTokenSymbol", + "offset": 0, + "slot": "3", + "type": "string" + } +] \ No newline at end of file diff --git a/packages/contracts-bedrock/snapshots/storageLayout/NativeAssetLiquidity.json b/packages/contracts-bedrock/snapshots/storageLayout/NativeAssetLiquidity.json new file mode 100644 index 0000000000000..0637a088a01e8 --- /dev/null +++ b/packages/contracts-bedrock/snapshots/storageLayout/NativeAssetLiquidity.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/packages/contracts-bedrock/src/L2/L1Block.sol b/packages/contracts-bedrock/src/L2/L1Block.sol index 31935dfab7eac..b9575c42d0503 100644 --- a/packages/contracts-bedrock/src/L2/L1Block.sol +++ b/packages/contracts-bedrock/src/L2/L1Block.sol @@ -3,9 +3,11 @@ pragma solidity 0.8.15; // Libraries import { Constants } from "src/libraries/Constants.sol"; +import { Predeploys } from "src/libraries/Predeploys.sol"; // Interfaces import { ISemver } from "interfaces/universal/ISemver.sol"; +import { ILiquidityController } from "interfaces/L2/ILiquidityController.sol"; /// @custom:proxied true /// @custom:predeploy 0x4200000000000000000000000000000000000015 @@ -20,6 +22,9 @@ contract L1Block is ISemver { addr_ = Constants.DEPOSITOR_ACCOUNT; } + /// @notice Whether the gas paying token is custom. + bool public immutable IS_CUSTOM_GAS_TOKEN; + /// @notice The latest L1 block number known by the L2 system. uint64 public number; @@ -61,9 +66,13 @@ contract L1Block is ISemver { /// @notice The scalar value applied to the operator fee. uint32 public operatorFeeScalar; - /// @custom:semver 1.6.1 + constructor(bool _isCustomGasToken) { + IS_CUSTOM_GAS_TOKEN = _isCustomGasToken; + } + + /// @custom:semver 1.6.2 function version() public pure virtual returns (string memory) { - return "1.6.1"; + return "1.6.2"; } /// @notice Returns the gas paying token, its decimals, name and symbol. @@ -75,21 +84,22 @@ contract L1Block is ISemver { /// @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_ = + IS_CUSTOM_GAS_TOKEN ? ILiquidityController(Predeploys.LIQUIDITY_CONTROLLER).gasPayingTokenName() : "Ether"; } /// @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_ = + IS_CUSTOM_GAS_TOKEN ? ILiquidityController(Predeploys.LIQUIDITY_CONTROLLER).gasPayingTokenSymbol() : "ETH"; } - /// @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; + /// @notice Returns whether the gas paying token is custom. + function isCustomGasToken() public view returns (bool is_) { + is_ = IS_CUSTOM_GAS_TOKEN; } /// @custom:legacy diff --git a/packages/contracts-bedrock/src/L2/L2ToL1MessagePasser.sol b/packages/contracts-bedrock/src/L2/L2ToL1MessagePasser.sol index b25a2a1248bc5..92662c71d13ac 100644 --- a/packages/contracts-bedrock/src/L2/L2ToL1MessagePasser.sol +++ b/packages/contracts-bedrock/src/L2/L2ToL1MessagePasser.sol @@ -6,9 +6,11 @@ import { Types } from "src/libraries/Types.sol"; import { Hashing } from "src/libraries/Hashing.sol"; import { Encoding } from "src/libraries/Encoding.sol"; import { Burn } from "src/libraries/Burn.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 0x4200000000000000000000000000000000000016 @@ -29,6 +31,9 @@ contract L2ToL1MessagePasser is ISemver { /// @notice A unique value hashed with each withdrawal. uint240 internal msgNonce; + /// @notice The error thrown when a withdrawal is initiated with value and custom gas token is used. + error NotAllowedOnCGTMode(); + /// @notice Emitted any time a withdrawal is initiated. /// @param nonce Unique value corresponding to each withdrawal. /// @param sender The L2 account address which initiated the withdrawal. @@ -51,8 +56,8 @@ contract L2ToL1MessagePasser is ISemver { /// @param amount Amount of ETh that was burned. event WithdrawerBalanceBurnt(uint256 indexed amount); - /// @custom:semver 1.1.2 - string public constant version = "1.1.2"; + /// @custom:semver 1.1.3 + string public constant version = "1.1.3"; /// @notice Allows users to withdraw ETH by sending directly to this contract. receive() external payable { @@ -74,6 +79,10 @@ contract L2ToL1MessagePasser is ISemver { /// @param _gasLimit Minimum gas limit for executing the message on L1. /// @param _data Data to forward to L1 target. function initiateWithdrawal(address _target, uint256 _gasLimit, bytes memory _data) public payable { + if (IL1Block(Predeploys.L1_BLOCK_ATTRIBUTES).isCustomGasToken() && msg.value > 0) { + revert NotAllowedOnCGTMode(); + } + bytes32 withdrawalHash = Hashing.hashWithdrawal( Types.WithdrawalTransaction({ nonce: messageNonce(), diff --git a/packages/contracts-bedrock/src/L2/LiquidityController.sol b/packages/contracts-bedrock/src/L2/LiquidityController.sol new file mode 100644 index 0000000000000..8003e13d912f9 --- /dev/null +++ b/packages/contracts-bedrock/src/L2/LiquidityController.sol @@ -0,0 +1,91 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.15; + +// Contracts +import { Initializable } from "@openzeppelin/contracts/proxy/utils/Initializable.sol"; +import { SafeSend } from "src/universal/SafeSend.sol"; + +// Libraries +import { Unauthorized } from "src/libraries/errors/CommonErrors.sol"; +import { Predeploys } from "src/libraries/Predeploys.sol"; + +// Interfaces +import { INativeAssetLiquidity } from "interfaces/L2/INativeAssetLiquidity.sol"; +import { IProxyAdmin } from "interfaces/universal/IProxyAdmin.sol"; +import { ISemver } from "interfaces/universal/ISemver.sol"; + +/// @custom:proxied true +/// @custom:predeploy 0x420000000000000000000000000000000000002A +/// @title LiquidityController +/// @notice The LiquidityController contract is responsible for controlling the liquidity of the native asset on the L2 +/// chain. +contract LiquidityController is ISemver, Initializable { + /// @notice Emitted when an address is authorized to mint/burn liquidity + /// @param minter The address that was authorized + event MinterAuthorized(address indexed minter); + + /// @notice Emitted when liquidity is minted + /// @param minter The address that minted the liquidity + /// @param to The address that received the minted liquidity + /// @param amount The amount of liquidity that was minted + event LiquidityMinted(address indexed minter, address indexed to, uint256 amount); + + /// @notice Emitted when liquidity is burned + /// @param minter The address that burned the liquidity + /// @param amount The amount of liquidity that was burned + event LiquidityBurned(address indexed minter, uint256 amount); + + /// @notice Semantic version. + /// @custom:semver 1.0.0 + string public constant version = "1.0.0"; + + /// @notice Mapping of addresses authorized to control liquidity operations + mapping(address => bool) public minters; + + /// @notice The name of the native asset + string public gasPayingTokenName; + + /// @notice The symbol of the native asset + string public gasPayingTokenSymbol; + + constructor() { + _disableInitializers(); + } + + /// @notice Initializer. + /// @param _gasPayingTokenName The name of the native asset + /// @param _gasPayingTokenSymbol The symbol of the native asset + function initialize(string memory _gasPayingTokenName, string memory _gasPayingTokenSymbol) external initializer { + gasPayingTokenName = _gasPayingTokenName; + gasPayingTokenSymbol = _gasPayingTokenSymbol; + } + + /// @notice Authorizes an address to perform liquidity control operations + /// @param _minter The address to authorize as a minter + function authorizeMinter(address _minter) external { + if (msg.sender != IProxyAdmin(Predeploys.PROXY_ADMIN).owner()) revert Unauthorized(); + minters[_minter] = true; + emit MinterAuthorized(_minter); + } + + /// @notice Mints native asset liquidity and sends it to a specified address + /// @param _to The address to receive the minted native asset + /// @param _amount The amount of native asset to mint and send + function mint(address _to, uint256 _amount) external { + if (!minters[msg.sender]) revert Unauthorized(); + INativeAssetLiquidity(Predeploys.NATIVE_ASSET_LIQUIDITY).withdraw(_amount); + + // This is a forced ETH send to the recipient, the recipient should NOT expect to be called + new SafeSend{ value: _amount }(payable(_to)); + + emit LiquidityMinted(msg.sender, _to, _amount); + } + + /// @notice Burns native asset liquidity by sending ETH to the contract + function burn() external payable { + if (!minters[msg.sender]) revert Unauthorized(); + INativeAssetLiquidity(Predeploys.NATIVE_ASSET_LIQUIDITY).deposit{ value: msg.value }(); + + emit LiquidityBurned(msg.sender, msg.value); + } +} diff --git a/packages/contracts-bedrock/src/L2/NativeAssetLiquidity.sol b/packages/contracts-bedrock/src/L2/NativeAssetLiquidity.sol new file mode 100644 index 0000000000000..e18ea7bbeb918 --- /dev/null +++ b/packages/contracts-bedrock/src/L2/NativeAssetLiquidity.sol @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.15; + +// Contracts +import { SafeSend } from "src/universal/SafeSend.sol"; + +// Libraries +import { Unauthorized } from "src/libraries/errors/CommonErrors.sol"; +import { Predeploys } from "src/libraries/Predeploys.sol"; +import { Burn } from "src/libraries/Burn.sol"; + +// Interfaces +import { ISemver } from "interfaces/universal/ISemver.sol"; +import { IProxyAdmin } from "interfaces/universal/IProxyAdmin.sol"; + +/// @custom:predeploy 0x4200000000000000000000000000000000000029 +/// @title NativeAssetLiquidity +/// @notice The NativeAssetLiquidity contract allows other contracts to access native asset liquidity +contract NativeAssetLiquidity is ISemver { + /// @notice Emitted when an address withdraws native asset liquidity. + event LiquidityWithdrawn(address indexed caller, uint256 value); + + /// @notice Emitted when an address deposits native asset liquidity. + event LiquidityDeposited(address indexed caller, uint256 value); + + /// @notice Emitted when an address burns native asset liquidity. + event LiquidityBurned(address indexed caller, uint256 value); + + /// @notice Semantic version. + /// @custom:semver 1.0.0 + string public constant version = "1.0.0"; + + /// @notice Allows an address to lock native asset liquidity into this contract. + function deposit() external payable { + if (msg.sender != Predeploys.LIQUIDITY_CONTROLLER) revert Unauthorized(); + + emit LiquidityDeposited(msg.sender, msg.value); + } + + /// @notice Allows an address to unlock native asset liquidity from this contract. + /// @param _amount The amount of liquidity to unlock. + function withdraw(uint256 _amount) external { + if (msg.sender != Predeploys.LIQUIDITY_CONTROLLER) revert Unauthorized(); + + new SafeSend{ value: _amount }(payable(msg.sender)); + + emit LiquidityWithdrawn(msg.sender, _amount); + } + + /// @notice Allows to burn native asset liquidity from this contract. + /// @dev Burn an arbitrary amount of native supply forever, ideally to be called only once by + /// the ProxyAdmin owner. + /// @param _amount The amount of liquidity to burn. + function burn(uint256 _amount) external { + if (msg.sender != IProxyAdmin(Predeploys.PROXY_ADMIN).owner()) revert Unauthorized(); + + Burn.eth(_amount); + + emit LiquidityBurned(msg.sender, _amount); + } +} diff --git a/packages/contracts-bedrock/src/L2/WETH.sol b/packages/contracts-bedrock/src/L2/WETH.sol index 42b51261a9838..3711bcec10f15 100644 --- a/packages/contracts-bedrock/src/L2/WETH.sol +++ b/packages/contracts-bedrock/src/L2/WETH.sol @@ -15,18 +15,18 @@ import { IL1Block } from "interfaces/L2/IL1Block.sol"; /// 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.1 - string public constant version = "1.1.1"; + /// @custom:semver 1.1.2 + string public constant version = "1.1.2"; /// @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/Predeploys.sol b/packages/contracts-bedrock/src/libraries/Predeploys.sol index baeb6a143575c..a58ab7c4f0290 100644 --- a/packages/contracts-bedrock/src/libraries/Predeploys.sol +++ b/packages/contracts-bedrock/src/libraries/Predeploys.sol @@ -113,6 +113,12 @@ library Predeploys { /// @notice Address of the SuperchainTokenBridge predeploy. address internal constant SUPERCHAIN_TOKEN_BRIDGE = 0x4200000000000000000000000000000000000028; + /// @notice Address of the NativeAssetLiquidity predeploy. + address internal constant NATIVE_ASSET_LIQUIDITY = 0x4200000000000000000000000000000000000029; + + /// @notice Address of the LiquidityController predeploy. + address internal constant LIQUIDITY_CONTROLLER = 0x420000000000000000000000000000000000002a; + /// @notice Returns the name of the predeploy at the given address. function getName(address _addr) internal pure returns (string memory out_) { require(isPredeployNamespace(_addr), "Predeploys: address must be a predeploy"); @@ -145,6 +151,8 @@ library Predeploys { if (_addr == OPTIMISM_SUPERCHAIN_ERC20_FACTORY) return "OptimismSuperchainERC20Factory"; if (_addr == OPTIMISM_SUPERCHAIN_ERC20_BEACON) return "OptimismSuperchainERC20Beacon"; if (_addr == SUPERCHAIN_TOKEN_BRIDGE) return "SuperchainTokenBridge"; + if (_addr == LIQUIDITY_CONTROLLER) return "LiquidityController"; + if (_addr == NATIVE_ASSET_LIQUIDITY) return "NativeAssetLiquidity"; revert("Predeploys: unnamed predeploy"); } @@ -157,7 +165,8 @@ library Predeploys { function isSupportedPredeploy( address _addr, uint256 _fork, - bool _enableCrossL2Inbox + bool _enableCrossL2Inbox, + bool _isCustomGasToken ) internal pure @@ -171,7 +180,9 @@ library Predeploys { || _addr == L1_FEE_VAULT || _addr == OPERATOR_FEE_VAULT || _addr == SCHEMA_REGISTRY || _addr == EAS || _addr == GOVERNANCE_TOKEN || (_fork >= uint256(Fork.INTEROP) && _enableCrossL2Inbox && _addr == CROSS_L2_INBOX) - || (_fork >= uint256(Fork.INTEROP) && _addr == L2_TO_L2_CROSS_DOMAIN_MESSENGER); + || (_fork >= uint256(Fork.INTEROP) && _addr == L2_TO_L2_CROSS_DOMAIN_MESSENGER) + || (_isCustomGasToken && _addr == LIQUIDITY_CONTROLLER) + || (_isCustomGasToken && _addr == NATIVE_ASSET_LIQUIDITY); } function isPredeployNamespace(address _addr) internal pure returns (bool) { diff --git a/packages/contracts-bedrock/test/L2/L1Block_CGT.t.sol b/packages/contracts-bedrock/test/L2/L1Block_CGT.t.sol new file mode 100644 index 0000000000000..6719587e12539 --- /dev/null +++ b/packages/contracts-bedrock/test/L2/L1Block_CGT.t.sol @@ -0,0 +1,325 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.15; + +// Testing +import { CommonTest } from "test/setup/CommonTest.sol"; + +// Libraries +import { Encoding } from "src/libraries/Encoding.sol"; +import { Constants } from "src/libraries/Constants.sol"; +import "src/libraries/L1BlockErrors.sol"; + +/// @title L1Block_CGT_TestInit +/// @notice Reusable test initialization for `L1Block` tests with custom gas token enabled. +contract L1Block_CGT_TestInit is CommonTest { + address depositor; + + /// @notice Sets up the test suite. + function setUp() public virtual override { + super.enableCustomGasToken(); + super.setUp(); + depositor = l1Block.DEPOSITOR_ACCOUNT(); + } +} + +/// @title L1Block_CGT_GasPayingToken_Test +/// @notice Tests the `gasPayingToken` function of the `L1Block` contract with custom gas token +/// enabled. +contract L1Block_CGT_GasPayingToken_Test is L1Block_CGT_TestInit { + /// @notice Tests that the `gasPayingToken` function returns the correct token address and + /// decimals. + function test_gasPayingToken_succeeds() external view { + (address token, uint8 decimals) = l1Block.gasPayingToken(); + assertEq(token, Constants.ETHER); + assertEq(uint256(decimals), uint256(18)); + } +} + +/// @title L1Block_CGT_GasPayingTokenName_Test +/// @notice Tests the `gasPayingTokenName` function of the `L1Block` contract with custom gas +/// token enabled. +contract L1Block_CGT_GasPayingTokenName_Test is L1Block_CGT_TestInit { + /// @notice Tests that the `gasPayingTokenName` function returns the correct token name. + function test_gasPayingTokenName_succeeds() external view { + assertEq(liquidityController.gasPayingTokenName(), l1Block.gasPayingTokenName()); + } +} + +/// @title L1Block_CGT_GasPayingTokenSymbol_Test +/// @notice Tests the `gasPayingTokenSymbol` function of the `L1Block` contract with custom gas +/// token enabled. +contract L1Block_CGT_GasPayingTokenSymbol_Test is L1Block_CGT_TestInit { + /// @notice Tests that the `gasPayingTokenSymbol` function returns the correct token symbol. + function test_gasPayingTokenSymbol_succeeds() external view { + assertEq(liquidityController.gasPayingTokenSymbol(), l1Block.gasPayingTokenSymbol()); + } +} + +/// @title L1Block_CGT_IsCustomGasToken_Test +/// @notice Tests the `isCustomGasToken` function of the `L1Block` contract with custom gas token +/// enabled. +contract L1Block_CGT_IsCustomGasToken_Test is L1Block_CGT_TestInit { + /// @notice Tests that the `isCustomGasToken` function returns false when no custom gas token + /// is used. + function test_isCustomGasToken_succeeds() external view { + assertTrue(l1Block.isCustomGasToken()); + } +} + +/// @title L1Block_CGT_SetL1BlockValues_Test +/// @notice Tests the `setL1BlockValues` function of the `L1Block` contract with custom gas token +/// enabled. +contract L1Block_CGT_SetL1BlockValues_Test is L1Block_CGT_TestInit { + /// @notice Tests that `setL1BlockValues` updates the values correctly. + function testFuzz_setL1BlockValues_succeeds( + uint64 n, + uint64 t, + uint256 b, + bytes32 h, + uint64 s, + bytes32 bt, + uint256 fo, + uint256 fs + ) + external + { + vm.prank(depositor); + l1Block.setL1BlockValues(n, t, b, h, s, bt, fo, fs); + assertEq(l1Block.number(), n); + assertEq(l1Block.timestamp(), t); + assertEq(l1Block.basefee(), b); + assertEq(l1Block.hash(), h); + assertEq(l1Block.sequenceNumber(), s); + assertEq(l1Block.batcherHash(), bt); + assertEq(l1Block.l1FeeOverhead(), fo); + assertEq(l1Block.l1FeeScalar(), fs); + } + + /// @notice Tests that `setL1BlockValues` can set max values. + function test_setL1BlockValues_succeeds() external { + vm.prank(depositor); + l1Block.setL1BlockValues({ + _number: type(uint64).max, + _timestamp: type(uint64).max, + _basefee: type(uint256).max, + _hash: keccak256(abi.encode(1)), + _sequenceNumber: type(uint64).max, + _batcherHash: bytes32(type(uint256).max), + _l1FeeOverhead: type(uint256).max, + _l1FeeScalar: type(uint256).max + }); + } + + /// @notice Tests that `setL1BlockValues` reverts if sender address is not the depositor + function test_setL1BlockValues_notDepositor_reverts() external { + vm.expectRevert("L1Block: only the depositor account can set L1 block values"); + l1Block.setL1BlockValues({ + _number: type(uint64).max, + _timestamp: type(uint64).max, + _basefee: type(uint256).max, + _hash: keccak256(abi.encode(1)), + _sequenceNumber: type(uint64).max, + _batcherHash: bytes32(type(uint256).max), + _l1FeeOverhead: type(uint256).max, + _l1FeeScalar: type(uint256).max + }); + } +} + +/// @title L1Block_CGT_SetL1BlockValuesEcotone_Test +/// @notice Tests the `setL1BlockValuesEcotone` function of the `L1Block` contract with custom gas +/// token enabled. +contract L1Block_CGT_SetL1BlockValuesEcotone_Test is L1Block_CGT_TestInit { + /// @notice Tests that setL1BlockValuesEcotone updates the values appropriately. + function testFuzz_setL1BlockValuesEcotone_succeeds( + uint32 baseFeeScalar, + uint32 blobBaseFeeScalar, + uint64 sequenceNumber, + uint64 timestamp, + uint64 number, + uint256 baseFee, + uint256 blobBaseFee, + bytes32 hash, + bytes32 batcherHash + ) + external + { + bytes memory functionCallDataPacked = Encoding.encodeSetL1BlockValuesEcotone( + baseFeeScalar, blobBaseFeeScalar, sequenceNumber, timestamp, number, baseFee, blobBaseFee, hash, batcherHash + ); + + vm.prank(depositor); + (bool success,) = address(l1Block).call(functionCallDataPacked); + assertTrue(success, "Function call failed"); + + assertEq(l1Block.baseFeeScalar(), baseFeeScalar); + assertEq(l1Block.blobBaseFeeScalar(), blobBaseFeeScalar); + assertEq(l1Block.sequenceNumber(), sequenceNumber); + assertEq(l1Block.timestamp(), timestamp); + assertEq(l1Block.number(), number); + assertEq(l1Block.basefee(), baseFee); + assertEq(l1Block.blobBaseFee(), blobBaseFee); + assertEq(l1Block.hash(), hash); + assertEq(l1Block.batcherHash(), batcherHash); + + // ensure we didn't accidentally pollute the 128 bits of the sequencenum+scalars slot that + // should be empty + bytes32 scalarsSlot = vm.load(address(l1Block), bytes32(uint256(3))); + bytes32 mask128 = hex"FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000000000000000000000000000"; + + assertEq(0, scalarsSlot & mask128); + + // ensure we didn't accidentally pollute the 128 bits of the number & timestamp slot that + // should be empty + bytes32 numberTimestampSlot = vm.load(address(l1Block), bytes32(uint256(0))); + assertEq(0, numberTimestampSlot & mask128); + } + + /// @notice Tests that `setL1BlockValuesEcotone` succeeds if sender address is the depositor + function test_setL1BlockValuesEcotone_isDepositor_succeeds() external { + bytes memory functionCallDataPacked = Encoding.encodeSetL1BlockValuesEcotone( + type(uint32).max, + type(uint32).max, + type(uint64).max, + type(uint64).max, + type(uint64).max, + type(uint256).max, + type(uint256).max, + bytes32(type(uint256).max), + bytes32(type(uint256).max) + ); + + vm.prank(depositor); + (bool success,) = address(l1Block).call(functionCallDataPacked); + assertTrue(success, "function call failed"); + } + + /// @notice Tests that `setL1BlockValuesEcotone` reverts if sender address is not the depositor + function test_setL1BlockValuesEcotone_notDepositor_reverts() external { + bytes memory functionCallDataPacked = Encoding.encodeSetL1BlockValuesEcotone( + type(uint32).max, + type(uint32).max, + type(uint64).max, + type(uint64).max, + type(uint64).max, + type(uint256).max, + type(uint256).max, + bytes32(type(uint256).max), + bytes32(type(uint256).max) + ); + + (bool success, bytes memory data) = address(l1Block).call(functionCallDataPacked); + assertTrue(!success, "function call should have failed"); + // make sure return value is the expected function selector for "NotDepositor()" + bytes memory expReturn = hex"3cc50b45"; + assertEq(data, expReturn); + } +} + +/// @title L1Block_CGTSetL1BlockValuesIsthmus_Test +/// @notice Tests the `setL1BlockValuesIsthmus` function of the `L1Block` contract with custom gas +/// token enabled. +contract L1Block_CGT_SetL1BlockValuesIsthmus_Test is L1Block_CGT_TestInit { + /// @notice Tests that setL1BlockValuesIsthmus updates the values appropriately. + function testFuzz_setL1BlockValuesIsthmus_succeeds( + uint32 baseFeeScalar, + uint32 blobBaseFeeScalar, + uint64 sequenceNumber, + uint64 timestamp, + uint64 number, + uint256 baseFee, + uint256 blobBaseFee, + bytes32 hash, + bytes32 batcherHash, + uint32 operatorFeeScalar, + uint64 operatorFeeConstant + ) + external + { + bytes memory functionCallDataPacked = Encoding.encodeSetL1BlockValuesIsthmus( + baseFeeScalar, + blobBaseFeeScalar, + sequenceNumber, + timestamp, + number, + baseFee, + blobBaseFee, + hash, + batcherHash, + operatorFeeScalar, + operatorFeeConstant + ); + + vm.prank(depositor); + (bool success,) = address(l1Block).call(functionCallDataPacked); + assertTrue(success, "Function call failed"); + + assertEq(l1Block.baseFeeScalar(), baseFeeScalar); + assertEq(l1Block.blobBaseFeeScalar(), blobBaseFeeScalar); + assertEq(l1Block.sequenceNumber(), sequenceNumber); + assertEq(l1Block.timestamp(), timestamp); + assertEq(l1Block.number(), number); + assertEq(l1Block.basefee(), baseFee); + assertEq(l1Block.blobBaseFee(), blobBaseFee); + assertEq(l1Block.hash(), hash); + assertEq(l1Block.batcherHash(), batcherHash); + assertEq(l1Block.operatorFeeScalar(), operatorFeeScalar); + assertEq(l1Block.operatorFeeConstant(), operatorFeeConstant); + + // ensure we didn't accidentally pollute the 128 bits of the sequencenum+scalars slot that + // should be empty + bytes32 scalarsSlot = vm.load(address(l1Block), bytes32(uint256(3))); + bytes32 mask128 = hex"FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000000000000000000000000000"; + + assertEq(0, scalarsSlot & mask128); + + // ensure we didn't accidentally pollute the 128 bits of the number & timestamp slot that + // should be empty + bytes32 numberTimestampSlot = vm.load(address(l1Block), bytes32(uint256(0))); + assertEq(0, numberTimestampSlot & mask128); + } + + /// @notice Tests that `setL1BlockValuesIsthmus` succeeds if sender address is the depositor + function test_setL1BlockValuesIsthmus_isDepositor_succeeds() external { + bytes memory functionCallDataPacked = Encoding.encodeSetL1BlockValuesIsthmus( + type(uint32).max, + type(uint32).max, + type(uint64).max, + type(uint64).max, + type(uint64).max, + type(uint256).max, + type(uint256).max, + bytes32(type(uint256).max), + bytes32(type(uint256).max), + type(uint32).max, + type(uint64).max + ); + + vm.prank(depositor); + (bool success,) = address(l1Block).call(functionCallDataPacked); + assertTrue(success, "function call failed"); + } + + /// @notice Tests that `setL1BlockValuesIsthmus` reverts if sender address is not the depositor + function test_setL1BlockValuesIsthmus_notDepositor_reverts() external { + bytes memory functionCallDataPacked = Encoding.encodeSetL1BlockValuesIsthmus( + type(uint32).max, + type(uint32).max, + type(uint64).max, + type(uint64).max, + type(uint64).max, + type(uint256).max, + type(uint256).max, + bytes32(type(uint256).max), + bytes32(type(uint256).max), + type(uint32).max, + type(uint64).max + ); + + (bool success, bytes memory data) = address(l1Block).call(functionCallDataPacked); + assertTrue(!success, "function call should have failed"); + // make sure return value is the expected function selector for "NotDepositor()" + bytes memory expReturn = hex"3cc50b45"; + assertEq(data, expReturn); + } +} diff --git a/packages/contracts-bedrock/test/L2/L2ToL1MessagePasser_CGT.t.sol b/packages/contracts-bedrock/test/L2/L2ToL1MessagePasser_CGT.t.sol new file mode 100644 index 0000000000000..2e6c9cb80583a --- /dev/null +++ b/packages/contracts-bedrock/test/L2/L2ToL1MessagePasser_CGT.t.sol @@ -0,0 +1,76 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.15; + +// Testing utilities +import { CommonTest } from "test/setup/CommonTest.sol"; + +// Libraries +import { Types } from "src/libraries/Types.sol"; +import { Hashing } from "src/libraries/Hashing.sol"; + +// Interfaces +import { IL2ToL1MessagePasser } from "interfaces/L2/IL2ToL1MessagePasser.sol"; + +/// @title L2ToL1MessagePasser_CGT_TestInit +/// @notice Tests the `L2ToL1MessagePasser` contract with a custom gas token enabled. +contract L2ToL1MessagePasser_CGT_TestInit is CommonTest { + /// @notice Sets up the test suite with custom gas token enabled. + function setUp() public override { + super.enableCustomGasToken(); + super.setUp(); + } +} + +/// @title L2ToL1MessagePasser_CGT_InitiateWithdrawal_Test +/// @notice Tests the `initiateWithdrawal` function of the `L2ToL1MessagePasser` contract with +/// custom gas token enabled. +contract L2ToL1MessagePasser_CGT_InitiateWithdrawal_Test is L2ToL1MessagePasser_CGT_TestInit { + /// @notice Tests that `initiateWithdrawal` succeeds and correctly sets the state of the + /// message passer for the withdrawal hash. + function testFuzz_initiateWithdrawal_withZeroValue_succeeds( + address _sender, + address _target, + uint256 _gasLimit, + bytes memory _data + ) + external + { + uint256 nonce = l2ToL1MessagePasser.messageNonce(); + + bytes32 withdrawalHash = Hashing.hashWithdrawal( + Types.WithdrawalTransaction({ + nonce: nonce, + sender: _sender, + target: _target, + value: 0, + gasLimit: _gasLimit, + data: _data + }) + ); + + vm.expectEmit(address(l2ToL1MessagePasser)); + emit MessagePassed(nonce, _sender, _target, 0, _gasLimit, _data, withdrawalHash); + + vm.prank(_sender); + l2ToL1MessagePasser.initiateWithdrawal{ value: 0 }(_target, _gasLimit, _data); + + assertEq(l2ToL1MessagePasser.sentMessages(withdrawalHash), true); + + bytes32 slot = keccak256(bytes.concat(withdrawalHash, bytes32(0))); + + assertEq(vm.load(address(l2ToL1MessagePasser), slot), bytes32(uint256(1))); + } + + /// @notice Tests that `initiateWithdrawal` fails when called with value and custom gas token + /// is enabled. + function testFuzz_initiateWithdrawal_withValue_fails(address _randomAddress, uint256 _value) external { + // Set initial state + _value = bound(_value, 1, type(uint256).max); + vm.deal(_randomAddress, _value); + + // Expect revert with NotAllowedOnCGTMode + vm.prank(_randomAddress); + vm.expectRevert(IL2ToL1MessagePasser.NotAllowedOnCGTMode.selector); + l2ToL1MessagePasser.initiateWithdrawal{ value: _value }({ _target: address(0), _gasLimit: 1, _data: "" }); + } +} diff --git a/packages/contracts-bedrock/test/L2/LiquidityController.t.sol b/packages/contracts-bedrock/test/L2/LiquidityController.t.sol new file mode 100644 index 0000000000000..000973ec9f5b2 --- /dev/null +++ b/packages/contracts-bedrock/test/L2/LiquidityController.t.sol @@ -0,0 +1,209 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.15; + +// Testing utilities +import { CommonTest } from "test/setup/CommonTest.sol"; +import { stdStorage, StdStorage } from "forge-std/Test.sol"; + +// Error imports +import { Unauthorized } from "src/libraries/errors/CommonErrors.sol"; + +// Libraries +import { Predeploys } from "src/libraries/Predeploys.sol"; + +// Interfaces +import { IProxyAdmin } from "interfaces/universal/IProxyAdmin.sol"; + +/// @title LiquidityController_TestInit +/// @notice Reusable test initialization for `LiquidityController` tests. +contract LiquidityController_TestInit is CommonTest { + using stdStorage for StdStorage; + + /// @notice Emitted when an address withdraws native asset liquidity. + event LiquidityWithdrawn(address indexed caller, uint256 value); + + /// @notice Emitted when an address deposits native asset liquidity. + event LiquidityDeposited(address indexed caller, uint256 value); + + /// @notice Emitted when an address is authorized to mint/burn liquidity + event MinterAuthorized(address indexed minter); + + /// @notice Emitted when liquidity is minted + event LiquidityMinted(address indexed minter, address indexed to, uint256 amount); + + /// @notice Emitted when liquidity is burned + event LiquidityBurned(address indexed minter, uint256 amount); + + /// @notice Test setup. + function setUp() public virtual override { + enableCustomGasToken(); + super.setUp(); + } + + /// @notice Tests that contract is set up correctly. + function test_setup_succeeds() public view { + assertEq(liquidityController.version(), "1.0.0"); + assertEq(liquidityController.gasPayingTokenName(), "Custom Gas Token"); + assertEq(liquidityController.gasPayingTokenSymbol(), "CGT"); + } + + /// @notice Shared modifier to authorize a minter. + modifier isAuthorizedMinter(address _minter) { + // Authorize the minter + stdstore.target(address(liquidityController)).sig(liquidityController.minters.selector).with_key(_minter) + .checked_write(true); + _; + } +} + +/// @title LiquidityController_AuthorizeMinter_Test +/// @notice Tests the `authorizeMinter` function of the `LiquidityController` contract. +contract LiquidityController_AuthorizeMinter_Test is LiquidityController_TestInit { + /// @notice Tests that the authorizeMinter function can be called by the owner. + function testFuzz_authorizeMinter_fromOwner_succeeds(address _minter) public { + // Expect emit MinterAuthorized event + vm.expectEmit(address(liquidityController)); + emit MinterAuthorized(_minter); + // Call the authorizeMinter function with owner as the caller + vm.prank(IProxyAdmin(Predeploys.PROXY_ADMIN).owner()); + liquidityController.authorizeMinter(_minter); + + // Assert minter is authorized + assertTrue(liquidityController.minters(_minter)); + } + + /// @notice Tests that the authorizeMinter function reverts when called by non-owner. + function testFuzz_authorizeMinter_fromNonOwner_fails(address _caller, address _minter) public { + vm.assume(_caller != IProxyAdmin(Predeploys.PROXY_ADMIN).owner()); + + // Call the authorizeMinter function with non-owner as the caller + vm.prank(_caller); + vm.expectRevert(Unauthorized.selector); + liquidityController.authorizeMinter(_minter); + + // Assert minter is not authorized + assertFalse(liquidityController.minters(_minter)); + } +} + +/// @title LiquidityController_Mint_Test +/// @notice Tests the `mint` function of the `LiquidityController` contract. +contract LiquidityController_Mint_Test is LiquidityController_TestInit { + address authorizedMinter = makeAddr("authorizedMinter"); + + /// @notice Tests that the mint function can be called by an authorized minter. + function testFuzz_mint_fromAuthorizedMinter_succeeds( + address _to, + uint256 _amount + ) + public + isAuthorizedMinter(authorizedMinter) + { + _amount = bound(_amount, 1, address(nativeAssetLiquidity).balance); + + // Record initial balances + uint256 nativeAssetBalanceBefore = address(nativeAssetLiquidity).balance; + uint256 toBalanceBefore = _to.balance; + + // Expect emit LiquidityWithdrawn event and call the mint function + vm.expectEmit(address(nativeAssetLiquidity)); + emit LiquidityWithdrawn(address(liquidityController), _amount); + // Expect emit LiquidityMinted event + vm.expectEmit(address(liquidityController)); + emit LiquidityMinted(authorizedMinter, _to, _amount); + vm.prank(authorizedMinter); + liquidityController.mint(_to, _amount); + + // Assert recipient and NativeAssetLiquidity balances are updated correctly + assertEq(_to.balance, toBalanceBefore + _amount); + assertEq(address(nativeAssetLiquidity).balance, nativeAssetBalanceBefore - _amount); + } + + /// @notice Tests that the mint function reverts when called by unauthorized address. + function testFuzz_mint_fromUnauthorizedCaller_fails(address _caller, address _to, uint256 _amount) public { + _amount = bound(_amount, 1, type(uint248).max); + + uint256 nativeAssetBalanceBefore = address(nativeAssetLiquidity).balance; + uint256 toBalanceBefore = _to.balance; + + // Call the mint function with unauthorized caller + vm.prank(_caller); + vm.expectRevert(Unauthorized.selector); + liquidityController.mint(_to, _amount); + + // Assert recipient and NativeAssetLiquidity balances remain unchanged + assertEq(_to.balance, toBalanceBefore); + assertEq(address(nativeAssetLiquidity).balance, nativeAssetBalanceBefore); + } + + /// @notice Tests that the mint function reverts when contract has insufficient balance. + function test_mint_insufficientBalance_fails() public isAuthorizedMinter(authorizedMinter) { + // Try to mint more than available balance + uint256 contractBalance = address(nativeAssetLiquidity).balance; + uint256 amount = bound(contractBalance, contractBalance + 1, type(uint256).max); + address to = makeAddr("recipient"); + + // Call the mint function with insufficient balance + vm.prank(authorizedMinter); + vm.expectRevert(); // Should revert due to insufficient balance in NativeAssetLiquidity + liquidityController.mint(to, amount); + + // Assert recipient and NativeAssetLiquidity balances remain unchanged + assertEq(to.balance, 0); + assertEq(address(nativeAssetLiquidity).balance, contractBalance); + } +} + +/// @title LiquidityController_Burn_Test +/// @notice Tests the `burn` function of the `LiquidityController` contract. +contract LiquidityController_Burn_Test is LiquidityController_TestInit { + address authorizedMinter = makeAddr("authorizedMinter"); + + /// @notice Tests that the burn function can be called by an authorized minter. + function testFuzz_burn_fromAuthorizedMinter_succeeds(uint256 _amount) public isAuthorizedMinter(authorizedMinter) { + _amount = bound(_amount, 0, type(uint248).max); + + // Deal the authorized minter with the amount to burn + vm.deal(authorizedMinter, _amount); + uint256 nativeAssetBalanceBefore = address(nativeAssetLiquidity).balance; + uint256 minterBalanceBefore = authorizedMinter.balance; + + // Expect emit LiquidityDeposited event and call the burn function + vm.expectEmit(address(nativeAssetLiquidity)); + emit LiquidityDeposited(address(liquidityController), _amount); + // Expect emit LiquidityBurned event + vm.expectEmit(address(liquidityController)); + emit LiquidityBurned(authorizedMinter, _amount); + vm.prank(authorizedMinter); + liquidityController.burn{ value: _amount }(); + + // Assert minter and NativeAssetLiquidity balances are updated correctly + assertEq(authorizedMinter.balance, minterBalanceBefore - _amount); + assertEq(address(nativeAssetLiquidity).balance, nativeAssetBalanceBefore + _amount); + } + + /// @notice Tests that the burn function reverts when called by unauthorized address. + function testFuzz_burn_fromUnauthorizedCaller_fails( + address _caller, + uint256 _amount + ) + public + isAuthorizedMinter(authorizedMinter) + { + _amount = bound(_amount, 0, type(uint248).max); + + // Deal the unauthorized caller with the amount to burn + vm.deal(_caller, _amount); + uint256 nativeAssetBalanceBefore = address(nativeAssetLiquidity).balance; + uint256 callerBalanceBefore = _caller.balance; + + // Call the burn function with unauthorized caller + vm.prank(_caller); + vm.expectRevert(Unauthorized.selector); + liquidityController.burn{ value: _amount }(); + + // Assert caller and NativeAssetLiquidity balances remain unchanged + assertEq(_caller.balance, callerBalanceBefore); + assertEq(address(nativeAssetLiquidity).balance, nativeAssetBalanceBefore); + } +} diff --git a/packages/contracts-bedrock/test/L2/NativeAssetLiquidity.t.sol b/packages/contracts-bedrock/test/L2/NativeAssetLiquidity.t.sol new file mode 100644 index 0000000000000..a966bcf66b6df --- /dev/null +++ b/packages/contracts-bedrock/test/L2/NativeAssetLiquidity.t.sol @@ -0,0 +1,182 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.15; + +// Testing utilities +import { CommonTest } from "test/setup/CommonTest.sol"; + +// Error imports +import { Unauthorized } from "src/libraries/errors/CommonErrors.sol"; + +// Libraries +import { Predeploys } from "src/libraries/Predeploys.sol"; + +// Interfaces +import { IProxyAdmin } from "interfaces/universal/IProxyAdmin.sol"; + +import "forge-std/console.sol"; + +/// @title NativeAssetLiquidity_TestInit +/// @notice Reusable test initialization for `NativeAssetLiquidity` tests. +contract NativeAssetLiquidity_TestInit is CommonTest { + /// @notice Emitted when an address withdraws native asset liquidity. + event LiquidityWithdrawn(address indexed caller, uint256 value); + + /// @notice Emitted when an address deposits native asset liquidity. + event LiquidityDeposited(address indexed caller, uint256 value); + + /// @notice Emitted when an address burns native asset liquidity. + event LiquidityBurned(address indexed caller, uint256 value); + + /// @notice Test setup. + function setUp() public virtual override { + enableCustomGasToken(); + super.setUp(); + } + + /// @notice Tests that contract is set up correctly. + function test_setup_succeeds() public view { + // Assert + assertEq(nativeAssetLiquidity.version(), "1.0.0"); + } +} + +/// @title NativeAssetLiquidity_Deposit_Test +/// @notice Tests the `deposit` function of the `NativeAssetLiquidity` contract. +contract NativeAssetLiquidity_Deposit_Test is NativeAssetLiquidity_TestInit { + /// @notice Tests that the deposit function can be called by the authorized caller. + /// @param _amount Amount of native asset (in wei) to call the deposit function with. + function testFuzz_deposit_fromAuthorizedCaller_succeeds(uint256 _amount) public { + _amount = bound(_amount, 0, type(uint248).max); + + // Deal the LiquidityController with the amount to deposit + vm.deal(address(liquidityController), _amount); + uint256 nativeAssetBalanceBefore = address(nativeAssetLiquidity).balance; + + // Expect emit LiquidityDeposited event + vm.expectEmit(address(nativeAssetLiquidity)); + emit LiquidityDeposited(address(liquidityController), _amount); + + // Call the deposit function with LiquidityController as the caller + vm.prank(address(liquidityController)); + nativeAssetLiquidity.deposit{ value: _amount }(); + + // Assert LiquidityController and NativeAssetLiquidity balances are updated correctly + assertEq(address(liquidityController).balance, 0); + assertEq(address(nativeAssetLiquidity).balance, nativeAssetBalanceBefore + _amount); + } + + /// @notice Tests that the deposit function always reverts when called by an unauthorized caller. + /// @param _amount Amount of native asset (in wei) to call the deposit function with. + /// @param _caller Address of the caller to call the deposit function with. + function testFuzz_deposit_fromUnauthorizedCaller_fails(uint256 _amount, address _caller) public { + vm.assume(_caller != address(liquidityController)); + _amount = bound(_amount, 0, type(uint248).max); + + // Deal the unauthorized caller with the amount to deposit + vm.deal(_caller, _amount); + uint256 nativeAssetBalanceBefore = address(nativeAssetLiquidity).balance; + + // Call the deposit function with unauthorized caller + vm.prank(_caller); + // Expect revert with Unauthorized + vm.expectRevert(Unauthorized.selector); + nativeAssetLiquidity.deposit{ value: _amount }(); + + // Assert caller and NativeAssetLiquidity balances remain unchanged + assertEq(_caller.balance, _amount); + assertEq(address(nativeAssetLiquidity).balance, nativeAssetBalanceBefore); + } +} + +/// @title NativeAssetLiquidity_Withdraw_Test +/// @notice Tests the `withdraw` function of the `NativeAssetLiquidity` contract. +contract NativeAssetLiquidity_Withdraw_Test is NativeAssetLiquidity_TestInit { + /// @notice Tests that the withdraw function can be called by the authorized caller. + /// @param _amount Amount of native asset (in wei) to call the withdraw function with. + function testFuzz_withdraw_fromAuthorizedCaller_succeeds(uint256 _amount) public { + _amount = bound(_amount, 1, type(uint248).max); + + // Deal NativeAssetLiquidity with the amount to withdraw + vm.deal(address(nativeAssetLiquidity), _amount); + uint256 nativeAssetBalanceBefore = address(nativeAssetLiquidity).balance; + uint256 controllerBalanceBefore = address(liquidityController).balance; + + // Expect emit LiquidityWithdrawn event + vm.expectEmit(address(nativeAssetLiquidity)); + emit LiquidityWithdrawn(address(liquidityController), _amount); + vm.prank(address(liquidityController)); + nativeAssetLiquidity.withdraw(_amount); + + // Assert LiquidityController and NativeAssetLiquidity balances are updated correctly + assertEq(address(liquidityController).balance, controllerBalanceBefore + _amount); + assertEq(address(nativeAssetLiquidity).balance, nativeAssetBalanceBefore - _amount); + } + + /// @notice Tests that the withdraw function always reverts when called by an unauthorized caller. + /// @param _amount Amount of native asset (in wei) to call the withdraw function with. + /// @param _caller Address of the caller to call the withdraw function with. + function testFuzz_withdraw_fromUnauthorizedCaller_fails(uint256 _amount, address _caller) public { + vm.assume(_caller != address(liquidityController)); + _amount = bound(_amount, 1, type(uint248).max); + + // Deal NativeAssetLiquidity with the amount to withdraw + vm.deal(address(nativeAssetLiquidity), _amount); + uint256 nativeAssetBalanceBefore = address(nativeAssetLiquidity).balance; + uint256 callerBalanceBefore = _caller.balance; + + // Call the withdraw function with unauthorized caller + vm.prank(_caller); + // Expect revert with Unauthorized + vm.expectRevert(Unauthorized.selector); + nativeAssetLiquidity.withdraw(_amount); + + // Assert caller and NativeAssetLiquidity balances remain unchanged + assertEq(_caller.balance, callerBalanceBefore); + assertEq(address(nativeAssetLiquidity).balance, nativeAssetBalanceBefore); + } + + /// @notice Tests that the withdraw function reverts when contract has insufficient balance. + function test_withdraw_insufficientBalance_fails() public { + // Try to withdraw more than available balance + uint256 contractBalance = address(nativeAssetLiquidity).balance; + uint256 amount = bound(contractBalance, contractBalance + 1, type(uint256).max); + + // Call the withdraw function with insufficient balance + vm.prank(address(liquidityController)); + // Expect revert with OutOfFunds + vm.expectRevert(); + nativeAssetLiquidity.withdraw(amount); + + // Assert contract and controller balances remain unchanged + assertEq(address(nativeAssetLiquidity).balance, contractBalance); + assertEq(address(liquidityController).balance, 0); + } +} + +/// @title NativeAssetLiquidity_Burn_Test +/// @notice Tests the `burn` function of the `NativeAssetLiquidity` contract. +contract NativeAssetLiquidity_Burn_Test is NativeAssetLiquidity_TestInit { + /// @notice Tests that the burn function can be called by the ProxyAdmin owner. + /// @param _amount Amount of native asset (in wei) to call the burn function with. + function test_burn_fromAuthorizedCaller_succeeds(uint256 _amount) public { + _amount = bound(_amount, 1, type(uint248).max); + + uint256 nativeAssetBalanceBefore = address(nativeAssetLiquidity).balance; + + address deployer = address(nativeAssetLiquidity); + uint256 nonce = vm.getNonce(deployer); + address precalculatedBurner = vm.computeCreateAddress(deployer, nonce); + + // Call the burn function with ProxyAdmin owner as the caller + vm.expectEmit(address(nativeAssetLiquidity)); + emit LiquidityBurned(IProxyAdmin(Predeploys.PROXY_ADMIN).owner(), _amount); + vm.prank(IProxyAdmin(Predeploys.PROXY_ADMIN).owner()); + nativeAssetLiquidity.burn(_amount); + + // Assert NativeAssetLiquidity balance is updated correctly + assertEq(address(nativeAssetLiquidity).balance, nativeAssetBalanceBefore - _amount); + + // Assert burner balance is 0 + assertEq(precalculatedBurner.balance, 0); + } +} diff --git a/packages/contracts-bedrock/test/libraries/Predeploys.t.sol b/packages/contracts-bedrock/test/libraries/Predeploys.t.sol index ff9cacefa5dbe..89bcb1434d041 100644 --- a/packages/contracts-bedrock/test/libraries/Predeploys.t.sol +++ b/packages/contracts-bedrock/test/libraries/Predeploys.t.sol @@ -42,7 +42,7 @@ contract Predeploys_TestInit is CommonTest { } /// @notice Internal test function for predeploys validation across different forks. - function _test_predeploys(Fork _fork, bool _enableCrossL2Inbox) internal { + function _test_predeploys(Fork _fork, bool _enableCrossL2Inbox, bool _isCustomGasToken) internal { uint256 count = 2048; uint160 prefix = uint160(0x420) << 148; @@ -57,7 +57,8 @@ contract Predeploys_TestInit is CommonTest { continue; } - bool isPredeploy = Predeploys.isSupportedPredeploy(addr, uint256(_fork), _enableCrossL2Inbox); + bool isPredeploy = + Predeploys.isSupportedPredeploy(addr, uint256(_fork), _enableCrossL2Inbox, _isCustomGasToken); bytes memory code = addr.code; if (isPredeploy) assertTrue(code.length > 0); @@ -133,7 +134,7 @@ contract Predeploys_Unclassified_Test is Predeploys_TestInit { /// @notice Tests that the predeploy addresses are set correctly. They have code /// and the proxied accounts have the correct admin. function test_predeploys_succeeds() external { - _test_predeploys(Fork.ISTHMUS, false); + _test_predeploys(Fork.ISTHMUS, false, false); } } @@ -150,12 +151,28 @@ contract Predeploys_UnclassifiedInterop_Test is Predeploys_TestInit { /// @notice Tests that the predeploy addresses are set correctly. They have code and the /// proxied accounts have the correct admin. Using interop with inbox. function test_predeploysWithInbox_succeeds() external { - _test_predeploys(Fork.INTEROP, true); + _test_predeploys(Fork.INTEROP, true, false); } /// @notice Tests that the predeploy addresses are set correctly. They have code and the /// proxied accounts have the correct admin. Using interop without inbox. function test_predeploysWithoutInbox_succeeds() external { - _test_predeploys(Fork.INTEROP, false); + _test_predeploys(Fork.INTEROP, false, false); + } +} + +/// @title Predeploys_CustomGasToken_Test +/// @notice Tests the `Predeploys` contract with custom gas token. +contract Predeploys_CustomGasToken_Test is Predeploys_TestInit { + /// @notice Test setup. Enabling custom gas token. + function setUp() public virtual override { + super.enableCustomGasToken(); + super.setUp(); + } + + /// @notice Tests that the predeploy addresses are set correctly. They have code and the + /// proxied accounts have the correct admin. Using custom gas token. + function test_predeploysWithCustomGasToken_succeeds() external { + _test_predeploys(Fork.ISTHMUS, false, true); } } diff --git a/packages/contracts-bedrock/test/scripts/L2Genesis.t.sol b/packages/contracts-bedrock/test/scripts/L2Genesis.t.sol index 400f3285bf50b..b8553a57a1043 100644 --- a/packages/contracts-bedrock/test/scripts/L2Genesis.t.sol +++ b/packages/contracts-bedrock/test/scripts/L2Genesis.t.sol @@ -15,6 +15,8 @@ import { IOptimismMintableERC721Factory } from "interfaces/L2/IOptimismMintableE import { IProxyAdmin } from "interfaces/universal/IProxyAdmin.sol"; import { IGovernanceToken } from "interfaces/governance/IGovernanceToken.sol"; import { IGasPriceOracle } from "interfaces/L2/IGasPriceOracle.sol"; +import { ILiquidityController } from "interfaces/L2/ILiquidityController.sol"; +import { INativeAssetLiquidity } from "interfaces/L2/INativeAssetLiquidity.sol"; /// @title L2Genesis_TestInit /// @notice Reusable test initialization for `L2Genesis` tests. @@ -47,7 +49,7 @@ contract L2Genesis_TestInit is Test { assertEq(Predeploys.PROXY_ADMIN, EIP1967Helper.getAdmin(addr)); // If it's not a supported predeploy, skip next checks. - if (!Predeploys.isSupportedPredeploy(addr, uint256(LATEST_FORK), true)) { + if (!Predeploys.isSupportedPredeploy(addr, uint256(LATEST_FORK), true, input.isCustomGasToken)) { continue; } @@ -102,6 +104,21 @@ contract L2Genesis_TestInit is Test { assertEq(gasPriceOracle.isFjord(), true); assertEq(gasPriceOracle.isIsthmus(), true); } + + function testCGT() internal view { + // Test LiquidityController deployment + ILiquidityController controller = ILiquidityController(Predeploys.LIQUIDITY_CONTROLLER); + assertEq(controller.gasPayingTokenName(), input.gasPayingTokenName); + assertEq(controller.gasPayingTokenSymbol(), input.gasPayingTokenSymbol); + + // Test NativeAssetLiquidity deployment and funding + INativeAssetLiquidity liquidity = INativeAssetLiquidity(Predeploys.NATIVE_ASSET_LIQUIDITY); + assertEq(address(liquidity).balance, type(uint248).max); + + // Verify predeploys have code + assertGt(Predeploys.LIQUIDITY_CONTROLLER.code.length, 0); + assertGt(Predeploys.NATIVE_ASSET_LIQUIDITY.code.length, 0); + } } /// @title L2Genesis_Run_Test @@ -128,7 +145,46 @@ contract L2Genesis_Run_Test is L2Genesis_TestInit { fork: uint256(LATEST_FORK), deployCrossL2Inbox: true, enableGovernance: true, - fundDevAccounts: true + fundDevAccounts: true, + isCustomGasToken: false, + gasPayingTokenName: "", + gasPayingTokenSymbol: "" + }); + genesis.run(input); + + testProxyAdmin(); + testPredeploys(); + testVaults(); + testGovernance(); + testFactories(); + testForks(); + } + + function test_run_cgt_succeeds() external { + input = L2Genesis.Input({ + l1ChainID: 1, + l2ChainID: 2, + l1CrossDomainMessengerProxy: payable(address(0x0000000000000000000000000000000000000001)), + l1StandardBridgeProxy: payable(address(0x0000000000000000000000000000000000000002)), + l1ERC721BridgeProxy: payable(address(0x0000000000000000000000000000000000000003)), + opChainProxyAdminOwner: address(0x0000000000000000000000000000000000000004), + sequencerFeeVaultRecipient: address(0x0000000000000000000000000000000000000005), + sequencerFeeVaultMinimumWithdrawalAmount: 1, + sequencerFeeVaultWithdrawalNetwork: 1, + baseFeeVaultRecipient: address(0x0000000000000000000000000000000000000006), + baseFeeVaultMinimumWithdrawalAmount: 1, + baseFeeVaultWithdrawalNetwork: 1, + l1FeeVaultRecipient: address(0x0000000000000000000000000000000000000007), + l1FeeVaultMinimumWithdrawalAmount: 1, + l1FeeVaultWithdrawalNetwork: 1, + governanceTokenOwner: address(0x0000000000000000000000000000000000000008), + fork: uint256(LATEST_FORK), + deployCrossL2Inbox: true, + enableGovernance: true, + fundDevAccounts: true, + isCustomGasToken: true, + gasPayingTokenName: "Custom Gas Token", + gasPayingTokenSymbol: "CGT" }); genesis.run(input); @@ -138,5 +194,6 @@ contract L2Genesis_Run_Test is L2Genesis_TestInit { testGovernance(); testFactories(); testForks(); + testCGT(); } } diff --git a/packages/contracts-bedrock/test/setup/CommonTest.sol b/packages/contracts-bedrock/test/setup/CommonTest.sol index 5e7b1b2d0c396..5e48b01561795 100644 --- a/packages/contracts-bedrock/test/setup/CommonTest.sol +++ b/packages/contracts-bedrock/test/setup/CommonTest.sol @@ -34,6 +34,7 @@ contract CommonTest is Test, Setup, Events { bool useAltDAOverride; bool useInteropOverride; + bool useCustomGasToken; /// @dev This value is only used in forked tests. During forked tests, the default is to perform the upgrade before /// running the tests. @@ -69,6 +70,9 @@ contract CommonTest is Test, Setup, Events { if (useUpgradedFork) { deploy.cfg().setUseUpgradedFork(true); } + if (useCustomGasToken) { + deploy.cfg().setIsCustomGasToken(true); + } if (isForkTest()) { // Skip any test suite which uses a nonstandard configuration. @@ -203,4 +207,9 @@ contract CommonTest is Test, Setup, Events { useUpgradedFork = false; } + + function enableCustomGasToken() public { + _checkNotDeployed("custom gas token"); + useCustomGasToken = true; + } } diff --git a/packages/contracts-bedrock/test/setup/Setup.sol b/packages/contracts-bedrock/test/setup/Setup.sol index 248c6a8d65433..739f1b8ad5376 100644 --- a/packages/contracts-bedrock/test/setup/Setup.sol +++ b/packages/contracts-bedrock/test/setup/Setup.sol @@ -60,6 +60,8 @@ import { ISuperchainTokenBridge } from "interfaces/L2/ISuperchainTokenBridge.sol import { IPermissionedDisputeGame } from "interfaces/dispute/IPermissionedDisputeGame.sol"; import { IFaultDisputeGame } from "interfaces/dispute/IFaultDisputeGame.sol"; import { ICrossL2Inbox } from "interfaces/L2/ICrossL2Inbox.sol"; +import { ILiquidityController } from "interfaces/L2/ILiquidityController.sol"; +import { INativeAssetLiquidity } from "interfaces/L2/INativeAssetLiquidity.sol"; /// @title Setup /// @dev This contact is responsible for setting up the contracts in state. It currently @@ -142,6 +144,8 @@ contract Setup { ISuperchainTokenBridge superchainTokenBridge = ISuperchainTokenBridge(Predeploys.SUPERCHAIN_TOKEN_BRIDGE); IOptimismSuperchainERC20Factory l2OptimismSuperchainERC20Factory = IOptimismSuperchainERC20Factory(Predeploys.OPTIMISM_SUPERCHAIN_ERC20_FACTORY); + ILiquidityController liquidityController = ILiquidityController(Predeploys.LIQUIDITY_CONTROLLER); + INativeAssetLiquidity nativeAssetLiquidity = INativeAssetLiquidity(Predeploys.NATIVE_ASSET_LIQUIDITY); /// @notice Indicates whether a test is running against a forked production network. function isForkTest() public view returns (bool) { @@ -318,7 +322,10 @@ contract Setup { fork: uint256(l2Fork), deployCrossL2Inbox: deploy.cfg().useInterop(), enableGovernance: deploy.cfg().enableGovernance(), - fundDevAccounts: deploy.cfg().fundDevAccounts() + fundDevAccounts: deploy.cfg().fundDevAccounts(), + isCustomGasToken: isCustomGasToken(), + gasPayingTokenName: "Custom Gas Token", + gasPayingTokenSymbol: "CGT" }) ); @@ -350,6 +357,8 @@ contract Setup { labelPredeploy(Predeploys.OPTIMISM_SUPERCHAIN_ERC20_FACTORY); labelPredeploy(Predeploys.OPTIMISM_SUPERCHAIN_ERC20_BEACON); labelPredeploy(Predeploys.SUPERCHAIN_TOKEN_BRIDGE); + labelPredeploy(Predeploys.NATIVE_ASSET_LIQUIDITY); + labelPredeploy(Predeploys.LIQUIDITY_CONTROLLER); // L2 Preinstalls labelPreinstall(Preinstalls.MultiCall3); @@ -379,4 +388,9 @@ contract Setup { function labelPreinstall(address _addr) internal { vm.label(_addr, Preinstalls.getName(_addr)); } + + /// @dev Returns whether custom gas token is enabled + function isCustomGasToken() internal view returns (bool) { + return deploy.cfg().isCustomGasToken(); + } } From c8fd3ff2204f9f331d802b15df84004afef16858 Mon Sep 17 00:00:00 2001 From: Ashitaka <96790496+ashitakah@users.noreply.github.com> Date: Thu, 14 Aug 2025 11:37:40 -0300 Subject: [PATCH 03/23] fix: var naming * fix: var naming * fix: var naming --- .../scripts/deploy/DeployConfig.s.sol | 1 - packages/contracts-bedrock/src/L2/L1Block.sol | 11 +++++------ 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/packages/contracts-bedrock/scripts/deploy/DeployConfig.s.sol b/packages/contracts-bedrock/scripts/deploy/DeployConfig.s.sol index f116c9975c41d..9fd0e3317711d 100644 --- a/packages/contracts-bedrock/scripts/deploy/DeployConfig.s.sol +++ b/packages/contracts-bedrock/scripts/deploy/DeployConfig.s.sol @@ -77,7 +77,6 @@ contract DeployConfig is Script { bool public isCustomGasToken; bool public useInterop; bool public useUpgradedFork; - bool public isCustomGasToken; function read(string memory _path) public { console.log("DeployConfig: reading file %s", _path); diff --git a/packages/contracts-bedrock/src/L2/L1Block.sol b/packages/contracts-bedrock/src/L2/L1Block.sol index b9575c42d0503..28c1e6522722c 100644 --- a/packages/contracts-bedrock/src/L2/L1Block.sol +++ b/packages/contracts-bedrock/src/L2/L1Block.sol @@ -23,7 +23,7 @@ contract L1Block is ISemver { } /// @notice Whether the gas paying token is custom. - bool public immutable IS_CUSTOM_GAS_TOKEN; + bool public immutable isCustomGasToken; /// @notice The latest L1 block number known by the L2 system. uint64 public number; @@ -67,7 +67,7 @@ contract L1Block is ISemver { uint32 public operatorFeeScalar; constructor(bool _isCustomGasToken) { - IS_CUSTOM_GAS_TOKEN = _isCustomGasToken; + isCustomGasToken = _isCustomGasToken; } /// @custom:semver 1.6.2 @@ -85,8 +85,7 @@ contract L1Block is ISemver { /// 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 view returns (string memory name_) { - name_ = - IS_CUSTOM_GAS_TOKEN ? ILiquidityController(Predeploys.LIQUIDITY_CONTROLLER).gasPayingTokenName() : "Ether"; + name_ = isCustomGasToken ? ILiquidityController(Predeploys.LIQUIDITY_CONTROLLER).gasPayingTokenName() : "Ether"; } /// @notice Returns the gas paying token symbol. @@ -94,12 +93,12 @@ contract L1Block is ISemver { /// This function cannot be removed because WETH depends on it. function gasPayingTokenSymbol() public view returns (string memory symbol_) { symbol_ = - IS_CUSTOM_GAS_TOKEN ? ILiquidityController(Predeploys.LIQUIDITY_CONTROLLER).gasPayingTokenSymbol() : "ETH"; + isCustomGasToken ? ILiquidityController(Predeploys.LIQUIDITY_CONTROLLER).gasPayingTokenSymbol() : "ETH"; } /// @notice Returns whether the gas paying token is custom. function isCustomGasToken() public view returns (bool is_) { - is_ = IS_CUSTOM_GAS_TOKEN; + is_ = isCustomGasToken; } /// @custom:legacy From 9da1cef64476c08e52d940f06209cc8fcd166169 Mon Sep 17 00:00:00 2001 From: niha <205694301+0xniha@users.noreply.github.com> Date: Thu, 14 Aug 2025 14:13:43 -0300 Subject: [PATCH 04/23] test: refactor to reuse L1Block repeated tests --- .../test/L2/L1Block_CGT.t.sol | 279 ++---------------- 1 file changed, 24 insertions(+), 255 deletions(-) diff --git a/packages/contracts-bedrock/test/L2/L1Block_CGT.t.sol b/packages/contracts-bedrock/test/L2/L1Block_CGT.t.sol index 6719587e12539..e62e5b3816b77 100644 --- a/packages/contracts-bedrock/test/L2/L1Block_CGT.t.sol +++ b/packages/contracts-bedrock/test/L2/L1Block_CGT.t.sol @@ -3,10 +3,13 @@ pragma solidity 0.8.15; // Testing import { CommonTest } from "test/setup/CommonTest.sol"; +import { + L1Block_SetL1BlockValues_Test, + L1Block_SetL1BlockValuesEcotone_Test, + L1Block_SetL1BlockValuesIsthmus_Test +} from "test/L2/L1Block.t.sol"; // Libraries -import { Encoding } from "src/libraries/Encoding.sol"; -import { Constants } from "src/libraries/Constants.sol"; import "src/libraries/L1BlockErrors.sol"; /// @title L1Block_CGT_TestInit @@ -22,19 +25,6 @@ contract L1Block_CGT_TestInit is CommonTest { } } -/// @title L1Block_CGT_GasPayingToken_Test -/// @notice Tests the `gasPayingToken` function of the `L1Block` contract with custom gas token -/// enabled. -contract L1Block_CGT_GasPayingToken_Test is L1Block_CGT_TestInit { - /// @notice Tests that the `gasPayingToken` function returns the correct token address and - /// decimals. - function test_gasPayingToken_succeeds() external view { - (address token, uint8 decimals) = l1Block.gasPayingToken(); - assertEq(token, Constants.ETHER); - assertEq(uint256(decimals), uint256(18)); - } -} - /// @title L1Block_CGT_GasPayingTokenName_Test /// @notice Tests the `gasPayingTokenName` function of the `L1Block` contract with custom gas /// token enabled. @@ -69,257 +59,36 @@ contract L1Block_CGT_IsCustomGasToken_Test is L1Block_CGT_TestInit { /// @title L1Block_CGT_SetL1BlockValues_Test /// @notice Tests the `setL1BlockValues` function of the `L1Block` contract with custom gas token /// enabled. -contract L1Block_CGT_SetL1BlockValues_Test is L1Block_CGT_TestInit { - /// @notice Tests that `setL1BlockValues` updates the values correctly. - function testFuzz_setL1BlockValues_succeeds( - uint64 n, - uint64 t, - uint256 b, - bytes32 h, - uint64 s, - bytes32 bt, - uint256 fo, - uint256 fs - ) - external - { - vm.prank(depositor); - l1Block.setL1BlockValues(n, t, b, h, s, bt, fo, fs); - assertEq(l1Block.number(), n); - assertEq(l1Block.timestamp(), t); - assertEq(l1Block.basefee(), b); - assertEq(l1Block.hash(), h); - assertEq(l1Block.sequenceNumber(), s); - assertEq(l1Block.batcherHash(), bt); - assertEq(l1Block.l1FeeOverhead(), fo); - assertEq(l1Block.l1FeeScalar(), fs); - } - - /// @notice Tests that `setL1BlockValues` can set max values. - function test_setL1BlockValues_succeeds() external { - vm.prank(depositor); - l1Block.setL1BlockValues({ - _number: type(uint64).max, - _timestamp: type(uint64).max, - _basefee: type(uint256).max, - _hash: keccak256(abi.encode(1)), - _sequenceNumber: type(uint64).max, - _batcherHash: bytes32(type(uint256).max), - _l1FeeOverhead: type(uint256).max, - _l1FeeScalar: type(uint256).max - }); - } - - /// @notice Tests that `setL1BlockValues` reverts if sender address is not the depositor - function test_setL1BlockValues_notDepositor_reverts() external { - vm.expectRevert("L1Block: only the depositor account can set L1 block values"); - l1Block.setL1BlockValues({ - _number: type(uint64).max, - _timestamp: type(uint64).max, - _basefee: type(uint256).max, - _hash: keccak256(abi.encode(1)), - _sequenceNumber: type(uint64).max, - _batcherHash: bytes32(type(uint256).max), - _l1FeeOverhead: type(uint256).max, - _l1FeeScalar: type(uint256).max - }); +contract L1Block_CGT_SetL1BlockValues_Test is L1Block_SetL1BlockValues_Test { + // Override setUp to enable custom gas token + // Re-use the test from L1Block.t.sol + function setUp() public override { + super.enableCustomGasToken(); + super.setUp(); + assertTrue(l1Block.isCustomGasToken()); } } /// @title L1Block_CGT_SetL1BlockValuesEcotone_Test /// @notice Tests the `setL1BlockValuesEcotone` function of the `L1Block` contract with custom gas /// token enabled. -contract L1Block_CGT_SetL1BlockValuesEcotone_Test is L1Block_CGT_TestInit { - /// @notice Tests that setL1BlockValuesEcotone updates the values appropriately. - function testFuzz_setL1BlockValuesEcotone_succeeds( - uint32 baseFeeScalar, - uint32 blobBaseFeeScalar, - uint64 sequenceNumber, - uint64 timestamp, - uint64 number, - uint256 baseFee, - uint256 blobBaseFee, - bytes32 hash, - bytes32 batcherHash - ) - external - { - bytes memory functionCallDataPacked = Encoding.encodeSetL1BlockValuesEcotone( - baseFeeScalar, blobBaseFeeScalar, sequenceNumber, timestamp, number, baseFee, blobBaseFee, hash, batcherHash - ); - - vm.prank(depositor); - (bool success,) = address(l1Block).call(functionCallDataPacked); - assertTrue(success, "Function call failed"); - - assertEq(l1Block.baseFeeScalar(), baseFeeScalar); - assertEq(l1Block.blobBaseFeeScalar(), blobBaseFeeScalar); - assertEq(l1Block.sequenceNumber(), sequenceNumber); - assertEq(l1Block.timestamp(), timestamp); - assertEq(l1Block.number(), number); - assertEq(l1Block.basefee(), baseFee); - assertEq(l1Block.blobBaseFee(), blobBaseFee); - assertEq(l1Block.hash(), hash); - assertEq(l1Block.batcherHash(), batcherHash); - - // ensure we didn't accidentally pollute the 128 bits of the sequencenum+scalars slot that - // should be empty - bytes32 scalarsSlot = vm.load(address(l1Block), bytes32(uint256(3))); - bytes32 mask128 = hex"FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000000000000000000000000000"; - - assertEq(0, scalarsSlot & mask128); - - // ensure we didn't accidentally pollute the 128 bits of the number & timestamp slot that - // should be empty - bytes32 numberTimestampSlot = vm.load(address(l1Block), bytes32(uint256(0))); - assertEq(0, numberTimestampSlot & mask128); - } - - /// @notice Tests that `setL1BlockValuesEcotone` succeeds if sender address is the depositor - function test_setL1BlockValuesEcotone_isDepositor_succeeds() external { - bytes memory functionCallDataPacked = Encoding.encodeSetL1BlockValuesEcotone( - type(uint32).max, - type(uint32).max, - type(uint64).max, - type(uint64).max, - type(uint64).max, - type(uint256).max, - type(uint256).max, - bytes32(type(uint256).max), - bytes32(type(uint256).max) - ); - - vm.prank(depositor); - (bool success,) = address(l1Block).call(functionCallDataPacked); - assertTrue(success, "function call failed"); - } - - /// @notice Tests that `setL1BlockValuesEcotone` reverts if sender address is not the depositor - function test_setL1BlockValuesEcotone_notDepositor_reverts() external { - bytes memory functionCallDataPacked = Encoding.encodeSetL1BlockValuesEcotone( - type(uint32).max, - type(uint32).max, - type(uint64).max, - type(uint64).max, - type(uint64).max, - type(uint256).max, - type(uint256).max, - bytes32(type(uint256).max), - bytes32(type(uint256).max) - ); - - (bool success, bytes memory data) = address(l1Block).call(functionCallDataPacked); - assertTrue(!success, "function call should have failed"); - // make sure return value is the expected function selector for "NotDepositor()" - bytes memory expReturn = hex"3cc50b45"; - assertEq(data, expReturn); +contract L1Block_CGT_SetL1BlockValuesEcotone_Test is L1Block_SetL1BlockValuesEcotone_Test { + // Override setUp to enable custom gas token + // Re-use the test from L1Block.t.sol + function setUp() public override { + super.enableCustomGasToken(); + super.setUp(); + assertTrue(l1Block.isCustomGasToken()); } } /// @title L1Block_CGTSetL1BlockValuesIsthmus_Test /// @notice Tests the `setL1BlockValuesIsthmus` function of the `L1Block` contract with custom gas /// token enabled. -contract L1Block_CGT_SetL1BlockValuesIsthmus_Test is L1Block_CGT_TestInit { - /// @notice Tests that setL1BlockValuesIsthmus updates the values appropriately. - function testFuzz_setL1BlockValuesIsthmus_succeeds( - uint32 baseFeeScalar, - uint32 blobBaseFeeScalar, - uint64 sequenceNumber, - uint64 timestamp, - uint64 number, - uint256 baseFee, - uint256 blobBaseFee, - bytes32 hash, - bytes32 batcherHash, - uint32 operatorFeeScalar, - uint64 operatorFeeConstant - ) - external - { - bytes memory functionCallDataPacked = Encoding.encodeSetL1BlockValuesIsthmus( - baseFeeScalar, - blobBaseFeeScalar, - sequenceNumber, - timestamp, - number, - baseFee, - blobBaseFee, - hash, - batcherHash, - operatorFeeScalar, - operatorFeeConstant - ); - - vm.prank(depositor); - (bool success,) = address(l1Block).call(functionCallDataPacked); - assertTrue(success, "Function call failed"); - - assertEq(l1Block.baseFeeScalar(), baseFeeScalar); - assertEq(l1Block.blobBaseFeeScalar(), blobBaseFeeScalar); - assertEq(l1Block.sequenceNumber(), sequenceNumber); - assertEq(l1Block.timestamp(), timestamp); - assertEq(l1Block.number(), number); - assertEq(l1Block.basefee(), baseFee); - assertEq(l1Block.blobBaseFee(), blobBaseFee); - assertEq(l1Block.hash(), hash); - assertEq(l1Block.batcherHash(), batcherHash); - assertEq(l1Block.operatorFeeScalar(), operatorFeeScalar); - assertEq(l1Block.operatorFeeConstant(), operatorFeeConstant); - - // ensure we didn't accidentally pollute the 128 bits of the sequencenum+scalars slot that - // should be empty - bytes32 scalarsSlot = vm.load(address(l1Block), bytes32(uint256(3))); - bytes32 mask128 = hex"FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000000000000000000000000000"; - - assertEq(0, scalarsSlot & mask128); - - // ensure we didn't accidentally pollute the 128 bits of the number & timestamp slot that - // should be empty - bytes32 numberTimestampSlot = vm.load(address(l1Block), bytes32(uint256(0))); - assertEq(0, numberTimestampSlot & mask128); - } - - /// @notice Tests that `setL1BlockValuesIsthmus` succeeds if sender address is the depositor - function test_setL1BlockValuesIsthmus_isDepositor_succeeds() external { - bytes memory functionCallDataPacked = Encoding.encodeSetL1BlockValuesIsthmus( - type(uint32).max, - type(uint32).max, - type(uint64).max, - type(uint64).max, - type(uint64).max, - type(uint256).max, - type(uint256).max, - bytes32(type(uint256).max), - bytes32(type(uint256).max), - type(uint32).max, - type(uint64).max - ); - - vm.prank(depositor); - (bool success,) = address(l1Block).call(functionCallDataPacked); - assertTrue(success, "function call failed"); - } - - /// @notice Tests that `setL1BlockValuesIsthmus` reverts if sender address is not the depositor - function test_setL1BlockValuesIsthmus_notDepositor_reverts() external { - bytes memory functionCallDataPacked = Encoding.encodeSetL1BlockValuesIsthmus( - type(uint32).max, - type(uint32).max, - type(uint64).max, - type(uint64).max, - type(uint64).max, - type(uint256).max, - type(uint256).max, - bytes32(type(uint256).max), - bytes32(type(uint256).max), - type(uint32).max, - type(uint64).max - ); - - (bool success, bytes memory data) = address(l1Block).call(functionCallDataPacked); - assertTrue(!success, "function call should have failed"); - // make sure return value is the expected function selector for "NotDepositor()" - bytes memory expReturn = hex"3cc50b45"; - assertEq(data, expReturn); +contract L1Block_CGT_SetL1BlockValuesIsthmus_Test is L1Block_SetL1BlockValuesIsthmus_Test { + function setUp() public override { + super.enableCustomGasToken(); + super.setUp(); + assertTrue(l1Block.isCustomGasToken()); } } From 078832fd710616dbdd59fd3a89277ad6667a8054 Mon Sep 17 00:00:00 2001 From: Ashitaka <96790496+ashitakah@users.noreply.github.com> Date: Thu, 14 Aug 2025 17:53:44 -0300 Subject: [PATCH 05/23] fix(cgt): stack too deep (#485) * fix: stack too deep * fix: scripts * fix: scripts --- op-chain-ops/genesis/config.go | 7 +++- .../deploy-config/hardhat.json | 1 + .../deploy-config/internal-devnet.json | 1 + .../deploy-config/mainnet.json | 1 + .../deploy-config/sepolia-devnet-0.json | 1 + .../deploy-config/sepolia.json | 1 + .../interfaces/L2/IL1Block.sol | 3 +- .../snapshots/abi/L1Block.json | 15 +------ .../snapshots/abi/OPContractsManager.json | 5 +++ .../abi/OPContractsManagerDeployer.json | 5 +++ .../snapshots/abi/OptimismPortal2.json | 5 +++ .../snapshots/abi/SystemConfig.json | 18 +++++++++ .../snapshots/semver-lock.json | 16 ++++---- .../snapshots/storageLayout/SystemConfig.json | 7 ++++ .../src/L1/OptimismPortal2.sol | 4 +- .../contracts-bedrock/src/L1/SystemConfig.sol | 39 ++++++++++--------- packages/contracts-bedrock/src/L2/L1Block.sol | 5 --- 17 files changed, 83 insertions(+), 51 deletions(-) diff --git a/op-chain-ops/genesis/config.go b/op-chain-ops/genesis/config.go index ad83381689873..f3df922996b01 100644 --- a/op-chain-ops/genesis/config.go +++ b/op-chain-ops/genesis/config.go @@ -275,6 +275,8 @@ func (d *GasPriceOracleDeployConfig) OperatorFeeParams() [32]byte { type GasTokenDeployConfig struct { // UseCustomGasToken is a flag to indicate that a custom gas token should be used UseCustomGasToken bool `json:"useCustomGasToken"` + // IsCustomGasToken is a flag to indicate that a custom gas token should be used (alternative field name) + IsCustomGasToken bool `json:"isCustomGasToken"` // CustomGasTokenAddress is the address of the ERC20 token to be used to pay for gas on L2. CustomGasTokenAddress common.Address `json:"customGasTokenAddress"` } @@ -282,7 +284,10 @@ type GasTokenDeployConfig struct { var _ ConfigChecker = (*GasTokenDeployConfig)(nil) func (d *GasTokenDeployConfig) Check(log log.Logger) error { - if d.UseCustomGasToken { + // Check if either field indicates custom gas token usage + isCustomGasToken := d.UseCustomGasToken || d.IsCustomGasToken + + if isCustomGasToken { if d.CustomGasTokenAddress == (common.Address{}) { return fmt.Errorf("%w: CustomGasTokenAddress cannot be address(0)", ErrInvalidDeployConfig) } diff --git a/packages/contracts-bedrock/deploy-config/hardhat.json b/packages/contracts-bedrock/deploy-config/hardhat.json index e81d125314e0f..e2c0191b7902a 100644 --- a/packages/contracts-bedrock/deploy-config/hardhat.json +++ b/packages/contracts-bedrock/deploy-config/hardhat.json @@ -63,5 +63,6 @@ "daResolveWindow": 100, "daBondSize": 1000, "daResolverRefundPercentage": 50, + "useCustomGasToken": false, "isCustomGasToken": false } diff --git a/packages/contracts-bedrock/deploy-config/internal-devnet.json b/packages/contracts-bedrock/deploy-config/internal-devnet.json index ba86edaee7d2c..04f7c0d886b3d 100644 --- a/packages/contracts-bedrock/deploy-config/internal-devnet.json +++ b/packages/contracts-bedrock/deploy-config/internal-devnet.json @@ -39,5 +39,6 @@ "systemConfigStartBlock": 8364212, "requiredProtocolVersion": "0x0000000000000000000000000000000000000000000000000000000000000000", "recommendedProtocolVersion": "0x0000000000000000000000000000000000000000000000000000000000000000", + "useCustomGasToken": false, "isCustomGasToken": false } diff --git a/packages/contracts-bedrock/deploy-config/mainnet.json b/packages/contracts-bedrock/deploy-config/mainnet.json index f81c4d131cb9a..e97986b2ec10a 100644 --- a/packages/contracts-bedrock/deploy-config/mainnet.json +++ b/packages/contracts-bedrock/deploy-config/mainnet.json @@ -56,5 +56,6 @@ "disputeGameFinalityDelaySeconds": 302400, "respectedGameType": 0, "useFaultProofs": true, + "useCustomGasToken": false, "isCustomGasToken": false } diff --git a/packages/contracts-bedrock/deploy-config/sepolia-devnet-0.json b/packages/contracts-bedrock/deploy-config/sepolia-devnet-0.json index 5f97a743f17f4..24dc841777cf1 100644 --- a/packages/contracts-bedrock/deploy-config/sepolia-devnet-0.json +++ b/packages/contracts-bedrock/deploy-config/sepolia-devnet-0.json @@ -80,5 +80,6 @@ "fundDevAccounts": false, "requiredProtocolVersion": "0x0000000000000000000000000000000000000005000000000000000000000000", "recommendedProtocolVersion": "0x0000000000000000000000000000000000000005000000000000000000000000", + "useCustomGasToken": false, "isCustomGasToken": false } diff --git a/packages/contracts-bedrock/deploy-config/sepolia.json b/packages/contracts-bedrock/deploy-config/sepolia.json index 31d0867938a8c..fdd254ff11130 100644 --- a/packages/contracts-bedrock/deploy-config/sepolia.json +++ b/packages/contracts-bedrock/deploy-config/sepolia.json @@ -55,5 +55,6 @@ "disputeGameFinalityDelaySeconds": 302400, "respectedGameType": 0, "useFaultProofs": true, + "useCustomGasToken": false, "isCustomGasToken": false } diff --git a/packages/contracts-bedrock/interfaces/L2/IL1Block.sol b/packages/contracts-bedrock/interfaces/L2/IL1Block.sol index bd9cf80589293..3ab26aa52c66f 100644 --- a/packages/contracts-bedrock/interfaces/L2/IL1Block.sol +++ b/packages/contracts-bedrock/interfaces/L2/IL1Block.sol @@ -2,7 +2,6 @@ pragma solidity ^0.8.0; interface IL1Block { - function IS_CUSTOM_GAS_TOKEN() external view returns (bool); function DEPOSITOR_ACCOUNT() external pure returns (address addr_); function baseFeeScalar() external view returns (uint32); function basefee() external view returns (uint256); @@ -13,7 +12,7 @@ interface IL1Block { 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 view 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); diff --git a/packages/contracts-bedrock/snapshots/abi/L1Block.json b/packages/contracts-bedrock/snapshots/abi/L1Block.json index 9fbc6933fb0bf..ba44e5ce56900 100644 --- a/packages/contracts-bedrock/snapshots/abi/L1Block.json +++ b/packages/contracts-bedrock/snapshots/abi/L1Block.json @@ -23,19 +23,6 @@ "stateMutability": "pure", "type": "function" }, - { - "inputs": [], - "name": "IS_CUSTOM_GAS_TOKEN", - "outputs": [ - { - "internalType": "bool", - "name": "", - "type": "bool" - } - ], - "stateMutability": "view", - "type": "function" - }, { "inputs": [], "name": "baseFeeScalar", @@ -164,7 +151,7 @@ "outputs": [ { "internalType": "bool", - "name": "is_", + "name": "", "type": "bool" } ], diff --git a/packages/contracts-bedrock/snapshots/abi/OPContractsManager.json b/packages/contracts-bedrock/snapshots/abi/OPContractsManager.json index 752d650aec7f8..553a3077babc9 100644 --- a/packages/contracts-bedrock/snapshots/abi/OPContractsManager.json +++ b/packages/contracts-bedrock/snapshots/abi/OPContractsManager.json @@ -308,6 +308,11 @@ "name": "l2ChainId", "type": "uint256" }, + { + "internalType": "bool", + "name": "isCustomGasToken", + "type": "bool" + }, { "internalType": "bytes", "name": "startingAnchorRoot", diff --git a/packages/contracts-bedrock/snapshots/abi/OPContractsManagerDeployer.json b/packages/contracts-bedrock/snapshots/abi/OPContractsManagerDeployer.json index 81fd1cef7bb3f..2118ad58e6606 100644 --- a/packages/contracts-bedrock/snapshots/abi/OPContractsManagerDeployer.json +++ b/packages/contracts-bedrock/snapshots/abi/OPContractsManagerDeployer.json @@ -191,6 +191,11 @@ "name": "l2ChainId", "type": "uint256" }, + { + "internalType": "bool", + "name": "isCustomGasToken", + "type": "bool" + }, { "internalType": "bytes", "name": "startingAnchorRoot", diff --git a/packages/contracts-bedrock/snapshots/abi/OptimismPortal2.json b/packages/contracts-bedrock/snapshots/abi/OptimismPortal2.json index 39d02adf5ec39..24e4b1c74a66e 100644 --- a/packages/contracts-bedrock/snapshots/abi/OptimismPortal2.json +++ b/packages/contracts-bedrock/snapshots/abi/OptimismPortal2.json @@ -1081,6 +1081,11 @@ "name": "OptimismPortal_NoReentrancy", "type": "error" }, + { + "inputs": [], + "name": "OptimismPortal_NotAllowedOnCGTMode", + "type": "error" + }, { "inputs": [], "name": "OptimismPortal_ProofNotOldEnough", diff --git a/packages/contracts-bedrock/snapshots/abi/SystemConfig.json b/packages/contracts-bedrock/snapshots/abi/SystemConfig.json index a295b986db223..f9c89f1d10678 100644 --- a/packages/contracts-bedrock/snapshots/abi/SystemConfig.json +++ b/packages/contracts-bedrock/snapshots/abi/SystemConfig.json @@ -406,6 +406,11 @@ "internalType": "contract ISuperchainConfig", "name": "_superchainConfig", "type": "address" + }, + { + "internalType": "bool", + "name": "_isCustomGasToken", + "type": "bool" } ], "name": "initialize", @@ -413,6 +418,19 @@ "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [], + "name": "isCustomGasToken", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [], "name": "l1CrossDomainMessenger", diff --git a/packages/contracts-bedrock/snapshots/semver-lock.json b/packages/contracts-bedrock/snapshots/semver-lock.json index f659b520547da..c9099b8d6a5b2 100644 --- a/packages/contracts-bedrock/snapshots/semver-lock.json +++ b/packages/contracts-bedrock/snapshots/semver-lock.json @@ -20,16 +20,16 @@ "sourceCodeHash": "0x11b35ee81f797b30ee834e2ffad52686d2100d7ee139db4299b7d854dba25550" }, "src/L1/OPContractsManager.sol:OPContractsManager": { - "initCodeHash": "0x4db53075da877f54a7098a78bc1dd9c0048e21d35e205e22e84d5642332186a6", - "sourceCodeHash": "0x534b46026c3b77242ee7ab5728515deffdf8143c1b3b819fefa661f8b0b1793b" + "initCodeHash": "0xb86dcf914d22c5f3871544ba71013e5137e9cfbc7d5359702b9220eb06628d7d", + "sourceCodeHash": "0x2dec26f399d4c164905ab5f60e7c4a8a27bb6ba32080df534802c976eb48e607" }, "src/L1/OPContractsManagerStandardValidator.sol:OPContractsManagerStandardValidator": { "initCodeHash": "0x4c9b9f7888ce14a672dae0f24af9cf20627b1629b5075a364ad17f4db0d06a70", "sourceCodeHash": "0xb65ed0b9cc62c13a053f1b416792802269be37409df917c31e1140f064cf1073" }, "src/L1/OptimismPortal2.sol:OptimismPortal2": { - "initCodeHash": "0x785b09610b2da65d248b49150fafc85b8369c921ddae95b0ea45608b1ce5cbc6", - "sourceCodeHash": "0x925821e7ca59f1799a900fbf5ce7d2c6bef35fc2636c306977d9889f60a987bb" + "initCodeHash": "0xb1d8fd9016739d0b366accf2fbd396087b26744fef41e127e9931b6fdf2865da", + "sourceCodeHash": "0x6175f24fbfb70f97ab9083064b80fb234b0178f6aae7f3b26fb3b56559646315" }, "src/L1/ProtocolVersions.sol:ProtocolVersions": { "initCodeHash": "0x5a76c8530cb24cf23d3baacc6eefaac226382af13f1e2a35535d2ec2b0573b29", @@ -40,8 +40,8 @@ "sourceCodeHash": "0xad12c20a00dc20683bd3f68e6ee254f968da6cc2d98930be6534107ee5cb11d9" }, "src/L1/SystemConfig.sol:SystemConfig": { - "initCodeHash": "0x07b7039de5b8a4dc57642ee9696e949d70516b7f6dce41dde4920efb17105ef2", - "sourceCodeHash": "0x997212ceadabb306c2abd31918b09bccbba0b21662c1d8930a3599831c374b13" + "initCodeHash": "0x2819aa4558b13a3091e9b65a892ff7ba16e53ba66530c28bb573efcd0561dc41", + "sourceCodeHash": "0xe57b4110614f269804638fa4a3448cf57ed46f4f4ca43c66dfcdd6414e1d5513" }, "src/L2/BaseFeeVault.sol:BaseFeeVault": { "initCodeHash": "0x9b664e3d84ad510091337b4aacaa494b142512e2f6f7fbcdb6210ed62ca9b885", @@ -60,8 +60,8 @@ "sourceCodeHash": "0x4351fe2ac1106c8c220b8cfe7839bc107c24d8084deb21259ac954f5a362725d" }, "src/L2/L1Block.sol:L1Block": { - "initCodeHash": "0x88a88621f893d0a96097c3480171055b69e57afc645b103aab86c188bdb9c2ef", - "sourceCodeHash": "0xa3f3cb02723ba452cc9b378bd5e012e771918fbf3c2db5506551682826677270" + "initCodeHash": "0xa1e214607633d287d7b8053a3f957089aced3f562a54eaed1cb578e1e1e77b49", + "sourceCodeHash": "0x6af4d7cf42a91a39a28680770585be65c76220206db96e55449a43fffb0e59d2" }, "src/L2/L1FeeVault.sol:L1FeeVault": { "initCodeHash": "0x9b664e3d84ad510091337b4aacaa494b142512e2f6f7fbcdb6210ed62ca9b885", diff --git a/packages/contracts-bedrock/snapshots/storageLayout/SystemConfig.json b/packages/contracts-bedrock/snapshots/storageLayout/SystemConfig.json index be5a739fa699e..89911064d1a8f 100644 --- a/packages/contracts-bedrock/snapshots/storageLayout/SystemConfig.json +++ b/packages/contracts-bedrock/snapshots/storageLayout/SystemConfig.json @@ -124,5 +124,12 @@ "offset": 0, "slot": "108", "type": "contract ISuperchainConfig" + }, + { + "bytes": "1", + "label": "isCustomGasToken", + "offset": 20, + "slot": "108", + "type": "bool" } ] \ No newline at end of file diff --git a/packages/contracts-bedrock/src/L1/OptimismPortal2.sol b/packages/contracts-bedrock/src/L1/OptimismPortal2.sol index c2bf33dae08b3..058b8e98b919b 100644 --- a/packages/contracts-bedrock/src/L1/OptimismPortal2.sol +++ b/packages/contracts-bedrock/src/L1/OptimismPortal2.sol @@ -235,9 +235,9 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ReinitializableBase error OptimismPortal_MigratingToSameRegistry(); /// @notice Semantic version. - /// @custom:semver 4.6.0 + /// @custom:semver 4.6.1 function version() public pure virtual returns (string memory) { - return "4.6.0"; + return "4.6.1"; } /// @param _proofMaturityDelaySeconds The proof maturity delay in seconds. diff --git a/packages/contracts-bedrock/src/L1/SystemConfig.sol b/packages/contracts-bedrock/src/L1/SystemConfig.sol index 048382afa7ec8..57882a6846003 100644 --- a/packages/contracts-bedrock/src/L1/SystemConfig.sol +++ b/packages/contracts-bedrock/src/L1/SystemConfig.sol @@ -145,9 +145,9 @@ contract SystemConfig is ProxyAdminOwnedBase, OwnableUpgradeable, Reinitializabl event ConfigUpdate(uint256 indexed version, UpdateType indexed updateType, bytes data); /// @notice Semantic version. - /// @custom:semver 3.4.0 + /// @custom:semver 3.4.1 function version() public pure virtual returns (string memory) { - return "3.4.0"; + return "3.4.1"; } /// @notice Constructs the SystemConfig contract. @@ -196,27 +196,28 @@ contract SystemConfig is ProxyAdminOwnedBase, OwnableUpgradeable, Reinitializabl __Ownable_init(); transferOwnership(_owner); - // These are set in ascending order of their UpdateTypes. - _setBatcherHash(_batcherHash); - _setGasConfigEcotone({ _basefeeScalar: _basefeeScalar, _blobbasefeeScalar: _blobbasefeeScalar }); - _setGasLimit(_gasLimit); - - Storage.setAddress(UNSAFE_BLOCK_SIGNER_SLOT, _unsafeBlockSigner); - Storage.setAddress(BATCH_INBOX_SLOT, _batchInbox); - Storage.setAddress(L1_CROSS_DOMAIN_MESSENGER_SLOT, _addresses.l1CrossDomainMessenger); - Storage.setAddress(L1_ERC_721_BRIDGE_SLOT, _addresses.l1ERC721Bridge); - Storage.setAddress(L1_STANDARD_BRIDGE_SLOT, _addresses.l1StandardBridge); - Storage.setAddress(OPTIMISM_PORTAL_SLOT, _addresses.optimismPortal); - Storage.setAddress(OPTIMISM_MINTABLE_ERC20_FACTORY_SLOT, _addresses.optimismMintableERC20Factory); + isCustomGasToken = _isCustomGasToken; + l2ChainId = _l2ChainId; + superchainConfig = _superchainConfig; - _setStartBlock(); + { + // These are set in ascending order of their UpdateTypes. + _setBatcherHash(_batcherHash); + _setGasConfigEcotone({ _basefeeScalar: _basefeeScalar, _blobbasefeeScalar: _blobbasefeeScalar }); + _setGasLimit(_gasLimit); - _setResourceConfig(_config); + Storage.setAddress(UNSAFE_BLOCK_SIGNER_SLOT, _unsafeBlockSigner); + Storage.setAddress(BATCH_INBOX_SLOT, _batchInbox); + Storage.setAddress(L1_CROSS_DOMAIN_MESSENGER_SLOT, _addresses.l1CrossDomainMessenger); + Storage.setAddress(L1_ERC_721_BRIDGE_SLOT, _addresses.l1ERC721Bridge); + Storage.setAddress(L1_STANDARD_BRIDGE_SLOT, _addresses.l1StandardBridge); + Storage.setAddress(OPTIMISM_PORTAL_SLOT, _addresses.optimismPortal); + Storage.setAddress(OPTIMISM_MINTABLE_ERC20_FACTORY_SLOT, _addresses.optimismMintableERC20Factory); - l2ChainId = _l2ChainId; - superchainConfig = _superchainConfig; + _setStartBlock(); - isCustomGasToken = _isCustomGasToken; + _setResourceConfig(_config); + } } /// @notice Upgrades the SystemConfig by adding a reference to the SuperchainConfig. diff --git a/packages/contracts-bedrock/src/L2/L1Block.sol b/packages/contracts-bedrock/src/L2/L1Block.sol index 28c1e6522722c..d344bb3af8bc7 100644 --- a/packages/contracts-bedrock/src/L2/L1Block.sol +++ b/packages/contracts-bedrock/src/L2/L1Block.sol @@ -96,11 +96,6 @@ contract L1Block is ISemver { isCustomGasToken ? ILiquidityController(Predeploys.LIQUIDITY_CONTROLLER).gasPayingTokenSymbol() : "ETH"; } - /// @notice Returns whether the gas paying token is custom. - function isCustomGasToken() public view returns (bool is_) { - is_ = isCustomGasToken; - } - /// @custom:legacy /// @notice Updates the L1 block values. /// @param _number L1 blocknumber. From 0353ecbe0f795c6802b8a66216cd1a3d1eb16535 Mon Sep 17 00:00:00 2001 From: niha <205694301+0xniha@users.noreply.github.com> Date: Thu, 14 Aug 2025 18:57:27 -0300 Subject: [PATCH 06/23] fix: remove gasPayingToken function in L1Block --- .../interfaces/L2/IL1Block.sol | 1 - .../snapshots/abi/L1Block.json | 18 ------------------ .../snapshots/semver-lock.json | 4 ++-- packages/contracts-bedrock/src/L2/L1Block.sol | 6 ------ .../contracts-bedrock/test/L2/L1Block.t.sol | 13 ------------- .../test/L2/L1Block_CGT.t.sol | 2 ++ 6 files changed, 4 insertions(+), 40 deletions(-) diff --git a/packages/contracts-bedrock/interfaces/L2/IL1Block.sol b/packages/contracts-bedrock/interfaces/L2/IL1Block.sol index 3ab26aa52c66f..2c4b3fa768f15 100644 --- a/packages/contracts-bedrock/interfaces/L2/IL1Block.sol +++ b/packages/contracts-bedrock/interfaces/L2/IL1Block.sol @@ -8,7 +8,6 @@ interface IL1Block { function batcherHash() external view returns (bytes32); function blobBaseFee() external view returns (uint256); function blobBaseFeeScalar() external view returns (uint32); - 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); diff --git a/packages/contracts-bedrock/snapshots/abi/L1Block.json b/packages/contracts-bedrock/snapshots/abi/L1Block.json index ba44e5ce56900..99a44e7792593 100644 --- a/packages/contracts-bedrock/snapshots/abi/L1Block.json +++ b/packages/contracts-bedrock/snapshots/abi/L1Block.json @@ -88,24 +88,6 @@ "stateMutability": "view", "type": "function" }, - { - "inputs": [], - "name": "gasPayingToken", - "outputs": [ - { - "internalType": "address", - "name": "addr_", - "type": "address" - }, - { - "internalType": "uint8", - "name": "decimals_", - "type": "uint8" - } - ], - "stateMutability": "pure", - "type": "function" - }, { "inputs": [], "name": "gasPayingTokenName", diff --git a/packages/contracts-bedrock/snapshots/semver-lock.json b/packages/contracts-bedrock/snapshots/semver-lock.json index c9099b8d6a5b2..63f11cd50fd84 100644 --- a/packages/contracts-bedrock/snapshots/semver-lock.json +++ b/packages/contracts-bedrock/snapshots/semver-lock.json @@ -60,8 +60,8 @@ "sourceCodeHash": "0x4351fe2ac1106c8c220b8cfe7839bc107c24d8084deb21259ac954f5a362725d" }, "src/L2/L1Block.sol:L1Block": { - "initCodeHash": "0xa1e214607633d287d7b8053a3f957089aced3f562a54eaed1cb578e1e1e77b49", - "sourceCodeHash": "0x6af4d7cf42a91a39a28680770585be65c76220206db96e55449a43fffb0e59d2" + "initCodeHash": "0x9bacfd0192360e6964c2f339720232e77ea2e78c3edfbbddb2b25596b6eb2f71", + "sourceCodeHash": "0x2ef85eba3ddc70530fed49d556419000ad0183debe2ae6bd71c29cec71732411" }, "src/L2/L1FeeVault.sol:L1FeeVault": { "initCodeHash": "0x9b664e3d84ad510091337b4aacaa494b142512e2f6f7fbcdb6210ed62ca9b885", diff --git a/packages/contracts-bedrock/src/L2/L1Block.sol b/packages/contracts-bedrock/src/L2/L1Block.sol index d344bb3af8bc7..10049cd71f82a 100644 --- a/packages/contracts-bedrock/src/L2/L1Block.sol +++ b/packages/contracts-bedrock/src/L2/L1Block.sol @@ -75,12 +75,6 @@ contract L1Block is ISemver { return "1.6.2"; } - /// @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; - } - /// @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. diff --git a/packages/contracts-bedrock/test/L2/L1Block.t.sol b/packages/contracts-bedrock/test/L2/L1Block.t.sol index 7a276b94df5d6..166f0982baacc 100644 --- a/packages/contracts-bedrock/test/L2/L1Block.t.sol +++ b/packages/contracts-bedrock/test/L2/L1Block.t.sol @@ -6,7 +6,6 @@ import { CommonTest } from "test/setup/CommonTest.sol"; // Libraries import { Encoding } from "src/libraries/Encoding.sol"; -import { Constants } from "src/libraries/Constants.sol"; import "src/libraries/L1BlockErrors.sol"; /// @title L1Block_ TestInit @@ -21,18 +20,6 @@ contract L1Block_TestInit is CommonTest { } } -/// @title L1Block_GasPayingToken_Test -/// @notice Tests the `gasPayingToken` function of the `L1Block` contract. -contract L1Block_GasPayingToken_Test is L1Block_TestInit { - /// @notice Tests that the `gasPayingToken` function returns the correct token address and - /// decimals. - function test_gasPayingToken_succeeds() external view { - (address token, uint8 decimals) = l1Block.gasPayingToken(); - assertEq(token, Constants.ETHER); - assertEq(uint256(decimals), uint256(18)); - } -} - /// @title L1Block_GasPayingTokenName_Test /// @notice Tests the `gasPayingTokenName` function of the `L1Block` contract. contract L1Block_GasPayingTokenName_Test is L1Block_TestInit { diff --git a/packages/contracts-bedrock/test/L2/L1Block_CGT.t.sol b/packages/contracts-bedrock/test/L2/L1Block_CGT.t.sol index e62e5b3816b77..1cebf22ff5f6a 100644 --- a/packages/contracts-bedrock/test/L2/L1Block_CGT.t.sol +++ b/packages/contracts-bedrock/test/L2/L1Block_CGT.t.sol @@ -86,6 +86,8 @@ contract L1Block_CGT_SetL1BlockValuesEcotone_Test is L1Block_SetL1BlockValuesEco /// @notice Tests the `setL1BlockValuesIsthmus` function of the `L1Block` contract with custom gas /// token enabled. contract L1Block_CGT_SetL1BlockValuesIsthmus_Test is L1Block_SetL1BlockValuesIsthmus_Test { + // Override setUp to enable custom gas token + // Re-use the test from L1Block.t.sols function setUp() public override { super.enableCustomGasToken(); super.setUp(); From b23bce5d1e695f6aceecc192601fde3750ba80ef Mon Sep 17 00:00:00 2001 From: niha <205694301+0xniha@users.noreply.github.com> Date: Fri, 15 Aug 2025 11:04:53 -0300 Subject: [PATCH 07/23] feat: add fund method to NativeAssetLiquidity --- .../interfaces/L2/INativeAssetLiquidity.sol | 3 ++ .../snapshots/abi/NativeAssetLiquidity.json | 31 +++++++++++++ .../snapshots/semver-lock.json | 4 +- .../src/L2/NativeAssetLiquidity.sol | 15 ++++++- .../test/L2/NativeAssetLiquidity.t.sol | 43 ++++++++++++++++++- 5 files changed, 92 insertions(+), 4 deletions(-) diff --git a/packages/contracts-bedrock/interfaces/L2/INativeAssetLiquidity.sol b/packages/contracts-bedrock/interfaces/L2/INativeAssetLiquidity.sol index 794450c2ccbc0..bce40a4eca4a3 100644 --- a/packages/contracts-bedrock/interfaces/L2/INativeAssetLiquidity.sol +++ b/packages/contracts-bedrock/interfaces/L2/INativeAssetLiquidity.sol @@ -5,12 +5,15 @@ import { ISemver } from "interfaces/universal/ISemver.sol"; interface INativeAssetLiquidity is ISemver { error Unauthorized(); + error InvalidAmount(); event LiquidityDeposited(address indexed caller, uint256 value); event LiquidityWithdrawn(address indexed caller, uint256 value); event LiquidityBurned(address indexed caller, uint256 value); + event LiquidityFunded(address indexed funder, uint256 value); function deposit() external payable; function withdraw(uint256 _amount) external; function burn(uint256 _amount) external; + function fund() external payable; } diff --git a/packages/contracts-bedrock/snapshots/abi/NativeAssetLiquidity.json b/packages/contracts-bedrock/snapshots/abi/NativeAssetLiquidity.json index 7dec0f3f6bfca..e4d9165d45169 100644 --- a/packages/contracts-bedrock/snapshots/abi/NativeAssetLiquidity.json +++ b/packages/contracts-bedrock/snapshots/abi/NativeAssetLiquidity.json @@ -19,6 +19,13 @@ "stateMutability": "payable", "type": "function" }, + { + "inputs": [], + "name": "fund", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, { "inputs": [], "name": "version", @@ -83,6 +90,25 @@ "name": "LiquidityDeposited", "type": "event" }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "funder", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "LiquidityFunded", + "type": "event" + }, { "anonymous": false, "inputs": [ @@ -102,6 +128,11 @@ "name": "LiquidityWithdrawn", "type": "event" }, + { + "inputs": [], + "name": "InvalidAmount", + "type": "error" + }, { "inputs": [], "name": "Unauthorized", diff --git a/packages/contracts-bedrock/snapshots/semver-lock.json b/packages/contracts-bedrock/snapshots/semver-lock.json index c9099b8d6a5b2..b91a5f238ab32 100644 --- a/packages/contracts-bedrock/snapshots/semver-lock.json +++ b/packages/contracts-bedrock/snapshots/semver-lock.json @@ -96,8 +96,8 @@ "sourceCodeHash": "0x753c1d9c2462393595f57c9a765440f649753e6ab74bf297f5d5d15541304e28" }, "src/L2/NativeAssetLiquidity.sol:NativeAssetLiquidity": { - "initCodeHash": "0xc532da05d70300ce98d32dec956b72d3ad476be7da61bb7937e8393579e920e4", - "sourceCodeHash": "0x796b1670108f32545ead802b1e839bdf67828e0c7ea12fd4b02ff67c125346af" + "initCodeHash": "0xc8133781ce3f433269b6ae0d67122f3a429717b5a478ff76e243dcb6884e193b", + "sourceCodeHash": "0x878cfcf923beb42fc5d1d7b6fc43e8545fcd748ee9c12824a404072d14713daf" }, "src/L2/OperatorFeeVault.sol:OperatorFeeVault": { "initCodeHash": "0x3d8c0d7736e8767f2f797da1c20c5fe30bd7f48a4cf75f376290481ad7c0f91f", diff --git a/packages/contracts-bedrock/src/L2/NativeAssetLiquidity.sol b/packages/contracts-bedrock/src/L2/NativeAssetLiquidity.sol index e18ea7bbeb918..d59bacf286eee 100644 --- a/packages/contracts-bedrock/src/L2/NativeAssetLiquidity.sol +++ b/packages/contracts-bedrock/src/L2/NativeAssetLiquidity.sol @@ -5,7 +5,6 @@ pragma solidity 0.8.15; import { SafeSend } from "src/universal/SafeSend.sol"; // Libraries -import { Unauthorized } from "src/libraries/errors/CommonErrors.sol"; import { Predeploys } from "src/libraries/Predeploys.sol"; import { Burn } from "src/libraries/Burn.sol"; @@ -13,6 +12,9 @@ import { Burn } from "src/libraries/Burn.sol"; import { ISemver } from "interfaces/universal/ISemver.sol"; import { IProxyAdmin } from "interfaces/universal/IProxyAdmin.sol"; +// Errors +import { Unauthorized, InvalidAmount } from "src/libraries/errors/CommonErrors.sol"; + /// @custom:predeploy 0x4200000000000000000000000000000000000029 /// @title NativeAssetLiquidity /// @notice The NativeAssetLiquidity contract allows other contracts to access native asset liquidity @@ -26,6 +28,9 @@ contract NativeAssetLiquidity is ISemver { /// @notice Emitted when an address burns native asset liquidity. event LiquidityBurned(address indexed caller, uint256 value); + /// @notice Emitted when funds are received. + event LiquidityFunded(address indexed funder, uint256 value); + /// @notice Semantic version. /// @custom:semver 1.0.0 string public constant version = "1.0.0"; @@ -58,4 +63,12 @@ contract NativeAssetLiquidity is ISemver { emit LiquidityBurned(msg.sender, _amount); } + + /// @notice Fund the contract by sending native asset. + /// @dev The function is payable to accept native asset. + function fund() external payable { + if (msg.value == 0) revert InvalidAmount(); + + emit LiquidityFunded(msg.sender, msg.value); + } } diff --git a/packages/contracts-bedrock/test/L2/NativeAssetLiquidity.t.sol b/packages/contracts-bedrock/test/L2/NativeAssetLiquidity.t.sol index a966bcf66b6df..ca7c298a80a98 100644 --- a/packages/contracts-bedrock/test/L2/NativeAssetLiquidity.t.sol +++ b/packages/contracts-bedrock/test/L2/NativeAssetLiquidity.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, InvalidAmount } from "src/libraries/errors/CommonErrors.sol"; // Libraries import { Predeploys } from "src/libraries/Predeploys.sol"; @@ -27,6 +27,9 @@ contract NativeAssetLiquidity_TestInit is CommonTest { /// @notice Emitted when an address burns native asset liquidity. event LiquidityBurned(address indexed caller, uint256 value); + /// @notice Emitted when an address funds the contract. + event LiquidityFunded(address indexed funder, uint256 value); + /// @notice Test setup. function setUp() public virtual override { enableCustomGasToken(); @@ -180,3 +183,41 @@ contract NativeAssetLiquidity_Burn_Test is NativeAssetLiquidity_TestInit { assertEq(precalculatedBurner.balance, 0); } } + +/// @title NativeAssetLiquidity_Fund_Test +/// @notice Tests the `fund` function of the `NativeAssetLiquidity` contract. +contract NativeAssetLiquidity_Fund_Test is NativeAssetLiquidity_TestInit { + /// @notice Tests that the fund function succeeds when called with a non-zero value. + /// @param _amount Amount of native asset (in wei) to call the fund function with. + /// @param _caller Address of the caller to call the fund function with. + function testFuzz_fund_succeeds(uint256 _amount, address _caller) public { + _amount = bound(_amount, 1, 1000 ether); + vm.assume(_caller != address(0)); + vm.assume(_caller != address(nativeAssetLiquidity)); // Prevent contract from calling itself + + // Deal caller with the amount to fund + vm.deal(_caller, _amount); + uint256 initialContractBalance = address(nativeAssetLiquidity).balance; + + // Expect emit LiquidityFunded event + vm.expectEmit(address(nativeAssetLiquidity)); + emit LiquidityFunded(_caller, _amount); + vm.prank(_caller); + nativeAssetLiquidity.fund{ value: _amount }(); + + // Assert caller and contract balances are updated correctly + assertEq(_caller.balance, 0); + assertEq(address(nativeAssetLiquidity).balance, initialContractBalance + _amount); + } + + /// @notice Tests that the fund function reverts when called with zero value. + function test_fund_zeroAmount_reverts() public { + uint256 initialContractBalance = address(nativeAssetLiquidity).balance; + // Expect revert with InvalidAmount + vm.expectRevert(InvalidAmount.selector); + nativeAssetLiquidity.fund{ value: 0 }(); + + // Assert contract balance does not change + assertEq(address(nativeAssetLiquidity).balance, initialContractBalance); + } +} From cec8a1f69a2a910186d6383506815eea55533e28 Mon Sep 17 00:00:00 2001 From: niha <205694301+0xniha@users.noreply.github.com> Date: Fri, 15 Aug 2025 13:56:34 -0300 Subject: [PATCH 08/23] refactor: remove burn method from NativeAssetLiquidity --- .../interfaces/L2/INativeAssetLiquidity.sol | 2 - .../snapshots/abi/NativeAssetLiquidity.json | 32 --------------- .../snapshots/semver-lock.json | 4 +- .../src/L2/NativeAssetLiquidity.sol | 17 -------- .../test/L2/NativeAssetLiquidity.t.sol | 39 ------------------- 5 files changed, 2 insertions(+), 92 deletions(-) diff --git a/packages/contracts-bedrock/interfaces/L2/INativeAssetLiquidity.sol b/packages/contracts-bedrock/interfaces/L2/INativeAssetLiquidity.sol index bce40a4eca4a3..003ac4d7bddce 100644 --- a/packages/contracts-bedrock/interfaces/L2/INativeAssetLiquidity.sol +++ b/packages/contracts-bedrock/interfaces/L2/INativeAssetLiquidity.sol @@ -9,11 +9,9 @@ interface INativeAssetLiquidity is ISemver { event LiquidityDeposited(address indexed caller, uint256 value); event LiquidityWithdrawn(address indexed caller, uint256 value); - event LiquidityBurned(address indexed caller, uint256 value); event LiquidityFunded(address indexed funder, uint256 value); function deposit() external payable; function withdraw(uint256 _amount) external; - function burn(uint256 _amount) external; function fund() external payable; } diff --git a/packages/contracts-bedrock/snapshots/abi/NativeAssetLiquidity.json b/packages/contracts-bedrock/snapshots/abi/NativeAssetLiquidity.json index e4d9165d45169..9bbeb8427231b 100644 --- a/packages/contracts-bedrock/snapshots/abi/NativeAssetLiquidity.json +++ b/packages/contracts-bedrock/snapshots/abi/NativeAssetLiquidity.json @@ -1,17 +1,4 @@ [ - { - "inputs": [ - { - "internalType": "uint256", - "name": "_amount", - "type": "uint256" - } - ], - "name": "burn", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, { "inputs": [], "name": "deposit", @@ -52,25 +39,6 @@ "stateMutability": "nonpayable", "type": "function" }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "address", - "name": "caller", - "type": "address" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "value", - "type": "uint256" - } - ], - "name": "LiquidityBurned", - "type": "event" - }, { "anonymous": false, "inputs": [ diff --git a/packages/contracts-bedrock/snapshots/semver-lock.json b/packages/contracts-bedrock/snapshots/semver-lock.json index dc16ab0c5b850..3e6031849351a 100644 --- a/packages/contracts-bedrock/snapshots/semver-lock.json +++ b/packages/contracts-bedrock/snapshots/semver-lock.json @@ -96,8 +96,8 @@ "sourceCodeHash": "0x753c1d9c2462393595f57c9a765440f649753e6ab74bf297f5d5d15541304e28" }, "src/L2/NativeAssetLiquidity.sol:NativeAssetLiquidity": { - "initCodeHash": "0xc8133781ce3f433269b6ae0d67122f3a429717b5a478ff76e243dcb6884e193b", - "sourceCodeHash": "0x878cfcf923beb42fc5d1d7b6fc43e8545fcd748ee9c12824a404072d14713daf" + "initCodeHash": "0x2c50c7cac8eab6867ffb969a65a8aa3026d415f2e9464726683ff6cd5da0b8f3", + "sourceCodeHash": "0x9432883dd4aa4d5ffc733ad99fa7bcc9cc8c319e654b385b8cd093a37a4c94cb" }, "src/L2/OperatorFeeVault.sol:OperatorFeeVault": { "initCodeHash": "0x3d8c0d7736e8767f2f797da1c20c5fe30bd7f48a4cf75f376290481ad7c0f91f", diff --git a/packages/contracts-bedrock/src/L2/NativeAssetLiquidity.sol b/packages/contracts-bedrock/src/L2/NativeAssetLiquidity.sol index d59bacf286eee..859818ad6cfda 100644 --- a/packages/contracts-bedrock/src/L2/NativeAssetLiquidity.sol +++ b/packages/contracts-bedrock/src/L2/NativeAssetLiquidity.sol @@ -6,11 +6,9 @@ import { SafeSend } from "src/universal/SafeSend.sol"; // Libraries import { Predeploys } from "src/libraries/Predeploys.sol"; -import { Burn } from "src/libraries/Burn.sol"; // Interfaces import { ISemver } from "interfaces/universal/ISemver.sol"; -import { IProxyAdmin } from "interfaces/universal/IProxyAdmin.sol"; // Errors import { Unauthorized, InvalidAmount } from "src/libraries/errors/CommonErrors.sol"; @@ -25,9 +23,6 @@ contract NativeAssetLiquidity is ISemver { /// @notice Emitted when an address deposits native asset liquidity. event LiquidityDeposited(address indexed caller, uint256 value); - /// @notice Emitted when an address burns native asset liquidity. - event LiquidityBurned(address indexed caller, uint256 value); - /// @notice Emitted when funds are received. event LiquidityFunded(address indexed funder, uint256 value); @@ -52,18 +47,6 @@ contract NativeAssetLiquidity is ISemver { emit LiquidityWithdrawn(msg.sender, _amount); } - /// @notice Allows to burn native asset liquidity from this contract. - /// @dev Burn an arbitrary amount of native supply forever, ideally to be called only once by - /// the ProxyAdmin owner. - /// @param _amount The amount of liquidity to burn. - function burn(uint256 _amount) external { - if (msg.sender != IProxyAdmin(Predeploys.PROXY_ADMIN).owner()) revert Unauthorized(); - - Burn.eth(_amount); - - emit LiquidityBurned(msg.sender, _amount); - } - /// @notice Fund the contract by sending native asset. /// @dev The function is payable to accept native asset. function fund() external payable { diff --git a/packages/contracts-bedrock/test/L2/NativeAssetLiquidity.t.sol b/packages/contracts-bedrock/test/L2/NativeAssetLiquidity.t.sol index ca7c298a80a98..c32a7a0eb3c9b 100644 --- a/packages/contracts-bedrock/test/L2/NativeAssetLiquidity.t.sol +++ b/packages/contracts-bedrock/test/L2/NativeAssetLiquidity.t.sol @@ -7,14 +7,6 @@ import { CommonTest } from "test/setup/CommonTest.sol"; // Error imports import { Unauthorized, InvalidAmount } from "src/libraries/errors/CommonErrors.sol"; -// Libraries -import { Predeploys } from "src/libraries/Predeploys.sol"; - -// Interfaces -import { IProxyAdmin } from "interfaces/universal/IProxyAdmin.sol"; - -import "forge-std/console.sol"; - /// @title NativeAssetLiquidity_TestInit /// @notice Reusable test initialization for `NativeAssetLiquidity` tests. contract NativeAssetLiquidity_TestInit is CommonTest { @@ -24,9 +16,6 @@ contract NativeAssetLiquidity_TestInit is CommonTest { /// @notice Emitted when an address deposits native asset liquidity. event LiquidityDeposited(address indexed caller, uint256 value); - /// @notice Emitted when an address burns native asset liquidity. - event LiquidityBurned(address indexed caller, uint256 value); - /// @notice Emitted when an address funds the contract. event LiquidityFunded(address indexed funder, uint256 value); @@ -156,34 +145,6 @@ contract NativeAssetLiquidity_Withdraw_Test is NativeAssetLiquidity_TestInit { } } -/// @title NativeAssetLiquidity_Burn_Test -/// @notice Tests the `burn` function of the `NativeAssetLiquidity` contract. -contract NativeAssetLiquidity_Burn_Test is NativeAssetLiquidity_TestInit { - /// @notice Tests that the burn function can be called by the ProxyAdmin owner. - /// @param _amount Amount of native asset (in wei) to call the burn function with. - function test_burn_fromAuthorizedCaller_succeeds(uint256 _amount) public { - _amount = bound(_amount, 1, type(uint248).max); - - uint256 nativeAssetBalanceBefore = address(nativeAssetLiquidity).balance; - - address deployer = address(nativeAssetLiquidity); - uint256 nonce = vm.getNonce(deployer); - address precalculatedBurner = vm.computeCreateAddress(deployer, nonce); - - // Call the burn function with ProxyAdmin owner as the caller - vm.expectEmit(address(nativeAssetLiquidity)); - emit LiquidityBurned(IProxyAdmin(Predeploys.PROXY_ADMIN).owner(), _amount); - vm.prank(IProxyAdmin(Predeploys.PROXY_ADMIN).owner()); - nativeAssetLiquidity.burn(_amount); - - // Assert NativeAssetLiquidity balance is updated correctly - assertEq(address(nativeAssetLiquidity).balance, nativeAssetBalanceBefore - _amount); - - // Assert burner balance is 0 - assertEq(precalculatedBurner.balance, 0); - } -} - /// @title NativeAssetLiquidity_Fund_Test /// @notice Tests the `fund` function of the `NativeAssetLiquidity` contract. contract NativeAssetLiquidity_Fund_Test is NativeAssetLiquidity_TestInit { From af5f254aabbdbaac8ed803782f7ae6bffd69c10e Mon Sep 17 00:00:00 2001 From: Hex <165055168+hexshire@users.noreply.github.com> Date: Fri, 15 Aug 2025 15:24:28 -0300 Subject: [PATCH 09/23] refactor(cgt): move cgt flag to portal * refactor: move cgt flag to portal * test(cgt): add custom gas token enabled * chore(cgt): nit remove unused param * test(cgt): update initialize test * refactor(cgt): use stdstore --- .../interfaces/L1/IOptimismPortal2.sol | 4 +- .../interfaces/L1/ISystemConfig.sol | 3 +- .../snapshots/abi/OptimismPortal2.json | 18 +++++ .../snapshots/abi/SystemConfig.json | 5 -- .../snapshots/semver-lock.json | 10 +-- .../storageLayout/OptimismPortal2.json | 7 ++ .../snapshots/storageLayout/SystemConfig.json | 7 -- .../src/L1/OPContractsManager.sol | 17 ++-- .../src/L1/OptimismPortal2.sol | 10 ++- .../contracts-bedrock/src/L1/SystemConfig.sol | 45 +++++------ .../test/L1/OptimismPortal2.t.sol | 80 +++++++++++++++++-- .../test/L1/SystemConfig.t.sol | 15 ++-- .../test/invariants/SystemConfig.t.sol | 3 +- .../test/opcm/SetDisputeGameImpl.t.sol | 3 +- .../test/vendor/Initializable.t.sol | 14 ++-- 15 files changed, 163 insertions(+), 78 deletions(-) diff --git a/packages/contracts-bedrock/interfaces/L1/IOptimismPortal2.sol b/packages/contracts-bedrock/interfaces/L1/IOptimismPortal2.sol index c549fb6266607..d9a527aad944d 100644 --- a/packages/contracts-bedrock/interfaces/L1/IOptimismPortal2.sol +++ b/packages/contracts-bedrock/interfaces/L1/IOptimismPortal2.sol @@ -89,10 +89,12 @@ interface IOptimismPortal2 is IProxyAdminOwnedBase { function initialize( ISystemConfig _systemConfig, IAnchorStateRegistry _anchorStateRegistry, - IETHLockbox _ethLockbox + IETHLockbox _ethLockbox, + bool _isCustomGasToken ) external; function initVersion() external view returns (uint8); + function isCustomGasToken() external view returns (bool); function l2Sender() external view returns (address); function minimumGasLimit(uint64 _byteCount) external pure returns (uint64); function numProofSubmitters(bytes32 _withdrawalHash) external view returns (uint256); diff --git a/packages/contracts-bedrock/interfaces/L1/ISystemConfig.sol b/packages/contracts-bedrock/interfaces/L1/ISystemConfig.sol index 8665abc5a7228..fee2c5bed82d7 100644 --- a/packages/contracts-bedrock/interfaces/L1/ISystemConfig.sol +++ b/packages/contracts-bedrock/interfaces/L1/ISystemConfig.sol @@ -58,8 +58,7 @@ interface ISystemConfig is IProxyAdminOwnedBase { address _batchInbox, Addresses memory _addresses, uint256 _l2ChainId, - ISuperchainConfig _superchainConfig, - bool _isCustomGasToken + ISuperchainConfig _superchainConfig ) external; function initVersion() external view returns (uint8); diff --git a/packages/contracts-bedrock/snapshots/abi/OptimismPortal2.json b/packages/contracts-bedrock/snapshots/abi/OptimismPortal2.json index 24e4b1c74a66e..f9751ab7443ae 100644 --- a/packages/contracts-bedrock/snapshots/abi/OptimismPortal2.json +++ b/packages/contracts-bedrock/snapshots/abi/OptimismPortal2.json @@ -299,6 +299,11 @@ "internalType": "contract IETHLockbox", "name": "_ethLockbox", "type": "address" + }, + { + "internalType": "bool", + "name": "_isCustomGasToken", + "type": "bool" } ], "name": "initialize", @@ -306,6 +311,19 @@ "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [], + "name": "isCustomGasToken", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [], "name": "l2Sender", diff --git a/packages/contracts-bedrock/snapshots/abi/SystemConfig.json b/packages/contracts-bedrock/snapshots/abi/SystemConfig.json index f9c89f1d10678..59caaf7559228 100644 --- a/packages/contracts-bedrock/snapshots/abi/SystemConfig.json +++ b/packages/contracts-bedrock/snapshots/abi/SystemConfig.json @@ -406,11 +406,6 @@ "internalType": "contract ISuperchainConfig", "name": "_superchainConfig", "type": "address" - }, - { - "internalType": "bool", - "name": "_isCustomGasToken", - "type": "bool" } ], "name": "initialize", diff --git a/packages/contracts-bedrock/snapshots/semver-lock.json b/packages/contracts-bedrock/snapshots/semver-lock.json index 3e6031849351a..8490d2c27c7ee 100644 --- a/packages/contracts-bedrock/snapshots/semver-lock.json +++ b/packages/contracts-bedrock/snapshots/semver-lock.json @@ -21,15 +21,15 @@ }, "src/L1/OPContractsManager.sol:OPContractsManager": { "initCodeHash": "0xb86dcf914d22c5f3871544ba71013e5137e9cfbc7d5359702b9220eb06628d7d", - "sourceCodeHash": "0x2dec26f399d4c164905ab5f60e7c4a8a27bb6ba32080df534802c976eb48e607" + "sourceCodeHash": "0xf8096cb7493b92c4d9631dbea6462f51b604ee4c7f98a3593100b157bb01b442" }, "src/L1/OPContractsManagerStandardValidator.sol:OPContractsManagerStandardValidator": { "initCodeHash": "0x4c9b9f7888ce14a672dae0f24af9cf20627b1629b5075a364ad17f4db0d06a70", "sourceCodeHash": "0xb65ed0b9cc62c13a053f1b416792802269be37409df917c31e1140f064cf1073" }, "src/L1/OptimismPortal2.sol:OptimismPortal2": { - "initCodeHash": "0xb1d8fd9016739d0b366accf2fbd396087b26744fef41e127e9931b6fdf2865da", - "sourceCodeHash": "0x6175f24fbfb70f97ab9083064b80fb234b0178f6aae7f3b26fb3b56559646315" + "initCodeHash": "0x308b65ecfb2cb74b06eb6ca32d3a54dc711b1e15b54bc5d510e39e1dcb91b99e", + "sourceCodeHash": "0x4bea9142f873f99bb44fdb50ed7e65041cc98f9b1c328d5e3812666d5e981ac9" }, "src/L1/ProtocolVersions.sol:ProtocolVersions": { "initCodeHash": "0x5a76c8530cb24cf23d3baacc6eefaac226382af13f1e2a35535d2ec2b0573b29", @@ -40,8 +40,8 @@ "sourceCodeHash": "0xad12c20a00dc20683bd3f68e6ee254f968da6cc2d98930be6534107ee5cb11d9" }, "src/L1/SystemConfig.sol:SystemConfig": { - "initCodeHash": "0x2819aa4558b13a3091e9b65a892ff7ba16e53ba66530c28bb573efcd0561dc41", - "sourceCodeHash": "0xe57b4110614f269804638fa4a3448cf57ed46f4f4ca43c66dfcdd6414e1d5513" + "initCodeHash": "0x31ee221cef680b15cc25013ae2142bef541c26b2ecc7665ec28fd4585f469cad", + "sourceCodeHash": "0xb53da5831a507d1ce896b18b3b2cd9091afa7200b9a0b3dea06f7475660cfd95" }, "src/L2/BaseFeeVault.sol:BaseFeeVault": { "initCodeHash": "0x9b664e3d84ad510091337b4aacaa494b142512e2f6f7fbcdb6210ed62ca9b885", diff --git a/packages/contracts-bedrock/snapshots/storageLayout/OptimismPortal2.json b/packages/contracts-bedrock/snapshots/storageLayout/OptimismPortal2.json index 8dc6639f30366..7bd556d4d55a4 100644 --- a/packages/contracts-bedrock/snapshots/storageLayout/OptimismPortal2.json +++ b/packages/contracts-bedrock/snapshots/storageLayout/OptimismPortal2.json @@ -145,5 +145,12 @@ "offset": 20, "slot": "63", "type": "bool" + }, + { + "bytes": "1", + "label": "isCustomGasToken", + "offset": 21, + "slot": "63", + "type": "bool" } ] \ No newline at end of file diff --git a/packages/contracts-bedrock/snapshots/storageLayout/SystemConfig.json b/packages/contracts-bedrock/snapshots/storageLayout/SystemConfig.json index 89911064d1a8f..be5a739fa699e 100644 --- a/packages/contracts-bedrock/snapshots/storageLayout/SystemConfig.json +++ b/packages/contracts-bedrock/snapshots/storageLayout/SystemConfig.json @@ -124,12 +124,5 @@ "offset": 0, "slot": "108", "type": "contract ISuperchainConfig" - }, - { - "bytes": "1", - "label": "isCustomGasToken", - "offset": 20, - "slot": "108", - "type": "bool" } ] \ No newline at end of file diff --git a/packages/contracts-bedrock/src/L1/OPContractsManager.sol b/packages/contracts-bedrock/src/L1/OPContractsManager.sol index 3500e82bd94ff..cac946f96f73b 100644 --- a/packages/contracts-bedrock/src/L1/OPContractsManager.sol +++ b/packages/contracts-bedrock/src/L1/OPContractsManager.sol @@ -1077,7 +1077,7 @@ contract OPContractsManagerDeployer is OPContractsManagerBase { output.opChainProxyAdmin, address(output.l1ERC721BridgeProxy), implementation.l1ERC721BridgeImpl, data ); - data = encodeOptimismPortalInitializer(output); + data = encodeOptimismPortalInitializer(output, _input); upgradeToAndCall( output.opChainProxyAdmin, address(output.optimismPortalProxy), implementation.optimismPortalImpl, data ); @@ -1229,7 +1229,10 @@ contract OPContractsManagerDeployer is OPContractsManagerBase { } /// @notice Helper method for encoding the OptimismPortal initializer data. - function encodeOptimismPortalInitializer(OPContractsManager.DeployOutput memory _output) + function encodeOptimismPortalInitializer( + OPContractsManager.DeployOutput memory _output, + OPContractsManager.DeployInput memory _input + ) internal view virtual @@ -1237,7 +1240,12 @@ contract OPContractsManagerDeployer is OPContractsManagerBase { { return abi.encodeCall( IOptimismPortal.initialize, - (_output.systemConfigProxy, _output.anchorStateRegistryProxy, _output.ethLockboxProxy) + ( + _output.systemConfigProxy, + _output.anchorStateRegistryProxy, + _output.ethLockboxProxy, + _input.isCustomGasToken + ) ); } @@ -1296,8 +1304,7 @@ contract OPContractsManagerDeployer is OPContractsManagerBase { chainIdToBatchInboxAddress(_input.l2ChainId), opChainAddrs, _input.l2ChainId, - _superchainConfig, - _input.isCustomGasToken + _superchainConfig ) ); } diff --git a/packages/contracts-bedrock/src/L1/OptimismPortal2.sol b/packages/contracts-bedrock/src/L1/OptimismPortal2.sol index 058b8e98b919b..c99b30769bf06 100644 --- a/packages/contracts-bedrock/src/L1/OptimismPortal2.sol +++ b/packages/contracts-bedrock/src/L1/OptimismPortal2.sol @@ -125,6 +125,9 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ReinitializableBase /// @notice Whether the OptimismPortal is using Super Roots or Output Roots. bool public superRootsActive; + /// @notice Whether the gas token is custom. + bool public isCustomGasToken; + /// @notice Emitted when a transaction is deposited from L1 to L2. The parameters of this event /// are read by the rollup node and used to derive deposit transactions on L2. /// @param from Address that triggered the deposit transaction. @@ -250,10 +253,12 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ReinitializableBase /// @param _systemConfig Address of the SystemConfig. /// @param _anchorStateRegistry Address of the AnchorStateRegistry. /// @param _ethLockbox Contract of the ETHLockbox. + /// @param _isCustomGasToken Whether the gas token is custom. function initialize( ISystemConfig _systemConfig, IAnchorStateRegistry _anchorStateRegistry, - IETHLockbox _ethLockbox + IETHLockbox _ethLockbox, + bool _isCustomGasToken ) external reinitializer(initVersion()) @@ -265,6 +270,7 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ReinitializableBase systemConfig = _systemConfig; anchorStateRegistry = _anchorStateRegistry; ethLockbox = _ethLockbox; + isCustomGasToken = _isCustomGasToken; // Set the l2Sender slot, only if it is currently empty. This signals the first // initialization of the contract. @@ -753,7 +759,7 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ReinitializableBase { // Handle ETH deposits: prevent when custom gas token is active, otherwise lock in ETHLockbox. if (msg.value > 0) { - if (systemConfig.isCustomGasToken()) { + if (isCustomGasToken) { revert OptimismPortal_NotAllowedOnCGTMode(); } ethLockbox.lockETH{ value: msg.value }(); diff --git a/packages/contracts-bedrock/src/L1/SystemConfig.sol b/packages/contracts-bedrock/src/L1/SystemConfig.sol index 57882a6846003..4d22c27b39cb1 100644 --- a/packages/contracts-bedrock/src/L1/SystemConfig.sol +++ b/packages/contracts-bedrock/src/L1/SystemConfig.sol @@ -135,9 +135,6 @@ contract SystemConfig is ProxyAdminOwnedBase, OwnableUpgradeable, Reinitializabl /// @notice The SuperchainConfig contract that manages the pause state. ISuperchainConfig public superchainConfig; - /// @notice Whether the gas token is custom. - bool public isCustomGasToken; - /// @notice Emitted when configuration is updated. /// @param version SystemConfig version. /// @param updateType Type of update. @@ -183,8 +180,7 @@ contract SystemConfig is ProxyAdminOwnedBase, OwnableUpgradeable, Reinitializabl address _batchInbox, SystemConfig.Addresses memory _addresses, uint256 _l2ChainId, - ISuperchainConfig _superchainConfig, - bool _isCustomGasToken + ISuperchainConfig _superchainConfig ) public reinitializer(initVersion()) @@ -196,28 +192,25 @@ contract SystemConfig is ProxyAdminOwnedBase, OwnableUpgradeable, Reinitializabl __Ownable_init(); transferOwnership(_owner); - isCustomGasToken = _isCustomGasToken; - l2ChainId = _l2ChainId; - superchainConfig = _superchainConfig; + // These are set in ascending order of their UpdateTypes. + _setBatcherHash(_batcherHash); + _setGasConfigEcotone({ _basefeeScalar: _basefeeScalar, _blobbasefeeScalar: _blobbasefeeScalar }); + _setGasLimit(_gasLimit); - { - // These are set in ascending order of their UpdateTypes. - _setBatcherHash(_batcherHash); - _setGasConfigEcotone({ _basefeeScalar: _basefeeScalar, _blobbasefeeScalar: _blobbasefeeScalar }); - _setGasLimit(_gasLimit); + Storage.setAddress(UNSAFE_BLOCK_SIGNER_SLOT, _unsafeBlockSigner); + Storage.setAddress(BATCH_INBOX_SLOT, _batchInbox); + Storage.setAddress(L1_CROSS_DOMAIN_MESSENGER_SLOT, _addresses.l1CrossDomainMessenger); + Storage.setAddress(L1_ERC_721_BRIDGE_SLOT, _addresses.l1ERC721Bridge); + Storage.setAddress(L1_STANDARD_BRIDGE_SLOT, _addresses.l1StandardBridge); + Storage.setAddress(OPTIMISM_PORTAL_SLOT, _addresses.optimismPortal); + Storage.setAddress(OPTIMISM_MINTABLE_ERC20_FACTORY_SLOT, _addresses.optimismMintableERC20Factory); - Storage.setAddress(UNSAFE_BLOCK_SIGNER_SLOT, _unsafeBlockSigner); - Storage.setAddress(BATCH_INBOX_SLOT, _batchInbox); - Storage.setAddress(L1_CROSS_DOMAIN_MESSENGER_SLOT, _addresses.l1CrossDomainMessenger); - Storage.setAddress(L1_ERC_721_BRIDGE_SLOT, _addresses.l1ERC721Bridge); - Storage.setAddress(L1_STANDARD_BRIDGE_SLOT, _addresses.l1StandardBridge); - Storage.setAddress(OPTIMISM_PORTAL_SLOT, _addresses.optimismPortal); - Storage.setAddress(OPTIMISM_MINTABLE_ERC20_FACTORY_SLOT, _addresses.optimismMintableERC20Factory); + _setStartBlock(); - _setStartBlock(); + _setResourceConfig(_config); - _setResourceConfig(_config); - } + l2ChainId = _l2ChainId; + superchainConfig = _superchainConfig; } /// @notice Upgrades the SystemConfig by adding a reference to the SuperchainConfig. @@ -504,4 +497,10 @@ contract SystemConfig is ProxyAdminOwnedBase, OwnableUpgradeable, Reinitializabl function guardian() public view returns (address) { return superchainConfig.guardian(); } + + /// @notice Returns whether the gas token is custom by reading from the OptimismPortal. + /// @return bool True if the gas token is custom, false otherwise. + function isCustomGasToken() public view returns (bool) { + return IOptimismPortal2(payable(optimismPortal())).isCustomGasToken(); + } } diff --git a/packages/contracts-bedrock/test/L1/OptimismPortal2.t.sol b/packages/contracts-bedrock/test/L1/OptimismPortal2.t.sol index 11c4c94ffc54b..bb616308e3e6e 100644 --- a/packages/contracts-bedrock/test/L1/OptimismPortal2.t.sol +++ b/packages/contracts-bedrock/test/L1/OptimismPortal2.t.sol @@ -9,6 +9,8 @@ import { CommonTest } from "test/setup/CommonTest.sol"; import { NextImpl } from "test/mocks/NextImpl.sol"; import { EIP1967Helper } from "test/mocks/EIP1967Helper.sol"; import { DisputeGameFactory_TestInit } from "test/dispute/DisputeGameFactory.t.sol"; +import { stdStorage, StdStorage } from "forge-std/StdStorage.sol"; +import { OptimismPortal2 } from "src/L1/OptimismPortal2.sol"; // Scripts import { ForgeArtifacts, StorageSlot } from "scripts/libraries/ForgeArtifacts.sol"; @@ -189,6 +191,7 @@ contract OptimismPortal2_Initialize_Test is OptimismPortal2_TestInit { assertEq(optimismPortal2.paused(), false); assertEq(address(optimismPortal2.systemConfig()), address(systemConfig)); assertEq(address(optimismPortal2.ethLockbox()), address(ethLockbox)); + assertFalse(OptimismPortal2(payable(address(optimismPortal2))).isCustomGasToken()); returnIfForkTest( "OptimismPortal2_Initialize_Test: Do not check guardian and respectedGameType on forked networks" @@ -234,7 +237,7 @@ contract OptimismPortal2_Initialize_Test is OptimismPortal2_TestInit { // Call the `initialize` function with the sender vm.prank(_sender); - optimismPortal2.initialize(systemConfig, anchorStateRegistry, ethLockbox); + optimismPortal2.initialize(systemConfig, anchorStateRegistry, ethLockbox, false); } } @@ -2064,13 +2067,7 @@ contract OptimismPortal2_DepositTransaction_Test is OptimismPortal2_TestInit { /// @notice Tests that `depositTransaction` reverts when the value is greater than 0 and the /// custom gas token is active. - function test_depositTransaction_customGasToken_reverts( - bytes memory _data, - uint64 _gasLimit, - uint256 _value - ) - external - { + function test_depositTransaction_customGasToken_reverts(bytes memory _data, uint256 _value) external { // Prevent overflow on an upgrade context _value = bound(_value, 1, type(uint256).max - address(ethLockbox).balance); // Set the custom gas token to true. @@ -2406,3 +2403,70 @@ contract OptimismPortal2_Params_Test is CommonTest { assertEq(slot21Expected, slot21After); } } + +/// @title OptimismPortal2_CustomGasToken_Test +/// @notice Test suite for OptimismPortal2 with custom gas token enabled. +contract OptimismPortal2_CustomGasToken_Test is OptimismPortal2_TestInit { + using stdStorage for StdStorage; + + /// @notice Sets up a portal with custom gas token enabled + function setUp() public override { + super.setUp(); + + // Use stdStorage to handle packed slot for isCustomGasToken + stdstore + .enable_packed_slots() + .target(address(optimismPortal2)) + .sig("isCustomGasToken()") + .checked_write(true); + } + + /// @notice Tests that isCustomGasToken storage is set correctly + function test_isCustomGasToken_succeeds() external view { + // Check that the public getter returns true + assertTrue(OptimismPortal2(payable(address(optimismPortal2))).isCustomGasToken()); + } + + /// @notice Tests that depositTransaction reverts when value > 0 and custom gas token is enabled + function testFuzz_depositTransaction_withValue_reverts(uint256 value) external { + value = bound(value, 1, type(uint128).max); + vm.deal(depositor, value); + + vm.prank(depositor); + vm.expectRevert(IOptimismPortal.OptimismPortal_NotAllowedOnCGTMode.selector); + optimismPortal2.depositTransaction{ value: value }({ + _to: address(0x40), + _value: value, + _gasLimit: 100_000, + _isCreation: false, + _data: hex"" + }); + } + + /// @notice Tests that depositTransaction succeeds when value = 0 and custom gas token is enabled + function test_depositTransaction_withZeroValue_succeeds() external { + vm.prank(depositor); + optimismPortal2.depositTransaction({ + _to: address(0x40), + _value: 0, + _gasLimit: 100_000, + _isCreation: false, + _data: hex"" + }); + // No revert expected + } + + /// @notice Tests that receive() reverts when custom gas token is enabled + function testFuzz_receive_reverts(uint256 value) external { + value = bound(value, 1, type(uint128).max); + vm.deal(depositor, value); + + address portal = address(optimismPortal2); + + vm.prank(depositor); + vm.expectRevert(IOptimismPortal.OptimismPortal_NotAllowedOnCGTMode.selector); + assembly { + pop(call(gas(), portal, value, 0, 0, 0, 0)) + } + } +} diff --git a/packages/contracts-bedrock/test/L1/SystemConfig.t.sol b/packages/contracts-bedrock/test/L1/SystemConfig.t.sol index b0eef32443f2c..0f814a49c6200 100644 --- a/packages/contracts-bedrock/test/L1/SystemConfig.t.sol +++ b/packages/contracts-bedrock/test/L1/SystemConfig.t.sol @@ -160,8 +160,7 @@ contract SystemConfig_Initialize_Test is SystemConfig_TestInit { optimismMintableERC20Factory: address(0) }), _l2ChainId: 1234, - _superchainConfig: ISuperchainConfig(address(0)), - _isCustomGasToken: false + _superchainConfig: ISuperchainConfig(address(0)) }); } @@ -217,8 +216,7 @@ contract SystemConfig_Initialize_Test is SystemConfig_TestInit { optimismMintableERC20Factory: address(0) }), _l2ChainId: 1234, - _superchainConfig: ISuperchainConfig(address(0)), - _isCustomGasToken: false + _superchainConfig: ISuperchainConfig(address(0)) }); } } @@ -345,8 +343,7 @@ contract SystemConfig_StartBlock_Test is SystemConfig_TestInit { optimismMintableERC20Factory: address(0) }), _l2ChainId: 1234, - _superchainConfig: ISuperchainConfig(address(0)), - _isCustomGasToken: false + _superchainConfig: ISuperchainConfig(address(0)) }); assertEq(systemConfig.startBlock(), block.number); } @@ -377,8 +374,7 @@ contract SystemConfig_StartBlock_Test is SystemConfig_TestInit { optimismMintableERC20Factory: address(0) }), _l2ChainId: 1234, - _superchainConfig: ISuperchainConfig(address(0)), - _isCustomGasToken: false + _superchainConfig: ISuperchainConfig(address(0)) }); assertEq(systemConfig.startBlock(), 1); } @@ -675,8 +671,7 @@ contract SystemConfig_SetResourceConfig_Test is SystemConfig_TestInit { optimismMintableERC20Factory: address(0) }), _l2ChainId: 1234, - _superchainConfig: ISuperchainConfig(address(0)), - _isCustomGasToken: false + _superchainConfig: ISuperchainConfig(address(0)) }); } } diff --git a/packages/contracts-bedrock/test/invariants/SystemConfig.t.sol b/packages/contracts-bedrock/test/invariants/SystemConfig.t.sol index a4a600717ad65..ebf75b3280ef6 100644 --- a/packages/contracts-bedrock/test/invariants/SystemConfig.t.sol +++ b/packages/contracts-bedrock/test/invariants/SystemConfig.t.sol @@ -47,8 +47,7 @@ contract SystemConfig_GasLimitBoundaries_Invariant is Test { optimismMintableERC20Factory: address(0) }), 1234, // _l2ChainId - ISuperchainConfig(address(0)), // _superchainConfig - false // _isCustomGasToken + ISuperchainConfig(address(0)) // _superchainConfig ) ) ); diff --git a/packages/contracts-bedrock/test/opcm/SetDisputeGameImpl.t.sol b/packages/contracts-bedrock/test/opcm/SetDisputeGameImpl.t.sol index a0c7d171406c4..d6e1d08523ca9 100644 --- a/packages/contracts-bedrock/test/opcm/SetDisputeGameImpl.t.sol +++ b/packages/contracts-bedrock/test/opcm/SetDisputeGameImpl.t.sol @@ -195,8 +195,7 @@ contract SetDisputeGameImpl_Test is Test { optimismMintableERC20Factory: address(7) }), 10, - ISuperchainConfig(address(supConfigProxy)), - false + ISuperchainConfig(address(supConfigProxy)) ) ); } diff --git a/packages/contracts-bedrock/test/vendor/Initializable.t.sol b/packages/contracts-bedrock/test/vendor/Initializable.t.sol index f86d989bb7a40..8b75fa1afeed5 100644 --- a/packages/contracts-bedrock/test/vendor/Initializable.t.sol +++ b/packages/contracts-bedrock/test/vendor/Initializable.t.sol @@ -123,7 +123,9 @@ contract Initializer_Test is CommonTest { InitializeableContract({ name: "OptimismPortal2Impl", target: EIP1967Helper.getImplementation(address(optimismPortal2)), - initCalldata: abi.encodeCall(optimismPortal2.initialize, (systemConfig, anchorStateRegistry, ethLockbox)) + initCalldata: abi.encodeCall( + optimismPortal2.initialize, (systemConfig, anchorStateRegistry, ethLockbox, false) + ) }) ); // OptimismPortal2Proxy @@ -131,7 +133,9 @@ contract Initializer_Test is CommonTest { InitializeableContract({ name: "OptimismPortal2Proxy", target: address(optimismPortal2), - initCalldata: abi.encodeCall(optimismPortal2.initialize, (systemConfig, anchorStateRegistry, ethLockbox)) + initCalldata: abi.encodeCall( + optimismPortal2.initialize, (systemConfig, anchorStateRegistry, ethLockbox, false) + ) }) ); @@ -166,8 +170,7 @@ contract Initializer_Test is CommonTest { optimismMintableERC20Factory: address(0) }), 0, - ISuperchainConfig(address(0)), - false + ISuperchainConfig(address(0)) ) ) }) @@ -203,8 +206,7 @@ contract Initializer_Test is CommonTest { optimismMintableERC20Factory: address(0) }), 0, - ISuperchainConfig(address(0)), - false + ISuperchainConfig(address(0)) ) ) }) From 20b8820a0a4c40bd8b7744db8eadecfcf3198f4a Mon Sep 17 00:00:00 2001 From: niha <205694301+0xniha@users.noreply.github.com> Date: Fri, 15 Aug 2025 16:16:36 -0300 Subject: [PATCH 10/23] fix: test validation for portal --- .../scripts/checks/test-validation/main.go | 1 + packages/contracts-bedrock/test/L1/OptimismPortal2.t.sol | 8 ++------ 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/packages/contracts-bedrock/scripts/checks/test-validation/main.go b/packages/contracts-bedrock/scripts/checks/test-validation/main.go index 02199eea88e3f..5f3ca2c183da3 100644 --- a/packages/contracts-bedrock/scripts/checks/test-validation/main.go +++ b/packages/contracts-bedrock/scripts/checks/test-validation/main.go @@ -361,6 +361,7 @@ var excludedPaths = []string{ "test/L2/CrossDomainOwnable3.t.sol", // Contains contracts not matching CrossDomainOwnable3 base name "test/L2/GasPriceOracle.t.sol", // Contains contracts not matching GasPriceOracle base name "test/universal/StandardBridge.t.sol", // Contains contracts not matching StandardBridge base name + "test/L1/OptimismPortal2.t.sol", // Contains contracts not matching OptimismPortal2 base name // PATHS EXCLUDED FROM FUNCTION NAME VALIDATION: // These paths are excluded because they don't pass the function name validation, which checks diff --git a/packages/contracts-bedrock/test/L1/OptimismPortal2.t.sol b/packages/contracts-bedrock/test/L1/OptimismPortal2.t.sol index bb616308e3e6e..b34c6d09e9361 100644 --- a/packages/contracts-bedrock/test/L1/OptimismPortal2.t.sol +++ b/packages/contracts-bedrock/test/L1/OptimismPortal2.t.sol @@ -2414,11 +2414,7 @@ contract OptimismPortal2_CustomGasToken_Test is OptimismPortal2_TestInit { super.setUp(); // Use stdStorage to handle packed slot for isCustomGasToken - stdstore - .enable_packed_slots() - .target(address(optimismPortal2)) - .sig("isCustomGasToken()") - .checked_write(true); + stdstore.enable_packed_slots().target(address(optimismPortal2)).sig("isCustomGasToken()").checked_write(true); } /// @notice Tests that isCustomGasToken storage is set correctly @@ -2457,7 +2453,7 @@ contract OptimismPortal2_CustomGasToken_Test is OptimismPortal2_TestInit { } /// @notice Tests that receive() reverts when custom gas token is enabled - function testFuzz_receive_reverts(uint256 value) external { + function testFuzz_receive_withCustomGasToken_reverts(uint256 value) external { value = bound(value, 1, type(uint128).max); vm.deal(depositor, value); From 5f53ccd5a8749b15b5c330339574982c1c07c5d0 Mon Sep 17 00:00:00 2001 From: hexshire Date: Mon, 18 Aug 2025 13:50:08 -0300 Subject: [PATCH 11/23] chore(cgt): run pre pr --- packages/contracts-bedrock/snapshots/semver-lock.json | 6 +++--- packages/contracts-bedrock/src/L1/OPContractsManager.sol | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/contracts-bedrock/snapshots/semver-lock.json b/packages/contracts-bedrock/snapshots/semver-lock.json index b9e5c2ea00e84..dbfca9064864b 100644 --- a/packages/contracts-bedrock/snapshots/semver-lock.json +++ b/packages/contracts-bedrock/snapshots/semver-lock.json @@ -20,8 +20,8 @@ "sourceCodeHash": "0x11b35ee81f797b30ee834e2ffad52686d2100d7ee139db4299b7d854dba25550" }, "src/L1/OPContractsManager.sol:OPContractsManager": { - "initCodeHash": "0x8c209f938d6aa21f1dbc93d50bed559c4cfe3d1b7b4f2cb81c4ea46e880b443c", - "sourceCodeHash": "0xb1264c7af50b6134c98cb82d1ffc7891adf97068fa7048ee70992fb94bc15bd1" + "initCodeHash": "0x1c67b4c8c3768e1ec81fcc4056084bafe170d29fba70f6a4492d953f1902195b", + "sourceCodeHash": "0x112a8f4d62699eddc103c8167b94cee8cb61b37fb5847c31a2956344d510a7ab" }, "src/L1/OPContractsManagerStandardValidator.sol:OPContractsManagerStandardValidator": { "initCodeHash": "0x4bf3bbdaf08989de57408b2ea88995e2f477b98add164dbf82e0dceb01417ef6", @@ -227,4 +227,4 @@ "initCodeHash": "0x2bfce526f82622288333d53ca3f43a0a94306ba1bab99241daa845f8f4b18bd4", "sourceCodeHash": "0xf49d7b0187912a6bb67926a3222ae51121e9239495213c975b3b4b217ee57a1b" } -} +} \ No newline at end of file diff --git a/packages/contracts-bedrock/src/L1/OPContractsManager.sol b/packages/contracts-bedrock/src/L1/OPContractsManager.sol index e4805e7afa6be..00098ca0cde10 100644 --- a/packages/contracts-bedrock/src/L1/OPContractsManager.sol +++ b/packages/contracts-bedrock/src/L1/OPContractsManager.sol @@ -1784,9 +1784,9 @@ contract OPContractsManager is ISemver { // -------- Constants and Variables -------- - /// @custom:semver 3.0.0 + /// @custom:semver 3.0.1 function version() public pure virtual returns (string memory) { - return "3.0.0"; + return "3.0.1"; } OPContractsManagerGameTypeAdder public immutable opcmGameTypeAdder; From a2c9b3a1ac11af26a298e41381e7b80ea090c72c Mon Sep 17 00:00:00 2001 From: Hex <165055168+hexshire@users.noreply.github.com> Date: Mon, 18 Aug 2025 17:41:20 -0300 Subject: [PATCH 12/23] chore(cgt): consolidate duplicated flags --- op-chain-ops/genesis/config.go | 16 +++------------- .../testdata/test-deploy-config-full.json | 3 +-- op-chain-ops/interopgen/recipe.go | 2 +- .../contracts-bedrock/deploy-config/hardhat.json | 1 - .../deploy-config/internal-devnet.json | 1 - .../contracts-bedrock/deploy-config/mainnet.json | 1 - .../deploy-config/sepolia-devnet-0.json | 1 - .../contracts-bedrock/deploy-config/sepolia.json | 1 - 8 files changed, 5 insertions(+), 21 deletions(-) diff --git a/op-chain-ops/genesis/config.go b/op-chain-ops/genesis/config.go index ee9ec2ea5ed91..c8fe6d50792e7 100644 --- a/op-chain-ops/genesis/config.go +++ b/op-chain-ops/genesis/config.go @@ -273,25 +273,15 @@ func (d *GasPriceOracleDeployConfig) OperatorFeeParams() [32]byte { // GasTokenDeployConfig configures the optional custom gas token functionality. type GasTokenDeployConfig struct { - // UseCustomGasToken is a flag to indicate that a custom gas token should be used - UseCustomGasToken bool `json:"useCustomGasToken"` - // IsCustomGasToken is a flag to indicate that a custom gas token should be used (alternative field name) + // IsCustomGasToken is a flag to indicate that a custom gas token should be used IsCustomGasToken bool `json:"isCustomGasToken"` - // CustomGasTokenAddress is the address of the ERC20 token to be used to pay for gas on L2. - CustomGasTokenAddress common.Address `json:"customGasTokenAddress"` } var _ ConfigChecker = (*GasTokenDeployConfig)(nil) func (d *GasTokenDeployConfig) Check(log log.Logger) error { - // Check if either field indicates custom gas token usage - isCustomGasToken := d.UseCustomGasToken || d.IsCustomGasToken - - if isCustomGasToken { - if d.CustomGasTokenAddress == (common.Address{}) { - return fmt.Errorf("%w: CustomGasTokenAddress cannot be address(0)", ErrInvalidDeployConfig) - } - log.Info("Using custom gas token", "address", d.CustomGasTokenAddress) + if d.IsCustomGasToken { + log.Info("Using custom gas token") } return nil } diff --git a/op-chain-ops/genesis/testdata/test-deploy-config-full.json b/op-chain-ops/genesis/testdata/test-deploy-config-full.json index 0f332842204f9..1e2c6b2d7111d 100644 --- a/op-chain-ops/genesis/testdata/test-deploy-config-full.json +++ b/op-chain-ops/genesis/testdata/test-deploy-config-full.json @@ -6,7 +6,6 @@ "maxSequencerDrift": 20, "sequencerWindowSize": 100, "channelTimeout": 30, - "customGasTokenAddress": "0x0000000000000000000000000000000000000000", "p2pSequencerAddress": "0x9965507d1a55bcc2695c58ba16fb37d819b0a4dc", "batchInboxAddress": "0x42000000000000000000000000000000000000ff", "batchSenderAddress": "0x3c44cdddb6a900fa2b585dd299e03d12fa4293bc", @@ -84,7 +83,7 @@ "proofMaturityDelaySeconds": 12, "disputeGameFinalityDelaySeconds": 6, "respectedGameType": 0, - "useCustomGasToken": false, + "isCustomGasToken": false, "useFaultProofs": false, "useAltDA": false, "daBondSize": 0, diff --git a/op-chain-ops/interopgen/recipe.go b/op-chain-ops/interopgen/recipe.go index 2d40c4451c94d..03e1927c95b5c 100644 --- a/op-chain-ops/interopgen/recipe.go +++ b/op-chain-ops/interopgen/recipe.go @@ -243,7 +243,7 @@ func (r *InteropDevL2Recipe) build(l1ChainID uint64, addrs devkeys.Addresses) (* GasPriceOracleBlobBaseFeeScalar: 810949, }, GasTokenDeployConfig: genesis.GasTokenDeployConfig{ - UseCustomGasToken: false, + IsCustomGasToken: false, }, OperatorDeployConfig: genesis.OperatorDeployConfig{ P2PSequencerAddress: sequencerP2P, diff --git a/packages/contracts-bedrock/deploy-config/hardhat.json b/packages/contracts-bedrock/deploy-config/hardhat.json index e2c0191b7902a..e81d125314e0f 100644 --- a/packages/contracts-bedrock/deploy-config/hardhat.json +++ b/packages/contracts-bedrock/deploy-config/hardhat.json @@ -63,6 +63,5 @@ "daResolveWindow": 100, "daBondSize": 1000, "daResolverRefundPercentage": 50, - "useCustomGasToken": false, "isCustomGasToken": false } diff --git a/packages/contracts-bedrock/deploy-config/internal-devnet.json b/packages/contracts-bedrock/deploy-config/internal-devnet.json index 04f7c0d886b3d..ba86edaee7d2c 100644 --- a/packages/contracts-bedrock/deploy-config/internal-devnet.json +++ b/packages/contracts-bedrock/deploy-config/internal-devnet.json @@ -39,6 +39,5 @@ "systemConfigStartBlock": 8364212, "requiredProtocolVersion": "0x0000000000000000000000000000000000000000000000000000000000000000", "recommendedProtocolVersion": "0x0000000000000000000000000000000000000000000000000000000000000000", - "useCustomGasToken": false, "isCustomGasToken": false } diff --git a/packages/contracts-bedrock/deploy-config/mainnet.json b/packages/contracts-bedrock/deploy-config/mainnet.json index e97986b2ec10a..f81c4d131cb9a 100644 --- a/packages/contracts-bedrock/deploy-config/mainnet.json +++ b/packages/contracts-bedrock/deploy-config/mainnet.json @@ -56,6 +56,5 @@ "disputeGameFinalityDelaySeconds": 302400, "respectedGameType": 0, "useFaultProofs": true, - "useCustomGasToken": false, "isCustomGasToken": false } diff --git a/packages/contracts-bedrock/deploy-config/sepolia-devnet-0.json b/packages/contracts-bedrock/deploy-config/sepolia-devnet-0.json index 24dc841777cf1..5f97a743f17f4 100644 --- a/packages/contracts-bedrock/deploy-config/sepolia-devnet-0.json +++ b/packages/contracts-bedrock/deploy-config/sepolia-devnet-0.json @@ -80,6 +80,5 @@ "fundDevAccounts": false, "requiredProtocolVersion": "0x0000000000000000000000000000000000000005000000000000000000000000", "recommendedProtocolVersion": "0x0000000000000000000000000000000000000005000000000000000000000000", - "useCustomGasToken": false, "isCustomGasToken": false } diff --git a/packages/contracts-bedrock/deploy-config/sepolia.json b/packages/contracts-bedrock/deploy-config/sepolia.json index fdd254ff11130..31d0867938a8c 100644 --- a/packages/contracts-bedrock/deploy-config/sepolia.json +++ b/packages/contracts-bedrock/deploy-config/sepolia.json @@ -55,6 +55,5 @@ "disputeGameFinalityDelaySeconds": 302400, "respectedGameType": 0, "useFaultProofs": true, - "useCustomGasToken": false, "isCustomGasToken": false } From 2bc0430b4c4402a7bed4eb46c70e23ecde1a1cd3 Mon Sep 17 00:00:00 2001 From: hexshire Date: Tue, 19 Aug 2025 13:27:56 -0300 Subject: [PATCH 13/23] chore(cgt): remove underscore --- .../scripts/checks/test-validation/main.go | 28 +++++++++---------- .../{L1Block_CGT.t.sol => L1BlockCGT.t.sol} | 10 +++---- ...CGT.t.sol => L2ToL1MessagePasserCGT.t.sol} | 6 ++-- 3 files changed, 22 insertions(+), 22 deletions(-) rename packages/contracts-bedrock/test/L2/{L1Block_CGT.t.sol => L1BlockCGT.t.sol} (92%) rename packages/contracts-bedrock/test/L2/{L2ToL1MessagePasser_CGT.t.sol => L2ToL1MessagePasserCGT.t.sol} (95%) diff --git a/packages/contracts-bedrock/scripts/checks/test-validation/main.go b/packages/contracts-bedrock/scripts/checks/test-validation/main.go index 5f3ca2c183da3..b260dfe2379dd 100644 --- a/packages/contracts-bedrock/scripts/checks/test-validation/main.go +++ b/packages/contracts-bedrock/scripts/checks/test-validation/main.go @@ -323,20 +323,20 @@ var excludedPaths = []string{ // Resolving these naming inconsistencies is outside the script's scope, but they are // documented here to avoid false validation failures while maintaining the validation rules // for standard contract tests. - "test/invariants/", // Invariant testing framework - no direct src counterpart - "test/opcm/", // OP Chain Manager tests - may have different structure - "test/scripts/", // Script tests - test deployment/utility scripts, not contracts - "test/integration/", // Integration tests - test multiple contracts together - "test/cannon/MIPS64Memory.t.sol", // Tests external MIPS implementation - "test/dispute/lib/LibClock.t.sol", // Tests library utilities - "test/dispute/lib/LibGameId.t.sol", // Tests library utilities - "test/setup/DeployVariations.t.sol", // Tests deployment variations - "test/universal/BenchmarkTest.t.sol", // Performance benchmarking tests - "test/universal/ExtendedPause.t.sol", // Tests extended functionality - "test/vendor/Initializable.t.sol", // Tests external vendor code - "test/vendor/InitializableOZv5.t.sol", // Tests external vendor code - "test/L2/L1Block_CGT.t.sol", // Tests L1Block with custom gas token - "test/L2/L2ToL1MessagePasser_CGT.t.sol", // Tests L2ToL1MessagePasser with custom gas token + "test/invariants/", // Invariant testing framework - no direct src counterpart + "test/opcm/", // OP Chain Manager tests - may have different structure + "test/scripts/", // Script tests - test deployment/utility scripts, not contracts + "test/integration/", // Integration tests - test multiple contracts together + "test/cannon/MIPS64Memory.t.sol", // Tests external MIPS implementation + "test/dispute/lib/LibClock.t.sol", // Tests library utilities + "test/dispute/lib/LibGameId.t.sol", // Tests library utilities + "test/setup/DeployVariations.t.sol", // Tests deployment variations + "test/universal/BenchmarkTest.t.sol", // Performance benchmarking tests + "test/universal/ExtendedPause.t.sol", // Tests extended functionality + "test/vendor/Initializable.t.sol", // Tests external vendor code + "test/vendor/InitializableOZv5.t.sol", // Tests external vendor code + "test/L2/L1BlockCGT.t.sol", // Tests L1Block with custom gas token + "test/L2/L2ToL1MessagePasserCGT.t.sol", // Tests L2ToL1MessagePasser with custom gas token // PATHS EXCLUDED FROM CONTRACT NAME FILE PATH VALIDATION: // These paths are excluded because they don't follow the standard naming convention where the diff --git a/packages/contracts-bedrock/test/L2/L1Block_CGT.t.sol b/packages/contracts-bedrock/test/L2/L1BlockCGT.t.sol similarity index 92% rename from packages/contracts-bedrock/test/L2/L1Block_CGT.t.sol rename to packages/contracts-bedrock/test/L2/L1BlockCGT.t.sol index 1cebf22ff5f6a..25bc80caa1812 100644 --- a/packages/contracts-bedrock/test/L2/L1Block_CGT.t.sol +++ b/packages/contracts-bedrock/test/L2/L1BlockCGT.t.sol @@ -12,9 +12,9 @@ import { // Libraries import "src/libraries/L1BlockErrors.sol"; -/// @title L1Block_CGT_TestInit +/// @title L1BlockCGT_TestInit /// @notice Reusable test initialization for `L1Block` tests with custom gas token enabled. -contract L1Block_CGT_TestInit is CommonTest { +contract L1BlockCGT_TestInit is CommonTest { address depositor; /// @notice Sets up the test suite. @@ -28,7 +28,7 @@ contract L1Block_CGT_TestInit is CommonTest { /// @title L1Block_CGT_GasPayingTokenName_Test /// @notice Tests the `gasPayingTokenName` function of the `L1Block` contract with custom gas /// token enabled. -contract L1Block_CGT_GasPayingTokenName_Test is L1Block_CGT_TestInit { +contract L1Block_CGT_GasPayingTokenName_Test is L1BlockCGT_TestInit { /// @notice Tests that the `gasPayingTokenName` function returns the correct token name. function test_gasPayingTokenName_succeeds() external view { assertEq(liquidityController.gasPayingTokenName(), l1Block.gasPayingTokenName()); @@ -38,7 +38,7 @@ contract L1Block_CGT_GasPayingTokenName_Test is L1Block_CGT_TestInit { /// @title L1Block_CGT_GasPayingTokenSymbol_Test /// @notice Tests the `gasPayingTokenSymbol` function of the `L1Block` contract with custom gas /// token enabled. -contract L1Block_CGT_GasPayingTokenSymbol_Test is L1Block_CGT_TestInit { +contract L1Block_CGT_GasPayingTokenSymbol_Test is L1BlockCGT_TestInit { /// @notice Tests that the `gasPayingTokenSymbol` function returns the correct token symbol. function test_gasPayingTokenSymbol_succeeds() external view { assertEq(liquidityController.gasPayingTokenSymbol(), l1Block.gasPayingTokenSymbol()); @@ -48,7 +48,7 @@ contract L1Block_CGT_GasPayingTokenSymbol_Test is L1Block_CGT_TestInit { /// @title L1Block_CGT_IsCustomGasToken_Test /// @notice Tests the `isCustomGasToken` function of the `L1Block` contract with custom gas token /// enabled. -contract L1Block_CGT_IsCustomGasToken_Test is L1Block_CGT_TestInit { +contract L1Block_CGT_IsCustomGasToken_Test is L1BlockCGT_TestInit { /// @notice Tests that the `isCustomGasToken` function returns false when no custom gas token /// is used. function test_isCustomGasToken_succeeds() external view { diff --git a/packages/contracts-bedrock/test/L2/L2ToL1MessagePasser_CGT.t.sol b/packages/contracts-bedrock/test/L2/L2ToL1MessagePasserCGT.t.sol similarity index 95% rename from packages/contracts-bedrock/test/L2/L2ToL1MessagePasser_CGT.t.sol rename to packages/contracts-bedrock/test/L2/L2ToL1MessagePasserCGT.t.sol index 2e6c9cb80583a..cf814bd5b56c6 100644 --- a/packages/contracts-bedrock/test/L2/L2ToL1MessagePasser_CGT.t.sol +++ b/packages/contracts-bedrock/test/L2/L2ToL1MessagePasserCGT.t.sol @@ -11,9 +11,9 @@ import { Hashing } from "src/libraries/Hashing.sol"; // Interfaces import { IL2ToL1MessagePasser } from "interfaces/L2/IL2ToL1MessagePasser.sol"; -/// @title L2ToL1MessagePasser_CGT_TestInit +/// @title L2ToL1MessagePasserCGT_TestInit /// @notice Tests the `L2ToL1MessagePasser` contract with a custom gas token enabled. -contract L2ToL1MessagePasser_CGT_TestInit is CommonTest { +contract L2ToL1MessagePasserCGT_TestInit is CommonTest { /// @notice Sets up the test suite with custom gas token enabled. function setUp() public override { super.enableCustomGasToken(); @@ -24,7 +24,7 @@ contract L2ToL1MessagePasser_CGT_TestInit is CommonTest { /// @title L2ToL1MessagePasser_CGT_InitiateWithdrawal_Test /// @notice Tests the `initiateWithdrawal` function of the `L2ToL1MessagePasser` contract with /// custom gas token enabled. -contract L2ToL1MessagePasser_CGT_InitiateWithdrawal_Test is L2ToL1MessagePasser_CGT_TestInit { +contract L2ToL1MessagePasser_CGT_InitiateWithdrawal_Test is L2ToL1MessagePasserCGT_TestInit { /// @notice Tests that `initiateWithdrawal` succeeds and correctly sets the state of the /// message passer for the withdrawal hash. function testFuzz_initiateWithdrawal_withZeroValue_succeeds( From 7dec74421bf9d1981509c65d11fe1284ac9e3dd3 Mon Sep 17 00:00:00 2001 From: niha <205694301+0xniha@users.noreply.github.com> Date: Tue, 19 Aug 2025 17:28:24 -0300 Subject: [PATCH 14/23] fix: cgt flag on depositTransaction test --- packages/contracts-bedrock/test/L1/OptimismPortal2.t.sol | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/contracts-bedrock/test/L1/OptimismPortal2.t.sol b/packages/contracts-bedrock/test/L1/OptimismPortal2.t.sol index 184b9644819ab..a753f01c40820 100644 --- a/packages/contracts-bedrock/test/L1/OptimismPortal2.t.sol +++ b/packages/contracts-bedrock/test/L1/OptimismPortal2.t.sol @@ -2189,6 +2189,8 @@ contract OptimismPortal2_CheckWithdrawal_Test is OptimismPortal2_TestInit { /// @title OptimismPortal2_DepositTransaction_Test /// @notice Test contract for OptimismPortal2 `depositTransaction` function. contract OptimismPortal2_DepositTransaction_Test is OptimismPortal2_TestInit { + using stdStorage for StdStorage; + /// @notice Tests that `depositTransaction` reverts when the destination address is non-zero /// for a contract creation deposit. function test_depositTransaction_contractCreation_reverts() external { @@ -2224,7 +2226,7 @@ contract OptimismPortal2_DepositTransaction_Test is OptimismPortal2_TestInit { // Prevent overflow on an upgrade context _value = bound(_value, 1, type(uint256).max - address(ethLockbox).balance); // Set the custom gas token to true. - vm.mockCall(address(systemConfig), abi.encodeCall(systemConfig.isCustomGasToken, ()), abi.encode(true)); + stdstore.enable_packed_slots().target(address(optimismPortal2)).sig("isCustomGasToken()").checked_write(true); uint64 gasLimit = optimismPortal2.minimumGasLimit(uint64(_data.length)); vm.deal(depositor, _value); From 0cfdc414647a9825121943ea3ccad602af769df2 Mon Sep 17 00:00:00 2001 From: niha <205694301+0xniha@users.noreply.github.com> Date: Tue, 19 Aug 2025 17:48:38 -0300 Subject: [PATCH 15/23] feat: add cgt flag check in finalizeWithdrawal --- .../snapshots/semver-lock.json | 4 +- .../src/L1/OptimismPortal2.sol | 5 + .../test/L1/OptimismPortal2.t.sol | 91 +++++++++++++++++++ 3 files changed, 98 insertions(+), 2 deletions(-) diff --git a/packages/contracts-bedrock/snapshots/semver-lock.json b/packages/contracts-bedrock/snapshots/semver-lock.json index dbfca9064864b..40c3e65a1a350 100644 --- a/packages/contracts-bedrock/snapshots/semver-lock.json +++ b/packages/contracts-bedrock/snapshots/semver-lock.json @@ -28,8 +28,8 @@ "sourceCodeHash": "0x36861c793b247f4922ecd77b1b153a0f2a47a129117fbe59e7e1f6498ef46c42" }, "src/L1/OptimismPortal2.sol:OptimismPortal2": { - "initCodeHash": "0x308b65ecfb2cb74b06eb6ca32d3a54dc711b1e15b54bc5d510e39e1dcb91b99e", - "sourceCodeHash": "0x4bea9142f873f99bb44fdb50ed7e65041cc98f9b1c328d5e3812666d5e981ac9" + "initCodeHash": "0xf81abd502df268c9dd61d722e80c143dc6973c8a11dae8bd6de5b47bfbaf8d1f", + "sourceCodeHash": "0x5c84380f6622a41392dd4cc22b0c0e28672831ff77a1a1d4026d36c50f3e07d6" }, "src/L1/ProtocolVersions.sol:ProtocolVersions": { "initCodeHash": "0x5a76c8530cb24cf23d3baacc6eefaac226382af13f1e2a35535d2ec2b0573b29", diff --git a/packages/contracts-bedrock/src/L1/OptimismPortal2.sol b/packages/contracts-bedrock/src/L1/OptimismPortal2.sol index c99b30769bf06..3b72a0ac9c892 100644 --- a/packages/contracts-bedrock/src/L1/OptimismPortal2.sol +++ b/packages/contracts-bedrock/src/L1/OptimismPortal2.sol @@ -636,6 +636,11 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ReinitializableBase ) public { + // Cannot finalize withdrawal with value when custom gas token mode is enabled. + if (isCustomGasToken && _tx.value > 0) { + revert OptimismPortal_NotAllowedOnCGTMode(); + } + // Cannot finalize withdrawal transactions while the system is paused. _assertNotPaused(); diff --git a/packages/contracts-bedrock/test/L1/OptimismPortal2.t.sol b/packages/contracts-bedrock/test/L1/OptimismPortal2.t.sol index a753f01c40820..dab03d51070b5 100644 --- a/packages/contracts-bedrock/test/L1/OptimismPortal2.t.sol +++ b/packages/contracts-bedrock/test/L1/OptimismPortal2.t.sol @@ -1194,6 +1194,8 @@ contract OptimismPortal2_ProveWithdrawalTransaction_Test is OptimismPortal2_Test /// @title OptimismPortal2_FinalizeWithdrawalTransaction_Test /// @notice Test contract for OptimismPortal2 `finalizeWithdrawalTransaction` function. contract OptimismPortal2_FinalizeWithdrawalTransaction_Test is OptimismPortal2_TestInit { + using stdStorage for StdStorage; + /// @notice Tests that `finalizeWithdrawalTransaction` reverts when the target is the portal /// contract or the lockbox. function test_finalizeWithdrawalTransaction_badTarget_reverts() external { @@ -1997,6 +1999,95 @@ contract OptimismPortal2_FinalizeWithdrawalTransaction_Test is OptimismPortal2_T optimismPortal2.finalizeWithdrawalTransaction(_defaultTx); assertTrue(optimismPortal2.finalizedWithdrawals(_withdrawalHash)); } + + /// @notice Tests that `finalizeWithdrawalTransaction` succeeds when the custom gas token mode + /// is enabled and the withdrawal transaction has no value. + function test_finalizeWithdrawalTransaction_withoutValueAndCustomGasToken_succeeds( + address _sender, + address _target, + uint256 _gasLimit, + bytes memory _data + ) + external + { + skipIfForkTest("Skipping on forked tests because of the L2ToL1MessageParser call below"); + + vm.assume( + _target != address(optimismPortal2) // Cannot call the optimism portal or a contract + && _target.code.length == 0 // No accounts with code + && _target != CONSOLE // The console has no code but behaves like a contract + && uint160(_target) > 9 // No precompiles (or zero address) + ); + + // Set the custom gas token to true. + stdstore.enable_packed_slots().target(address(optimismPortal2)).sig("isCustomGasToken()").checked_write(true); + + uint256 gasLimit = bound(_gasLimit, 0, 50_000_000); + uint256 nonce = l2ToL1MessagePasser.messageNonce(); + + // Get a withdrawal transaction and mock proof from the differential testing script. + Types.WithdrawalTransaction memory _tx = Types.WithdrawalTransaction({ + nonce: nonce, + sender: _sender, + target: _target, + value: 0, + gasLimit: gasLimit, + data: _data + }); + ( + bytes32 stateRoot, + bytes32 storageRoot, + bytes32 outputRoot, + bytes32 withdrawalHash, + bytes[] memory withdrawalProof + ) = ffi.getProveWithdrawalTransactionInputs(_tx); + + // Create the output root proof + Types.OutputRootProof memory proof = Types.OutputRootProof({ + version: bytes32(uint256(0)), + stateRoot: stateRoot, + messagePasserStorageRoot: storageRoot, + latestBlockhash: bytes32(uint256(0)) + }); + + // Ensure the values returned from ffi are correct + assertEq(outputRoot, Hashing.hashOutputRootProof(proof)); + assertEq(withdrawalHash, Hashing.hashWithdrawal(_tx)); + + // Setup the dispute game to return the output root + vm.mockCall(address(game), abi.encodeCall(game.rootClaim, ()), abi.encode(outputRoot)); + + // Prove the withdrawal transaction + optimismPortal2.proveWithdrawalTransaction(_tx, _proposedGameIndex, proof, withdrawalProof); + (IDisputeGame _game,) = optimismPortal2.provenWithdrawals(withdrawalHash, address(this)); + assertTrue(_game.rootClaim().raw() != bytes32(0)); + + // Resolve the dispute game + game.resolveClaim(0, 0); + game.resolve(); + + // Warp past the finalization period + vm.warp(block.timestamp + optimismPortal2.proofMaturityDelaySeconds() + 1); + + // Finalize the withdrawal transaction + vm.expectCallMinGas(_tx.target, _tx.value, uint64(_tx.gasLimit), _tx.data); + optimismPortal2.finalizeWithdrawalTransaction(_tx); + assertTrue(optimismPortal2.finalizedWithdrawals(withdrawalHash)); + } + + /// @notice Tests that `finalizeWithdrawalTransaction` reverts when the custom gas token mode + /// is enabled and the withdrawal transaction has a value. + function test_finalizeWithdrawalTransaction_withValueAndCustomGasToken_reverts() external { + // Set the custom gas token to true. + stdstore.enable_packed_slots().target(address(optimismPortal2)).sig("isCustomGasToken()").checked_write(true); + + // Set the withdrawal transaction value to a non-zero value. + _defaultTx.value = bound(uint256(1), 1, type(uint256).max); + + // Finalize the withdrawal transaction. This should revert. + vm.expectRevert(IOptimismPortal.OptimismPortal_NotAllowedOnCGTMode.selector); + optimismPortal2.finalizeWithdrawalTransaction(_defaultTx); + } } /// @title OptimismPortal2_FinalizeWithdrawalTransactionExternalProof_Test From 8ab97c79aa0015c73dd541dbe9ca951f25edd6a9 Mon Sep 17 00:00:00 2001 From: niha <205694301+0xniha@users.noreply.github.com> Date: Tue, 19 Aug 2025 18:11:35 -0300 Subject: [PATCH 16/23] refactor: add setIsCustomGasToken internal function --- .../test/L1/OptimismPortal2.t.sol | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/packages/contracts-bedrock/test/L1/OptimismPortal2.t.sol b/packages/contracts-bedrock/test/L1/OptimismPortal2.t.sol index dab03d51070b5..1ddf36a5ab62e 100644 --- a/packages/contracts-bedrock/test/L1/OptimismPortal2.t.sol +++ b/packages/contracts-bedrock/test/L1/OptimismPortal2.t.sol @@ -34,6 +34,8 @@ import { IETHLockbox } from "interfaces/L1/IETHLockbox.sol"; import { IProxyAdminOwnedBase } from "interfaces/L1/IProxyAdminOwnedBase.sol"; contract OptimismPortal2_TestInit is DisputeGameFactory_TestInit { + using stdStorage for StdStorage; + address depositor; Types.WithdrawalTransaction _defaultTx; @@ -149,6 +151,13 @@ contract OptimismPortal2_TestInit is DisputeGameFactory_TestInit { // Store the new value at the correct slot/offset. vm.store(address(optimismPortal2), bytes32(slot.slot), newValue); } + + /// @notice Sets the isCustomGasToken variable to true. + function setIsCustomGasToken(bool _isCustomGasToken) public { + stdstore.enable_packed_slots().target(address(optimismPortal2)).sig("isCustomGasToken()").checked_write( + _isCustomGasToken + ); + } } /// @title OptimismPortal2_Version_Test @@ -2020,7 +2029,7 @@ contract OptimismPortal2_FinalizeWithdrawalTransaction_Test is OptimismPortal2_T ); // Set the custom gas token to true. - stdstore.enable_packed_slots().target(address(optimismPortal2)).sig("isCustomGasToken()").checked_write(true); + setIsCustomGasToken(true); uint256 gasLimit = bound(_gasLimit, 0, 50_000_000); uint256 nonce = l2ToL1MessagePasser.messageNonce(); @@ -2079,7 +2088,7 @@ contract OptimismPortal2_FinalizeWithdrawalTransaction_Test is OptimismPortal2_T /// is enabled and the withdrawal transaction has a value. function test_finalizeWithdrawalTransaction_withValueAndCustomGasToken_reverts() external { // Set the custom gas token to true. - stdstore.enable_packed_slots().target(address(optimismPortal2)).sig("isCustomGasToken()").checked_write(true); + setIsCustomGasToken(true); // Set the withdrawal transaction value to a non-zero value. _defaultTx.value = bound(uint256(1), 1, type(uint256).max); @@ -2317,7 +2326,7 @@ contract OptimismPortal2_DepositTransaction_Test is OptimismPortal2_TestInit { // Prevent overflow on an upgrade context _value = bound(_value, 1, type(uint256).max - address(ethLockbox).balance); // Set the custom gas token to true. - stdstore.enable_packed_slots().target(address(optimismPortal2)).sig("isCustomGasToken()").checked_write(true); + setIsCustomGasToken(true); uint64 gasLimit = optimismPortal2.minimumGasLimit(uint64(_data.length)); vm.deal(depositor, _value); @@ -2659,8 +2668,8 @@ contract OptimismPortal2_CustomGasToken_Test is OptimismPortal2_TestInit { function setUp() public override { super.setUp(); - // Use stdStorage to handle packed slot for isCustomGasToken - stdstore.enable_packed_slots().target(address(optimismPortal2)).sig("isCustomGasToken()").checked_write(true); + // Set isCustomGasToken to true. + setIsCustomGasToken(true); } /// @notice Tests that isCustomGasToken storage is set correctly From f2ed1ec7b09e65f7ada46766a111ab11a398660c Mon Sep 17 00:00:00 2001 From: niha <205694301+0xniha@users.noreply.github.com> Date: Tue, 19 Aug 2025 18:36:50 -0300 Subject: [PATCH 17/23] fix: remove unused using-for directives --- packages/contracts-bedrock/test/L1/OptimismPortal2.t.sol | 6 ------ 1 file changed, 6 deletions(-) diff --git a/packages/contracts-bedrock/test/L1/OptimismPortal2.t.sol b/packages/contracts-bedrock/test/L1/OptimismPortal2.t.sol index 1ddf36a5ab62e..74fccd788ce1c 100644 --- a/packages/contracts-bedrock/test/L1/OptimismPortal2.t.sol +++ b/packages/contracts-bedrock/test/L1/OptimismPortal2.t.sol @@ -1203,8 +1203,6 @@ contract OptimismPortal2_ProveWithdrawalTransaction_Test is OptimismPortal2_Test /// @title OptimismPortal2_FinalizeWithdrawalTransaction_Test /// @notice Test contract for OptimismPortal2 `finalizeWithdrawalTransaction` function. contract OptimismPortal2_FinalizeWithdrawalTransaction_Test is OptimismPortal2_TestInit { - using stdStorage for StdStorage; - /// @notice Tests that `finalizeWithdrawalTransaction` reverts when the target is the portal /// contract or the lockbox. function test_finalizeWithdrawalTransaction_badTarget_reverts() external { @@ -2289,8 +2287,6 @@ contract OptimismPortal2_CheckWithdrawal_Test is OptimismPortal2_TestInit { /// @title OptimismPortal2_DepositTransaction_Test /// @notice Test contract for OptimismPortal2 `depositTransaction` function. contract OptimismPortal2_DepositTransaction_Test is OptimismPortal2_TestInit { - using stdStorage for StdStorage; - /// @notice Tests that `depositTransaction` reverts when the destination address is non-zero /// for a contract creation deposit. function test_depositTransaction_contractCreation_reverts() external { @@ -2662,8 +2658,6 @@ contract OptimismPortal2_Params_Test is CommonTest { /// @title OptimismPortal2_CustomGasToken_Test /// @notice Test suite for OptimismPortal2 with custom gas token enabled. contract OptimismPortal2_CustomGasToken_Test is OptimismPortal2_TestInit { - using stdStorage for StdStorage; - /// @notice Sets up a portal with custom gas token enabled function setUp() public override { super.setUp(); From fdd80deca970b24310c63b5499885d6518ba42fe Mon Sep 17 00:00:00 2001 From: Hex <165055168+hexshire@users.noreply.github.com> Date: Thu, 21 Aug 2025 12:03:53 -0300 Subject: [PATCH 18/23] refactor(cgt): add flag setter --- .../interfaces/L2/IL1Block.sol | 27 +++++---- .../contracts-bedrock/scripts/L2Genesis.s.sol | 20 +------ .../snapshots/abi/L1Block.json | 20 +++---- .../snapshots/semver-lock.json | 4 +- packages/contracts-bedrock/src/L2/L1Block.sol | 39 +++++++++--- .../test/L2/L1BlockCGT.t.sol | 59 ++++++++++++++++++- .../test/L2/L2ToL1MessagePasserCGT.t.sol | 9 ++- .../contracts-bedrock/test/setup/Setup.sol | 7 +-- 8 files changed, 122 insertions(+), 63 deletions(-) diff --git a/packages/contracts-bedrock/interfaces/L2/IL1Block.sol b/packages/contracts-bedrock/interfaces/L2/IL1Block.sol index 2c4b3fa768f15..c8c2c9119ceb1 100644 --- a/packages/contracts-bedrock/interfaces/L2/IL1Block.sol +++ b/packages/contracts-bedrock/interfaces/L2/IL1Block.sol @@ -3,21 +3,23 @@ pragma solidity ^0.8.0; interface IL1Block { function DEPOSITOR_ACCOUNT() external pure returns (address addr_); - function baseFeeScalar() external view returns (uint32); + function number() external view returns (uint64); + function timestamp() external view returns (uint64); 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 gasPayingTokenName() external view returns (string memory name_); - function gasPayingTokenSymbol() external view returns (string memory symbol_); function hash() external view returns (bytes32); - function isCustomGasToken() external view returns (bool); + function sequenceNumber() external view returns (uint64); + function blobBaseFeeScalar() external view returns (uint32); + function baseFeeScalar() external view returns (uint32); + function batcherHash() external view returns (bytes32); function l1FeeOverhead() external view returns (uint256); function l1FeeScalar() external view returns (uint256); - function number() external view returns (uint64); - function operatorFeeScalar() external view returns (uint32); + function blobBaseFee() external view returns (uint256); function operatorFeeConstant() external view returns (uint64); - function sequenceNumber() external view returns (uint64); + function operatorFeeScalar() external view returns (uint32); + function version() external pure returns (string memory); + function isCustomGasToken() external view returns (bool isCustom_); + function gasPayingTokenName() external view returns (string memory name_); + function gasPayingTokenSymbol() external view returns (string memory symbol_); function setL1BlockValues( uint64 _number, uint64 _timestamp, @@ -31,8 +33,7 @@ interface IL1Block { external; function setL1BlockValuesEcotone() external; function setL1BlockValuesIsthmus() external; - function timestamp() external view returns (uint64); - function version() external pure returns (string memory); + function setCustomGasToken() external; - function __constructor__(bool _isCustomGasToken) external; + function __constructor__() external; } diff --git a/packages/contracts-bedrock/scripts/L2Genesis.s.sol b/packages/contracts-bedrock/scripts/L2Genesis.s.sol index ef18c52dcb065..07e0bc0a60241 100644 --- a/packages/contracts-bedrock/scripts/L2Genesis.s.sol +++ b/packages/contracts-bedrock/scripts/L2Genesis.s.sol @@ -224,7 +224,7 @@ contract L2Genesis is Script { setOptimismMintableERC20Factory(); // 12 setL1BlockNumber(); // 13 setL2ERC721Bridge(_input.l1ERC721BridgeProxy); // 14 - setL1Block(_input.isCustomGasToken); // 15 + setL1Block(); // 15 setL2ToL1MessagePasser(); // 16 setOptimismMintableERC721Factory(_input); // 17 setProxyAdmin(_input); // 18 @@ -355,22 +355,8 @@ contract L2Genesis is Script { } /// @notice This predeploy is following the safety invariant #1. - function setL1Block(bool _isCustomGasToken) internal { - IL1Block l1Block = IL1Block( - DeployUtils.create1({ - _name: "L1Block", - _args: DeployUtils.encodeConstructor(abi.encodeCall(IL1Block.__constructor__, (_isCustomGasToken))) - }) - ); - - // Note: L1 block attributes are set to 0. - // Before the first user-tx the state is overwritten with actual L1 attributes. - address impl = Predeploys.predeployToCodeNamespace(Predeploys.L1_BLOCK_ATTRIBUTES); - vm.etch(impl, address(l1Block).code); - - /// Reset so its not included state dump - vm.etch(address(l1Block), ""); - vm.resetNonce(address(l1Block)); + function setL1Block() internal { + _setImplementationCode(Predeploys.L1_BLOCK_ATTRIBUTES); } /// @notice This predeploy is following the safety invariant #1. diff --git a/packages/contracts-bedrock/snapshots/abi/L1Block.json b/packages/contracts-bedrock/snapshots/abi/L1Block.json index 99a44e7792593..55b23f0699bc4 100644 --- a/packages/contracts-bedrock/snapshots/abi/L1Block.json +++ b/packages/contracts-bedrock/snapshots/abi/L1Block.json @@ -1,15 +1,4 @@ [ - { - "inputs": [ - { - "internalType": "bool", - "name": "_isCustomGasToken", - "type": "bool" - } - ], - "stateMutability": "nonpayable", - "type": "constructor" - }, { "inputs": [], "name": "DEPOSITOR_ACCOUNT", @@ -133,7 +122,7 @@ "outputs": [ { "internalType": "bool", - "name": "", + "name": "isCustom_", "type": "bool" } ], @@ -218,6 +207,13 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [], + "name": "setCustomGasToken", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [ { diff --git a/packages/contracts-bedrock/snapshots/semver-lock.json b/packages/contracts-bedrock/snapshots/semver-lock.json index 40c3e65a1a350..141d6a65d2c9c 100644 --- a/packages/contracts-bedrock/snapshots/semver-lock.json +++ b/packages/contracts-bedrock/snapshots/semver-lock.json @@ -60,8 +60,8 @@ "sourceCodeHash": "0x4351fe2ac1106c8c220b8cfe7839bc107c24d8084deb21259ac954f5a362725d" }, "src/L2/L1Block.sol:L1Block": { - "initCodeHash": "0x9bacfd0192360e6964c2f339720232e77ea2e78c3edfbbddb2b25596b6eb2f71", - "sourceCodeHash": "0x2ef85eba3ddc70530fed49d556419000ad0183debe2ae6bd71c29cec71732411" + "initCodeHash": "0x77f825c0828c113a550022989cdab9c5a0d324c80a421abf8ca27022e4766609", + "sourceCodeHash": "0xca797382d9075577ba2aa9542e12d21b4e5061c95096cb0f630ed8229c396bc3" }, "src/L2/L1FeeVault.sol:L1FeeVault": { "initCodeHash": "0x9b664e3d84ad510091337b4aacaa494b142512e2f6f7fbcdb6210ed62ca9b885", diff --git a/packages/contracts-bedrock/src/L2/L1Block.sol b/packages/contracts-bedrock/src/L2/L1Block.sol index 10049cd71f82a..5fa53b0c48790 100644 --- a/packages/contracts-bedrock/src/L2/L1Block.sol +++ b/packages/contracts-bedrock/src/L2/L1Block.sol @@ -17,14 +17,16 @@ import { ILiquidityController } from "interfaces/L2/ILiquidityController.sol"; /// 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 { + /// @notice Storage slot for the isCustomGasToken flag + /// @dev bytes32(uint256(keccak256("l1block.isCustomGasToken")) - 1) + bytes32 private constant IS_CUSTOM_GAS_TOKEN_SLOT = + 0xd2ff82c9b477ff6a09f530b1c627ffb4b0b81e2ae2ba427f824162e8dad020aa; + /// @notice Address of the special depositor account. function DEPOSITOR_ACCOUNT() public pure returns (address addr_) { addr_ = Constants.DEPOSITOR_ACCOUNT; } - /// @notice Whether the gas paying token is custom. - bool public immutable isCustomGasToken; - /// @notice The latest L1 block number known by the L2 system. uint64 public number; @@ -66,20 +68,25 @@ contract L1Block is ISemver { /// @notice The scalar value applied to the operator fee. uint32 public operatorFeeScalar; - constructor(bool _isCustomGasToken) { - isCustomGasToken = _isCustomGasToken; - } - /// @custom:semver 1.6.2 function version() public pure virtual returns (string memory) { return "1.6.2"; } + /// @notice Returns whether the gas paying token is custom. + function isCustomGasToken() public view returns (bool isCustom_) { + bytes32 slot = IS_CUSTOM_GAS_TOKEN_SLOT; + assembly { + isCustom_ := sload(slot) + } + } + /// @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 view returns (string memory name_) { - name_ = isCustomGasToken ? ILiquidityController(Predeploys.LIQUIDITY_CONTROLLER).gasPayingTokenName() : "Ether"; + name_ = + isCustomGasToken() ? ILiquidityController(Predeploys.LIQUIDITY_CONTROLLER).gasPayingTokenName() : "Ether"; } /// @notice Returns the gas paying token symbol. @@ -87,7 +94,7 @@ contract L1Block is ISemver { /// This function cannot be removed because WETH depends on it. function gasPayingTokenSymbol() public view returns (string memory symbol_) { symbol_ = - isCustomGasToken ? ILiquidityController(Predeploys.LIQUIDITY_CONTROLLER).gasPayingTokenSymbol() : "ETH"; + isCustomGasToken() ? ILiquidityController(Predeploys.LIQUIDITY_CONTROLLER).gasPayingTokenSymbol() : "ETH"; } /// @custom:legacy @@ -210,4 +217,18 @@ contract L1Block is ISemver { sstore(operatorFeeConstant.slot, shr(160, calldataload(164))) } } + + /// @notice Set chain to use custom gas token (callable by depositor account) + function setCustomGasToken() external { + require( + msg.sender == Constants.DEPOSITOR_ACCOUNT, + "L1Block: only the depositor account can set isCustomGasToken flag" + ); + require(isCustomGasToken() == false, "L1Block: CustomGasToken already active"); + + bytes32 slot = IS_CUSTOM_GAS_TOKEN_SLOT; + assembly { + sstore(slot, 1) + } + } } diff --git a/packages/contracts-bedrock/test/L2/L1BlockCGT.t.sol b/packages/contracts-bedrock/test/L2/L1BlockCGT.t.sol index 25bc80caa1812..f1a39c5e735c4 100644 --- a/packages/contracts-bedrock/test/L2/L1BlockCGT.t.sol +++ b/packages/contracts-bedrock/test/L2/L1BlockCGT.t.sol @@ -22,6 +22,10 @@ contract L1BlockCGT_TestInit is CommonTest { super.enableCustomGasToken(); super.setUp(); depositor = l1Block.DEPOSITOR_ACCOUNT(); + + // Manually activate custom gas token since we removed the constructor parameter + vm.prank(depositor); + l1Block.setCustomGasToken(); } } @@ -65,7 +69,9 @@ contract L1Block_CGT_SetL1BlockValues_Test is L1Block_SetL1BlockValues_Test { function setUp() public override { super.enableCustomGasToken(); super.setUp(); - assertTrue(l1Block.isCustomGasToken()); + // Manually activate custom gas token since we removed the constructor parameter + vm.prank(depositor); + l1Block.setCustomGasToken(); } } @@ -78,7 +84,9 @@ contract L1Block_CGT_SetL1BlockValuesEcotone_Test is L1Block_SetL1BlockValuesEco function setUp() public override { super.enableCustomGasToken(); super.setUp(); - assertTrue(l1Block.isCustomGasToken()); + // Manually activate custom gas token since we removed the constructor parameter + vm.prank(depositor); + l1Block.setCustomGasToken(); } } @@ -91,6 +99,53 @@ contract L1Block_CGT_SetL1BlockValuesIsthmus_Test is L1Block_SetL1BlockValuesIst function setUp() public override { super.enableCustomGasToken(); super.setUp(); + // Manually activate custom gas token since we removed the constructor parameter + vm.prank(depositor); + l1Block.setCustomGasToken(); + } +} + +/// @title L1Block_CGT_SetCustomGasToken_Test +/// @notice Tests the `setCustomGasToken` function of the `L1Block` contract. +contract L1Block_CGT_SetCustomGasToken_Test is L1BlockCGT_TestInit { + /// @notice Tests that `setCustomGasToken` reverts if called twice. + function test_setCustomGasToken_alreadyActive_reverts() external { + // This test uses the setUp that already activates custom gas token assertTrue(l1Block.isCustomGasToken()); + + vm.expectRevert("L1Block: CustomGasToken already active"); + vm.prank(depositor); + l1Block.setCustomGasToken(); + } +} + +/// @title L1Block_SetCustomGasToken_Test +/// @notice Tests the `setCustomGasToken` function of the `L1Block` contract without CGT enabled. +contract L1Block_SetCustomGasToken_Test is CommonTest { + address depositor; + + /// @notice Sets up the test suite. + function setUp() public virtual override { + // Don't enable custom gas token - test the activation process + super.setUp(); + depositor = l1Block.DEPOSITOR_ACCOUNT(); + } + + /// @notice Tests that `setCustomGasToken` updates the flag correctly when called by depositor. + function test_setCustomGasToken_succeeds() external { + assertFalse(l1Block.isCustomGasToken()); + + vm.prank(depositor); + l1Block.setCustomGasToken(); + + assertTrue(l1Block.isCustomGasToken()); + } + + /// @notice Tests that `setCustomGasToken` reverts if sender address is not the depositor. + function test_setCustomGasToken_notDepositor_reverts(address nonDepositor) external { + vm.assume(nonDepositor != depositor); + vm.expectRevert("L1Block: only the depositor account can set isCustomGasToken flag"); + vm.prank(nonDepositor); + l1Block.setCustomGasToken(); } } diff --git a/packages/contracts-bedrock/test/L2/L2ToL1MessagePasserCGT.t.sol b/packages/contracts-bedrock/test/L2/L2ToL1MessagePasserCGT.t.sol index cf814bd5b56c6..9f014af6169ca 100644 --- a/packages/contracts-bedrock/test/L2/L2ToL1MessagePasserCGT.t.sol +++ b/packages/contracts-bedrock/test/L2/L2ToL1MessagePasserCGT.t.sol @@ -18,13 +18,18 @@ contract L2ToL1MessagePasserCGT_TestInit is CommonTest { function setUp() public override { super.enableCustomGasToken(); super.setUp(); + + // Manually activate custom gas token since we removed the constructor parameter + address depositor = l1Block.DEPOSITOR_ACCOUNT(); + vm.prank(depositor); + l1Block.setCustomGasToken(); } } -/// @title L2ToL1MessagePasser_CGT_InitiateWithdrawal_Test +/// @title L2ToL1MessagePasserCGT_InitiateWithdrawal_Test /// @notice Tests the `initiateWithdrawal` function of the `L2ToL1MessagePasser` contract with /// custom gas token enabled. -contract L2ToL1MessagePasser_CGT_InitiateWithdrawal_Test is L2ToL1MessagePasserCGT_TestInit { +contract L2ToL1MessagePasserCGT_InitiateWithdrawal_Test is L2ToL1MessagePasserCGT_TestInit { /// @notice Tests that `initiateWithdrawal` succeeds and correctly sets the state of the /// message passer for the withdrawal hash. function testFuzz_initiateWithdrawal_withZeroValue_succeeds( diff --git a/packages/contracts-bedrock/test/setup/Setup.sol b/packages/contracts-bedrock/test/setup/Setup.sol index 74fd5128f24f3..9e5c9e200b41d 100644 --- a/packages/contracts-bedrock/test/setup/Setup.sol +++ b/packages/contracts-bedrock/test/setup/Setup.sol @@ -328,7 +328,7 @@ contract Setup { deployCrossL2Inbox: deploy.cfg().useInterop(), enableGovernance: deploy.cfg().enableGovernance(), fundDevAccounts: deploy.cfg().fundDevAccounts(), - isCustomGasToken: isCustomGasToken(), + isCustomGasToken: deploy.cfg().isCustomGasToken(), gasPayingTokenName: "Custom Gas Token", gasPayingTokenSymbol: "CGT" }) @@ -393,9 +393,4 @@ contract Setup { function labelPreinstall(address _addr) internal { vm.label(_addr, Preinstalls.getName(_addr)); } - - /// @dev Returns whether custom gas token is enabled - function isCustomGasToken() internal view returns (bool) { - return deploy.cfg().isCustomGasToken(); - } } From 37a849d76d5257f8dd8d42d0602a238af006d7a4 Mon Sep 17 00:00:00 2001 From: niha <205694301+0xniha@users.noreply.github.com> Date: Thu, 21 Aug 2025 15:13:30 -0300 Subject: [PATCH 19/23] chore(cgt): add withdrawal network type check on fee vaults deploy * feat: add withdrawal network type check on fee vaults deploy * test: add l2genesis revert test cases for invalid withdrawal network types --- .../contracts-bedrock/scripts/L2Genesis.s.sol | 32 ++++++++++++------- .../test/scripts/L2Genesis.t.sol | 32 ++++++++++++++++++- 2 files changed, 52 insertions(+), 12 deletions(-) diff --git a/packages/contracts-bedrock/scripts/L2Genesis.s.sol b/packages/contracts-bedrock/scripts/L2Genesis.s.sol index 07e0bc0a60241..5d4cf2556e9cc 100644 --- a/packages/contracts-bedrock/scripts/L2Genesis.s.sol +++ b/packages/contracts-bedrock/scripts/L2Genesis.s.sol @@ -298,6 +298,12 @@ contract L2Genesis is Script { /// @notice This predeploy is following the safety invariant #2, function setSequencerFeeVault(Input memory _input) internal { + Types.WithdrawalNetwork withdrawalNetwork = Types.WithdrawalNetwork(_input.sequencerFeeVaultWithdrawalNetwork); + + if (_input.isCustomGasToken && withdrawalNetwork == Types.WithdrawalNetwork.L1) { + revert("SequencerFeeVault: withdrawalNetwork type cannot be L1 when custom gas token is enabled"); + } + ISequencerFeeVault vault = ISequencerFeeVault( DeployUtils.create1({ _name: "SequencerFeeVault", @@ -307,7 +313,7 @@ contract L2Genesis is Script { ( _input.sequencerFeeVaultRecipient, _input.sequencerFeeVaultMinimumWithdrawalAmount, - Types.WithdrawalNetwork(_input.sequencerFeeVaultWithdrawalNetwork) + withdrawalNetwork ) ) ) @@ -388,17 +394,19 @@ contract L2Genesis is Script { /// @notice This predeploy is following the safety invariant #2. function setBaseFeeVault(Input memory _input) internal { + Types.WithdrawalNetwork withdrawalNetwork = Types.WithdrawalNetwork(_input.baseFeeVaultWithdrawalNetwork); + + if (_input.isCustomGasToken && withdrawalNetwork == Types.WithdrawalNetwork.L1) { + revert("BaseFeeVault: withdrawalNetwork type cannot be L1 when custom gas token is enabled"); + } + IBaseFeeVault vault = IBaseFeeVault( DeployUtils.create1({ _name: "BaseFeeVault", _args: DeployUtils.encodeConstructor( abi.encodeCall( IBaseFeeVault.__constructor__, - ( - _input.baseFeeVaultRecipient, - _input.baseFeeVaultMinimumWithdrawalAmount, - Types.WithdrawalNetwork(_input.baseFeeVaultWithdrawalNetwork) - ) + (_input.baseFeeVaultRecipient, _input.baseFeeVaultMinimumWithdrawalAmount, withdrawalNetwork) ) ) }) @@ -414,17 +422,19 @@ contract L2Genesis is Script { /// @notice This predeploy is following the safety invariant #2. function setL1FeeVault(Input memory _input) internal { + Types.WithdrawalNetwork withdrawalNetwork = Types.WithdrawalNetwork(_input.l1FeeVaultWithdrawalNetwork); + + if (_input.isCustomGasToken && withdrawalNetwork == Types.WithdrawalNetwork.L1) { + revert("L1FeeVault: withdrawalNetwork type cannot be L1 when custom gas token is enabled"); + } + IL1FeeVault vault = IL1FeeVault( DeployUtils.create1({ _name: "L1FeeVault", _args: DeployUtils.encodeConstructor( abi.encodeCall( IL1FeeVault.__constructor__, - ( - _input.l1FeeVaultRecipient, - _input.l1FeeVaultMinimumWithdrawalAmount, - Types.WithdrawalNetwork(_input.l1FeeVaultWithdrawalNetwork) - ) + (_input.l1FeeVaultRecipient, _input.l1FeeVaultMinimumWithdrawalAmount, withdrawalNetwork) ) ) }) diff --git a/packages/contracts-bedrock/test/scripts/L2Genesis.t.sol b/packages/contracts-bedrock/test/scripts/L2Genesis.t.sol index b8553a57a1043..9f7f86a14ded8 100644 --- a/packages/contracts-bedrock/test/scripts/L2Genesis.t.sol +++ b/packages/contracts-bedrock/test/scripts/L2Genesis.t.sol @@ -160,7 +160,8 @@ contract L2Genesis_Run_Test is L2Genesis_TestInit { testForks(); } - function test_run_cgt_succeeds() external { + /// @dev Modifier to set up the input for L2Genesis with CGT enabled. + modifier setInputCGTEnabled() { input = L2Genesis.Input({ l1ChainID: 1, l2ChainID: 2, @@ -186,6 +187,12 @@ contract L2Genesis_Run_Test is L2Genesis_TestInit { gasPayingTokenName: "Custom Gas Token", gasPayingTokenSymbol: "CGT" }); + _; + } + + /// @notice Tests that the run function succeeds when CGT is enabled. + /// @dev Tests that LiquidityController and NativeAssetLiquidity are deployed. + function test_run_cgt_succeeds() external setInputCGTEnabled { genesis.run(input); testProxyAdmin(); @@ -196,4 +203,27 @@ contract L2Genesis_Run_Test is L2Genesis_TestInit { testForks(); testCGT(); } + + /// @notice Tests that the run function reverts when CGT is enabled and the withdrawal network type of the FeeVaults + /// is L1. + function test_run_cgt_reverts() external setInputCGTEnabled { + // Expect revert when sequencerFeeVaultWithdrawalNetwork is L1 + input.sequencerFeeVaultWithdrawalNetwork = 0; + vm.expectRevert("SequencerFeeVault: withdrawalNetwork type cannot be L1 when custom gas token is enabled"); + genesis.run(input); + // Reset sequencerFeeVaultWithdrawalNetwork input to L2 + input.sequencerFeeVaultWithdrawalNetwork = 1; + + // Expect revert when baseFeeVaultWithdrawalNetwork is L1 + input.baseFeeVaultWithdrawalNetwork = 0; + vm.expectRevert("BaseFeeVault: withdrawalNetwork type cannot be L1 when custom gas token is enabled"); + genesis.run(input); + // Reset baseFeeVaultWithdrawalNetwork input to L2 + input.baseFeeVaultWithdrawalNetwork = 1; + + // Expect revert when l1FeeVaultWithdrawalNetwork is L1 + input.l1FeeVaultWithdrawalNetwork = 0; + vm.expectRevert("L1FeeVault: withdrawalNetwork type cannot be L1 when custom gas token is enabled"); + genesis.run(input); + } } From 9d382eaa2ab8f96f5fca0b0750776e8e83779ff2 Mon Sep 17 00:00:00 2001 From: niha <205694301+0xniha@users.noreply.github.com> Date: Mon, 1 Sep 2025 13:52:01 -0300 Subject: [PATCH 20/23] fix: set withdrawal network as L2 for feevaults when cgt enabled --- .../scripts/deploy/DeployConfig.s.sol | 15 +++++++++++++++ .../contracts-bedrock/test/setup/CommonTest.sol | 3 +++ 2 files changed, 18 insertions(+) diff --git a/packages/contracts-bedrock/scripts/deploy/DeployConfig.s.sol b/packages/contracts-bedrock/scripts/deploy/DeployConfig.s.sol index 9fd0e3317711d..22acab2414294 100644 --- a/packages/contracts-bedrock/scripts/deploy/DeployConfig.s.sol +++ b/packages/contracts-bedrock/scripts/deploy/DeployConfig.s.sol @@ -223,6 +223,21 @@ contract DeployConfig is Script { isCustomGasToken = _isCustomGasToken; } + /// @notice Allow the `baseFeeVaultWithdrawalNetwork` config to be overridden in testing environments + function setBaseFeeVaultWithdrawalNetwork(uint256 _baseFeeVaultWithdrawalNetwork) public { + baseFeeVaultWithdrawalNetwork = _baseFeeVaultWithdrawalNetwork; + } + + /// @notice Allow the `l1FeeVaultWithdrawalNetwork` config to be overridden in testing environments + function setL1FeeVaultWithdrawalNetwork(uint256 _l1FeeVaultWithdrawalNetwork) public { + l1FeeVaultWithdrawalNetwork = _l1FeeVaultWithdrawalNetwork; + } + + /// @notice Allow the `sequencerFeeVaultWithdrawalNetwork` config to be overridden in testing environments + function setSequencerFeeVaultWithdrawalNetwork(uint256 _sequencerFeeVaultWithdrawalNetwork) public { + sequencerFeeVaultWithdrawalNetwork = _sequencerFeeVaultWithdrawalNetwork; + } + function latestGenesisFork() internal view returns (Fork) { if (l2GenesisHoloceneTimeOffset == 0) { return Fork.HOLOCENE; diff --git a/packages/contracts-bedrock/test/setup/CommonTest.sol b/packages/contracts-bedrock/test/setup/CommonTest.sol index 5e48b01561795..fad13a485af28 100644 --- a/packages/contracts-bedrock/test/setup/CommonTest.sol +++ b/packages/contracts-bedrock/test/setup/CommonTest.sol @@ -72,6 +72,9 @@ contract CommonTest is Test, Setup, Events { } if (useCustomGasToken) { deploy.cfg().setIsCustomGasToken(true); + deploy.cfg().setBaseFeeVaultWithdrawalNetwork(1); + deploy.cfg().setL1FeeVaultWithdrawalNetwork(1); + deploy.cfg().setSequencerFeeVaultWithdrawalNetwork(1); } if (isForkTest()) { From 7501512c60a278f673f1e163d68ca13fcfc572ef Mon Sep 17 00:00:00 2001 From: niha <205694301+0xniha@users.noreply.github.com> Date: Mon, 1 Sep 2025 15:33:36 -0300 Subject: [PATCH 21/23] feat(cgt): add deauthorizeMinter to liquidity controller (#506) * feat: add deauthorizeMinter to liquidity controller * refactor: replace set minter to false for delete --- .../interfaces/L2/ILiquidityController.sol | 2 + .../snapshots/abi/LiquidityController.json | 26 +++++++++++ .../snapshots/semver-lock.json | 4 +- .../src/L2/LiquidityController.sol | 12 ++++++ .../test/L2/LiquidityController.t.sol | 43 +++++++++++++++++++ 5 files changed, 85 insertions(+), 2 deletions(-) diff --git a/packages/contracts-bedrock/interfaces/L2/ILiquidityController.sol b/packages/contracts-bedrock/interfaces/L2/ILiquidityController.sol index 4e06c94431eba..b20214702abb0 100644 --- a/packages/contracts-bedrock/interfaces/L2/ILiquidityController.sol +++ b/packages/contracts-bedrock/interfaces/L2/ILiquidityController.sol @@ -9,10 +9,12 @@ interface ILiquidityController is ISemver { event Initialized(uint8 version); event MinterAuthorized(address indexed minter); + event MinterDeauthorized(address indexed minter); event LiquidityMinted(address indexed minter, address indexed to, uint256 amount); event LiquidityBurned(address indexed minter, uint256 amount); function authorizeMinter(address _minter) external; + function deauthorizeMinter(address _minter) external; function mint(address _to, uint256 _amount) external; function burn() external payable; function minters(address) external view returns (bool); diff --git a/packages/contracts-bedrock/snapshots/abi/LiquidityController.json b/packages/contracts-bedrock/snapshots/abi/LiquidityController.json index 120c8f8bbac16..e44eeb5b0f2c0 100644 --- a/packages/contracts-bedrock/snapshots/abi/LiquidityController.json +++ b/packages/contracts-bedrock/snapshots/abi/LiquidityController.json @@ -24,6 +24,19 @@ "stateMutability": "payable", "type": "function" }, + { + "inputs": [ + { + "internalType": "address", + "name": "_minter", + "type": "address" + } + ], + "name": "deauthorizeMinter", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [], "name": "gasPayingTokenName", @@ -188,6 +201,19 @@ "name": "MinterAuthorized", "type": "event" }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "minter", + "type": "address" + } + ], + "name": "MinterDeauthorized", + "type": "event" + }, { "inputs": [], "name": "Unauthorized", diff --git a/packages/contracts-bedrock/snapshots/semver-lock.json b/packages/contracts-bedrock/snapshots/semver-lock.json index 141d6a65d2c9c..55d2e5ab1325c 100644 --- a/packages/contracts-bedrock/snapshots/semver-lock.json +++ b/packages/contracts-bedrock/snapshots/semver-lock.json @@ -92,8 +92,8 @@ "sourceCodeHash": "0xbea4229c5c6988243dbc7cf5a086ddd412fe1f2903b8e20d56699fec8de0c2c9" }, "src/L2/LiquidityController.sol:LiquidityController": { - "initCodeHash": "0x1692f8b5a14c14d579afc56964f9e2b48f6b32b2443ac0ded9d5c050866664de", - "sourceCodeHash": "0x753c1d9c2462393595f57c9a765440f649753e6ab74bf297f5d5d15541304e28" + "initCodeHash": "0x8a21d16da3377aa61375755d37010c0c37c99817fdce9d16f78f0ab9bc3a84c5", + "sourceCodeHash": "0xb4fca8d4532e88e0dd53d3643aa4022953e37c745753009b3c8f9bcca5806f57" }, "src/L2/NativeAssetLiquidity.sol:NativeAssetLiquidity": { "initCodeHash": "0x2c50c7cac8eab6867ffb969a65a8aa3026d415f2e9464726683ff6cd5da0b8f3", diff --git a/packages/contracts-bedrock/src/L2/LiquidityController.sol b/packages/contracts-bedrock/src/L2/LiquidityController.sol index 8003e13d912f9..fa92db0f984b6 100644 --- a/packages/contracts-bedrock/src/L2/LiquidityController.sol +++ b/packages/contracts-bedrock/src/L2/LiquidityController.sol @@ -24,6 +24,10 @@ contract LiquidityController is ISemver, Initializable { /// @param minter The address that was authorized event MinterAuthorized(address indexed minter); + /// @notice Emitted when an address is deauthorized to mint/burn liquidity + /// @param minter The address that was deauthorized + event MinterDeauthorized(address indexed minter); + /// @notice Emitted when liquidity is minted /// @param minter The address that minted the liquidity /// @param to The address that received the minted liquidity @@ -68,6 +72,14 @@ contract LiquidityController is ISemver, Initializable { emit MinterAuthorized(_minter); } + /// @notice Deauthorizes an address from performing liquidity control operations + /// @param _minter The address to deauthorize as a minter + function deauthorizeMinter(address _minter) external { + if (msg.sender != IProxyAdmin(Predeploys.PROXY_ADMIN).owner()) revert Unauthorized(); + delete minters[_minter]; + emit MinterDeauthorized(_minter); + } + /// @notice Mints native asset liquidity and sends it to a specified address /// @param _to The address to receive the minted native asset /// @param _amount The amount of native asset to mint and send diff --git a/packages/contracts-bedrock/test/L2/LiquidityController.t.sol b/packages/contracts-bedrock/test/L2/LiquidityController.t.sol index 000973ec9f5b2..730d471c5022e 100644 --- a/packages/contracts-bedrock/test/L2/LiquidityController.t.sol +++ b/packages/contracts-bedrock/test/L2/LiquidityController.t.sol @@ -25,6 +25,9 @@ contract LiquidityController_TestInit is CommonTest { /// @notice Emitted when an address deposits native asset liquidity. event LiquidityDeposited(address indexed caller, uint256 value); + /// @notice Emitted when an address is deauthorized to mint/burn liquidity + event MinterDeauthorized(address indexed minter); + /// @notice Emitted when an address is authorized to mint/burn liquidity event MinterAuthorized(address indexed minter); @@ -86,6 +89,46 @@ contract LiquidityController_AuthorizeMinter_Test is LiquidityController_TestIni } } +/// @title LiquidityController_DeauthorizeMinter_Test +/// @notice Tests the `deauthorizeMinter` function of the `LiquidityController` contract. +contract LiquidityController_DeauthorizeMinter_Test is LiquidityController_TestInit { + using stdStorage for StdStorage; + + /// @notice Tests that the deauthorizeMinter function can be called by the owner. + function testFuzz_deauthorizeMinter_fromOwner_succeeds(address _minter) public { + // Set minter to authorized + stdstore.target(address(liquidityController)).sig(liquidityController.minters.selector).with_key(_minter) + .checked_write(true); + + // Expect emit MinterDeauthorized event + vm.expectEmit(address(liquidityController)); + emit MinterDeauthorized(_minter); + // Call the deauthorizeMinter function with owner as the caller + vm.prank(IProxyAdmin(Predeploys.PROXY_ADMIN).owner()); + liquidityController.deauthorizeMinter(_minter); + + // Assert minter is deauthorized + assertFalse(liquidityController.minters(_minter)); + } + + /// @notice Tests that the deauthorizeMinter function reverts when called by non-owner. + function testFuzz_deauthorizeMinter_fromNonOwner_fails(address _caller, address _minter) public { + vm.assume(_caller != IProxyAdmin(Predeploys.PROXY_ADMIN).owner()); + + // Set minter to authorized + stdstore.target(address(liquidityController)).sig(liquidityController.minters.selector).with_key(_minter) + .checked_write(true); + + // Call the deauthorizeMinter function with non-owner as the caller + vm.prank(_caller); + vm.expectRevert(Unauthorized.selector); + liquidityController.deauthorizeMinter(_minter); + + // Assert minter is still authorized + assertTrue(liquidityController.minters(_minter)); + } +} + /// @title LiquidityController_Mint_Test /// @notice Tests the `mint` function of the `LiquidityController` contract. contract LiquidityController_Mint_Test is LiquidityController_TestInit { From af448792b08ae97cb3c4f542d8a297133f398fc8 Mon Sep 17 00:00:00 2001 From: niha <205694301+0xniha@users.noreply.github.com> Date: Tue, 2 Sep 2025 15:10:47 -0300 Subject: [PATCH 22/23] chore(cgt): deployer scripts (#508) * feat: add cgt config in op-deployer * feat: add cgt config in op-chain-ops * feat: add enable cgt in op-up & sysgo deployer * test: add isCustomGasToken tests for DeployOPChain * feat: add cgt integration script * chore(cgt): make l1 rpc ip static * feat: add cgt config to chain intent and fix deployer * test: add cgt tests in op deployer * test: add cgt test in intent builder * refactor: intent test * feat: add cgt standard values * fix: l2genesis override * refactor: remove config log and change builder test * feat: add cgt check in checkSystemConfig * fix: deploy opchain tests * chore: remove cgt demo config --------- Co-authored-by: hexshire --- op-chain-ops/genesis/config.go | 11 ++++- op-chain-ops/interopgen/deploy.go | 4 ++ op-chain-ops/interopgen/recipe.go | 4 +- .../deployer/integration_test/apply_test.go | 5 +++ op-deployer/pkg/deployer/opcm/l2genesis.go | 3 ++ op-deployer/pkg/deployer/opcm/opchain.go | 1 + .../pkg/deployer/pipeline/l2genesis.go | 9 ++++ .../pkg/deployer/pipeline/l2genesis_test.go | 15 +++++++ op-deployer/pkg/deployer/pipeline/opchain.go | 1 + op-deployer/pkg/deployer/standard/standard.go | 3 ++ .../pkg/deployer/state/chain_intent.go | 19 +++++++- .../pkg/deployer/state/deploy_config.go | 6 +++ .../pkg/deployer/state/deploy_config_test.go | 5 +++ op-deployer/pkg/deployer/state/intent.go | 13 ++++++ op-deployer/pkg/deployer/state/intent_test.go | 45 +++++++++++++++++++ op-devstack/sysgo/deployer.go | 8 ++++ op-e2e/config/init.go | 5 +++ op-e2e/e2eutils/intentbuilder/builder.go | 9 ++++ op-e2e/e2eutils/intentbuilder/builder_test.go | 6 +++ .../scripts/deploy/ChainAssertions.sol | 2 + .../test/opcm/DeployOPChain.t.sol | 21 +++++++++ 21 files changed, 191 insertions(+), 4 deletions(-) diff --git a/op-chain-ops/genesis/config.go b/op-chain-ops/genesis/config.go index c8fe6d50792e7..151849df2dccb 100644 --- a/op-chain-ops/genesis/config.go +++ b/op-chain-ops/genesis/config.go @@ -275,13 +275,22 @@ func (d *GasPriceOracleDeployConfig) OperatorFeeParams() [32]byte { type GasTokenDeployConfig struct { // IsCustomGasToken is a flag to indicate that a custom gas token should be used IsCustomGasToken bool `json:"isCustomGasToken"` + // GasPayingTokenName represents the custom gas token name. + GasPayingTokenName string `json:"gasPayingTokenName"` + // GasPayingTokenSymbol represents the custom gas token symbol. + GasPayingTokenSymbol string `json:"gasPayingTokenSymbol"` } var _ ConfigChecker = (*GasTokenDeployConfig)(nil) func (d *GasTokenDeployConfig) Check(log log.Logger) error { if d.IsCustomGasToken { - log.Info("Using custom gas token") + if d.GasPayingTokenName == "" { + return fmt.Errorf("%w: GasPayingTokenName cannot be empty", ErrInvalidDeployConfig) + } + if d.GasPayingTokenSymbol == "" { + return fmt.Errorf("%w: GasPayingTokenSymbol cannot be empty", ErrInvalidDeployConfig) + } } return nil } diff --git a/op-chain-ops/interopgen/deploy.go b/op-chain-ops/interopgen/deploy.go index b5c1aab406872..6028da8c89d54 100644 --- a/op-chain-ops/interopgen/deploy.go +++ b/op-chain-ops/interopgen/deploy.go @@ -242,6 +242,7 @@ func DeployL2ToL1(l1Host *script.Host, superCfg *SuperchainConfig, superDeployme AllowCustomDisputeParameters: true, OperatorFeeScalar: cfg.GasPriceOracleOperatorFeeScalar, OperatorFeeConstant: cfg.GasPriceOracleOperatorFeeConstant, + IsCustomGasToken: cfg.IsCustomGasToken, }) if err != nil { return nil, fmt.Errorf("failed to deploy L2 OP chain: %w", err) @@ -324,6 +325,9 @@ func GenesisL2(l2Host *script.Host, cfg *L2Config, deployment *L2Deployment, mul DeployCrossL2Inbox: multichainDepSet, EnableGovernance: cfg.EnableGovernance, FundDevAccounts: cfg.FundDevAccounts, + IsCustomGasToken: cfg.IsCustomGasToken, + GasPayingTokenName: cfg.GasPayingTokenName, + GasPayingTokenSymbol: cfg.GasPayingTokenSymbol, }); err != nil { return fmt.Errorf("failed L2 genesis: %w", err) } diff --git a/op-chain-ops/interopgen/recipe.go b/op-chain-ops/interopgen/recipe.go index 03e1927c95b5c..87bf2921181a1 100644 --- a/op-chain-ops/interopgen/recipe.go +++ b/op-chain-ops/interopgen/recipe.go @@ -243,7 +243,9 @@ func (r *InteropDevL2Recipe) build(l1ChainID uint64, addrs devkeys.Addresses) (* GasPriceOracleBlobBaseFeeScalar: 810949, }, GasTokenDeployConfig: genesis.GasTokenDeployConfig{ - IsCustomGasToken: false, + IsCustomGasToken: false, + GasPayingTokenName: "Custom Gas Token", + GasPayingTokenSymbol: "CGT", }, OperatorDeployConfig: genesis.OperatorDeployConfig{ P2PSequencerAddress: sequencerP2P, diff --git a/op-deployer/pkg/deployer/integration_test/apply_test.go b/op-deployer/pkg/deployer/integration_test/apply_test.go index 39d29ed6d8865..1c8c1d6c8fc2c 100644 --- a/op-deployer/pkg/deployer/integration_test/apply_test.go +++ b/op-deployer/pkg/deployer/integration_test/apply_test.go @@ -713,6 +713,11 @@ func newChainIntent(t *testing.T, dk *devkeys.MnemonicDevKeys, l1ChainID *big.In Proposer: addrFor(t, dk, devkeys.ProposerRole.Key(l1ChainID)), Challenger: addrFor(t, dk, devkeys.ChallengerRole.Key(l1ChainID)), }, + CustomGasToken: &state.CustomGasToken{ + Enabled: standard.CustomGasTokenEnabled, + Name: standard.CustomGasTokenName, + Symbol: standard.CustomGasTokenSymbol, + }, } } diff --git a/op-deployer/pkg/deployer/opcm/l2genesis.go b/op-deployer/pkg/deployer/opcm/l2genesis.go index 3f91196cc85fc..ff52f208b504d 100644 --- a/op-deployer/pkg/deployer/opcm/l2genesis.go +++ b/op-deployer/pkg/deployer/opcm/l2genesis.go @@ -28,6 +28,9 @@ type L2GenesisInput struct { DeployCrossL2Inbox bool EnableGovernance bool FundDevAccounts bool + IsCustomGasToken bool + GasPayingTokenName string + GasPayingTokenSymbol string } type L2GenesisScript script.DeployScriptWithoutOutput[L2GenesisInput] diff --git a/op-deployer/pkg/deployer/opcm/opchain.go b/op-deployer/pkg/deployer/opcm/opchain.go index 9d87fb4979a4e..af40a7ea71623 100644 --- a/op-deployer/pkg/deployer/opcm/opchain.go +++ b/op-deployer/pkg/deployer/opcm/opchain.go @@ -40,6 +40,7 @@ type DeployOPChainInput struct { DisputeClockExtension uint64 DisputeMaxClockDuration uint64 AllowCustomDisputeParameters bool + IsCustomGasToken bool OperatorFeeScalar uint32 OperatorFeeConstant uint64 diff --git a/op-deployer/pkg/deployer/pipeline/l2genesis.go b/op-deployer/pkg/deployer/pipeline/l2genesis.go index bb3f514358bce..566b6532451f5 100644 --- a/op-deployer/pkg/deployer/pipeline/l2genesis.go +++ b/op-deployer/pkg/deployer/pipeline/l2genesis.go @@ -22,6 +22,9 @@ import ( ) type l2GenesisOverrides struct { + IsCustomGasToken bool `json:"isCustomGasToken"` + GasPayingTokenName string `json:"gasPayingTokenName"` + GasPayingTokenSymbol string `json:"gasPayingTokenSymbol"` FundDevAccounts bool `json:"fundDevAccounts"` BaseFeeVaultMinimumWithdrawalAmount *hexutil.Big `json:"baseFeeVaultMinimumWithdrawalAmount"` L1FeeVaultMinimumWithdrawalAmount *hexutil.Big `json:"l1FeeVaultMinimumWithdrawalAmount"` @@ -94,6 +97,9 @@ func GenerateL2Genesis(pEnv *Env, intent *state.Intent, bundle ArtifactsBundle, DeployCrossL2Inbox: len(intent.Chains) > 1, EnableGovernance: overrides.EnableGovernance, FundDevAccounts: overrides.FundDevAccounts, + IsCustomGasToken: thisIntent.CustomGasToken.Enabled, + GasPayingTokenName: thisIntent.CustomGasToken.Name, + GasPayingTokenSymbol: thisIntent.CustomGasToken.Symbol, }); err != nil { return fmt.Errorf("failed to call L2Genesis script: %w", err) } @@ -156,6 +162,9 @@ func wdNetworkToBig(wd genesis.WithdrawalNetwork) *big.Int { func defaultOverrides() l2GenesisOverrides { return l2GenesisOverrides{ + IsCustomGasToken: false, + GasPayingTokenName: "Custom Gas Token", + GasPayingTokenSymbol: "CGT", FundDevAccounts: false, BaseFeeVaultMinimumWithdrawalAmount: standard.VaultMinWithdrawalAmount, L1FeeVaultMinimumWithdrawalAmount: standard.VaultMinWithdrawalAmount, diff --git a/op-deployer/pkg/deployer/pipeline/l2genesis_test.go b/op-deployer/pkg/deployer/pipeline/l2genesis_test.go index aed2a1e782390..19493e51cba5b 100644 --- a/op-deployer/pkg/deployer/pipeline/l2genesis_test.go +++ b/op-deployer/pkg/deployer/pipeline/l2genesis_test.go @@ -53,6 +53,9 @@ func TestCalculateL2GenesisOverrides(t *testing.T) { SequencerFeeVaultWithdrawalNetwork: "local", EnableGovernance: false, GovernanceTokenOwner: standard.GovernanceTokenOwner, + IsCustomGasToken: false, + GasPayingTokenName: "Custom Gas Token", + GasPayingTokenSymbol: "CGT", }, expectedSchedule: func() *genesis.UpgradeScheduleDeployConfig { return standard.DefaultHardforkScheduleForTag("") @@ -73,6 +76,9 @@ func TestCalculateL2GenesisOverrides(t *testing.T) { "enableGovernance": true, "governanceTokenOwner": "0x1111111111111111111111111111111111111111", "l2GenesisInteropTimeOffset": "0x1234", + "isCustomGasToken": false, + "gasPayingTokenName": "Custom Gas Token", + "gasPayingTokenSymbol": "CGT", }, }, chainIntent: &state.ChainIntent{}, @@ -87,6 +93,9 @@ func TestCalculateL2GenesisOverrides(t *testing.T) { SequencerFeeVaultWithdrawalNetwork: "remote", EnableGovernance: true, GovernanceTokenOwner: common.HexToAddress("0x1111111111111111111111111111111111111111"), + IsCustomGasToken: false, + GasPayingTokenName: "Custom Gas Token", + GasPayingTokenSymbol: "CGT", }, expectedSchedule: func() *genesis.UpgradeScheduleDeployConfig { sched := standard.DefaultHardforkScheduleForTag("") @@ -114,6 +123,9 @@ func TestCalculateL2GenesisOverrides(t *testing.T) { "enableGovernance": true, "governanceTokenOwner": "0x1111111111111111111111111111111111111111", "l2GenesisInteropTimeOffset": "0x1234", + "isCustomGasToken": false, + "gasPayingTokenName": "Custom Gas Token", + "gasPayingTokenSymbol": "CGT", }, }, expectError: false, @@ -127,6 +139,9 @@ func TestCalculateL2GenesisOverrides(t *testing.T) { SequencerFeeVaultWithdrawalNetwork: "remote", EnableGovernance: true, GovernanceTokenOwner: common.HexToAddress("0x1111111111111111111111111111111111111111"), + IsCustomGasToken: false, + GasPayingTokenName: "Custom Gas Token", + GasPayingTokenSymbol: "CGT", }, expectedSchedule: func() *genesis.UpgradeScheduleDeployConfig { sched := standard.DefaultHardforkScheduleForTag("") diff --git a/op-deployer/pkg/deployer/pipeline/opchain.go b/op-deployer/pkg/deployer/pipeline/opchain.go index f44721dfd5711..e9657a7a3d7d6 100644 --- a/op-deployer/pkg/deployer/pipeline/opchain.go +++ b/op-deployer/pkg/deployer/pipeline/opchain.go @@ -109,6 +109,7 @@ func makeDCI(intent *state.Intent, thisIntent *state.ChainIntent, chainID common DisputeClockExtension: proofParams.DisputeClockExtension, // 3 hours (input in seconds) DisputeMaxClockDuration: proofParams.DisputeMaxClockDuration, // 3.5 days (input in seconds) AllowCustomDisputeParameters: proofParams.DangerouslyAllowCustomDisputeParameters, + IsCustomGasToken: thisIntent.CustomGasToken.Enabled, OperatorFeeScalar: thisIntent.OperatorFeeScalar, OperatorFeeConstant: thisIntent.OperatorFeeConstant, }, nil diff --git a/op-deployer/pkg/deployer/standard/standard.go b/op-deployer/pkg/deployer/standard/standard.go index 4b3379979593b..7306a7cdd9183 100644 --- a/op-deployer/pkg/deployer/standard/standard.go +++ b/op-deployer/pkg/deployer/standard/standard.go @@ -34,6 +34,9 @@ const ( Eip1559DenominatorCanyon uint64 = 250 Eip1559Denominator uint64 = 50 Eip1559Elasticity uint64 = 6 + CustomGasTokenEnabled bool = false + CustomGasTokenName string = "" + CustomGasTokenSymbol string = "" ContractsV160Tag = "op-contracts/v1.6.0" ContractsV180Tag = "op-contracts/v1.8.0-rc.4" diff --git a/op-deployer/pkg/deployer/state/chain_intent.go b/op-deployer/pkg/deployer/state/chain_intent.go index 3ad1a3dea3b00..c69a11a532718 100644 --- a/op-deployer/pkg/deployer/state/chain_intent.go +++ b/op-deployer/pkg/deployer/state/chain_intent.go @@ -56,6 +56,12 @@ type L2DevGenesisParams struct { Prefund map[common.Address]*hexutil.U256 `json:"prefund" toml:"prefund"` } +type CustomGasToken struct { + Enabled bool `json:"enabled" toml:"enabled"` + Name string `json:"name" toml:"name"` + Symbol string `json:"symbol" toml:"symbol"` +} + type ChainIntent struct { ID common.Hash `json:"id" toml:"id"` BaseFeeVaultRecipient common.Address `json:"baseFeeVaultRecipient" toml:"baseFeeVaultRecipient"` @@ -70,8 +76,8 @@ type ChainIntent struct { AdditionalDisputeGames []AdditionalDisputeGame `json:"dangerousAdditionalDisputeGames" toml:"dangerousAdditionalDisputeGames,omitempty"` OperatorFeeScalar uint32 `json:"operatorFeeScalar,omitempty" toml:"operatorFeeScalar,omitempty"` OperatorFeeConstant uint64 `json:"operatorFeeConstant,omitempty" toml:"operatorFeeConstant,omitempty"` - L1StartBlockHash *common.Hash `json:"l1StartBlockHash,omitempty" toml:"l1StartBlockHash,omitempty"` - + L1StartBlockHash *common.Hash `json:"l1StartBlockHash,omitempty" toml:"l1StartBlockHash,omitempty"` + CustomGasToken *CustomGasToken `json:"customGasToken" toml:"customGasToken"` // Optional. For development purposes only. Only enabled if the operation mode targets a genesis-file output. L2DevGenesisParams *L2DevGenesisParams `json:"l2DevGenesisParams,omitempty" toml:"l2DevGenesisParams,omitempty"` } @@ -111,6 +117,15 @@ func (c *ChainIntent) Check() error { return fmt.Errorf("%w: chainId=%s", ErrFeeVaultZeroAddress, c.ID) } + if c.CustomGasToken != nil && c.CustomGasToken.Enabled { + if c.CustomGasToken.Name == "" { + return fmt.Errorf("%w: CustomGasToken.Name cannot be empty when enabled, chainId=%s", ErrIncompatibleValue, c.ID) + } + if c.CustomGasToken.Symbol == "" { + return fmt.Errorf("%w: CustomGasToken.Symbol cannot be empty when enabled, chainId=%s", ErrIncompatibleValue, c.ID) + } + } + if c.DangerousAltDAConfig.UseAltDA { return c.DangerousAltDAConfig.Check(nil) } diff --git a/op-deployer/pkg/deployer/state/deploy_config.go b/op-deployer/pkg/deployer/state/deploy_config.go index 05123b143e323..943d9df8c9e0e 100644 --- a/op-deployer/pkg/deployer/state/deploy_config.go +++ b/op-deployer/pkg/deployer/state/deploy_config.go @@ -71,6 +71,12 @@ func CombineDeployConfig(intent *Intent, chainIntent *ChainIntent, state *State, EIP1559Elasticity: chainIntent.Eip1559Elasticity, }, + GasTokenDeployConfig: genesis.GasTokenDeployConfig{ + IsCustomGasToken: chainIntent.CustomGasToken.Enabled, + GasPayingTokenName: chainIntent.CustomGasToken.Name, + GasPayingTokenSymbol: chainIntent.CustomGasToken.Symbol, + }, + // STOP! This struct sets the _default_ upgrade schedule for all chains. // Any upgrades you enable here will be enabled for all new deployments. // In-development hardforks should never be activated here. Instead, they diff --git a/op-deployer/pkg/deployer/state/deploy_config_test.go b/op-deployer/pkg/deployer/state/deploy_config_test.go index 53d33d5874f2b..d8b7247df252c 100644 --- a/op-deployer/pkg/deployer/state/deploy_config_test.go +++ b/op-deployer/pkg/deployer/state/deploy_config_test.go @@ -31,6 +31,11 @@ func TestCombineDeployConfig(t *testing.T) { UnsafeBlockSigner: common.HexToAddress("0xabc"), Batcher: common.HexToAddress("0xdef"), }, + CustomGasToken: &CustomGasToken{ + Enabled: false, + Name: "Test", + Symbol: "TEST", + }, } state := State{ SuperchainDeployment: &addresses.SuperchainContracts{ProtocolVersionsProxy: common.HexToAddress("0x123")}, diff --git a/op-deployer/pkg/deployer/state/intent.go b/op-deployer/pkg/deployer/state/intent.go index 410f0bd4369db..70fe84abd78c4 100644 --- a/op-deployer/pkg/deployer/state/intent.go +++ b/op-deployer/pkg/deployer/state/intent.go @@ -154,6 +154,9 @@ func (c *Intent) validateStandardValues() error { if len(chain.AdditionalDisputeGames) > 0 { return fmt.Errorf("%w: chainId=%s additionalDisputeGames must be nil", ErrNonStandardValue, chain.ID) } + if chain.CustomGasToken != nil && (chain.CustomGasToken.Enabled != standard.CustomGasTokenEnabled) { + return fmt.Errorf("%w: chainId=%s custom gas token not allowed in standard configuration", ErrNonStandardValue, chain.ID) + } } challenger, _ := standard.ChallengerAddressFor(c.L1ChainID) @@ -294,6 +297,11 @@ func NewIntentCustom(l1ChainId uint64, l2ChainIds []common.Hash) (Intent, error) for _, l2ChainID := range l2ChainIds { intent.Chains = append(intent.Chains, &ChainIntent{ ID: l2ChainID, + CustomGasToken: &CustomGasToken{ + Enabled: standard.CustomGasTokenEnabled, + Name: standard.CustomGasTokenName, + Symbol: standard.CustomGasTokenSymbol, + }, }) } return intent, nil @@ -337,6 +345,11 @@ func NewIntentStandard(l1ChainId uint64, l2ChainIds []common.Hash) (Intent, erro L1ProxyAdminOwner: l1ProxyAdminOwner, L2ProxyAdminOwner: l2ProxyAdminOwner, }, + CustomGasToken: &CustomGasToken{ + Enabled: standard.CustomGasTokenEnabled, + Name: standard.CustomGasTokenName, + Symbol: standard.CustomGasTokenSymbol, + }, }) } return intent, nil diff --git a/op-deployer/pkg/deployer/state/intent_test.go b/op-deployer/pkg/deployer/state/intent_test.go index 8ffb0060db3c5..ca0ec71735fa2 100644 --- a/op-deployer/pkg/deployer/state/intent_test.go +++ b/op-deployer/pkg/deployer/state/intent_test.go @@ -87,6 +87,17 @@ func TestValidateStandardValues(t *testing.T) { }, ErrIncompatibleValue, }, + { + "CustomGasToken", + func(intent *Intent) { + intent.Chains[0].CustomGasToken = &CustomGasToken{ + Enabled: true, + Name: "Custom Gas Token", + Symbol: "CGT", + } + }, + ErrNonStandardValue, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -131,6 +142,10 @@ func TestValidateCustomValues(t *testing.T) { err = intent.Check() require.NoError(t, err) + setCustomGasToken(&intent) + err = intent.Check() + require.NoError(t, err) + tests := []struct { name string mutator func(intent *Intent) @@ -155,6 +170,28 @@ func TestValidateCustomValues(t *testing.T) { }, ErrIncompatibleValue, }, + { + "empty custom gas token name when enabled", + func(intent *Intent) { + intent.Chains[0].CustomGasToken = &CustomGasToken{ + Enabled: true, + Name: "", + Symbol: "CGT", + } + }, + ErrIncompatibleValue, + }, + { + "empty custom gas token symbol when enabled", + func(intent *Intent) { + intent.Chains[0].CustomGasToken = &CustomGasToken{ + Enabled: true, + Name: "Custom Gas Token", + Symbol: "", + } + }, + ErrIncompatibleValue, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -211,3 +248,11 @@ func setFeeAddresses(intent *Intent) { intent.Chains[0].L1FeeVaultRecipient = common.HexToAddress("0x09") intent.Chains[0].SequencerFeeVaultRecipient = common.HexToAddress("0x0A") } + +func setCustomGasToken(intent *Intent) { + intent.Chains[0].CustomGasToken = &CustomGasToken{ + Enabled: true, + Name: "Custom Gas Token", + Symbol: "CGT", + } +} diff --git a/op-devstack/sysgo/deployer.go b/op-devstack/sysgo/deployer.go index f01bb2240ed2d..aca99b2e81c1b 100644 --- a/op-devstack/sysgo/deployer.go +++ b/op-devstack/sysgo/deployer.go @@ -304,6 +304,14 @@ func WithDisputeGameFinalityDelaySeconds(seconds uint64) DeployerOption { } } +func WithCustomGasToken(enabled bool, name, symbol string) DeployerOption { + return func(p devtest.P, keys devkeys.Keys, builder intentbuilder.Builder) { + for _, l2Cfg := range builder.L2s() { + l2Cfg.WithCustomGasToken(enabled, name, symbol) + } + } +} + func (wb *worldBuilder) buildL1Genesis() { wb.require.NotNil(wb.output.L1DevGenesis, "must have L1 genesis outer config") wb.require.NotNil(wb.output.L1StateDump, "must have L1 genesis alloc") diff --git a/op-e2e/config/init.go b/op-e2e/config/init.go index 04aa0fd620925..962a24f389a13 100644 --- a/op-e2e/config/init.go +++ b/op-e2e/config/init.go @@ -396,6 +396,11 @@ func defaultIntent(root string, loc *artifacts.Locator, deployer common.Address, Proposer: addrs.Proposer, Challenger: common.HexToAddress("0x15d34AAf54267DB7D7c367839AAf71A00a2C6A65"), }, + CustomGasToken: &state.CustomGasToken{ + Enabled: false, + Name: "", + Symbol: "", + }, AdditionalDisputeGames: []state.AdditionalDisputeGame{ { ChainProofParams: state.ChainProofParams{ diff --git a/op-e2e/e2eutils/intentbuilder/builder.go b/op-e2e/e2eutils/intentbuilder/builder.go index 88482bbc11ad0..8b8598b9ed773 100644 --- a/op-e2e/e2eutils/intentbuilder/builder.go +++ b/op-e2e/e2eutils/intentbuilder/builder.go @@ -48,6 +48,7 @@ type L2Configurator interface { WithL1StartBlockHash(hash common.Hash) WithAdditionalDisputeGames(games []state.AdditionalDisputeGame) WithFinalizationPeriodSeconds(value uint64) + WithCustomGasToken(enabled bool, name, symbol string) ContractsConfigurator L2VaultsConfigurator L2RolesConfigurator @@ -386,6 +387,14 @@ func (c *l2Configurator) WithEIP1559Denominator(value uint64) { c.builder.intent.Chains[c.chainIndex].Eip1559Denominator = value } +func (c *l2Configurator) WithCustomGasToken(enabled bool, name, symbol string) { + c.builder.intent.Chains[c.chainIndex].CustomGasToken = &state.CustomGasToken{ + Enabled: enabled, + Name: name, + Symbol: symbol, + } +} + func (c *l2Configurator) WithEIP1559Elasticity(value uint64) { c.builder.intent.Chains[c.chainIndex].Eip1559Elasticity = value } diff --git a/op-e2e/e2eutils/intentbuilder/builder_test.go b/op-e2e/e2eutils/intentbuilder/builder_test.go index eeffb15a14d9e..39abaf38d6098 100644 --- a/op-e2e/e2eutils/intentbuilder/builder_test.go +++ b/op-e2e/e2eutils/intentbuilder/builder_test.go @@ -68,6 +68,7 @@ func TestBuilder(t *testing.T) { require.Equal(t, eth.ChainIDFromUInt64(420), l2Config.ChainID()) l2Config.WithBlockTime(2) l2Config.WithL1StartBlockHash(common.HexToHash("0x5678")) + l2Config.WithCustomGasToken(false, "Custom Gas Token", "CGT") // Test ContractsConfigurator methods l2Config.WithL1ContractsLocator("http://l1.example.com") @@ -159,6 +160,11 @@ func TestBuilder(t *testing.T) { Eip1559Elasticity: 10, OperatorFeeScalar: 100, OperatorFeeConstant: 200, + CustomGasToken: &state.CustomGasToken{ + Enabled: false, + Name: "Custom Gas Token", + Symbol: "CGT", + }, DeployOverrides: map[string]any{ "l2BlockTime": uint64(2), "l2GenesisRegolithTimeOffset": hexutil.Uint64(0), diff --git a/packages/contracts-bedrock/scripts/deploy/ChainAssertions.sol b/packages/contracts-bedrock/scripts/deploy/ChainAssertions.sol index 8c693f0ca8871..c5f5fa5e1277b 100644 --- a/packages/contracts-bedrock/scripts/deploy/ChainAssertions.sol +++ b/packages/contracts-bedrock/scripts/deploy/ChainAssertions.sol @@ -91,6 +91,8 @@ library ChainAssertions { require(config.l1StandardBridge() == _contracts.L1StandardBridge, "CHECK-SCFG-180"); require(config.optimismPortal() == _contracts.OptimismPortal, "CHECK-SCFG-200"); require(config.optimismMintableERC20Factory() == _contracts.OptimismMintableERC20Factory, "CHECK-SCFG-210"); + // Check custom gas token + require(config.isCustomGasToken() == _doi.isCustomGasToken(), "CHECK-SCFG-220"); } else { require(config.owner() == address(0), "CHECK-SCFG-220"); require(config.overhead() == 0, "CHECK-SCFG-230"); diff --git a/packages/contracts-bedrock/test/opcm/DeployOPChain.t.sol b/packages/contracts-bedrock/test/opcm/DeployOPChain.t.sol index faa3f466f4085..cc82f7518d6a5 100644 --- a/packages/contracts-bedrock/test/opcm/DeployOPChain.t.sol +++ b/packages/contracts-bedrock/test/opcm/DeployOPChain.t.sol @@ -56,6 +56,7 @@ contract DeployOPChainInput_Test is Test { doi.set(doi.blobBaseFeeScalar.selector, blobBaseFeeScalar); doi.set(doi.l2ChainId.selector, l2ChainId); doi.set(doi.allowCustomDisputeParameters.selector, true); + doi.set(doi.isCustomGasToken.selector, false); doi.set(doi.opcm.selector, opcm); vm.etch(opcm, hex"01"); @@ -71,6 +72,7 @@ contract DeployOPChainInput_Test is Test { assertEq(l2ChainId, doi.l2ChainId(), "1000"); assertEq(opcm, address(doi.opcm()), "1100"); assertEq(true, doi.allowCustomDisputeParameters(), "1200"); + assertEq(false, doi.isCustomGasToken(), "1300"); } function test_getters_whenNotSet_reverts() public { @@ -328,6 +330,7 @@ contract DeployOPChain_TestBase is Test { IOPContractsManager opcm = IOPContractsManager(address(0)); string saltMixer = "defaultSaltMixer"; uint64 gasLimit = 60_000_000; + bool isCustomGasToken = false; // Configurable dispute game parameters. uint32 disputeGameType = GameType.unwrap(GameTypes.PERMISSIONED_CANNON); bytes32 disputeAbsolutePrestate = hex"038512e02c4c3f7bdaec27d00edf55b7155e0905301e1a88083e4e0a6764d54c"; @@ -400,6 +403,7 @@ contract DeployOPChain_Test is DeployOPChain_TestBase { basefeeScalar = uint32(uint256(hash(_seed, 6))); blobBaseFeeScalar = uint32(uint256(hash(_seed, 7))); l2ChainId = uint256(hash(_seed, 8)); + isCustomGasToken = bool(uint256(hash(_seed, 9)) % 2 == 0); doi.set(doi.opChainProxyAdminOwner.selector, opChainProxyAdminOwner); doi.set(doi.systemConfigOwner.selector, systemConfigOwner); @@ -419,6 +423,7 @@ contract DeployOPChain_Test is DeployOPChain_TestBase { doi.set(doi.disputeSplitDepth.selector, disputeSplitDepth); doi.set(doi.disputeClockExtension.selector, disputeClockExtension); doi.set(doi.disputeMaxClockDuration.selector, disputeMaxClockDuration); + doi.set(doi.isCustomGasToken.selector, isCustomGasToken); deployOPChain.run(doi, doo); @@ -442,6 +447,7 @@ contract DeployOPChain_Test is DeployOPChain_TestBase { assertEq(disputeSplitDepth, doi.disputeSplitDepth(), "1500"); assertEq(disputeClockExtension, Duration.unwrap(doi.disputeClockExtension()), "1600"); assertEq(disputeMaxClockDuration, Duration.unwrap(doi.disputeMaxClockDuration()), "1700"); + assertEq(isCustomGasToken, doi.isCustomGasToken(), "1800"); // Assert inputs were properly passed through to the contract initializers. assertEq(address(doo.opChainProxyAdmin().owner()), opChainProxyAdminOwner, "2100"); @@ -486,6 +492,20 @@ contract DeployOPChain_Test is DeployOPChain_TestBase { assertEq(doo.permissionedDisputeGame().splitDepth(), disputeSplitDepth + 1); } + function test_isCustomGasToken_whenTrue_succeeds() public { + setDOI(); + doi.set(doi.isCustomGasToken.selector, true); + deployOPChain.run(doi, doo); + assertEq(doi.isCustomGasToken(), true, "CGT-300"); + } + + function test_isCustomGasToken_whenFalse_succeeds() public { + setDOI(); + doi.set(doi.isCustomGasToken.selector, false); + deployOPChain.run(doi, doo); + assertEq(doi.isCustomGasToken(), false, "CGT-400"); + } + function setDOI() internal { doi.set(doi.opChainProxyAdminOwner.selector, opChainProxyAdminOwner); doi.set(doi.systemConfigOwner.selector, systemConfigOwner); @@ -505,5 +525,6 @@ contract DeployOPChain_Test is DeployOPChain_TestBase { doi.set(doi.disputeSplitDepth.selector, disputeSplitDepth); doi.set(doi.disputeClockExtension.selector, disputeClockExtension); doi.set(doi.disputeMaxClockDuration.selector, disputeMaxClockDuration); + doi.set(doi.isCustomGasToken.selector, false); } } From 53e37472bc7456e805493159da264c6cac3349da Mon Sep 17 00:00:00 2001 From: Hex <165055168+hexshire@users.noreply.github.com> Date: Tue, 2 Sep 2025 16:13:43 -0300 Subject: [PATCH 23/23] chore: update comment Co-authored-by: AgusDuha <81362284+agusduha@users.noreply.github.com> Signed-off-by: Hex <165055168+hexshire@users.noreply.github.com> --- packages/contracts-bedrock/scripts/L2Genesis.s.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/contracts-bedrock/scripts/L2Genesis.s.sol b/packages/contracts-bedrock/scripts/L2Genesis.s.sol index 5d4cf2556e9cc..0da11d9afd987 100644 --- a/packages/contracts-bedrock/scripts/L2Genesis.s.sol +++ b/packages/contracts-bedrock/scripts/L2Genesis.s.sol @@ -243,7 +243,7 @@ contract L2Genesis is Script { } if (_input.isCustomGasToken) { setLiquidityController(_input); // 29 - setNativeAssetLiquidity(); // 30 + setNativeAssetLiquidity(); // 2A } }