Skip to content
2 changes: 2 additions & 0 deletions packages/contracts-bedrock/scripts/Artifacts.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
Expand Down
134 changes: 62 additions & 72 deletions packages/contracts-bedrock/scripts/L2Genesis.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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)
});
}

Expand Down Expand Up @@ -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)
});
}

Expand Down Expand Up @@ -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++) {
Expand Down
10 changes: 10 additions & 0 deletions packages/contracts-bedrock/scripts/libraries/ForgeArtifacts.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
29 changes: 23 additions & 6 deletions packages/contracts-bedrock/test/libraries/Predeploys.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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))
);
}
}
}
}
Expand Down
38 changes: 34 additions & 4 deletions packages/contracts-bedrock/test/scripts/L2Genesis.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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 {
Expand All @@ -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 {
Expand Down