diff --git a/packages/contracts-bedrock/scripts/Artifacts.s.sol b/packages/contracts-bedrock/scripts/Artifacts.s.sol index 5858c8aa4c597..fac400dd68b36 100644 --- a/packages/contracts-bedrock/scripts/Artifacts.s.sol +++ b/packages/contracts-bedrock/scripts/Artifacts.s.sol @@ -130,6 +130,8 @@ contract Artifacts { return payable(Predeploys.OPTIMISM_SUPERCHAIN_ERC20_BEACON); } else if (digest == keccak256(bytes("SuperchainTokenBridge"))) { return payable(Predeploys.SUPERCHAIN_TOKEN_BRIDGE); + } else if (digest == keccak256(bytes("FeeSplitter"))) { + return payable(Predeploys.FEE_SPLITTER); } return payable(address(0)); } diff --git a/packages/contracts-bedrock/scripts/L2Genesis.s.sol b/packages/contracts-bedrock/scripts/L2Genesis.s.sol index aaff71e026a1e..cd6a7d7dc5bac 100644 --- a/packages/contracts-bedrock/scripts/L2Genesis.s.sol +++ b/packages/contracts-bedrock/scripts/L2Genesis.s.sol @@ -312,25 +312,12 @@ contract L2Genesis is Script { /// @notice This predeploy is following the safety invariant #2, function setSequencerFeeVault(Input memory _input) internal { - address recipient; - Types.WithdrawalNetwork network; - if (_input.useRevenueShare) { - recipient = Predeploys.FEE_SPLITTER; - network = Types.WithdrawalNetwork.L2; - } else { - recipient = _input.sequencerFeeVaultRecipient; - network = Types.WithdrawalNetwork(_input.sequencerFeeVaultWithdrawalNetwork); - } - - address impl = _setImplementationCode(Predeploys.SEQUENCER_FEE_WALLET); - - /// Initialize the implemenation using max value for min withdrawal amount to make it unusable - ISequencerFeeVault(payable(impl)).initialize(address(0), type(uint256).max, Types.WithdrawalNetwork.L1); - // Initialize the predeploy - ISequencerFeeVault(payable(Predeploys.SEQUENCER_FEE_WALLET)).initialize({ - _recipient: recipient, + _setFeeVault({ + _vaultAddr: Predeploys.SEQUENCER_FEE_WALLET, + _useRevenueShare: _input.useRevenueShare, + _recipient: _input.sequencerFeeVaultRecipient, _minWithdrawalAmount: _input.sequencerFeeVaultMinimumWithdrawalAmount, - _withdrawalNetwork: network + _withdrawalNetwork: Types.WithdrawalNetwork(_input.sequencerFeeVaultWithdrawalNetwork) }); } @@ -402,73 +389,34 @@ contract L2Genesis is Script { /// @notice This predeploy is following the safety invariant #2. function setBaseFeeVault(Input memory _input) internal { - address recipient; - Types.WithdrawalNetwork network; - if (_input.useRevenueShare) { - recipient = Predeploys.FEE_SPLITTER; - network = Types.WithdrawalNetwork.L2; - } else { - recipient = _input.baseFeeVaultRecipient; - network = Types.WithdrawalNetwork(_input.baseFeeVaultWithdrawalNetwork); - } - - address impl = _setImplementationCode(Predeploys.BASE_FEE_VAULT); - - /// Initialize the implementation using max value for min withdrawal amount to make it unusable - IBaseFeeVault(payable(impl)).initialize(address(0), type(uint256).max, Types.WithdrawalNetwork.L1); - // Initialize the predeploy - IBaseFeeVault(payable(Predeploys.BASE_FEE_VAULT)).initialize({ - _recipient: recipient, + _setFeeVault({ + _vaultAddr: Predeploys.BASE_FEE_VAULT, + _useRevenueShare: _input.useRevenueShare, + _recipient: _input.baseFeeVaultRecipient, _minWithdrawalAmount: _input.baseFeeVaultMinimumWithdrawalAmount, - _withdrawalNetwork: network + _withdrawalNetwork: Types.WithdrawalNetwork(_input.baseFeeVaultWithdrawalNetwork) }); } /// @notice This predeploy is following the safety invariant #2. function setL1FeeVault(Input memory _input) internal { - address recipient; - Types.WithdrawalNetwork network; - if (_input.useRevenueShare) { - recipient = Predeploys.FEE_SPLITTER; - network = Types.WithdrawalNetwork.L2; - } else { - recipient = _input.l1FeeVaultRecipient; - network = Types.WithdrawalNetwork(_input.l1FeeVaultWithdrawalNetwork); - } - - address impl = _setImplementationCode(Predeploys.L1_FEE_VAULT); - - /// Initialize the implementation using max value for min withdrawal amount to make it unusable - IL1FeeVault(payable(impl)).initialize(address(0), type(uint256).max, Types.WithdrawalNetwork.L1); - // Initialize the predeploy - IL1FeeVault(payable(Predeploys.L1_FEE_VAULT)).initialize({ - _recipient: recipient, + _setFeeVault({ + _vaultAddr: Predeploys.L1_FEE_VAULT, + _useRevenueShare: _input.useRevenueShare, + _recipient: _input.l1FeeVaultRecipient, _minWithdrawalAmount: _input.l1FeeVaultMinimumWithdrawalAmount, - _withdrawalNetwork: network + _withdrawalNetwork: Types.WithdrawalNetwork(_input.l1FeeVaultWithdrawalNetwork) }); } /// @notice This predeploy is following the safety invariant #2. function setOperatorFeeVault(Input memory _input) internal { - address recipient; - Types.WithdrawalNetwork network; - if (_input.useRevenueShare) { - recipient = Predeploys.FEE_SPLITTER; - network = Types.WithdrawalNetwork.L2; - } else { - recipient = _input.operatorFeeVaultRecipient; - network = Types.WithdrawalNetwork(_input.operatorFeeVaultWithdrawalNetwork); - } - - address impl = _setImplementationCode(Predeploys.OPERATOR_FEE_VAULT); - - /// Initialize the implementation using max value for min withdrawal amount to make it unusable - IOperatorFeeVault(payable(impl)).initialize(address(0), type(uint256).max, Types.WithdrawalNetwork.L1); - // Initialize the predeploy - IOperatorFeeVault(payable(Predeploys.OPERATOR_FEE_VAULT)).initialize({ - _recipient: recipient, + _setFeeVault({ + _vaultAddr: Predeploys.OPERATOR_FEE_VAULT, + _useRevenueShare: _input.useRevenueShare, + _recipient: _input.operatorFeeVaultRecipient, _minWithdrawalAmount: _input.operatorFeeVaultMinimumWithdrawalAmount, - _withdrawalNetwork: network + _withdrawalNetwork: Types.WithdrawalNetwork(_input.operatorFeeVaultWithdrawalNetwork) }); } @@ -678,6 +626,48 @@ contract L2Genesis is Script { return impl; } + /// @notice Helper function to set up a fee vault predeploy with revenue sharing support. + /// This follows safety invariant #2 (initializable contracts). + /// @param _vaultAddr The predeploy address of the fee vault. + /// @param _useRevenueShare Whether revenue sharing is enabled. + /// @param _recipient The recipient address (ignored if revenue sharing is enabled). + /// @param _minWithdrawalAmount The minimum withdrawal amount (ignored if revenue sharing is enabled). + /// @param _withdrawalNetwork The withdrawal network (ignored if revenue sharing is enabled). + function _setFeeVault( + address _vaultAddr, + bool _useRevenueShare, + address _recipient, + uint256 _minWithdrawalAmount, + Types.WithdrawalNetwork _withdrawalNetwork + ) + internal + { + address recipient; + Types.WithdrawalNetwork network; + uint256 minWithdrawalAmount; + + if (_useRevenueShare) { + recipient = Predeploys.FEE_SPLITTER; + network = Types.WithdrawalNetwork.L2; + minWithdrawalAmount = 0; + } else { + recipient = _recipient; + network = _withdrawalNetwork; + minWithdrawalAmount = _minWithdrawalAmount; + } + + address impl = _setImplementationCode(_vaultAddr); + + /// Initialize the implementation using max value for min withdrawal amount to make it unusable + IFeeVault(payable(impl)).initialize(address(0), type(uint256).max, Types.WithdrawalNetwork.L1); + // Initialize the predeploy + IFeeVault(payable(_vaultAddr)).initialize({ + _recipient: recipient, + _minWithdrawalAmount: minWithdrawalAmount, + _withdrawalNetwork: network + }); + } + /// @notice Funds the default dev accounts with ether function fundDevAccounts() internal { for (uint256 i; i < devAccounts.length; i++) { diff --git a/packages/contracts-bedrock/scripts/libraries/ForgeArtifacts.sol b/packages/contracts-bedrock/scripts/libraries/ForgeArtifacts.sol index de00f7e4ec621..847145434b564 100644 --- a/packages/contracts-bedrock/scripts/libraries/ForgeArtifacts.sol +++ b/packages/contracts-bedrock/scripts/libraries/ForgeArtifacts.sol @@ -218,6 +218,16 @@ library ForgeArtifacts { initialized_ = uint8((uint256(slotVal) >> (slot.offset * 8)) & 0xFF) != 0; } + /// @notice Checks if a contract is initialized using OpenZeppelin v5 namespaced storage pattern. + /// OZ v5 storage slot: keccak256(abi.encode(uint256(keccak256("openzeppelin.storage.Initializable")) - 1)) + /// & ~bytes32(uint256(0xff)) + function isInitializedV5(address _addr) internal view returns (bool) { + bytes32 INITIALIZABLE_STORAGE_SLOT = 0xf0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a00; + bytes32 slotVal = vm.load(_addr, INITIALIZABLE_STORAGE_SLOT); + // In OZ v5, byte 0 is _initialized, byte 1 is _initializing + return uint8(uint256(slotVal) & 0xFF) != 0; + } + /// @notice Returns the names of all contracts in a given directory. /// @param _path The path to search for contracts. /// @param _pathExcludes An array of paths to exclude from the search. diff --git a/packages/contracts-bedrock/test/libraries/Predeploys.t.sol b/packages/contracts-bedrock/test/libraries/Predeploys.t.sol index 0073dc933799a..936ec8f000c51 100644 --- a/packages/contracts-bedrock/test/libraries/Predeploys.t.sol +++ b/packages/contracts-bedrock/test/libraries/Predeploys.t.sol @@ -28,18 +28,25 @@ abstract contract Predeploys_TestInit is CommonTest { return _addr == Predeploys.L1_MESSAGE_SENDER; } - /// @notice Returns true if the predeploy is initializable. - function _isInitializable(address _addr) internal pure returns (bool) { + /// @notice Returns true if the predeploy is initializable and uses OpenZeppelin v4 storage pattern. + /// These contracts have _initialized in the regular storage layout. + function _isInitializableV4(address _addr) internal pure returns (bool) { return _addr == Predeploys.L2_CROSS_DOMAIN_MESSENGER || _addr == Predeploys.L2_STANDARD_BRIDGE || _addr == Predeploys.L2_ERC721_BRIDGE || _addr == Predeploys.OPTIMISM_MINTABLE_ERC20_FACTORY || _addr == Predeploys.FEE_SPLITTER; } + /// @notice Returns true if the predeploy is initializable and uses OpenZeppelin v5 namespaced storage (EIP-7201). + /// These contracts store _initialized in a namespaced slot, not in the regular storage layout. + function _isInitializableV5(address _addr) internal pure returns (bool) { + return _addr == Predeploys.SEQUENCER_FEE_WALLET || _addr == Predeploys.BASE_FEE_VAULT + || _addr == Predeploys.L1_FEE_VAULT || _addr == Predeploys.OPERATOR_FEE_VAULT; + } + /// @notice Returns true if the predeploy uses immutables. function _usesImmutables(address _addr) internal pure returns (bool) { - return _addr == Predeploys.OPTIMISM_MINTABLE_ERC721_FACTORY || _addr == Predeploys.SEQUENCER_FEE_WALLET - || _addr == Predeploys.BASE_FEE_VAULT || _addr == Predeploys.L1_FEE_VAULT - || _addr == Predeploys.OPERATOR_FEE_VAULT || _addr == Predeploys.EAS || _addr == Predeploys.GOVERNANCE_TOKEN; + return _addr == Predeploys.OPTIMISM_MINTABLE_ERC721_FACTORY || _addr == Predeploys.EAS + || _addr == Predeploys.GOVERNANCE_TOKEN; } /// @notice Internal test function for predeploys validation across different forks. @@ -99,10 +106,20 @@ abstract contract Predeploys_TestInit is CommonTest { assertEq(implAddr.code, supposedCode, "proxy implementation contract should match contract source"); } - if (_isInitializable(addr)) { + if (_isInitializableV4(addr)) { assertTrue(ForgeArtifacts.isInitialized({ _name: cname, _address: addr })); assertTrue(ForgeArtifacts.isInitialized({ _name: cname, _address: implAddr })); } + + if (_isInitializableV5(addr)) { + assertTrue( + ForgeArtifacts.isInitializedV5(addr), string.concat("V5 proxy not initialized: ", vm.toString(addr)) + ); + assertTrue( + ForgeArtifacts.isInitializedV5(implAddr), + string.concat("V5 implementation not initialized: ", vm.toString(implAddr)) + ); + } } } } diff --git a/packages/contracts-bedrock/test/scripts/L2Genesis.t.sol b/packages/contracts-bedrock/test/scripts/L2Genesis.t.sol index 05cac30d1ce36..5c043befb6621 100644 --- a/packages/contracts-bedrock/test/scripts/L2Genesis.t.sol +++ b/packages/contracts-bedrock/test/scripts/L2Genesis.t.sol @@ -10,6 +10,7 @@ import { ISuperchainRevSharesCalculator } from "interfaces/L2/ISuperchainRevShar import { ISequencerFeeVault } from "interfaces/L2/ISequencerFeeVault.sol"; import { IBaseFeeVault } from "interfaces/L2/IBaseFeeVault.sol"; import { IL1FeeVault } from "interfaces/L2/IL1FeeVault.sol"; +import { IOperatorFeeVault } from "interfaces/L2/IOperatorFeeVault.sol"; import { IOptimismMintableERC20Factory } from "interfaces/universal/IOptimismMintableERC20Factory.sol"; import { IOptimismMintableERC721Factory } from "interfaces/L2/IOptimismMintableERC721Factory.sol"; import { IProxyAdmin } from "interfaces/universal/IProxyAdmin.sol"; @@ -69,18 +70,35 @@ abstract contract L2Genesis_TestInit is Test { IBaseFeeVault baseFeeVault = IBaseFeeVault(payable(Predeploys.BASE_FEE_VAULT)); IL1FeeVault l1FeeVault = IL1FeeVault(payable(Predeploys.L1_FEE_VAULT)); ISequencerFeeVault sequencerFeeVault = ISequencerFeeVault(payable(Predeploys.SEQUENCER_FEE_WALLET)); + IOperatorFeeVault operatorFeeVault = IOperatorFeeVault(payable(Predeploys.OPERATOR_FEE_VAULT)); + assertEq(baseFeeVault.RECIPIENT(), input.baseFeeVaultRecipient); assertEq(baseFeeVault.recipient(), input.baseFeeVaultRecipient); assertEq(baseFeeVault.MIN_WITHDRAWAL_AMOUNT(), input.baseFeeVaultMinimumWithdrawalAmount); + assertEq(baseFeeVault.minWithdrawalAmount(), input.baseFeeVaultMinimumWithdrawalAmount); assertEq(uint8(baseFeeVault.WITHDRAWAL_NETWORK()), uint8(input.baseFeeVaultWithdrawalNetwork)); + assertEq(uint8(baseFeeVault.withdrawalNetwork()), uint8(input.baseFeeVaultWithdrawalNetwork)); + assertEq(l1FeeVault.RECIPIENT(), input.l1FeeVaultRecipient); assertEq(l1FeeVault.recipient(), input.l1FeeVaultRecipient); assertEq(l1FeeVault.MIN_WITHDRAWAL_AMOUNT(), input.l1FeeVaultMinimumWithdrawalAmount); + assertEq(l1FeeVault.minWithdrawalAmount(), input.l1FeeVaultMinimumWithdrawalAmount); assertEq(uint8(l1FeeVault.WITHDRAWAL_NETWORK()), uint8(input.l1FeeVaultWithdrawalNetwork)); + assertEq(uint8(l1FeeVault.withdrawalNetwork()), uint8(input.l1FeeVaultWithdrawalNetwork)); + assertEq(sequencerFeeVault.RECIPIENT(), input.sequencerFeeVaultRecipient); assertEq(sequencerFeeVault.recipient(), input.sequencerFeeVaultRecipient); assertEq(sequencerFeeVault.MIN_WITHDRAWAL_AMOUNT(), input.sequencerFeeVaultMinimumWithdrawalAmount); + assertEq(sequencerFeeVault.minWithdrawalAmount(), input.sequencerFeeVaultMinimumWithdrawalAmount); assertEq(uint8(sequencerFeeVault.WITHDRAWAL_NETWORK()), uint8(input.sequencerFeeVaultWithdrawalNetwork)); + assertEq(uint8(sequencerFeeVault.withdrawalNetwork()), uint8(input.sequencerFeeVaultWithdrawalNetwork)); + + assertEq(operatorFeeVault.RECIPIENT(), input.operatorFeeVaultRecipient); + assertEq(operatorFeeVault.recipient(), input.operatorFeeVaultRecipient); + assertEq(operatorFeeVault.MIN_WITHDRAWAL_AMOUNT(), input.operatorFeeVaultMinimumWithdrawalAmount); + assertEq(operatorFeeVault.minWithdrawalAmount(), input.operatorFeeVaultMinimumWithdrawalAmount); + assertEq(uint8(operatorFeeVault.WITHDRAWAL_NETWORK()), uint8(input.operatorFeeVaultWithdrawalNetwork)); + assertEq(uint8(operatorFeeVault.withdrawalNetwork()), uint8(input.operatorFeeVaultWithdrawalNetwork)); } function testVaultsWithRevenueShare() internal view { @@ -90,20 +108,32 @@ abstract contract L2Genesis_TestInit is Test { IFeeVault operatorFeeVault = IFeeVault(payable(Predeploys.OPERATOR_FEE_VAULT)); assertEq(baseFeeVault.recipient(), Predeploys.FEE_SPLITTER); - assertEq(baseFeeVault.MIN_WITHDRAWAL_AMOUNT(), input.baseFeeVaultMinimumWithdrawalAmount); + assertEq(baseFeeVault.RECIPIENT(), Predeploys.FEE_SPLITTER); + assertEq(baseFeeVault.MIN_WITHDRAWAL_AMOUNT(), 0); + assertEq(baseFeeVault.minWithdrawalAmount(), 0); assertEq(uint8(baseFeeVault.WITHDRAWAL_NETWORK()), uint8(Types.WithdrawalNetwork.L2)); + assertEq(uint8(baseFeeVault.withdrawalNetwork()), uint8(Types.WithdrawalNetwork.L2)); + assertEq(l1FeeVault.RECIPIENT(), Predeploys.FEE_SPLITTER); assertEq(l1FeeVault.recipient(), Predeploys.FEE_SPLITTER); - assertEq(l1FeeVault.MIN_WITHDRAWAL_AMOUNT(), input.l1FeeVaultMinimumWithdrawalAmount); + assertEq(l1FeeVault.MIN_WITHDRAWAL_AMOUNT(), 0); + assertEq(l1FeeVault.minWithdrawalAmount(), 0); assertEq(uint8(l1FeeVault.WITHDRAWAL_NETWORK()), uint8(Types.WithdrawalNetwork.L2)); + assertEq(uint8(l1FeeVault.withdrawalNetwork()), uint8(Types.WithdrawalNetwork.L2)); + assertEq(sequencerFeeVault.RECIPIENT(), Predeploys.FEE_SPLITTER); assertEq(sequencerFeeVault.recipient(), Predeploys.FEE_SPLITTER); - assertEq(sequencerFeeVault.MIN_WITHDRAWAL_AMOUNT(), input.sequencerFeeVaultMinimumWithdrawalAmount); + assertEq(sequencerFeeVault.MIN_WITHDRAWAL_AMOUNT(), 0); + assertEq(sequencerFeeVault.minWithdrawalAmount(), 0); assertEq(uint8(sequencerFeeVault.WITHDRAWAL_NETWORK()), uint8(Types.WithdrawalNetwork.L2)); + assertEq(uint8(sequencerFeeVault.withdrawalNetwork()), uint8(Types.WithdrawalNetwork.L2)); + assertEq(operatorFeeVault.RECIPIENT(), Predeploys.FEE_SPLITTER); assertEq(operatorFeeVault.recipient(), Predeploys.FEE_SPLITTER); - assertEq(operatorFeeVault.MIN_WITHDRAWAL_AMOUNT(), input.operatorFeeVaultMinimumWithdrawalAmount); + assertEq(operatorFeeVault.MIN_WITHDRAWAL_AMOUNT(), 0); + assertEq(operatorFeeVault.minWithdrawalAmount(), 0); assertEq(uint8(operatorFeeVault.WITHDRAWAL_NETWORK()), uint8(Types.WithdrawalNetwork.L2)); + assertEq(uint8(operatorFeeVault.withdrawalNetwork()), uint8(Types.WithdrawalNetwork.L2)); } function testGovernance() internal view {