diff --git a/packages/contracts-bedrock/interfaces/L1/ILiquidityMigrator.sol b/packages/contracts-bedrock/interfaces/L1/ILiquidityMigrator.sol deleted file mode 100644 index f66b142bfdc..00000000000 --- a/packages/contracts-bedrock/interfaces/L1/ILiquidityMigrator.sol +++ /dev/null @@ -1,18 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; - -import { ISharedLockbox } from "interfaces/L1/ISharedLockbox.sol"; - -/// @title ILiquidityMigrator -/// @notice Interface for the LiquidityMigrator contract -interface ILiquidityMigrator { - event ETHMigrated(uint256 amount); - - function __constructor__(address _sharedLockbox) external; - - function SHARED_LOCKBOX() external view returns (ISharedLockbox); - - function migrateETH() external; - - function version() external view returns (string memory); -} diff --git a/packages/contracts-bedrock/interfaces/L1/IOptimismPortal2.sol b/packages/contracts-bedrock/interfaces/L1/IOptimismPortal2.sol index 9e094a2e5ea..436a467ce29 100644 --- a/packages/contracts-bedrock/interfaces/L1/IOptimismPortal2.sol +++ b/packages/contracts-bedrock/interfaces/L1/IOptimismPortal2.sol @@ -7,7 +7,6 @@ import { IDisputeGame } from "interfaces/dispute/IDisputeGame.sol"; import { IDisputeGameFactory } from "interfaces/dispute/IDisputeGameFactory.sol"; import { ISystemConfig } from "interfaces/L1/ISystemConfig.sol"; import { ISuperchainConfig } from "interfaces/L1/ISuperchainConfig.sol"; -import { ISharedLockbox } from "interfaces/L1/ISharedLockbox.sol"; interface IOptimismPortal2 { error CustomGasTokenNotSupported(); @@ -71,7 +70,6 @@ interface IOptimismPortal2 { function disputeGameBlacklist(IDisputeGame) external view returns (bool); function disputeGameFactory() external view returns (IDisputeGameFactory); function disputeGameFinalityDelaySeconds() external view returns (uint256); - function sharedLockbox() external view returns (ISharedLockbox); function donateETH() external payable; function finalizeWithdrawalTransaction(Types.WithdrawalTransaction memory _tx) external; function finalizeWithdrawalTransactionExternalProof( diff --git a/packages/contracts-bedrock/interfaces/L1/IOptimismPortalInterop.sol b/packages/contracts-bedrock/interfaces/L1/IOptimismPortalInterop.sol index 189fb1fed9c..ae2b41a5457 100644 --- a/packages/contracts-bedrock/interfaces/L1/IOptimismPortalInterop.sol +++ b/packages/contracts-bedrock/interfaces/L1/IOptimismPortalInterop.sol @@ -45,6 +45,7 @@ interface IOptimismPortalInterop { event WithdrawalFinalized(bytes32 indexed withdrawalHash, bool success); event WithdrawalProven(bytes32 indexed withdrawalHash, address indexed from, address indexed to); event WithdrawalProvenExtension1(bytes32 indexed withdrawalHash, address indexed proofSubmitter); + event ETHMigrated(uint256 amount); receive() external payable; @@ -118,6 +119,8 @@ interface IOptimismPortalInterop { function superchainConfig() external view returns (ISuperchainConfig); function systemConfig() external view returns (ISystemConfig); function version() external pure returns (string memory); + function migrateLiquidity() external; + function migrated() external view returns (bool); function __constructor__(uint256 _proofMaturityDelaySeconds, uint256 _disputeGameFinalityDelaySeconds) external; } diff --git a/packages/contracts-bedrock/interfaces/L1/ISharedLockbox.sol b/packages/contracts-bedrock/interfaces/L1/ISharedLockbox.sol index 029fa91fe00..f79031f31a3 100644 --- a/packages/contracts-bedrock/interfaces/L1/ISharedLockbox.sol +++ b/packages/contracts-bedrock/interfaces/L1/ISharedLockbox.sol @@ -2,32 +2,26 @@ pragma solidity ^0.8.0; import { ISemver } from "interfaces/universal/ISemver.sol"; -import { ISuperchainConfig } from "interfaces/L1/ISuperchainConfig.sol"; +import { ISuperchainConfigInterop } from "interfaces/L1/ISuperchainConfigInterop.sol"; /// @title ISharedLockbox /// @notice Interface for the SharedLockbox contract interface ISharedLockbox is ISemver { error Unauthorized(); - error Paused(); + error InvalidInitialization(); + error NotInitializing(); + event Initialized(uint64 version); event ETHLocked(address indexed portal, uint256 amount); - event ETHUnlocked(address indexed portal, uint256 amount); - event PortalAuthorized(address indexed portal); - function SUPERCHAIN_CONFIG() external view returns (ISuperchainConfig); - - function authorizedPortals(address) external view returns (bool); - - function __constructor__(address _superchainConfig) external; - + function superchainConfig() external view returns (ISuperchainConfigInterop superchainConfig_); + function initialize(address _superchainConfig) external; function paused() external view returns (bool); - function unlockETH(uint256 _value) external; - function lockETH() external payable; - function authorizePortal(address _portal) external; + function __constructor__() external; } diff --git a/packages/contracts-bedrock/interfaces/L1/ISuperchainConfig.sol b/packages/contracts-bedrock/interfaces/L1/ISuperchainConfig.sol index f02ac310bab..b4809fd612b 100644 --- a/packages/contracts-bedrock/interfaces/L1/ISuperchainConfig.sol +++ b/packages/contracts-bedrock/interfaces/L1/ISuperchainConfig.sol @@ -1,39 +1,25 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; -import { IDependencySet } from "interfaces/L2/IDependencySet.sol"; -import { ISharedLockbox } from "interfaces/L1/ISharedLockbox.sol"; - -interface ISuperchainConfig is IDependencySet { +interface ISuperchainConfig { enum UpdateType { - GUARDIAN + GUARDIAN, + CLUSTER_MANAGER } event ConfigUpdate(UpdateType indexed updateType, bytes data); event Initialized(uint8 version); event Paused(string identifier); event Unpaused(); - event DependencyAdded(uint256 indexed chainId, address indexed systemConfig, address indexed portal); - - error Unauthorized(); - error DependencySetTooLarge(); - error InvalidChainID(); - error DependencyAlreadyAdded(); function GUARDIAN_SLOT() external view returns (bytes32); function PAUSED_SLOT() external view returns (bytes32); - function DEPENDENCY_MANAGER_SLOT() external view returns (bytes32); - function SHARED_LOCKBOX() external view returns (ISharedLockbox); function guardian() external view returns (address guardian_); - function dependencyManager() external view returns (address dependencyManager_); - function initialize(address _guardian, address _dependencyManager, bool _paused) external; + function initialize(address _guardian, bool _paused) external; function pause(string memory _identifier) external; function paused() external view returns (bool paused_); function unpause() external; - function version() external view returns (string memory); - function addDependency(uint256 _chainId, address _systemConfig) external; - function dependencySet() external view returns (uint256[] memory); - function dependencySetSize() external view returns (uint8); + function version() external pure returns (string memory); - function __constructor__(address _sharedLockbox) external; + function __constructor__() external; } diff --git a/packages/contracts-bedrock/interfaces/L1/ISuperchainConfigInterop.sol b/packages/contracts-bedrock/interfaces/L1/ISuperchainConfigInterop.sol new file mode 100644 index 00000000000..505fbd6d489 --- /dev/null +++ b/packages/contracts-bedrock/interfaces/L1/ISuperchainConfigInterop.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import { IDependencySet } from "interfaces/L2/IDependencySet.sol"; +import { ISharedLockbox } from "interfaces/L1/ISharedLockbox.sol"; +import { ISuperchainConfig } from "interfaces/L1/ISuperchainConfig.sol"; + +interface ISuperchainConfigInterop is IDependencySet, ISuperchainConfig { + event DependencyAdded(uint256 indexed chainId, address indexed systemConfig, address indexed portal); + + error Unauthorized(); + error DependencySetTooLarge(); + error DependencyAlreadyAdded(); + error InvalidSuperchainConfig(); + error PortalAlreadyAuthorized(); + + function CLUSTER_MANAGER_SLOT() external view returns (bytes32); + function sharedLockbox() external view returns (ISharedLockbox sharedLockbox_); + function clusterManager() external view returns (address clusterManager_); + function initialize(address _guardian, bool _paused, address _clusterManager, address _sharedLockbox) external; + function version() external pure returns (string memory); + function addDependency(uint256 _chainId, address _systemConfig) external; + function dependencySet() external view returns (uint256[] memory); + function dependencySetSize() external view returns (uint8); + function authorizedPortals(address _portal) external view returns (bool); + + function __constructor__() external; +} diff --git a/packages/contracts-bedrock/interfaces/L2/ICrossL2Inbox.sol b/packages/contracts-bedrock/interfaces/L2/ICrossL2Inbox.sol index 9cee200b712..646bb699be6 100644 --- a/packages/contracts-bedrock/interfaces/L2/ICrossL2Inbox.sol +++ b/packages/contracts-bedrock/interfaces/L2/ICrossL2Inbox.sol @@ -12,23 +12,6 @@ struct Identifier { /// @title ICrossL2Inbox /// @notice Interface for the CrossL2Inbox contract. interface ICrossL2Inbox { - error ReentrantCall(); - - /// @notice Thrown when the caller is not DEPOSITOR_ACCOUNT when calling `setInteropStart()` - error NotDepositor(); - - /// @notice Thrown when attempting to set interop start when it's already set. - error InteropStartAlreadySet(); - - /// @notice Thrown when a non-written transient storage slot is attempted to be read from. - error NotEntered(); - - /// @notice Thrown when trying to execute a cross chain message with an invalid Identifier timestamp. - error InvalidTimestamp(); - - /// @notice Thrown when trying to execute a cross chain message and the target call fails. - error TargetCallFailed(); - /// @notice Thrown when trying to execute a cross chain message on a deposit transaction. error NoExecutingDeposits(); @@ -36,33 +19,6 @@ interface ICrossL2Inbox { function version() external view returns (string memory); - /// @notice Returns the interop start timestamp. - /// @return interopStart_ interop start timestamp. - function interopStart() external view returns (uint256 interopStart_); - - /// @notice Returns the origin address of the Identifier. - function origin() external view returns (address); - - /// @notice Returns the block number of the Identifier. - function blockNumber() external view returns (uint256); - - /// @notice Returns the log index of the Identifier. - function logIndex() external view returns (uint256); - - /// @notice Returns the timestamp of the Identifier. - function timestamp() external view returns (uint256); - - /// @notice Returns the chain ID of the Identifier. - function chainId() external view returns (uint256); - - function setInteropStart() external; - - /// @notice Executes a cross chain message on the destination chain. - /// @param _id An Identifier pointing to the initiating message. - /// @param _target Account that is called with _msg. - /// @param _message The message payload, matching the initiating message. - function executeMessage(Identifier calldata _id, address _target, bytes calldata _message) external payable; - /// @notice Validates a cross chain message on the destination chain /// and emits an ExecutingMessage event. This function is useful /// for applications that understand the schema of the _message payload and want to diff --git a/packages/contracts-bedrock/interfaces/L2/IDependencyManager.sol b/packages/contracts-bedrock/interfaces/L2/IDependencyManager.sol new file mode 100644 index 00000000000..97061b6e141 --- /dev/null +++ b/packages/contracts-bedrock/interfaces/L2/IDependencyManager.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import { ISemver } from "interfaces/universal/ISemver.sol"; + +/// @title IDependencyManager +/// @notice Interface for the DependencyManager contract. +interface IDependencyManager is ISemver { + error DependencySetSizeTooLarge(); + error AlreadyDependency(); + error Unauthorized(); + + event DependencyAdded(uint256 indexed chainId, address indexed systemConfig, address indexed superchainConfig); + + function addDependency(address _superchainConfig, uint256 _chainId, address _systemConfig) external; + function isInDependencySet(uint256 _chainId) external view returns (bool); + function dependencySetSize() external view returns (uint8); + function dependencySet() external view returns (uint256[] memory); + + function __constructor__() external; +} diff --git a/packages/contracts-bedrock/interfaces/L2/IL2ToL2CrossDomainMessenger.sol b/packages/contracts-bedrock/interfaces/L2/IL2ToL2CrossDomainMessenger.sol index c6c2c293c7f..58f07a9c92d 100644 --- a/packages/contracts-bedrock/interfaces/L2/IL2ToL2CrossDomainMessenger.sol +++ b/packages/contracts-bedrock/interfaces/L2/IL2ToL2CrossDomainMessenger.sol @@ -42,6 +42,9 @@ interface IL2ToL2CrossDomainMessenger { /// @notice Thrown when a call to the target contract during message relay fails. error TargetCallFailed(); + /// @notice Thrown when attempting to use a chain ID that is not in the dependency set. + error InvalidChainId(); + /// @notice Emitted whenever a message is sent to a destination /// @param destination Chain ID of the destination chain. /// @param target Target contract or wallet address. diff --git a/packages/contracts-bedrock/scripts/Artifacts.s.sol b/packages/contracts-bedrock/scripts/Artifacts.s.sol index 97bd3cbf847..84324b415ca 100644 --- a/packages/contracts-bedrock/scripts/Artifacts.s.sol +++ b/packages/contracts-bedrock/scripts/Artifacts.s.sol @@ -152,6 +152,8 @@ abstract 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("DependencyManager"))) { + return payable(Predeploys.DEPENDENCY_MANAGER); } return payable(address(0)); } diff --git a/packages/contracts-bedrock/scripts/L2Genesis.s.sol b/packages/contracts-bedrock/scripts/L2Genesis.s.sol index 309b20b8a8c..62bebe653cc 100644 --- a/packages/contracts-bedrock/scripts/L2Genesis.s.sol +++ b/packages/contracts-bedrock/scripts/L2Genesis.s.sol @@ -280,6 +280,7 @@ contract L2Genesis is Deployer { setOptimismSuperchainERC20Factory(); // 26 setOptimismSuperchainERC20Beacon(); // 27 setSuperchainTokenBridge(); // 28 + setDependencyManager(); // 29 } } @@ -595,6 +596,12 @@ contract L2Genesis is Deployer { _setImplementationCode(Predeploys.SUPERCHAIN_TOKEN_BRIDGE); } + /// @notice This predeploy is following the safety invariant #1. + /// This contract has no initializer. + function setDependencyManager() internal { + _setImplementationCode(Predeploys.DEPENDENCY_MANAGER); + } + /// @notice Sets all the preinstalls. function setPreinstalls() public { address tmpSetPreinstalls = address(uint160(uint256(keccak256("SetPreinstalls")))); diff --git a/packages/contracts-bedrock/scripts/deploy/ChainAssertions.sol b/packages/contracts-bedrock/scripts/deploy/ChainAssertions.sol index f5d7426fd35..b924a49fa5b 100644 --- a/packages/contracts-bedrock/scripts/deploy/ChainAssertions.sol +++ b/packages/contracts-bedrock/scripts/deploy/ChainAssertions.sol @@ -24,8 +24,8 @@ import { OPContractsManager } from "src/L1/OPContractsManager.sol"; import { IResourceMetering } from "interfaces/L1/IResourceMetering.sol"; import { ISystemConfig } from "interfaces/L1/ISystemConfig.sol"; import { ISuperchainConfig } from "interfaces/L1/ISuperchainConfig.sol"; +import { ISuperchainConfigInterop } from "interfaces/L1/ISuperchainConfigInterop.sol"; import { ISharedLockbox } from "interfaces/L1/ISharedLockbox.sol"; -import { ILiquidityMigrator } from "interfaces/L1/ILiquidityMigrator.sol"; import { IL1CrossDomainMessenger } from "interfaces/L1/IL1CrossDomainMessenger.sol"; import { IOptimismPortal2 } from "interfaces/L1/IOptimismPortal2.sol"; import { IL1ERC721Bridge } from "interfaces/L1/IL1ERC721Bridge.sol"; @@ -441,6 +441,7 @@ library ChainAssertions { view { ISuperchainConfig superchainConfig = ISuperchainConfig(_contracts.SuperchainConfig); + console.log( "Running chain assertions on the SuperchainConfig %s at %s", _isProxy ? "proxy" : "implementation", @@ -465,6 +466,36 @@ library ChainAssertions { } } + /// @notice Asserts that the SuperchainConfigInterop is setup correctly + function checkSuperchainConfigInterop( + Types.ContractSet memory _contracts, + DeployConfig _cfg, + bool _isPaused, + bool _isProxy + ) + internal + view + { + ISuperchainConfigInterop superchainConfig = ISuperchainConfigInterop(_contracts.SuperchainConfig); + ISharedLockbox sharedLockbox = ISharedLockbox(_contracts.SharedLockbox); + + console.log( + "Running chain assertions on the SuperchainConfigInterop %s at %s", + _isProxy ? "proxy" : "implementation", + address(superchainConfig) + ); + + if (_isProxy) { + require(superchainConfig.clusterManager() == _cfg.finalSystemOwner(), "CHECK-SCI-10"); + require(address(superchainConfig.sharedLockbox()) == address(sharedLockbox), "CHECK-SCI-20"); + } else { + require(superchainConfig.clusterManager() == address(0), "CHECK-SCI-30"); + require(address(superchainConfig.sharedLockbox()) == address(0), "CHECK-SCI-40"); + } + + checkSuperchainConfig(_contracts, _cfg, _isPaused, _isProxy); + } + /// @notice Asserts that the OPContractsManager is setup correctly function checkOPContractsManager( Types.ContractSet memory _contracts, @@ -545,7 +576,7 @@ library ChainAssertions { /// @notice Asserts that the SharedLockbox is setup correctly function checkSharedLockbox(Types.ContractSet memory _contracts, bool _isProxy) internal view { ISharedLockbox sharedLockbox = ISharedLockbox(_contracts.SharedLockbox); - ISuperchainConfig superchainConfig = ISuperchainConfig(_contracts.SuperchainConfig); + ISuperchainConfigInterop superchainConfig = ISuperchainConfigInterop(_contracts.SuperchainConfig); console.log( "Running chain assertions on the SharedLockbox %s at %s", @@ -554,13 +585,14 @@ library ChainAssertions { ); require(address(sharedLockbox) != address(0), "CHECK-SLB-10"); - require(sharedLockbox.SUPERCHAIN_CONFIG() == superchainConfig, "CHECK-SLB-20"); - } - /// @notice Asserts that the LiquidityMigrator is setup correctly - function checkLiquidityMigrator(Types.ContractSet memory _contracts, address _liquidityMigrator) internal view { - ISharedLockbox sharedLockbox = ISharedLockbox(_contracts.SharedLockbox); + // Check that the contract is initialized + DeployUtils.assertInitializedOZv5({ _contractAddress: address(sharedLockbox), _isProxy: _isProxy }); - require(ILiquidityMigrator(_liquidityMigrator).SHARED_LOCKBOX() == sharedLockbox, "LM-10"); + if (_isProxy) { + require(sharedLockbox.superchainConfig() == superchainConfig, "CHECK-SLB-20"); + } else { + require(address(sharedLockbox.superchainConfig()) == address(0), "CHECK-SLB-30"); + } } } diff --git a/packages/contracts-bedrock/scripts/deploy/Deploy.s.sol b/packages/contracts-bedrock/scripts/deploy/Deploy.s.sol index 3b2dfd7b712..bd780b36bf8 100644 --- a/packages/contracts-bedrock/scripts/deploy/Deploy.s.sol +++ b/packages/contracts-bedrock/scripts/deploy/Deploy.s.sol @@ -16,7 +16,12 @@ import { StateDiff } from "scripts/libraries/StateDiff.sol"; import { Process } from "scripts/libraries/Process.sol"; import { ChainAssertions } from "scripts/deploy/ChainAssertions.sol"; import { DeployUtils } from "scripts/libraries/DeployUtils.sol"; -import { DeploySuperchainInput, DeploySuperchain, DeploySuperchainOutput } from "scripts/deploy/DeploySuperchain.s.sol"; +import { + DeploySuperchainInput, + DeploySuperchain, + DeploySuperchainInterop, + DeploySuperchainOutput +} from "scripts/deploy/DeploySuperchain.s.sol"; import { DeployImplementationsInput, DeployImplementations, @@ -213,7 +218,7 @@ contract Deploy is Deployer { // Set up the Superchain if needed. if (_needsSuperchain) { - deploySuperchain(); + deploySuperchain({ _isInterop: cfg.useInterop() }); } deployImplementations({ _isInterop: cfg.useInterop() }); @@ -255,7 +260,9 @@ contract Deploy is Deployer { /// 1. The SuperchainConfig contract /// 2. The ProtocolVersions contract /// 3. The SharedLockbox contract - function deploySuperchain() public { + function deploySuperchain(bool _isInterop) public { + require(_isInterop == cfg.useInterop(), "Deploy: Interop setting mismatch."); + console.log("Setting up Superchain"); DeploySuperchain ds = new DeploySuperchain(); (DeploySuperchainInput dsi, DeploySuperchainOutput dso) = ds.etchIOContracts(); @@ -268,7 +275,11 @@ contract Deploy is Deployer { dsi.set(dsi.paused.selector, false); dsi.set(dsi.requiredProtocolVersion.selector, ProtocolVersion.wrap(cfg.requiredProtocolVersion())); dsi.set(dsi.recommendedProtocolVersion.selector, ProtocolVersion.wrap(cfg.recommendedProtocolVersion())); + dsi.set(dsi.isInterop.selector, _isInterop); + if (_isInterop) { + ds = DeploySuperchain(new DeploySuperchainInterop()); + } // Run the deployment script. ds.run(dsi, dso); save("SuperchainProxyAdmin", address(dso.superchainProxyAdmin())); @@ -276,33 +287,49 @@ contract Deploy is Deployer { save("SuperchainConfigImpl", address(dso.superchainConfigImpl())); save("ProtocolVersionsProxy", address(dso.protocolVersionsProxy())); save("ProtocolVersionsImpl", address(dso.protocolVersionsImpl())); - save("SharedLockboxProxy", address(dso.sharedLockboxProxy())); - save("SharedLockboxImpl", address(dso.sharedLockboxImpl())); - save("LiquidityMigrator", address(dso.liquidityMigratorImpl())); + + if (_isInterop) { + save("SharedLockboxProxy", address(dso.sharedLockboxProxy())); + save("SharedLockboxImpl", address(dso.sharedLockboxImpl())); + } // First run assertions for the ProtocolVersions, SuperchainConfig and SharedLockbox proxy contracts. Types.ContractSet memory contracts = _proxies(); ChainAssertions.checkProtocolVersions({ _contracts: contracts, _cfg: cfg, _isProxy: true }); ChainAssertions.checkSuperchainConfig({ _contracts: contracts, _cfg: cfg, _isProxy: true, _isPaused: false }); - ChainAssertions.checkSharedLockbox({ _contracts: contracts, _isProxy: true }); - // Test the LiquidityMigrator contract is setup correctly. - ChainAssertions.checkLiquidityMigrator({ - _contracts: contracts, - _liquidityMigrator: mustGetAddress("LiquidityMigrator") - }); - - // Then replace the SharedLockbox proxy with the implementation address and run assertions on it. - contracts.SharedLockbox = mustGetAddress("SharedLockboxImpl"); - ChainAssertions.checkSharedLockbox({ _contracts: contracts, _isProxy: false }); + if (_isInterop) { + ChainAssertions.checkSuperchainConfigInterop({ + _contracts: contracts, + _cfg: cfg, + _isProxy: true, + _isPaused: false + }); + ChainAssertions.checkSharedLockbox({ _contracts: contracts, _isProxy: true }); + } // Then replace the ProtocolVersions proxy with the implementation address and run assertions on it. contracts.ProtocolVersions = mustGetAddress("ProtocolVersionsImpl"); ChainAssertions.checkProtocolVersions({ _contracts: contracts, _cfg: cfg, _isProxy: false }); - // Finally replace the SuperchainConfig proxy with the implementation address and run assertions on it. - contracts.SuperchainConfig = mustGetAddress("SuperchainConfigImpl"); - ChainAssertions.checkSuperchainConfig({ _contracts: contracts, _cfg: cfg, _isPaused: false, _isProxy: false }); + if (_isInterop) { + // Then replace the SharedLockbox proxy with the implementation address and run assertions on it. + contracts.SharedLockbox = mustGetAddress("SharedLockboxImpl"); + ChainAssertions.checkSharedLockbox({ _contracts: contracts, _isProxy: false }); + + // Finally replace the SuperchainConfig proxy with the implementation address and run assertions on it. + contracts.SuperchainConfig = mustGetAddress("SuperchainConfigImpl"); + ChainAssertions.checkSuperchainConfigInterop({ + _contracts: contracts, + _cfg: cfg, + _isPaused: false, + _isProxy: false + }); + } else { + // Finally replace the SuperchainConfig proxy with the implementation address and run assertions on it. + contracts.SuperchainConfig = mustGetAddress("SuperchainConfigImpl"); + ChainAssertions.checkSuperchainConfig({ _contracts: contracts, _cfg: cfg, _isPaused: false, _isProxy: false }); + } } /// @notice Deploy all of the implementations @@ -326,7 +353,6 @@ contract Deploy is Deployer { ); dii.set(dii.superchainConfigProxy.selector, mustGetAddress("SuperchainConfigProxy")); dii.set(dii.protocolVersionsProxy.selector, mustGetAddress("ProtocolVersionsProxy")); - dii.set(dii.sharedLockboxProxy.selector, mustGetAddress("SharedLockboxProxy")); dii.set(dii.salt.selector, _implSalt()); if (_isInterop) { diff --git a/packages/contracts-bedrock/scripts/deploy/DeployImplementations.s.sol b/packages/contracts-bedrock/scripts/deploy/DeployImplementations.s.sol index 27b44954810..7b1c2579efe 100644 --- a/packages/contracts-bedrock/scripts/deploy/DeployImplementations.s.sol +++ b/packages/contracts-bedrock/scripts/deploy/DeployImplementations.s.sol @@ -8,7 +8,6 @@ import { LibString } from "@solady/utils/LibString.sol"; import { IResourceMetering } from "interfaces/L1/IResourceMetering.sol"; import { ISuperchainConfig } from "interfaces/L1/ISuperchainConfig.sol"; import { IProtocolVersions } from "interfaces/L1/IProtocolVersions.sol"; -import { ISharedLockbox } from "interfaces/L1/ISharedLockbox.sol"; import { Bytes } from "src/libraries/Bytes.sol"; @@ -50,7 +49,6 @@ contract DeployImplementationsInput is BaseDeployIO { // Outputs from DeploySuperchain.s.sol. ISuperchainConfig internal _superchainConfigProxy; IProtocolVersions internal _protocolVersionsProxy; - ISharedLockbox internal _sharedLockboxProxy; string internal _standardVersionsToml; @@ -86,7 +84,6 @@ contract DeployImplementationsInput is BaseDeployIO { require(_addr != address(0), "DeployImplementationsInput: cannot set zero address"); if (_sel == this.superchainConfigProxy.selector) _superchainConfigProxy = ISuperchainConfig(_addr); else if (_sel == this.protocolVersionsProxy.selector) _protocolVersionsProxy = IProtocolVersions(_addr); - else if (_sel == this.sharedLockboxProxy.selector) _sharedLockboxProxy = ISharedLockbox(_addr); else revert("DeployImplementationsInput: unknown selector"); } @@ -152,11 +149,6 @@ contract DeployImplementationsInput is BaseDeployIO { require(address(_protocolVersionsProxy) != address(0), "DeployImplementationsInput: not set"); return _protocolVersionsProxy; } - - function sharedLockboxProxy() public view returns (ISharedLockbox) { - require(address(_sharedLockboxProxy) != address(0), "DeployImplementationsInput: not set"); - return _sharedLockboxProxy; - } } contract DeployImplementationsOutput is BaseDeployIO { diff --git a/packages/contracts-bedrock/scripts/deploy/DeployOwnership.s.sol b/packages/contracts-bedrock/scripts/deploy/DeployOwnership.s.sol index 34d128714d8..7f1febf8536 100644 --- a/packages/contracts-bedrock/scripts/deploy/DeployOwnership.s.sol +++ b/packages/contracts-bedrock/scripts/deploy/DeployOwnership.s.sol @@ -324,7 +324,7 @@ contract DeployOwnership is Deploy { _salt: _implSalt(), _name: "SuperchainConfig", _nick: "SuperchainConfigImpl", - _args: DeployUtils.encodeConstructor(abi.encodeCall(ISuperchainConfig.__constructor__, (address(0)))) + _args: DeployUtils.encodeConstructor(abi.encodeCall(ISuperchainConfig.__constructor__, ())) }) ); diff --git a/packages/contracts-bedrock/scripts/deploy/DeploySuperchain.s.sol b/packages/contracts-bedrock/scripts/deploy/DeploySuperchain.s.sol index ba41a8dfeaf..2b0972600b3 100644 --- a/packages/contracts-bedrock/scripts/deploy/DeploySuperchain.s.sol +++ b/packages/contracts-bedrock/scripts/deploy/DeploySuperchain.s.sol @@ -5,11 +5,11 @@ import { Script } from "forge-std/Script.sol"; import { stdToml } from "forge-std/StdToml.sol"; import { ISuperchainConfig } from "interfaces/L1/ISuperchainConfig.sol"; +import { ISuperchainConfigInterop } from "interfaces/L1/ISuperchainConfigInterop.sol"; import { IProtocolVersions, ProtocolVersion } from "interfaces/L1/IProtocolVersions.sol"; import { IProxyAdmin } from "interfaces/universal/IProxyAdmin.sol"; import { IProxy } from "interfaces/universal/IProxy.sol"; import { ISharedLockbox } from "interfaces/L1/ISharedLockbox.sol"; -import { ILiquidityMigrator } from "interfaces/L1/ILiquidityMigrator.sol"; import { DeployUtils } from "scripts/libraries/DeployUtils.sol"; import { Solarray } from "scripts/libraries/Solarray.sol"; @@ -90,6 +90,7 @@ contract DeploySuperchainInput is BaseDeployIO { bool internal _paused; ProtocolVersion internal _recommendedProtocolVersion; ProtocolVersion internal _requiredProtocolVersion; + bool internal _isInterop; // These `set` methods let each input be set individually. The selector of an input's getter method // is used to determine which field to set. @@ -103,6 +104,7 @@ contract DeploySuperchainInput is BaseDeployIO { function set(bytes4 _sel, bool _value) public { if (_sel == this.paused.selector) _paused = _value; + else if (_sel == this.isInterop.selector) _isInterop = _value; else revert("DeploySuperchainInput: unknown selector"); } @@ -152,6 +154,10 @@ contract DeploySuperchainInput is BaseDeployIO { ); return _recommendedProtocolVersion; } + + function isInterop() public view returns (bool) { + return _isInterop; + } } // All contracts of the form `DeployOutput` should inherit from `BaseDeployIO`, as it provides @@ -166,7 +172,6 @@ contract DeploySuperchainOutput is BaseDeployIO { IProxyAdmin internal _superchainProxyAdmin; ISharedLockbox internal _sharedLockboxImpl; ISharedLockbox internal _sharedLockboxProxy; - ILiquidityMigrator internal _liquidityMigratorImpl; // This method lets each field be set individually. The selector of an output's getter method // is used to determine which field to set. @@ -179,7 +184,6 @@ contract DeploySuperchainOutput is BaseDeployIO { else if (_sel == this.protocolVersionsProxy.selector) _protocolVersionsProxy = IProtocolVersions(_address); else if (_sel == this.sharedLockboxImpl.selector) _sharedLockboxImpl = ISharedLockbox(_address); else if (_sel == this.sharedLockboxProxy.selector) _sharedLockboxProxy = ISharedLockbox(_address); - else if (_sel == this.liquidityMigratorImpl.selector) _liquidityMigratorImpl = ILiquidityMigrator(_address); else revert("DeploySuperchainOutput: unknown selector"); } @@ -191,26 +195,37 @@ contract DeploySuperchainOutput is BaseDeployIO { address(this.superchainConfigImpl()), address(this.superchainConfigProxy()), address(this.protocolVersionsImpl()), - address(this.protocolVersionsProxy()), - address(this.sharedLockboxImpl()), - address(this.sharedLockboxProxy()), - address(this.liquidityMigratorImpl()) + address(this.protocolVersionsProxy()) ); + + if (_dsi.isInterop()) { + address[] memory interopAddrs = + Solarray.addresses(address(this.sharedLockboxImpl()), address(this.sharedLockboxProxy())); + addrs = Solarray.extend(addrs, interopAddrs); + } + DeployUtils.assertValidContractAddresses(addrs); // To read the implementations we prank as the zero address due to the proxyCallIfNotAdmin modifier. vm.startPrank(address(0)); address actualSuperchainConfigImpl = IProxy(payable(address(_superchainConfigProxy))).implementation(); address actualProtocolVersionsImpl = IProxy(payable(address(_protocolVersionsProxy))).implementation(); - address actualSharedLockboxImpl = IProxy(payable(address(_sharedLockboxProxy))).implementation(); vm.stopPrank(); require(actualSuperchainConfigImpl == address(_superchainConfigImpl), "100"); // nosemgrep: // sol-style-malformed-require require(actualProtocolVersionsImpl == address(_protocolVersionsImpl), "200"); // nosemgrep: // sol-style-malformed-require - require(actualSharedLockboxImpl == address(_sharedLockboxImpl), "300"); // nosemgrep: - // sol-style-malformed-require + + // Assert interop deployment. + if (_dsi.isInterop()) { + vm.startPrank(address(0)); + address actualSharedLockboxImpl = IProxy(payable(address(_sharedLockboxProxy))).implementation(); + vm.stopPrank(); + + require(actualSharedLockboxImpl == address(_sharedLockboxImpl), "300"); // nosemgrep: + // sol-style-malformed-require + } assertValidDeploy(_dsi); } @@ -250,18 +265,16 @@ contract DeploySuperchainOutput is BaseDeployIO { return _sharedLockboxProxy; } - function liquidityMigratorImpl() public view returns (ILiquidityMigrator) { - DeployUtils.assertValidContractAddress(address(_liquidityMigratorImpl)); - return _liquidityMigratorImpl; - } - // -------- Deployment Assertions -------- function assertValidDeploy(DeploySuperchainInput _dsi) public { assertValidSuperchainProxyAdmin(_dsi); assertValidSuperchainConfig(_dsi); assertValidProtocolVersions(_dsi); - assertValidSharedLockbox(); - assertValidLiquidityMigrator(); + + if (_dsi.isInterop()) { + assertValidSuperchainConfigInterop(_dsi); + assertValidSharedLockbox(); + } } function assertValidSuperchainProxyAdmin(DeploySuperchainInput _dsi) internal view { @@ -293,6 +306,19 @@ contract DeploySuperchainOutput is BaseDeployIO { require(superchainConfig.paused() == false, "SUPCON-60"); } + function assertValidSuperchainConfigInterop(DeploySuperchainInput _dsi) internal view { + // Proxy checks. + ISuperchainConfigInterop superchainConfig = ISuperchainConfigInterop(address(superchainConfigProxy())); + + require(superchainConfig.clusterManager() == _dsi.superchainProxyAdminOwner(), "SUPCONI-10"); + require(superchainConfig.sharedLockbox() == sharedLockboxProxy(), "SUPCONI-20"); + + // Implementation checks + superchainConfig = ISuperchainConfigInterop(address(superchainConfigImpl())); + require(superchainConfig.clusterManager() == address(0), "SUPCONI-30"); + require(address(superchainConfig.sharedLockbox()) == address(0), "SUPCONI-40"); + } + function assertValidProtocolVersions(DeploySuperchainInput _dsi) internal { // Proxy checks. IProtocolVersions pv = protocolVersionsProxy(); @@ -321,22 +347,17 @@ contract DeploySuperchainOutput is BaseDeployIO { function assertValidSharedLockbox() internal { // Proxy checks. ISharedLockbox sl = sharedLockboxProxy(); + DeployUtils.assertInitializedOZv5({ _contractAddress: address(sl), _isProxy: true }); vm.startPrank(address(0)); require(IProxy(payable(address(sl))).implementation() == address(sharedLockboxImpl()), "SLB-10"); require(IProxy(payable(address(sl))).admin() == address(superchainProxyAdmin()), "SLB-20"); - require(sl.SUPERCHAIN_CONFIG() == superchainConfigProxy(), "SLB-30"); + require(address(sl.superchainConfig()) == address(superchainConfigProxy()), "SLB-30"); vm.stopPrank(); // Implementation checks. sl = sharedLockboxImpl(); - require(sl.SUPERCHAIN_CONFIG() == superchainConfigProxy(), "SLB-40"); - } - - function assertValidLiquidityMigrator() internal view { - // Implementation checks. - ILiquidityMigrator lm = liquidityMigratorImpl(); - require(lm.SHARED_LOCKBOX() == sharedLockboxProxy(), "LM-10"); + require(address(sl.superchainConfig()) == address(0), "SLB-40"); } } @@ -345,13 +366,6 @@ contract DeploySuperchainOutput is BaseDeployIO { // default sender would be the broadcaster during test, but the broadcaster needs to be the deployer // since they are set to the initial proxy admin owner. contract DeploySuperchain is Script { - // The `PrecalculatedAddresses` stores the precalculated addresses so then they can be checked on the actual - // deployment. - struct PrecalculatedAddresses { - address superchainConfigProxy; - address sharedLockboxProxy; - } - // -------- Core Deployment Methods -------- function run(DeploySuperchainInput _dsi, DeploySuperchainOutput _dso) public { @@ -397,38 +411,56 @@ contract DeploySuperchain is Script { } function deploySuperchain(DeploySuperchainInput _dsi, DeploySuperchainOutput _dso) public { - // Precalculate the proxies addresses. Needed since there are circular dependencies between them. - PrecalculatedAddresses memory precalculatedAddresses; - precalculatedAddresses.superchainConfigProxy = vm.computeCreateAddress(msg.sender, vm.getNonce(msg.sender) + 4); - precalculatedAddresses.sharedLockboxProxy = vm.computeCreateAddress(msg.sender, vm.getNonce(msg.sender) + 8); - // Deploy implementation contracts - deploySuperchainImplementationContracts(_dsi, _dso, precalculatedAddresses); + deploySuperchainImplementationContracts(_dsi, _dso); // Deploy proxy contracts - deployAndInitializeSuperchainProxyContracts(_dsi, _dso, precalculatedAddresses); + deployAndInitializeSuperchainProxyContracts(_dsi, _dso); } function deploySuperchainImplementationContracts( DeploySuperchainInput, - DeploySuperchainOutput _dso, - PrecalculatedAddresses memory _precalculatedAddresses + DeploySuperchainOutput _dso ) - internal + public + virtual { - vm.startBroadcast(msg.sender); + // Deploy the SuperchainConfig implementation contract. + deploySuperchainConfigImplementation(_dso); - // Deploy SuperchainConfig implementation + // Deploy the ProtocolVersions implementation contract. + deployProtocolVersionsImplementation(_dso); + } + + function deployAndInitializeSuperchainProxyContracts( + DeploySuperchainInput _dsi, + DeploySuperchainOutput _dso + ) + public + virtual + { + // Deploy the SuperchainConfig proxy contract. + deploySuperchainConfigProxy(_dsi, _dso); + + // Deploy the ProtocolVersions proxy contract. + deployProtocolVersionsProxy(_dsi, _dso); + } + + function deploySuperchainConfigImplementation(DeploySuperchainOutput _dso) public virtual { + vm.broadcast(msg.sender); ISuperchainConfig superchainConfigImpl = ISuperchainConfig( DeployUtils.create1({ _name: "SuperchainConfig", - _args: DeployUtils.encodeConstructor( - abi.encodeCall(ISuperchainConfig.__constructor__, (_precalculatedAddresses.sharedLockboxProxy)) - ) + _args: DeployUtils.encodeConstructor(abi.encodeCall(ISuperchainConfig.__constructor__, ())) }) ); - // Deploy ProtocolVersions implementation + vm.label(address(superchainConfigImpl), "SuperchainConfigImpl"); + _dso.set(_dso.superchainConfigImpl.selector, address(superchainConfigImpl)); + } + + function deployProtocolVersionsImplementation(DeploySuperchainOutput _dso) public virtual { + vm.broadcast(msg.sender); IProtocolVersions protocolVersionsImpl = IProtocolVersions( DeployUtils.create1({ _name: "ProtocolVersions", @@ -436,53 +468,15 @@ contract DeploySuperchain is Script { }) ); - // Deploy SharedLockbox implementation - ISharedLockbox sharedLockboxImpl = ISharedLockbox( - DeployUtils.create1({ - _name: "SharedLockbox", - _args: DeployUtils.encodeConstructor( - abi.encodeCall(ISharedLockbox.__constructor__, (_precalculatedAddresses.superchainConfigProxy)) - ) - }) - ); - - // Deploy LiquidityMigrator implementation - ILiquidityMigrator liquidityMigratorImpl = ILiquidityMigrator( - DeployUtils.create1({ - _name: "LiquidityMigrator", - _args: DeployUtils.encodeConstructor( - abi.encodeCall(ILiquidityMigrator.__constructor__, (_precalculatedAddresses.sharedLockboxProxy)) - ) - }) - ); - - vm.stopBroadcast(); - - vm.label(address(superchainConfigImpl), "SuperchainConfigImpl"); vm.label(address(protocolVersionsImpl), "ProtocolVersionsImpl"); - vm.label(address(sharedLockboxImpl), "SharedLockboxImpl"); - vm.label(address(liquidityMigratorImpl), "LiquidityMigratorImpl"); - - _dso.set(_dso.superchainConfigImpl.selector, address(superchainConfigImpl)); _dso.set(_dso.protocolVersionsImpl.selector, address(protocolVersionsImpl)); - _dso.set(_dso.sharedLockboxImpl.selector, address(sharedLockboxImpl)); - _dso.set(_dso.liquidityMigratorImpl.selector, address(liquidityMigratorImpl)); } - function deployAndInitializeSuperchainProxyContracts( - DeploySuperchainInput _dsi, - DeploySuperchainOutput _dso, - PrecalculatedAddresses memory _precalculatedAddresses - ) - internal - { - IProxyAdmin superchainProxyAdmin = _dso.superchainProxyAdmin(); - - // Deploy SuperchainConfig proxy + function deploySuperchainConfigProxy(DeploySuperchainInput _dsi, DeploySuperchainOutput _dso) public virtual { ISuperchainConfig superchainConfigProxy; { + IProxyAdmin superchainProxyAdmin = _dso.superchainProxyAdmin(); address guardian = _dsi.guardian(); - address dependencyManager = _dsi.superchainProxyAdminOwner(); bool paused = _dsi.paused(); vm.startBroadcast(msg.sender); @@ -494,24 +488,29 @@ contract DeploySuperchain is Script { ) }) ); + superchainProxyAdmin.upgradeAndCall( payable(address(superchainConfigProxy)), address(_dso.superchainConfigImpl()), - abi.encodeCall(ISuperchainConfig.initialize, (guardian, dependencyManager, paused)) + abi.encodeCall(ISuperchainConfig.initialize, (guardian, paused)) ); vm.stopBroadcast(); } - // Deploy ProtocolVersions proxy + vm.label(address(superchainConfigProxy), "SuperchainConfigProxy"); + _dso.set(_dso.superchainConfigProxy.selector, address(superchainConfigProxy)); + } + + function deployProtocolVersionsProxy(DeploySuperchainInput _dsi, DeploySuperchainOutput _dso) public { IProtocolVersions protocolVersionsProxy; { + IProxyAdmin superchainProxyAdmin = _dso.superchainProxyAdmin(); address protocolVersionsOwner = _dsi.protocolVersionsOwner(); ProtocolVersion requiredProtocolVersion = _dsi.requiredProtocolVersion(); ProtocolVersion recommendedProtocolVersion = _dsi.recommendedProtocolVersion(); IProtocolVersions protocolVersions = _dso.protocolVersionsImpl(); vm.startBroadcast(msg.sender); - // Deploy ProtocolVersion proxy protocolVersionsProxy = IProtocolVersions( DeployUtils.create1({ _name: "Proxy", @@ -520,6 +519,7 @@ contract DeploySuperchain is Script { ) }) ); + superchainProxyAdmin.upgradeAndCall( payable(address(protocolVersionsProxy)), address(protocolVersions), @@ -531,37 +531,8 @@ contract DeploySuperchain is Script { vm.stopBroadcast(); } - // Deploy SharedLockbox proxy - vm.startBroadcast(msg.sender); - ISharedLockbox sharedLockboxProxy = ISharedLockbox( - DeployUtils.create1({ - _name: "Proxy", - _args: DeployUtils.encodeConstructor( - abi.encodeCall(IProxy.__constructor__, (address(superchainProxyAdmin))) - ) - }) - ); - superchainProxyAdmin.upgrade(payable(address(sharedLockboxProxy)), address(_dso.sharedLockboxImpl())); - vm.stopBroadcast(); - - vm.label(address(superchainConfigProxy), "SuperchainConfigProxy"); - _dso.set(_dso.superchainConfigProxy.selector, address(superchainConfigProxy)); - // To ensure deployments are correct, check that the precalculated address matches the actual address. - require( - address(superchainConfigProxy) == _precalculatedAddresses.superchainConfigProxy, - "SuperchainConfig: expected address mismatch" - ); - vm.label(address(protocolVersionsProxy), "ProtocolVersionsProxy"); _dso.set(_dso.protocolVersionsProxy.selector, address(protocolVersionsProxy)); - - vm.label(address(sharedLockboxProxy), "SharedLockboxProxy"); - _dso.set(_dso.sharedLockboxProxy.selector, address(sharedLockboxProxy)); - // To ensure deployments are correct, check that the precalculated address matches the actual address. - require( - address(sharedLockboxProxy) == _precalculatedAddresses.sharedLockboxProxy, - "SharedLockbox: expected address mismatch" - ); } function transferProxyAdminOwnership(DeploySuperchainInput _dsi, DeploySuperchainOutput _dso) public { @@ -592,3 +563,129 @@ contract DeploySuperchain is Script { dso_ = DeploySuperchainOutput(DeployUtils.toIOAddress(msg.sender, "optimism.DeploySuperchainOutput")); } } + +/// @notice This contract is an extension of the `DeploySuperchain` contract that adds the deployment of the +/// SharedLockbox implementation and proxy contracts. This contract is used when deploying the +/// Superchain in an interop environment. It also overrides the `deploySuperchainConfigImplementation` +/// and `deploySuperchainConfigProxy` methods to deploy the `SuperchainConfigInterop` implementation +/// and proxy contracts. +contract DeploySuperchainInterop is DeploySuperchain { + function deploySuperchainImplementationContracts( + DeploySuperchainInput _dsi, + DeploySuperchainOutput _dso + ) + public + override + { + super.deploySuperchainImplementationContracts(_dsi, _dso); + + deploySharedLockboxImplementation(_dso); + } + + function deployAndInitializeSuperchainProxyContracts( + DeploySuperchainInput _dsi, + DeploySuperchainOutput _dso + ) + public + override + { + // Precalculate the SuperchainConfig address. Needed in the SharedLockbox initialization. + address _precalculatedSuperchainConfigProxy = vm.computeCreateAddress(msg.sender, vm.getNonce(msg.sender) + 2); + + deploySharedLockboxProxy(_dso, _precalculatedSuperchainConfigProxy); + + super.deployAndInitializeSuperchainProxyContracts(_dsi, _dso); + + // To ensure deployments are correct, check that the precalculated address matches the actual address. + require( + address(_dso.superchainConfigProxy()) == _precalculatedSuperchainConfigProxy, + "SuperchainConifg: expected address mismatch" + ); + } + + function deploySharedLockboxImplementation(DeploySuperchainOutput _dso) public virtual { + vm.broadcast(msg.sender); + ISharedLockbox sharedLockboxImpl = ISharedLockbox( + DeployUtils.create1({ + _name: "SharedLockbox", + _args: DeployUtils.encodeConstructor(abi.encodeCall(ISharedLockbox.__constructor__, ())) + }) + ); + + vm.label(address(sharedLockboxImpl), "SharedLockboxImpl"); + _dso.set(_dso.sharedLockboxImpl.selector, address(sharedLockboxImpl)); + } + + function deploySharedLockboxProxy(DeploySuperchainOutput _dso, address _superchainConfigProxy) public { + ISharedLockbox sharedLockboxProxy; + { + IProxyAdmin superchainProxyAdmin = _dso.superchainProxyAdmin(); + + vm.startBroadcast(msg.sender); + sharedLockboxProxy = ISharedLockbox( + DeployUtils.create1({ + _name: "Proxy", + _args: DeployUtils.encodeConstructor( + abi.encodeCall(IProxy.__constructor__, (address(superchainProxyAdmin))) + ) + }) + ); + + superchainProxyAdmin.upgradeAndCall( + payable(address(sharedLockboxProxy)), + address(_dso.sharedLockboxImpl()), + abi.encodeCall(ISharedLockbox.initialize, (_superchainConfigProxy)) + ); + vm.stopBroadcast(); + } + + vm.label(address(sharedLockboxProxy), "SharedLockboxProxy"); + _dso.set(_dso.sharedLockboxProxy.selector, address(sharedLockboxProxy)); + } + + function deploySuperchainConfigImplementation(DeploySuperchainOutput _dso) public override { + vm.broadcast(msg.sender); + ISuperchainConfigInterop superchainConfigImpl = ISuperchainConfigInterop( + DeployUtils.create1({ + _name: "SuperchainConfigInterop", + _args: DeployUtils.encodeConstructor(abi.encodeCall(ISuperchainConfigInterop.__constructor__, ())) + }) + ); + + vm.label(address(superchainConfigImpl), "SuperchainConfigImpl"); + _dso.set(_dso.superchainConfigImpl.selector, address(superchainConfigImpl)); + } + + function deploySuperchainConfigProxy(DeploySuperchainInput _dsi, DeploySuperchainOutput _dso) public override { + ISuperchainConfigInterop superchainConfigProxy; + { + IProxyAdmin superchainProxyAdmin = _dso.superchainProxyAdmin(); + address guardian = _dsi.guardian(); + address clusterManager = _dsi.superchainProxyAdminOwner(); + bool paused = _dsi.paused(); + address sharedLockboxProxy = address(_dso.sharedLockboxProxy()); + + vm.startBroadcast(msg.sender); + superchainConfigProxy = ISuperchainConfigInterop( + DeployUtils.create1({ + _name: "Proxy", + _args: DeployUtils.encodeConstructor( + abi.encodeCall(IProxy.__constructor__, (address(superchainProxyAdmin))) + ) + }) + ); + + superchainProxyAdmin.upgradeAndCall( + payable(address(superchainConfigProxy)), + address(_dso.superchainConfigImpl()), + abi.encodeCall( + ISuperchainConfigInterop.initialize, (guardian, paused, clusterManager, sharedLockboxProxy) + ) + ); + vm.stopBroadcast(); + + vm.label(address(superchainConfigProxy), "SuperchainConfigProxy"); + _dso.set(_dso.superchainConfigProxy.selector, address(superchainConfigProxy)); + } + } +} diff --git a/packages/contracts-bedrock/scripts/libraries/DeployUtils.sol b/packages/contracts-bedrock/scripts/libraries/DeployUtils.sol index 02ae5176548..757ca33d1cc 100644 --- a/packages/contracts-bedrock/scripts/libraries/DeployUtils.sol +++ b/packages/contracts-bedrock/scripts/libraries/DeployUtils.sol @@ -364,4 +364,19 @@ library DeployUtils { require(val == type(uint8).max, "DeployUtils: storage value is not 0xff at the given slot and offset"); } } + + function assertInitializedOZv5(address _contractAddress, bool _isProxy) internal view { + // keccak256(abi.encode(uint256(keccak256("openzeppelin.storage.Initializable")) - 1)) & ~bytes32(uint256(0xff)) + bytes32 INITIALIZABLE_STORAGE = 0xf0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a00; + + bytes32 slotVal = vm.load(_contractAddress, INITIALIZABLE_STORAGE); + uint64 initialized = uint64(uint256(slotVal) & 0xFFFFFFFFFFFFFFFF); + if (_isProxy) { + require(initialized == 1, "DeployUtils: storage value is not 1 at the given slot and offset"); + } else { + require( + initialized == type(uint64).max, "DeployUtils: storage value is not 0xff at the given slot and offset" + ); + } + } } diff --git a/packages/contracts-bedrock/snapshots/.gas-snapshot b/packages/contracts-bedrock/snapshots/.gas-snapshot index d95bcda2a72..b9ae66d23c9 100644 --- a/packages/contracts-bedrock/snapshots/.gas-snapshot +++ b/packages/contracts-bedrock/snapshots/.gas-snapshot @@ -4,10 +4,10 @@ GasBenchMark_L1BlockInterop_SetValuesInterop:test_setL1BlockValuesInterop_benchm GasBenchMark_L1BlockInterop_SetValuesInterop_Warm:test_setL1BlockValuesInterop_benchmark() (gas: 5099) GasBenchMark_L1Block_SetValuesEcotone:test_setL1BlockValuesEcotone_benchmark() (gas: 158531) GasBenchMark_L1Block_SetValuesEcotone_Warm:test_setL1BlockValuesEcotone_benchmark() (gas: 7597) -GasBenchMark_L1CrossDomainMessenger:test_sendMessage_benchmark_0() (gas: 369371) -GasBenchMark_L1CrossDomainMessenger:test_sendMessage_benchmark_1() (gas: 2967533) -GasBenchMark_L1StandardBridge_Deposit:test_depositERC20_benchmark_0() (gas: 564520) -GasBenchMark_L1StandardBridge_Deposit:test_depositERC20_benchmark_1() (gas: 4076496) -GasBenchMark_L1StandardBridge_Deposit:test_depositETH_benchmark_0() (gas: 467036) -GasBenchMark_L1StandardBridge_Deposit:test_depositETH_benchmark_1() (gas: 3512785) -GasBenchMark_L1StandardBridge_Finalize:test_finalizeETHWithdrawal_benchmark() (gas: 72715) \ No newline at end of file +GasBenchMark_L1CrossDomainMessenger:test_sendMessage_benchmark_0() (gas: 369356) +GasBenchMark_L1CrossDomainMessenger:test_sendMessage_benchmark_1() (gas: 2967518) +GasBenchMark_L1StandardBridge_Deposit:test_depositERC20_benchmark_0() (gas: 564505) +GasBenchMark_L1StandardBridge_Deposit:test_depositERC20_benchmark_1() (gas: 4076653) +GasBenchMark_L1StandardBridge_Deposit:test_depositETH_benchmark_0() (gas: 466945) +GasBenchMark_L1StandardBridge_Deposit:test_depositETH_benchmark_1() (gas: 3512694) +GasBenchMark_L1StandardBridge_Finalize:test_finalizeETHWithdrawal_benchmark() (gas: 72670) \ No newline at end of file diff --git a/packages/contracts-bedrock/snapshots/abi/CrossL2Inbox.json b/packages/contracts-bedrock/snapshots/abi/CrossL2Inbox.json index 436e8b17bb1..b89344e5104 100644 --- a/packages/contracts-bedrock/snapshots/abi/CrossL2Inbox.json +++ b/packages/contracts-bedrock/snapshots/abi/CrossL2Inbox.json @@ -1,139 +1,4 @@ [ - { - "inputs": [], - "name": "blockNumber", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "chainId", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "components": [ - { - "internalType": "address", - "name": "origin", - "type": "address" - }, - { - "internalType": "uint256", - "name": "blockNumber", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "logIndex", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "timestamp", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "chainId", - "type": "uint256" - } - ], - "internalType": "struct Identifier", - "name": "_id", - "type": "tuple" - }, - { - "internalType": "address", - "name": "_target", - "type": "address" - }, - { - "internalType": "bytes", - "name": "_message", - "type": "bytes" - } - ], - "name": "executeMessage", - "outputs": [], - "stateMutability": "payable", - "type": "function" - }, - { - "inputs": [], - "name": "interopStart", - "outputs": [ - { - "internalType": "uint256", - "name": "interopStart_", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "logIndex", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "origin", - "outputs": [ - { - "internalType": "address", - "name": "", - "type": "address" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "setInteropStart", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [], - "name": "timestamp", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, { "inputs": [ { @@ -238,39 +103,9 @@ "name": "ExecutingMessage", "type": "event" }, - { - "inputs": [], - "name": "InteropStartAlreadySet", - "type": "error" - }, - { - "inputs": [], - "name": "InvalidTimestamp", - "type": "error" - }, { "inputs": [], "name": "NoExecutingDeposits", "type": "error" - }, - { - "inputs": [], - "name": "NotDepositor", - "type": "error" - }, - { - "inputs": [], - "name": "NotEntered", - "type": "error" - }, - { - "inputs": [], - "name": "ReentrantCall", - "type": "error" - }, - { - "inputs": [], - "name": "TargetCallFailed", - "type": "error" } ] \ No newline at end of file diff --git a/packages/contracts-bedrock/snapshots/abi/DependencyManager.json b/packages/contracts-bedrock/snapshots/abi/DependencyManager.json new file mode 100644 index 00000000000..fab9b7c97ff --- /dev/null +++ b/packages/contracts-bedrock/snapshots/abi/DependencyManager.json @@ -0,0 +1,123 @@ +[ + { + "inputs": [ + { + "internalType": "address", + "name": "_superchainConfig", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_chainId", + "type": "uint256" + }, + { + "internalType": "address", + "name": "_systemConfig", + "type": "address" + } + ], + "name": "addDependency", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "dependencySet", + "outputs": [ + { + "internalType": "uint256[]", + "name": "", + "type": "uint256[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "dependencySetSize", + "outputs": [ + { + "internalType": "uint8", + "name": "", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_chainId", + "type": "uint256" + } + ], + "name": "isInDependencySet", + "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": true, + "internalType": "uint256", + "name": "chainId", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "address", + "name": "systemConfig", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "superchainConfig", + "type": "address" + } + ], + "name": "DependencyAdded", + "type": "event" + }, + { + "inputs": [], + "name": "AlreadyDependency", + "type": "error" + }, + { + "inputs": [], + "name": "DependencySetSizeTooLarge", + "type": "error" + }, + { + "inputs": [], + "name": "Unauthorized", + "type": "error" + } +] \ No newline at end of file diff --git a/packages/contracts-bedrock/snapshots/abi/L2ToL2CrossDomainMessenger.json b/packages/contracts-bedrock/snapshots/abi/L2ToL2CrossDomainMessenger.json index e4cea3651b3..3d381ec9ae5 100644 --- a/packages/contracts-bedrock/snapshots/abi/L2ToL2CrossDomainMessenger.json +++ b/packages/contracts-bedrock/snapshots/abi/L2ToL2CrossDomainMessenger.json @@ -253,6 +253,11 @@ "name": "IdOriginNotL2ToL2CrossDomainMessenger", "type": "error" }, + { + "inputs": [], + "name": "InvalidChainId", + "type": "error" + }, { "inputs": [], "name": "MessageAlreadyRelayed", diff --git a/packages/contracts-bedrock/snapshots/abi/LiquidityMigrator.json b/packages/contracts-bedrock/snapshots/abi/LiquidityMigrator.json deleted file mode 100644 index c1c723a9026..00000000000 --- a/packages/contracts-bedrock/snapshots/abi/LiquidityMigrator.json +++ /dev/null @@ -1,59 +0,0 @@ -[ - { - "inputs": [ - { - "internalType": "address", - "name": "_sharedLockbox", - "type": "address" - } - ], - "stateMutability": "nonpayable", - "type": "constructor" - }, - { - "inputs": [], - "name": "SHARED_LOCKBOX", - "outputs": [ - { - "internalType": "contract ISharedLockbox", - "name": "", - "type": "address" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "migrateETH", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [], - "name": "version", - "outputs": [ - { - "internalType": "string", - "name": "", - "type": "string" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": false, - "internalType": "uint256", - "name": "amount", - "type": "uint256" - } - ], - "name": "ETHMigrated", - "type": "event" - } -] \ No newline at end of file diff --git a/packages/contracts-bedrock/snapshots/abi/OptimismPortal2.json b/packages/contracts-bedrock/snapshots/abi/OptimismPortal2.json index 2d05708e080..99f31298fe6 100644 --- a/packages/contracts-bedrock/snapshots/abi/OptimismPortal2.json +++ b/packages/contracts-bedrock/snapshots/abi/OptimismPortal2.json @@ -643,19 +643,6 @@ "stateMutability": "nonpayable", "type": "function" }, - { - "inputs": [], - "name": "sharedLockbox", - "outputs": [ - { - "internalType": "contract ISharedLockbox", - "name": "", - "type": "address" - } - ], - "stateMutability": "view", - "type": "function" - }, { "inputs": [], "name": "superchainConfig", diff --git a/packages/contracts-bedrock/snapshots/abi/OptimismPortalInterop.json b/packages/contracts-bedrock/snapshots/abi/OptimismPortalInterop.json index 275fed32772..9f25f323f33 100644 --- a/packages/contracts-bedrock/snapshots/abi/OptimismPortalInterop.json +++ b/packages/contracts-bedrock/snapshots/abi/OptimismPortalInterop.json @@ -354,6 +354,26 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [], + "name": "migrateLiquidity", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "migrated", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [ { @@ -726,6 +746,19 @@ "name": "DisputeGameBlacklisted", "type": "event" }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "ETHMigrated", + "type": "event" + }, { "anonymous": false, "inputs": [ diff --git a/packages/contracts-bedrock/snapshots/abi/SharedLockbox.json b/packages/contracts-bedrock/snapshots/abi/SharedLockbox.json index ef196133952..c48887ea9c1 100644 --- a/packages/contracts-bedrock/snapshots/abi/SharedLockbox.json +++ b/packages/contracts-bedrock/snapshots/abi/SharedLockbox.json @@ -1,60 +1,22 @@ [ { - "inputs": [ - { - "internalType": "address", - "name": "_superchainConfig", - "type": "address" - } - ], + "inputs": [], "stateMutability": "nonpayable", "type": "constructor" }, - { - "inputs": [], - "name": "SUPERCHAIN_CONFIG", - "outputs": [ - { - "internalType": "contract ISuperchainConfig", - "name": "", - "type": "address" - } - ], - "stateMutability": "view", - "type": "function" - }, { "inputs": [ { "internalType": "address", - "name": "_portal", + "name": "_superchainConfig", "type": "address" } ], - "name": "authorizePortal", + "name": "initialize", "outputs": [], "stateMutability": "nonpayable", "type": "function" }, - { - "inputs": [ - { - "internalType": "address", - "name": "", - "type": "address" - } - ], - "name": "authorizedPortals", - "outputs": [ - { - "internalType": "bool", - "name": "", - "type": "bool" - } - ], - "stateMutability": "view", - "type": "function" - }, { "inputs": [], "name": "lockETH", @@ -75,6 +37,19 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [], + "name": "superchainConfig", + "outputs": [ + { + "internalType": "contract ISuperchainConfigInterop", + "name": "superchainConfig_", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [ { @@ -139,6 +114,19 @@ "name": "ETHUnlocked", "type": "event" }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint64", + "name": "version", + "type": "uint64" + } + ], + "name": "Initialized", + "type": "event" + }, { "anonymous": false, "inputs": [ @@ -152,6 +140,16 @@ "name": "PortalAuthorized", "type": "event" }, + { + "inputs": [], + "name": "InvalidInitialization", + "type": "error" + }, + { + "inputs": [], + "name": "NotInitializing", + "type": "error" + }, { "inputs": [], "name": "Paused", diff --git a/packages/contracts-bedrock/snapshots/abi/SuperchainConfig.json b/packages/contracts-bedrock/snapshots/abi/SuperchainConfig.json index 9778ede92ea..451569d5062 100644 --- a/packages/contracts-bedrock/snapshots/abi/SuperchainConfig.json +++ b/packages/contracts-bedrock/snapshots/abi/SuperchainConfig.json @@ -1,28 +1,9 @@ [ { - "inputs": [ - { - "internalType": "address", - "name": "_sharedLockbox", - "type": "address" - } - ], + "inputs": [], "stateMutability": "nonpayable", "type": "constructor" }, - { - "inputs": [], - "name": "DEPENDENCY_MANAGER_SLOT", - "outputs": [ - { - "internalType": "bytes32", - "name": "", - "type": "bytes32" - } - ], - "stateMutability": "view", - "type": "function" - }, { "inputs": [], "name": "GUARDIAN_SLOT", @@ -49,76 +30,6 @@ "stateMutability": "view", "type": "function" }, - { - "inputs": [], - "name": "SHARED_LOCKBOX", - "outputs": [ - { - "internalType": "contract ISharedLockbox", - "name": "", - "type": "address" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "_chainId", - "type": "uint256" - }, - { - "internalType": "address", - "name": "_systemConfig", - "type": "address" - } - ], - "name": "addDependency", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [], - "name": "dependencyManager", - "outputs": [ - { - "internalType": "address", - "name": "dependencyManager_", - "type": "address" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "dependencySet", - "outputs": [ - { - "internalType": "uint256[]", - "name": "", - "type": "uint256[]" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "dependencySetSize", - "outputs": [ - { - "internalType": "uint8", - "name": "", - "type": "uint8" - } - ], - "stateMutability": "view", - "type": "function" - }, { "inputs": [], "name": "guardian", @@ -139,11 +50,6 @@ "name": "_guardian", "type": "address" }, - { - "internalType": "address", - "name": "_dependencyManager", - "type": "address" - }, { "internalType": "bool", "name": "_paused", @@ -155,25 +61,6 @@ "stateMutability": "nonpayable", "type": "function" }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "_chainId", - "type": "uint256" - } - ], - "name": "isInDependencySet", - "outputs": [ - { - "internalType": "bool", - "name": "", - "type": "bool" - } - ], - "stateMutability": "view", - "type": "function" - }, { "inputs": [ { @@ -217,7 +104,7 @@ "type": "string" } ], - "stateMutability": "view", + "stateMutability": "pure", "type": "function" }, { @@ -239,31 +126,6 @@ "name": "ConfigUpdate", "type": "event" }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "uint256", - "name": "chainId", - "type": "uint256" - }, - { - "indexed": true, - "internalType": "address", - "name": "systemConfig", - "type": "address" - }, - { - "indexed": true, - "internalType": "address", - "name": "portal", - "type": "address" - } - ], - "name": "DependencyAdded", - "type": "event" - }, { "anonymous": false, "inputs": [ @@ -295,25 +157,5 @@ "inputs": [], "name": "Unpaused", "type": "event" - }, - { - "inputs": [], - "name": "DependencyAlreadyAdded", - "type": "error" - }, - { - "inputs": [], - "name": "DependencySetTooLarge", - "type": "error" - }, - { - "inputs": [], - "name": "InvalidChainID", - "type": "error" - }, - { - "inputs": [], - "name": "Unauthorized", - "type": "error" } ] \ No newline at end of file diff --git a/packages/contracts-bedrock/snapshots/abi/SuperchainConfigInterop.json b/packages/contracts-bedrock/snapshots/abi/SuperchainConfigInterop.json new file mode 100644 index 00000000000..c3e1a0f6958 --- /dev/null +++ b/packages/contracts-bedrock/snapshots/abi/SuperchainConfigInterop.json @@ -0,0 +1,355 @@ +[ + { + "inputs": [], + "name": "CLUSTER_MANAGER_SLOT", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "GUARDIAN_SLOT", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "PAUSED_SLOT", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_chainId", + "type": "uint256" + }, + { + "internalType": "address", + "name": "_systemConfig", + "type": "address" + } + ], + "name": "addDependency", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_portal", + "type": "address" + } + ], + "name": "authorizedPortals", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "clusterManager", + "outputs": [ + { + "internalType": "address", + "name": "clusterManager_", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "dependencySet", + "outputs": [ + { + "internalType": "uint256[]", + "name": "", + "type": "uint256[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "dependencySetSize", + "outputs": [ + { + "internalType": "uint8", + "name": "", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "guardian", + "outputs": [ + { + "internalType": "address", + "name": "guardian_", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_guardian", + "type": "address" + }, + { + "internalType": "bool", + "name": "_paused", + "type": "bool" + } + ], + "name": "initialize", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_guardian", + "type": "address" + }, + { + "internalType": "bool", + "name": "_paused", + "type": "bool" + }, + { + "internalType": "address", + "name": "_clusterManager", + "type": "address" + }, + { + "internalType": "address", + "name": "_sharedLockbox", + "type": "address" + } + ], + "name": "initialize", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_chainId", + "type": "uint256" + } + ], + "name": "isInDependencySet", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "string", + "name": "_identifier", + "type": "string" + } + ], + "name": "pause", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "paused", + "outputs": [ + { + "internalType": "bool", + "name": "paused_", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "sharedLockbox", + "outputs": [ + { + "internalType": "contract ISharedLockbox", + "name": "sharedLockbox_", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "unpause", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "version", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "enum SuperchainConfig.UpdateType", + "name": "updateType", + "type": "uint8" + }, + { + "indexed": false, + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "ConfigUpdate", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "chainId", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "address", + "name": "systemConfig", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "portal", + "type": "address" + } + ], + "name": "DependencyAdded", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint8", + "name": "version", + "type": "uint8" + } + ], + "name": "Initialized", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "string", + "name": "identifier", + "type": "string" + } + ], + "name": "Paused", + "type": "event" + }, + { + "anonymous": false, + "inputs": [], + "name": "Unpaused", + "type": "event" + }, + { + "inputs": [], + "name": "DependencyAlreadyAdded", + "type": "error" + }, + { + "inputs": [], + "name": "DependencySetTooLarge", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidSuperchainConfig", + "type": "error" + }, + { + "inputs": [], + "name": "PortalAlreadyAuthorized", + "type": "error" + }, + { + "inputs": [], + "name": "Unauthorized", + "type": "error" + } +] \ No newline at end of file diff --git a/packages/contracts-bedrock/snapshots/semver-lock.json b/packages/contracts-bedrock/snapshots/semver-lock.json index 616f4915d7d..5e03078c406 100644 --- a/packages/contracts-bedrock/snapshots/semver-lock.json +++ b/packages/contracts-bedrock/snapshots/semver-lock.json @@ -15,33 +15,33 @@ "initCodeHash": "0x7d7030359826f64714ef0c2a5198901812fb0a99e949f23fe54ccf87a0df2e67", "sourceCodeHash": "0xa91b445bdc666a02ba18e3b91ba94b6d54bbe65da714002fc734814201319d57" }, - "src/L1/LiquidityMigrator.sol": { - "initCodeHash": "0x708f764a2de821caa3d520c93f1951e24128b136a5c41b06e2b1444a1a34e2e8", - "sourceCodeHash": "0x4f719e707583e2b23b9fcbd6e70935df099f45ac0efc50d1156051609bc26f69" - }, "src/L1/OPContractsManager.sol": { "initCodeHash": "0x4b413cbe79bd10d41d8f3e9f0408e773dd49ced823d457b9f9aa92f446828105", "sourceCodeHash": "0xe5179a20ae40d4e4773c52df98bac67e73e04044bec9e8750073b4e2f14fe81b" }, "src/L1/OptimismPortal2.sol": { - "initCodeHash": "0x10d33a81a25ff45e7cd6396c4f99ad586665a9c0706f8357a3244c0410bf2729", - "sourceCodeHash": "0x5aa05704d392c6f5b5414a5709c9d8e2da5b4eb4eed6d25feacc571b262e97ee" + "initCodeHash": "0x554049cefba17dabf4ae3b992ef9d4d3c25743b61289906f3deabf90bf3ddad0", + "sourceCodeHash": "0xfc2e42014c74389c45cf2c29ef0404db63bc0888187a9e2a28d1e919e601c4dc" }, "src/L1/OptimismPortalInterop.sol": { - "initCodeHash": "0xa4b0fda7eaba55abdb1709d7df4875352cb51d9cb49d3134d9f2f6e0bb9a465d", - "sourceCodeHash": "0xcbd298a6a8bdb5afc8ee18796a3a506104e43ac9532e5e4c56885128f2c234f2" + "initCodeHash": "0x59b9c40297b84461349520420253cab92883956df7851261f1cab384da362ad1", + "sourceCodeHash": "0xadac4a408d74286d97f090086988e7c746377bf631c4e5d80427c68da436322f" }, "src/L1/ProtocolVersions.sol": { "initCodeHash": "0x0000ec89712d8b4609873f1ba76afffd4205bf9110818995c90134dbec12e91e", "sourceCodeHash": "0xd4284db247fc1e686bd4b57755c7ac9a073a173d6df4f93448eb7fb5518882f7" }, "src/L1/SharedLockbox.sol": { - "initCodeHash": "0x914d95090b8d5b37744a030cd9ac5a2b57c367d695f6a6ae3ad3aa894c599c3c", - "sourceCodeHash": "0x6467cbe384f707e42a661b2f14c316fd7744ceacb91123df8e1a681755e58f85" + "initCodeHash": "0x68ad298c875810ed64e09c714c2feed6d132c8c9442d33355dd4a278fc9ac66b", + "sourceCodeHash": "0x6037e76fb5f4052fb4a73550b3eb3e569470dc1ef80c8f3c38f729ca7bd6ec58" }, "src/L1/SuperchainConfig.sol": { - "initCodeHash": "0x358d0b58ec3749fd7c61543548225bc7a946e8ddcac61d74ba8bf76337a28ed4", - "sourceCodeHash": "0x53b5514e15c386abcd5209763e9454f257ab88972c59ac777eb7511beb2fa1bd" + "initCodeHash": "0xf20b1ea7a64a32a8cf0cd69f93d27ffd3154b651a9530d2d4d0e8d55d30b53c9", + "sourceCodeHash": "0xed2c0eca24f007f2be7a06422171425b91f5005fe947ecb3a3330bbac1565182" + }, + "src/L1/SuperchainConfigInterop.sol": { + "initCodeHash": "0x8a51d90aaa078feedb078baae78bb1807bb86742f14108d6d7264c4b8437e3f7", + "sourceCodeHash": "0x109aeff1a34e5a5aab6331e3aa4f69aec9591f3d908338205639569c26d3dbbe" }, "src/L1/SystemConfig.sol": { "initCodeHash": "0xbb18eef17cdc1d0d307b0241e818820063e3ce3c7021ea3bb3a85ff6e79659e1", @@ -56,8 +56,12 @@ "sourceCodeHash": "0xfa56426153227e798150f6becc30a33fd20a3c6e0d73c797a3922dd631acbb57" }, "src/L2/CrossL2Inbox.sol": { - "initCodeHash": "0x12c325f9898460dc42f54febcfe312a4a92d6b78b78586f54c7be9ae8d5b39be", - "sourceCodeHash": "0xec115e27a78d76a47d8fbb6454d049a5f8314fe0283a4a9cbd44f286388cb0f0" + "initCodeHash": "0x8bbf318357b2529530b538095d6ef238c8791006295c95bbe4a5025828a1bc13", + "sourceCodeHash": "0xc3b3f050c2b54b9513a50bfce2fb916eeae4fefc097788d240d8479a02d0fc13" + }, + "src/L2/DependencyManager.sol": { + "initCodeHash": "0xb2e0d759cef5b82efd3b4644b6490c1db2d6d865075937d5b07f23121912a46d", + "sourceCodeHash": "0x9db9b334dcd328e9c1647de18a55ee240683c6296bb0684286b87c751b349f31" }, "src/L2/ETHLiquidity.sol": { "initCodeHash": "0xbb16de6a3f678db7301694a000f315154f25f9660c8dcec4b0bef20bc7cfdebd", @@ -100,8 +104,8 @@ "sourceCodeHash": "0xaef8ea36c5b78cd12e0e62811d51db627ccf0dfd2cc5479fb707a10ef0d42048" }, "src/L2/L2ToL2CrossDomainMessenger.sol": { - "initCodeHash": "0x115a00cfd2432d8b230302ec9a22f611ec23420aecd4b35d3d1d42041db820a9", - "sourceCodeHash": "0x067e879cd1acf2348e0729dd02395c064d24cf4571fdeade8bc2eefd8accec87" + "initCodeHash": "0x86f1ff98c5ffe19a400c65a112b3e4f16242dcbcb635f636d63f25dbd3732d58", + "sourceCodeHash": "0x00237042c23a463b0aaa05fa95c819d246ca8fb8deb6a63510d85bd2a988fce1" }, "src/L2/OptimismMintableERC721.sol": { "initCodeHash": "0xcfa6ad9997a422aef5a19a490a0a535bc870ee34b1f5258c2949eb3680f71e8a", diff --git a/packages/contracts-bedrock/snapshots/storageLayout/DependencyManager.json b/packages/contracts-bedrock/snapshots/storageLayout/DependencyManager.json new file mode 100644 index 00000000000..7995fc18635 --- /dev/null +++ b/packages/contracts-bedrock/snapshots/storageLayout/DependencyManager.json @@ -0,0 +1,9 @@ +[ + { + "bytes": "64", + "label": "_dependencySet", + "offset": 0, + "slot": "0", + "type": "struct EnumerableSet.UintSet" + } +] \ No newline at end of file diff --git a/packages/contracts-bedrock/snapshots/storageLayout/LiquidityMigrator.json b/packages/contracts-bedrock/snapshots/storageLayout/LiquidityMigrator.json deleted file mode 100644 index 0637a088a01..00000000000 --- a/packages/contracts-bedrock/snapshots/storageLayout/LiquidityMigrator.json +++ /dev/null @@ -1 +0,0 @@ -[] \ No newline at end of file diff --git a/packages/contracts-bedrock/snapshots/storageLayout/SharedLockbox.json b/packages/contracts-bedrock/snapshots/storageLayout/SharedLockbox.json index 7c441f14dcc..0637a088a01 100644 --- a/packages/contracts-bedrock/snapshots/storageLayout/SharedLockbox.json +++ b/packages/contracts-bedrock/snapshots/storageLayout/SharedLockbox.json @@ -1,9 +1 @@ -[ - { - "bytes": "32", - "label": "authorizedPortals", - "offset": 0, - "slot": "0", - "type": "mapping(address => bool)" - } -] \ No newline at end of file +[] \ No newline at end of file diff --git a/packages/contracts-bedrock/snapshots/storageLayout/SuperchainConfig.json b/packages/contracts-bedrock/snapshots/storageLayout/SuperchainConfig.json index 9416523abb2..70e559b7bf6 100644 --- a/packages/contracts-bedrock/snapshots/storageLayout/SuperchainConfig.json +++ b/packages/contracts-bedrock/snapshots/storageLayout/SuperchainConfig.json @@ -12,12 +12,5 @@ "offset": 1, "slot": "0", "type": "bool" - }, - { - "bytes": "64", - "label": "_dependencySet", - "offset": 0, - "slot": "1", - "type": "struct EnumerableSet.UintSet" } ] \ No newline at end of file diff --git a/packages/contracts-bedrock/snapshots/storageLayout/SuperchainConfigInterop.json b/packages/contracts-bedrock/snapshots/storageLayout/SuperchainConfigInterop.json new file mode 100644 index 00000000000..70e559b7bf6 --- /dev/null +++ b/packages/contracts-bedrock/snapshots/storageLayout/SuperchainConfigInterop.json @@ -0,0 +1,16 @@ +[ + { + "bytes": "1", + "label": "_initialized", + "offset": 0, + "slot": "0", + "type": "uint8" + }, + { + "bytes": "1", + "label": "_initializing", + "offset": 1, + "slot": "0", + "type": "bool" + } +] \ No newline at end of file diff --git a/packages/contracts-bedrock/src/L1/LiquidityMigrator.sol b/packages/contracts-bedrock/src/L1/LiquidityMigrator.sol deleted file mode 100644 index 37c3326c7f1..00000000000 --- a/packages/contracts-bedrock/src/L1/LiquidityMigrator.sol +++ /dev/null @@ -1,37 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.15; - -// Interfaces -import { ISemver } from "interfaces/universal/ISemver.sol"; -import { ISharedLockbox } from "interfaces/L1/ISharedLockbox.sol"; - -/// @custom:proxied true -/// @title LiquidityMigrator -/// @notice A contract to migrate the OptimisPortal's ETH balance to the SharedLockbox. One-time use logic, executed in -/// a batch of transactions to enable the SharedLockbox interaction within the OptimismPortal. -contract LiquidityMigrator is ISemver { - /// @notice Emitted when the contract's ETH balance is migrated to the SharedLockbox. - /// @param amount The amount corresponding to the contract's ETH balance migrated. - event ETHMigrated(uint256 amount); - - /// @notice The SharedLockbox contract. - ISharedLockbox public immutable SHARED_LOCKBOX; - - /// @notice Semantic version. - /// @custom:semver 1.0.0-beta.1 - string public constant version = "1.0.0-beta.1"; - - /// @notice Constructs the LiquidityMigrator contract. - /// @param _sharedLockbox The address of the SharedLockbox contract. - constructor(address _sharedLockbox) { - SHARED_LOCKBOX = ISharedLockbox(_sharedLockbox); - } - - /// @notice Migrates the contract's whole ETH balance to the SharedLockbox. - /// One-time use logic upgraded over OptimismPortalProxy address and then deprecated by another approval. - function migrateETH() external { - uint256 balance = address(this).balance; - SHARED_LOCKBOX.lockETH{ value: balance }(); - emit ETHMigrated(balance); - } -} diff --git a/packages/contracts-bedrock/src/L1/OptimismPortal2.sol b/packages/contracts-bedrock/src/L1/OptimismPortal2.sol index f9f61200c4f..74364e01a30 100644 --- a/packages/contracts-bedrock/src/L1/OptimismPortal2.sol +++ b/packages/contracts-bedrock/src/L1/OptimismPortal2.sol @@ -45,7 +45,6 @@ import { ISuperchainConfig } from "interfaces/L1/ISuperchainConfig.sol"; import { IDisputeGameFactory } from "interfaces/dispute/IDisputeGameFactory.sol"; import { IDisputeGame } from "interfaces/dispute/IDisputeGame.sol"; import { IL1Block } from "interfaces/L2/IL1Block.sol"; -import { ISharedLockbox } from "interfaces/L1/ISharedLockbox.sol"; /// @notice This is temporary. Error thrown when a chain uses a custom gas token. error CustomGasTokenNotSupported(); @@ -275,11 +274,6 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ISemver { return DISPUTE_GAME_FINALITY_DELAY_SECONDS; } - /// @notice Getter for the address of the shared lockbox. - function sharedLockbox() public view returns (ISharedLockbox) { - return superchainConfig.SHARED_LOCKBOX(); - } - /// @notice Computes the minimum gas limit for a deposit. /// The minimum gas limit linearly increases based on the size of the calldata. /// This is to prevent users from creating L2 resource usage without paying for it. @@ -433,8 +427,9 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ISemver { bool success; (address token,) = gasPayingToken(); if (token == Constants.ETHER) { - // Unlock and receive the ETH from the shared lockbox. - if (_tx.value != 0) sharedLockbox().unlockETH(_tx.value); + // This function unlocks ETH from the SharedLockbox when using the OptimismPortalInterop contract. + // If the interop version is not used, this function is a no-ops. + if (_tx.value != 0) _unlockETH(_tx.value); // Trigger the call to the target contract. We use a custom low level method // SafeCall.callWithMinGas to ensure two key properties @@ -575,10 +570,9 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ISemver { if (token != Constants.ETHER && msg.value != 0) revert NoValue(); - if (token == Constants.ETHER && msg.value != 0) { - // Lock the ETH in the shared lockbox. - sharedLockbox().lockETH{ value: msg.value }(); - } + // This function locks ETH in the SharedLockbox when using the OptimismPortalInterop contract. + // If the interop version is not used, this function is a no-ops. + if (token == Constants.ETHER && msg.value != 0) _lockETH(); _depositTransaction({ _to: _to, @@ -750,4 +744,11 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ISemver { function numProofSubmitters(bytes32 _withdrawalHash) external view returns (uint256) { return proofSubmitters[_withdrawalHash].length; } + + /// @notice No-op function to be used to lock ETH in the SharedLockbox in the interop contract. + function _lockETH() internal virtual { } + + /// @notice No-op function to be used to unlock ETH from the SharedLockbox in the interop contract. + /// @param _value Amount of ETH to unlock + function _unlockETH(uint256 _value) internal virtual { } } diff --git a/packages/contracts-bedrock/src/L1/OptimismPortalInterop.sol b/packages/contracts-bedrock/src/L1/OptimismPortalInterop.sol index 466efab8da0..9d6f34cf864 100644 --- a/packages/contracts-bedrock/src/L1/OptimismPortalInterop.sol +++ b/packages/contracts-bedrock/src/L1/OptimismPortalInterop.sol @@ -11,6 +11,8 @@ import { Unauthorized } from "src/libraries/PortalErrors.sol"; // Interfaces import { IL1BlockInterop, ConfigType } from "interfaces/L2/IL1BlockInterop.sol"; +import { ISharedLockbox } from "interfaces/L1/ISharedLockbox.sol"; +import { ISuperchainConfigInterop } from "interfaces/L1/ISuperchainConfigInterop.sol"; /// @custom:proxied true /// @title OptimismPortalInterop @@ -18,6 +20,29 @@ import { IL1BlockInterop, ConfigType } from "interfaces/L2/IL1BlockInterop.sol"; /// and L2. Messages sent directly to the OptimismPortal have no form of replayability. /// Users are encouraged to use the L1CrossDomainMessenger for a higher-level interface. contract OptimismPortalInterop is OptimismPortal2 { + /// @notice Emitted when the contract migrates the ETH liquidity to the SharedLockbox. + /// @param amount Amount of ETH migrated. + event ETHMigrated(uint256 amount); + + /// @notice Storage slot that the OptimismPortalStorage struct is stored at. + /// keccak256(abi.encode(uint256(keccak256("optimismPortal.storage")) - 1)) & ~bytes32(uint256(0xff)); + bytes32 internal constant OPTIMISM_PORTAL_STORAGE_SLOT = + 0x554bed1aae13f6a1ca3b124bc567e2e458d6903a211d2d3a4ec21fca3b2b6c00; + + /// @notice Storage struct for the OptimismPortal specific storage data. + /// @custom:storage-location erc7201:OptimismPortal.storage + struct OptimismPortalStorage { + /// @notice A flag indicating whether the contract has migrated the ETH liquidity to the SharedLockbox. + bool migrated; + } + + /// @notice Returns the storage for the OptimismPortalStorage. + function _storage() private pure returns (OptimismPortalStorage storage storage_) { + assembly { + storage_.slot := OPTIMISM_PORTAL_STORAGE_SLOT + } + } + constructor( uint256 _proofMaturityDelaySeconds, uint256 _disputeGameFinalityDelaySeconds @@ -54,4 +79,42 @@ contract OptimismPortalInterop is OptimismPortal2 { ) ); } + + /// @notice Getter for the address of the shared lockbox. + function sharedLockbox() public view returns (ISharedLockbox) { + return ISuperchainConfigInterop(address(superchainConfig)).sharedLockbox(); + } + + /// @notice Getter for the migrated flag. + function migrated() external view returns (bool) { + return _storage().migrated; + } + + /// @notice Unlock and receive the ETH from the shared lockbox. + /// @param _value Amount of ETH to unlock. + function _unlockETH(uint256 _value) internal virtual override { + OptimismPortalStorage storage s = _storage(); + if (s.migrated) sharedLockbox().unlockETH(_value); + } + + /// @notice Locks the ETH in the shared lockbox. + function _lockETH() internal virtual override { + OptimismPortalStorage storage s = _storage(); + if (s.migrated) sharedLockbox().lockETH{ value: msg.value }(); + } + + /// @notice Migrates the ETH liquidity to the SharedLockbox. This function will only be called once by the + /// SuperchainConfig when adding this chain to the dependency set. + function migrateLiquidity() external { + if (msg.sender != address(superchainConfig)) revert Unauthorized(); + + OptimismPortalStorage storage s = _storage(); + s.migrated = true; + + uint256 ethBalance = address(this).balance; + + sharedLockbox().lockETH{ value: ethBalance }(); + + emit ETHMigrated(ethBalance); + } } diff --git a/packages/contracts-bedrock/src/L1/SharedLockbox.sol b/packages/contracts-bedrock/src/L1/SharedLockbox.sol index 896f185dca8..2775accd39d 100644 --- a/packages/contracts-bedrock/src/L1/SharedLockbox.sol +++ b/packages/contracts-bedrock/src/L1/SharedLockbox.sol @@ -1,16 +1,23 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.15; +pragma solidity 0.8.25; +// Contracts +import { Initializable } from "@openzeppelin/contracts-v5/proxy/utils/Initializable.sol"; + +// Libraries +import { Unauthorized, Paused } from "src/libraries/errors/CommonErrors.sol"; +import { Storage } from "src/libraries/Storage.sol"; + +// Interfaces import { ISemver } from "interfaces/universal/ISemver.sol"; import { IOptimismPortal2 as IOptimismPortal } from "interfaces/L1/IOptimismPortal2.sol"; -import { ISuperchainConfig } from "interfaces/L1/ISuperchainConfig.sol"; -import { Unauthorized, Paused } from "src/libraries/errors/CommonErrors.sol"; +import { ISuperchainConfigInterop } from "interfaces/L1/ISuperchainConfigInterop.sol"; /// @custom:proxied true /// @title SharedLockbox /// @notice Manages ETH liquidity locking and unlocking for authorized OptimismPortals, enabling unified ETH liquidity /// management across chains in the superchain cluster. -contract SharedLockbox is ISemver { +contract SharedLockbox is Initializable, ISemver { /// @notice Emitted when ETH is locked in the lockbox by an authorized portal. /// @param portal The address of the portal that locked the ETH. /// @param amount The amount of ETH locked. @@ -26,10 +33,7 @@ contract SharedLockbox is ISemver { event PortalAuthorized(address indexed portal); /// @notice The address of the SuperchainConfig contract. - ISuperchainConfig public immutable SUPERCHAIN_CONFIG; - - /// @notice OptimismPortals that are part of the dependency cluster authorized to interact with the SharedLockbox - mapping(address => bool) public authorizedPortals; + bytes32 internal constant SUPERCHAIN_CONFIG_SLOT = bytes32(uint256(keccak256("sharedLockbox.superchainConfig")) - 1); /// @notice Semantic version. /// @custom:semver 1.0.0-beta.1 @@ -38,9 +42,19 @@ contract SharedLockbox is ISemver { } /// @notice Constructs the SharedLockbox contract. + constructor() { + _disableInitializers(); + } + + /// @notice Initializer. /// @param _superchainConfig The address of the SuperchainConfig contract. - constructor(address _superchainConfig) { - SUPERCHAIN_CONFIG = ISuperchainConfig(_superchainConfig); + function initialize(address _superchainConfig) external initializer { + Storage.setAddress(SUPERCHAIN_CONFIG_SLOT, _superchainConfig); + } + + /// @notice Getter for the SuperchainConfig contract. + function superchainConfig() public view returns (ISuperchainConfigInterop superchainConfig_) { + superchainConfig_ = ISuperchainConfigInterop(Storage.getAddress(SUPERCHAIN_CONFIG_SLOT)); } /// @notice Reverts when paused. @@ -50,13 +64,13 @@ contract SharedLockbox is ISemver { /// @notice Getter for the current paused status. function paused() public view returns (bool) { - return SUPERCHAIN_CONFIG.paused(); + return superchainConfig().paused(); } /// @notice Locks ETH in the lockbox. /// Called by an authorized portal when migrating its ETH liquidity or when depositing with some ETH value. function lockETH() external payable { - if (!authorizedPortals[msg.sender]) revert Unauthorized(); + if (!superchainConfig().authorizedPortals(msg.sender)) revert Unauthorized(); emit ETHLocked(msg.sender, msg.value); } @@ -65,19 +79,10 @@ contract SharedLockbox is ISemver { /// Called by an authorized portal when finalizing a withdrawal that requires ETH. function unlockETH(uint256 _value) external { _whenNotPaused(); - if (!authorizedPortals[msg.sender]) revert Unauthorized(); + if (!superchainConfig().authorizedPortals(msg.sender)) revert Unauthorized(); // Using `donateETH` to avoid triggering a deposit IOptimismPortal(payable(msg.sender)).donateETH{ value: _value }(); emit ETHUnlocked(msg.sender, _value); } - - /// @notice Authorizes a portal to interact with the lockbox. - function authorizePortal(address _portal) external { - _whenNotPaused(); - if (msg.sender != address(SUPERCHAIN_CONFIG)) revert Unauthorized(); - - authorizedPortals[_portal] = true; - emit PortalAuthorized(_portal); - } } diff --git a/packages/contracts-bedrock/src/L1/SuperchainConfig.sol b/packages/contracts-bedrock/src/L1/SuperchainConfig.sol index 4cb6cb98b72..526eed8d8d7 100644 --- a/packages/contracts-bedrock/src/L1/SuperchainConfig.sol +++ b/packages/contracts-bedrock/src/L1/SuperchainConfig.sol @@ -6,10 +6,6 @@ import { Initializable } from "@openzeppelin/contracts/proxy/utils/Initializable // Libraries import { Storage } from "src/libraries/Storage.sol"; -import { EnumerableSet } from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; -import { Unauthorized } from "src/libraries/errors/CommonErrors.sol"; -import { ISystemConfig } from "interfaces/L1/ISystemConfig.sol"; -import { ISharedLockbox } from "interfaces/L1/ISharedLockbox.sol"; // Interfaces import { ISemver } from "interfaces/universal/ISemver.sol"; @@ -19,14 +15,12 @@ import { ISemver } from "interfaces/universal/ISemver.sol"; /// @title SuperchainConfig /// @notice The SuperchainConfig contract is used to manage configuration of global superchain values. contract SuperchainConfig is Initializable, ISemver { - using EnumerableSet for EnumerableSet.UintSet; - /// @notice Enum representing different types of updates. /// @custom:value GUARDIAN Represents an update to the guardian. - /// @custom:value DEPENDENCY_MANAGER Represents an update to the dependency manager. + /// @custom:value CLUSTER_MANAGER Represents an update to the cluster manager. enum UpdateType { GUARDIAN, - DEPENDENCY_MANAGER + CLUSTER_MANAGER } /// @notice Whether or not the Superchain is paused. @@ -36,14 +30,6 @@ contract SuperchainConfig is Initializable, ISemver { /// It can only be modified by an upgrade. bytes32 public constant GUARDIAN_SLOT = bytes32(uint256(keccak256("superchainConfig.guardian")) - 1); - /// @notice The address of the dependency manager, which can add a chain to the dependency set. - /// It can only be modified by an upgrade. - bytes32 public constant DEPENDENCY_MANAGER_SLOT = - bytes32(uint256(keccak256("superchainConfig.dependencyManager")) - 1); - - // The Shared Lockbox contract - ISharedLockbox public immutable SHARED_LOCKBOX; - /// @notice Emitted when the pause is triggered. /// @param identifier A string helping to identify provenance of the pause transaction. event Paused(string identifier); @@ -56,41 +42,26 @@ contract SuperchainConfig is Initializable, ISemver { /// @param data Encoded update data. event ConfigUpdate(UpdateType indexed updateType, bytes data); - /// @notice Emitted when a new dependency is added as part of the dependency set. - /// @param chainId The chain ID. - /// @param systemConfig The address of the SystemConfig contract. - /// @param portal The address of the OptimismPortal contract. - event DependencyAdded(uint256 indexed chainId, address indexed systemConfig, address indexed portal); - - /// @notice Thrown when the dependency set is too large to add a new dependency. - error DependencySetTooLarge(); - - /// @notice Thrown when the input chain ID is the same as the current chain ID. - error InvalidChainID(); - - /// @notice Thrown when the input dependency is already added to the set. - error DependencyAlreadyAdded(); - /// @notice Semantic version. /// @custom:semver 1.1.1-beta.5 - string public constant version = "1.1.1-beta.5"; - - // Dependency set of chains that are part of the same cluster - EnumerableSet.UintSet internal _dependencySet; + function version() public pure virtual returns (string memory) { + return "1.1.1-beta.5"; + } /// @notice Constructs the SuperchainConfig contract. - constructor(address _sharedLockbox) { - SHARED_LOCKBOX = ISharedLockbox(_sharedLockbox); + constructor() { _disableInitializers(); } /// @notice Initializer. /// @param _guardian Address of the guardian, can pause the OptimismPortal. - /// @param _dependencyManager Address of the dependencyManager, can add a chain to the dependency set. /// @param _paused Initial paused status. - function initialize(address _guardian, address _dependencyManager, bool _paused) external initializer { + function initialize(address _guardian, bool _paused) external initializer { + _initialize(_guardian, _paused); + } + + function _initialize(address _guardian, bool _paused) internal { _setGuardian(_guardian); - _setDependencyManager(_dependencyManager); if (_paused) { _pause("Initializer paused"); } @@ -101,11 +72,6 @@ contract SuperchainConfig is Initializable, ISemver { guardian_ = Storage.getAddress(GUARDIAN_SLOT); } - /// @notice Getter for the dependency manager address. - function dependencyManager() public view returns (address dependencyManager_) { - dependencyManager_ = Storage.getAddress(DEPENDENCY_MANAGER_SLOT); - } - /// @notice Getter for the current paused status. function paused() public view returns (bool paused_) { paused_ = Storage.getBool(PAUSED_SLOT); @@ -139,49 +105,4 @@ contract SuperchainConfig is Initializable, ISemver { Storage.setAddress(GUARDIAN_SLOT, _guardian); emit ConfigUpdate(UpdateType.GUARDIAN, abi.encode(_guardian)); } - - /// @notice Sets the dependency manager address. This is only callable during initialization, so an upgrade - /// will be required to change the dependency manager. - /// @param _dependencyManager The new dependency manager address. - function _setDependencyManager(address _dependencyManager) internal { - Storage.setAddress(DEPENDENCY_MANAGER_SLOT, _dependencyManager); - emit ConfigUpdate(UpdateType.DEPENDENCY_MANAGER, abi.encode(_dependencyManager)); - } - - /// @notice Adds a new dependency to the dependency set. It also authorizes it's OptimismPortal on the - /// SharedLockbox. Can only be called by the dependency manager. - /// @param _chainId The chain ID. - /// @param _systemConfig The SystemConfig contract address of the chain to add. - function addDependency(uint256 _chainId, address _systemConfig) external { - if (msg.sender != dependencyManager()) revert Unauthorized(); - - if (_dependencySet.length() == type(uint8).max) revert DependencySetTooLarge(); - if (_chainId == block.chainid) revert InvalidChainID(); - - // Add to the dependency set and check it is not already added (`add()` returns false if it already exists) - if (!_dependencySet.add(_chainId)) revert DependencyAlreadyAdded(); - - // Authorize the portal on the shared lockbox - address portal = ISystemConfig(_systemConfig).optimismPortal(); - SHARED_LOCKBOX.authorizePortal(portal); - - emit DependencyAdded(_chainId, _systemConfig, portal); - } - - /// @notice Checks if a chain is part or not of the dependency set. - /// @param _chainId The chain ID to check for. - function isInDependencySet(uint256 _chainId) public view returns (bool) { - return _dependencySet.contains(_chainId); - } - - /// @notice Getter for the chain ids list on the dependency set. - function dependencySet() external view returns (uint256[] memory) { - return _dependencySet.values(); - } - - /// @notice Returns the size of the dependency set. - /// @return The size of the dependency set. - function dependencySetSize() external view returns (uint8) { - return uint8(_dependencySet.length()); - } } diff --git a/packages/contracts-bedrock/src/L1/SuperchainConfigInterop.sol b/packages/contracts-bedrock/src/L1/SuperchainConfigInterop.sol new file mode 100644 index 00000000000..46031be4eb8 --- /dev/null +++ b/packages/contracts-bedrock/src/L1/SuperchainConfigInterop.sol @@ -0,0 +1,184 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.15; + +// Contracts +import { SuperchainConfig } from "src/L1/SuperchainConfig.sol"; + +// Libraries +import { Storage } from "src/libraries/Storage.sol"; +import { EnumerableSet } from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; +import { Unauthorized } from "src/libraries/errors/CommonErrors.sol"; +import { Predeploys } from "src/libraries/Predeploys.sol"; + +// Interfaces +import { ISystemConfig } from "interfaces/L1/ISystemConfig.sol"; +import { ISharedLockbox } from "interfaces/L1/ISharedLockbox.sol"; +import { IOptimismPortalInterop } from "interfaces/L1/IOptimismPortalInterop.sol"; + +/// @custom:proxied true +/// @custom:audit none This contracts is not yet audited. +/// @title SuperchainConfigInterop +/// @notice The SuperchainConfig contract is used to manage configuration of global superchain values. +/// The interop version of the contract adds the ability to add dependencies to the dependency set +/// and authorize OptimismPortals to interact with the SharedLockbox. +contract SuperchainConfigInterop is SuperchainConfig { + using EnumerableSet for EnumerableSet.UintSet; + + /// @notice The address of the cluster manager, which can add a chain to the dependency set. + /// It can only be modified by an upgrade. + bytes32 public constant CLUSTER_MANAGER_SLOT = bytes32(uint256(keccak256("superchainConfig.clusterManager")) - 1); + + /// @notice Emitted when a new dependency is added as part of the dependency set. + /// @param chainId The chain ID. + /// @param systemConfig The address of the SystemConfig contract. + /// @param portal The address of the OptimismPortal contract. + event DependencyAdded(uint256 indexed chainId, address indexed systemConfig, address indexed portal); + + /// @notice Thrown when the dependency set is too large to add a new dependency. + error DependencySetTooLarge(); + + /// @notice Thrown when the input dependency is already added to the set. + error DependencyAlreadyAdded(); + + /// @notice Thrown when a OptimismPortal does not have the right SuperchainConfig. + error InvalidSuperchainConfig(); + + /// @notice Thrown when trying to add an OptimismPortal that is already authorized. + error PortalAlreadyAuthorized(); + + /// @notice Semantic version. + /// @custom:semver +interop-beta.1 + function version() public pure override returns (string memory) { + return string.concat(super.version(), "+interop-beta.1"); + } + + /// @notice Storage slot that the SuperchainConfigDependencies struct is stored at. + /// keccak256(abi.encode(uint256(keccak256("superchainConfig.dependencies")) - 1)) & ~bytes32(uint256(0xff)); + bytes32 internal constant SUPERCHAIN_CONFIG_DEPENDENCIES_SLOT = + 0x342033bc92db70f979584a5299db090f7892d8d8c6e2e81871d9009f08fc2400; + + /// @notice Storage struct for the SuperchainConfig dependencies data. + /// @custom:storage-location erc7201:superchainConfig.dependencies + struct SuperchainConfigDependencies { + /// @notice The Shared Lockbox contract + ISharedLockbox sharedLockbox; + /// @notice Dependency set of chains that are part of the same cluster + EnumerableSet.UintSet dependencySet; + /// @notice OptimismPortals that are part of the dependency cluster + mapping(address => bool) authorizedPortals; + } + + /// @notice Returns the storage for the SuperchainConfigDependencies. + function _dependenciesStorage() private pure returns (SuperchainConfigDependencies storage storage_) { + assembly { + storage_.slot := SUPERCHAIN_CONFIG_DEPENDENCIES_SLOT + } + } + + /// @notice Initializer. + /// @param _guardian Address of the guardian, can pause the OptimismPortal. + /// @param _paused Initial paused status. + /// @param _clusterManager Address of the clusterManager, can add a chain to the dependency set. + /// @param _sharedLockbox Address of the SharedLockbox contract. + function initialize( + address _guardian, + bool _paused, + address _clusterManager, + address _sharedLockbox + ) + external + initializer + { + _initialize(_guardian, _paused); + + _setClusterManager(_clusterManager); + + SuperchainConfigDependencies storage dependenciesStorage = _dependenciesStorage(); + dependenciesStorage.sharedLockbox = ISharedLockbox(_sharedLockbox); + } + + /// @notice Getter for the cluster manager address. + function clusterManager() public view returns (address clusterManager_) { + clusterManager_ = Storage.getAddress(CLUSTER_MANAGER_SLOT); + } + + /// @notice Sets the cluster manager address. This is only callable during initialization, so an upgrade + /// will be required to change the cluster manager. + /// @param _clusterManager The new cluster manager address. + function _setClusterManager(address _clusterManager) internal { + Storage.setAddress(CLUSTER_MANAGER_SLOT, _clusterManager); + emit ConfigUpdate(UpdateType.CLUSTER_MANAGER, abi.encode(_clusterManager)); + } + + /// @notice Adds a new dependency to the dependency set. It also authorizes it's OptimismPortal on the + /// SharedLockbox and migrate it's ETH liquidity to it. Can only be called by an authorized + /// OptimismPortal via a withdrawal transaction initiated by the DependencyManager. + /// @param _chainId The chain ID to add. + /// @param _systemConfig The SystemConfig contract address of the chain to add. + function addDependency(uint256 _chainId, address _systemConfig) external { + SuperchainConfigDependencies storage dependenciesStorage = _dependenciesStorage(); + + if (msg.sender != clusterManager()) { + if (!dependenciesStorage.authorizedPortals[msg.sender]) revert Unauthorized(); + if (IOptimismPortalInterop(payable(msg.sender)).l2Sender() != Predeploys.DEPENDENCY_MANAGER) { + revert Unauthorized(); + } + } + + if (dependenciesStorage.dependencySet.length() == type(uint8).max) revert DependencySetTooLarge(); + + // Add to the dependency set and check it is not already added (`add()` returns false if it already exists) + if (!dependenciesStorage.dependencySet.add(_chainId)) revert DependencyAlreadyAdded(); + + address portal = ISystemConfig(_systemConfig).optimismPortal(); + _joinSharedLockbox(portal); + + emit DependencyAdded(_chainId, _systemConfig, portal); + } + + /// @notice Authorize a portal to interact with the SharedLockbox. It also migrates the ETH liquidity + /// from the portal to the SharedLockbox. + /// @param _portal The address of the portal to authorize. + function _joinSharedLockbox(address _portal) internal { + SuperchainConfigDependencies storage dependenciesStorage = _dependenciesStorage(); + + if (address(IOptimismPortalInterop(payable(_portal)).superchainConfig()) != address(this)) { + revert InvalidSuperchainConfig(); + } + + if (dependenciesStorage.authorizedPortals[_portal]) revert PortalAlreadyAuthorized(); + + dependenciesStorage.authorizedPortals[_portal] = true; + + // Migrate the ETH liquidity from the OptimismPortal to the SharedLockbox + IOptimismPortalInterop(payable(_portal)).migrateLiquidity(); + } + + /// @notice Getter for the SharedLockbox contract. + function sharedLockbox() public view returns (ISharedLockbox sharedLockbox_) { + sharedLockbox_ = _dependenciesStorage().sharedLockbox; + } + + /// @notice Checks if a chain is part or not of the dependency set. + /// @param _chainId The chain ID to check for. + function isInDependencySet(uint256 _chainId) public view returns (bool) { + return _dependenciesStorage().dependencySet.contains(_chainId); + } + + /// @notice Getter for the chain ids list on the dependency set. + function dependencySet() external view returns (uint256[] memory) { + return _dependenciesStorage().dependencySet.values(); + } + + /// @notice Returns the size of the dependency set. + /// @return The size of the dependency set. + function dependencySetSize() external view returns (uint8) { + return uint8(_dependenciesStorage().dependencySet.length()); + } + + /// @notice Checks if a portal is authorized to interact with the SharedLockbox. + /// @param _portal The address of the portal to check for. + function authorizedPortals(address _portal) public view returns (bool) { + return _dependenciesStorage().authorizedPortals[_portal]; + } +} diff --git a/packages/contracts-bedrock/src/L2/CrossL2Inbox.sol b/packages/contracts-bedrock/src/L2/CrossL2Inbox.sol index d93b15175c5..9fcc1a7c2fa 100644 --- a/packages/contracts-bedrock/src/L2/CrossL2Inbox.sol +++ b/packages/contracts-bedrock/src/L2/CrossL2Inbox.sol @@ -3,28 +3,11 @@ pragma solidity 0.8.25; // Libraries import { Predeploys } from "src/libraries/Predeploys.sol"; -import { TransientContext, TransientReentrancyAware } from "src/libraries/TransientContext.sol"; -import { SafeCall } from "src/libraries/SafeCall.sol"; // Interfaces import { ISemver } from "interfaces/universal/ISemver.sol"; import { IL1BlockInterop } from "interfaces/L2/IL1BlockInterop.sol"; -/// @notice Thrown when the caller is not DEPOSITOR_ACCOUNT when calling `setInteropStart()` -error NotDepositor(); - -/// @notice Thrown when attempting to set interop start when it's already set. -error InteropStartAlreadySet(); - -/// @notice Thrown when a non-written transient storage slot is attempted to be read from. -error NotEntered(); - -/// @notice Thrown when trying to execute a cross chain message with an invalid Identifier timestamp. -error InvalidTimestamp(); - -/// @notice Thrown when trying to execute a cross chain message and the target call fails. -error TargetCallFailed(); - /// @notice Thrown when trying to execute a cross chain message on a deposit transaction. error NoExecutingDeposits(); @@ -42,35 +25,7 @@ struct Identifier { /// @title CrossL2Inbox /// @notice The CrossL2Inbox is responsible for executing a cross chain message on the destination /// chain. It is permissionless to execute a cross chain message on behalf of any user. -contract CrossL2Inbox is ISemver, TransientReentrancyAware { - /// @notice Storage slot that the interop start timestamp is stored at. - /// Equal to bytes32(uint256(keccak256("crossl2inbox.interopstart")) - 1) - bytes32 internal constant INTEROP_START_SLOT = 0x5c769ee0ee8887661922049dc52480bb60322d765161507707dd9b190af5c149; - - /// @notice Transient storage slot that the origin for an Identifier is stored at. - /// Equal to bytes32(uint256(keccak256("crossl2inbox.identifier.origin")) - 1) - bytes32 internal constant ORIGIN_SLOT = 0xd2b7c5071ec59eb3ff0017d703a8ea513a7d0da4779b0dbefe845808c300c815; - - /// @notice Transient storage slot that the blockNumber for an Identifier is stored at. - /// Equal to bytes32(uint256(keccak256("crossl2inbox.identifier.blocknumber")) - 1) - bytes32 internal constant BLOCK_NUMBER_SLOT = 0x5a1da0738b7fdc60047c07bb519beb02aa32a8619de57e6258da1f1c2e020ccc; - - /// @notice Transient storage slot that the logIndex for an Identifier is stored at. - /// Equal to bytes32(uint256(keccak256("crossl2inbox.identifier.logindex")) - 1) - bytes32 internal constant LOG_INDEX_SLOT = 0xab8acc221aecea88a685fabca5b88bf3823b05f335b7b9f721ca7fe3ffb2c30d; - - /// @notice Transient storage slot that the timestamp for an Identifier is stored at. - /// Equal to bytes32(uint256(keccak256("crossl2inbox.identifier.timestamp")) - 1) - bytes32 internal constant TIMESTAMP_SLOT = 0x2e148a404a50bb94820b576997fd6450117132387be615e460fa8c5e11777e02; - - /// @notice Transient storage slot that the chainId for an Identifier is stored at. - /// Equal to bytes32(uint256(keccak256("crossl2inbox.identifier.chainid")) - 1) - bytes32 internal constant CHAINID_SLOT = 0x6e0446e8b5098b8c8193f964f1b567ec3a2bdaeba33d36acb85c1f1d3f92d313; - - /// @notice The address that represents the system caller responsible for L1 attributes - /// transactions. - address internal constant DEPOSITOR_ACCOUNT = 0xDeaDDEaDDeAdDeAdDEAdDEaddeAddEAdDEAd0001; - +contract CrossL2Inbox is ISemver { /// @notice Semantic version. /// @custom:semver 1.0.0-beta.12 string public constant version = "1.0.0-beta.12"; @@ -80,90 +35,6 @@ contract CrossL2Inbox is ISemver, TransientReentrancyAware { /// @param id Encoded Identifier of the message. event ExecutingMessage(bytes32 indexed msgHash, Identifier id); - /// @notice Sets the Interop Start Timestamp for this chain. Can only be performed once and when the caller is the - /// DEPOSITOR_ACCOUNT. - function setInteropStart() external { - // Check that caller is the DEPOSITOR_ACCOUNT - if (msg.sender != DEPOSITOR_ACCOUNT) revert NotDepositor(); - - // Check that it has not been set already - if (interopStart() != 0) revert InteropStartAlreadySet(); - - // Set Interop Start to block.timestamp - assembly { - sstore(INTEROP_START_SLOT, timestamp()) - } - } - - /// @notice Returns the interop start timestamp. - /// @return interopStart_ interop start timestamp. - function interopStart() public view returns (uint256 interopStart_) { - assembly { - interopStart_ := sload(INTEROP_START_SLOT) - } - } - - /// @notice Returns the origin address of the Identifier. If not entered, reverts. - /// @return Origin address of the Identifier. - function origin() external view notEntered returns (address) { - return address(uint160(TransientContext.get(ORIGIN_SLOT))); - } - - /// @notice Returns the block number of the Identifier. If not entered, reverts. - /// @return Block number of the Identifier. - function blockNumber() external view notEntered returns (uint256) { - return TransientContext.get(BLOCK_NUMBER_SLOT); - } - - /// @notice Returns the log index of the Identifier. If not entered, reverts. - /// @return Log index of the Identifier. - function logIndex() external view notEntered returns (uint256) { - return TransientContext.get(LOG_INDEX_SLOT); - } - - /// @notice Returns the timestamp of the Identifier. If not entered, reverts. - /// @return Timestamp of the Identifier. - function timestamp() external view notEntered returns (uint256) { - return TransientContext.get(TIMESTAMP_SLOT); - } - - /// @notice Returns the chain ID of the Identifier. If not entered, reverts. - /// @return _chainId The chain ID of the Identifier. - function chainId() external view notEntered returns (uint256) { - return TransientContext.get(CHAINID_SLOT); - } - - /// @notice Executes a cross chain message on the destination chain. - /// @param _id Identifier of the message. - /// @param _target Target address to call. - /// @param _message Message payload to call target with. - function executeMessage( - Identifier calldata _id, - address _target, - bytes memory _message - ) - external - payable - reentrantAware - { - // We need to know if this is being called on a depositTx - if (IL1BlockInterop(Predeploys.L1_BLOCK_ATTRIBUTES).isDeposit()) revert NoExecutingDeposits(); - - // Check the Identifier. - _checkIdentifier(_id); - - // Store the Identifier in transient storage. - _storeIdentifier(_id); - - // Call the target account with the message payload. - bool success = SafeCall.call(_target, msg.value, _message); - - // Revert if the target call failed. - if (!success) revert TargetCallFailed(); - - emit ExecutingMessage(keccak256(_message), _id); - } - /// @notice Validates a cross chain message on the destination chain /// and emits an ExecutingMessage event. This function is useful /// for applications that understand the schema of the _message payload and want to @@ -174,26 +45,6 @@ contract CrossL2Inbox is ISemver, TransientReentrancyAware { // We need to know if this is being called on a depositTx if (IL1BlockInterop(Predeploys.L1_BLOCK_ATTRIBUTES).isDeposit()) revert NoExecutingDeposits(); - // Check the Identifier. - _checkIdentifier(_id); - emit ExecutingMessage(_msgHash, _id); } - - /// @notice Validates that for a given cross chain message identifier - /// it's timestamp is not in the future. - /// @param _id Identifier of the message. - function _checkIdentifier(Identifier calldata _id) internal view { - if (_id.timestamp > block.timestamp || _id.timestamp <= interopStart()) revert InvalidTimestamp(); - } - - /// @notice Stores the Identifier in transient storage. - /// @param _id Identifier to store. - function _storeIdentifier(Identifier calldata _id) internal { - TransientContext.set(ORIGIN_SLOT, uint160(_id.origin)); - TransientContext.set(BLOCK_NUMBER_SLOT, _id.blockNumber); - TransientContext.set(LOG_INDEX_SLOT, _id.logIndex); - TransientContext.set(TIMESTAMP_SLOT, _id.timestamp); - TransientContext.set(CHAINID_SLOT, _id.chainId); - } } diff --git a/packages/contracts-bedrock/src/L2/DependencyManager.sol b/packages/contracts-bedrock/src/L2/DependencyManager.sol new file mode 100644 index 00000000000..e705f27e02a --- /dev/null +++ b/packages/contracts-bedrock/src/L2/DependencyManager.sol @@ -0,0 +1,83 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.25; + +// Libraries +import { EnumerableSet } from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; +import { Predeploys } from "src/libraries/Predeploys.sol"; +import { Unauthorized } from "src/libraries/errors/CommonErrors.sol"; +import { Constants } from "src/libraries/Constants.sol"; + +// Interfaces +import { ISemver } from "interfaces/universal/ISemver.sol"; +import { IL2ToL1MessagePasser } from "interfaces/L2/IL2ToL1MessagePasser.sol"; +import { ISuperchainConfigInterop } from "interfaces/L1/ISuperchainConfigInterop.sol"; + +/// @custom:proxied true +/// @custom:predeploy 0x4200000000000000000000000000000000000029 +/// @title DependencyManager +/// @notice The DependencyManager contract is used to manage the interop dependency set. This set contains the chain IDs +/// that the current chain is dependent on. When updating the dependency set, the DependencyManager will +/// initiate a withdrawal tx to update the dependency set on L1. +contract DependencyManager is ISemver { + using EnumerableSet for EnumerableSet.UintSet; + + /// @notice Error when the interop dependency set size is too large. + error DependencySetSizeTooLarge(); + + /// @notice Error when a chain ID already in the interop dependency set is attempted to be added. + error AlreadyDependency(); + + /// @notice Event emitted when a new dependency is added to the interop dependency set. + event DependencyAdded(uint256 indexed chainId, address indexed systemConfig, address indexed superchainConfig); + + /// @notice The minimum gas limit for the withdrawal tx to update the dependency set on L1. + uint256 internal constant ADD_DEPENDENCY_WITHDRAWWAL_GAS_LIMIT = 400_000; + + /// @notice The interop dependency set, containing the chain IDs in it. + EnumerableSet.UintSet internal _dependencySet; + + /// @notice Semantic version. + /// @custom:semver 1.0.0-beta.1 + string public constant version = "1.0.0-beta.1"; + + /// @notice Adds a new dependency to the dependency set. This function is only callable by the derivation pipeline. + /// It will initiate a withdrawal tx to update the dependency set on L1. + /// @param _superchainConfig Address of the SuperchainConfig contract on L1. + /// @param _chainId The new chain's ID to add to the dependency set. + /// @param _systemConfig The new chain's SystemConfig contract address on L1. + function addDependency(address _superchainConfig, uint256 _chainId, address _systemConfig) external { + if (msg.sender != Constants.DEPOSITOR_ACCOUNT) revert Unauthorized(); + + if (_dependencySet.length() == type(uint8).max) revert DependencySetSizeTooLarge(); + + if (_chainId == block.chainid || !_dependencySet.add(_chainId)) revert AlreadyDependency(); + + // Initiate a withdrawal tx to update the dependency set on L1. + IL2ToL1MessagePasser(payable(Predeploys.L2_TO_L1_MESSAGE_PASSER)).initiateWithdrawal( + _superchainConfig, + ADD_DEPENDENCY_WITHDRAWWAL_GAS_LIMIT, + abi.encodeCall(ISuperchainConfigInterop.addDependency, (_chainId, _systemConfig)) + ); + + emit DependencyAdded(_chainId, _systemConfig, _superchainConfig); + } + + /// @notice Returns true if a chain ID is in the interop dependency set and false otherwise. + /// The chain's chain ID is always considered to be in the dependency set. + /// @param _chainId The chain ID to check. + /// @return True if the chain ID to check is in the interop dependency set. False otherwise. + function isInDependencySet(uint256 _chainId) public view returns (bool) { + return _chainId == block.chainid || _dependencySet.contains(_chainId); + } + + /// @notice Returns the size of the interop dependency set. + /// @return The size of the interop dependency set. + function dependencySetSize() external view returns (uint8) { + return uint8(_dependencySet.length()); + } + + /// @notice Getter for the chain ids list on the dependency set. + function dependencySet() external view returns (uint256[] memory) { + return _dependencySet.values(); + } +} diff --git a/packages/contracts-bedrock/src/L2/L2ToL2CrossDomainMessenger.sol b/packages/contracts-bedrock/src/L2/L2ToL2CrossDomainMessenger.sol index 4672e999cbb..ae73b0de33d 100644 --- a/packages/contracts-bedrock/src/L2/L2ToL2CrossDomainMessenger.sol +++ b/packages/contracts-bedrock/src/L2/L2ToL2CrossDomainMessenger.sol @@ -9,6 +9,7 @@ import { TransientReentrancyAware } from "src/libraries/TransientContext.sol"; // Interfaces import { ISemver } from "interfaces/universal/ISemver.sol"; +import { IDependencySet } from "interfaces/L2/IDependencySet.sol"; import { ICrossL2Inbox, Identifier } from "interfaces/L2/ICrossL2Inbox.sol"; /// @notice Thrown when a non-written slot in transient storage is attempted to be read from. @@ -41,6 +42,9 @@ error ReentrantCall(); /// @notice Thrown when a call to the target contract during message relay fails. error TargetCallFailed(); +/// @notice Thrown when attempting to use a chain ID that is not in the dependency set. +error InvalidChainId(); + /// @custom:proxied true /// @custom:predeploy 0x4200000000000000000000000000000000000023 /// @title L2ToL2CrossDomainMessenger @@ -132,6 +136,7 @@ contract L2ToL2CrossDomainMessenger is ISemver, TransientReentrancyAware { if (_destination == block.chainid) revert MessageDestinationSameChain(); if (_target == Predeploys.CROSS_L2_INBOX) revert MessageTargetCrossL2Inbox(); if (_target == Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER) revert MessageTargetL2ToL2CrossDomainMessenger(); + if (!IDependencySet(Predeploys.DEPENDENCY_MANAGER).isInDependencySet(_destination)) revert InvalidChainId(); uint256 nonce = messageNonce(); emit SentMessage(_destination, _target, nonce, msg.sender, _message); diff --git a/packages/contracts-bedrock/src/libraries/Predeploys.sol b/packages/contracts-bedrock/src/libraries/Predeploys.sol index 30cad3b1f85..8283eb8aca6 100644 --- a/packages/contracts-bedrock/src/libraries/Predeploys.sol +++ b/packages/contracts-bedrock/src/libraries/Predeploys.sol @@ -108,6 +108,9 @@ library Predeploys { /// @notice Address of the SuperchainTokenBridge predeploy. address internal constant SUPERCHAIN_TOKEN_BRIDGE = 0x4200000000000000000000000000000000000028; + /// @notice Address of the DependencyManager predeploy. + address internal constant DEPENDENCY_MANAGER = 0x4200000000000000000000000000000000000029; + /// @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"); @@ -139,6 +142,7 @@ 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 == DEPENDENCY_MANAGER) return "DependencyManager"; revert("Predeploys: unnamed predeploy"); } @@ -159,7 +163,7 @@ library Predeploys { || (_useInterop && _addr == SUPERCHAIN_WETH) || (_useInterop && _addr == ETH_LIQUIDITY) || (_useInterop && _addr == OPTIMISM_SUPERCHAIN_ERC20_FACTORY) || (_useInterop && _addr == OPTIMISM_SUPERCHAIN_ERC20_BEACON) - || (_useInterop && _addr == SUPERCHAIN_TOKEN_BRIDGE); + || (_useInterop && _addr == SUPERCHAIN_TOKEN_BRIDGE) || (_useInterop && _addr == DEPENDENCY_MANAGER); } function isPredeployNamespace(address _addr) internal pure returns (bool) { diff --git a/packages/contracts-bedrock/test/L1/L1StandardBridge.t.sol b/packages/contracts-bedrock/test/L1/L1StandardBridge.t.sol index af0b61beca6..973fc62fe12 100644 --- a/packages/contracts-bedrock/test/L1/L1StandardBridge.t.sol +++ b/packages/contracts-bedrock/test/L1/L1StandardBridge.t.sol @@ -166,66 +166,12 @@ contract L1StandardBridge_Pause_TestFail is CommonTest { contract L1StandardBridge_Initialize_TestFail is CommonTest { } -contract L1StandardBridge_Receive_Test is CommonTest { - /// @dev Tests receive bridges ETH successfully. - function test_receive_succeeds() external { - uint256 portalBalanceBefore = address(optimismPortal2).balance; - uint256 lockboxBalanceBefore = address(sharedLockbox).balance; - - // The legacy event must be emitted for backwards compatibility - vm.expectEmit(address(l1StandardBridge)); - emit ETHDepositInitiated(alice, alice, 100, hex""); - - vm.expectEmit(address(l1StandardBridge)); - emit ETHBridgeInitiated(alice, alice, 100, hex""); - - vm.expectCall( - address(l1CrossDomainMessenger), - abi.encodeCall( - ICrossDomainMessenger.sendMessage, - ( - address(l2StandardBridge), - abi.encodeCall(StandardBridge.finalizeBridgeETH, (alice, alice, 100, hex"")), - 200_000 - ) - ) - ); - - vm.prank(alice, alice); - (bool success,) = address(l1StandardBridge).call{ value: 100 }(hex""); - assertEq(success, true); - assertEq(address(optimismPortal2).balance, portalBalanceBefore); - assertEq(address(sharedLockbox).balance, lockboxBalanceBefore + 100); - } -} - -contract L1StandardBridge_Receive_TestFail is CommonTest { - /// @dev Tests receive function reverts with custom gas token. - function testFuzz_receive_customGasToken_reverts(uint256 _value) external { - // TODO(opcm upgrades): remove skip once upgrade path is implemented - skipIfForkTest("L1StandardBridge_Receive_TestFail: gas paying token functionality DNE on op mainnet"); - - vm.prank(alice, alice); - vm.mockCall( - address(systemConfig), abi.encodeCall(systemConfig.gasPayingToken, ()), abi.encode(address(1), uint8(18)) - ); - vm.deal(alice, _value); - (bool success, bytes memory data) = address(l1StandardBridge).call{ value: _value }(hex""); - assertFalse(success); - assembly { - data := add(data, 0x04) - } - assertEq(abi.decode(data, (string)), "StandardBridge: cannot bridge ETH with custom gas token"); - } -} - contract PreBridgeETH is CommonTest { /// @dev Asserts the expected calls and events for bridging ETH depending /// on whether the bridge call is legacy or not. - function _preBridgeETH(bool isLegacy, uint256 value) internal { + function _preBridgeETH(bool isLegacy, uint256 value) internal virtual { if (!isForkTest()) { - assertEq(address(optimismPortal2).balance, 0); - assertEq(address(sharedLockbox).balance, 0); + assertEq(address(optimismPortal2).balance, 0, "OptimismPortal: balance should be 0"); } uint256 nonce = l1CrossDomainMessenger.messageNonce(); @@ -286,9 +232,188 @@ contract PreBridgeETH is CommonTest { vm.prank(alice, alice); } + + /// @dev Asserts the expected calls and events for bridging ETH to a different + /// address depending on whether the bridge call is legacy or not. + function _preBridgeETHTo(bool isLegacy, uint256 value) internal { + uint256 nonce = l1CrossDomainMessenger.messageNonce(); + uint256 version = 0; // Internal constant in the OptimismPortal: DEPOSIT_VERSION + address l1MessengerAliased = AddressAliasHelper.applyL1ToL2Alias(address(l1CrossDomainMessenger)); + + if (isLegacy) { + vm.expectCall( + address(l1StandardBridge), value, abi.encodeCall(l1StandardBridge.depositETHTo, (bob, 60000, hex"dead")) + ); + } else { + vm.expectCall( + address(l1StandardBridge), value, abi.encodeCall(l1StandardBridge.bridgeETHTo, (bob, 60000, hex"dead")) + ); + } + + bytes memory message = abi.encodeCall(StandardBridge.finalizeBridgeETH, (alice, bob, value, hex"dead")); + + // the L1 bridge should call + // L1CrossDomainMessenger.sendMessage + vm.expectCall( + address(l1CrossDomainMessenger), + abi.encodeCall(ICrossDomainMessenger.sendMessage, (address(l2StandardBridge), message, 60000)) + ); + + bytes memory innerMessage = abi.encodeCall( + ICrossDomainMessenger.relayMessage, + (nonce, address(l1StandardBridge), address(l2StandardBridge), value, 60000, message) + ); + + uint64 baseGas = l1CrossDomainMessenger.baseGas(message, 60000); + vm.expectCall( + address(optimismPortal2), + abi.encodeCall( + IOptimismPortal2.depositTransaction, + (address(l2CrossDomainMessenger), value, baseGas, false, innerMessage) + ) + ); + + bytes memory opaqueData = abi.encodePacked(uint256(value), uint256(value), baseGas, false, innerMessage); + + vm.expectEmit(address(l1StandardBridge)); + emit ETHDepositInitiated(alice, bob, value, hex"dead"); + + vm.expectEmit(address(l1StandardBridge)); + emit ETHBridgeInitiated(alice, bob, value, hex"dead"); + + // OptimismPortal emits a TransactionDeposited event on `depositTransaction` call + vm.expectEmit(address(optimismPortal2)); + emit TransactionDeposited(l1MessengerAliased, address(l2CrossDomainMessenger), version, opaqueData); + + // SentMessage event emitted by the CrossDomainMessenger + vm.expectEmit(address(l1CrossDomainMessenger)); + emit SentMessage(address(l2StandardBridge), address(l1StandardBridge), message, nonce, 60000); + + // SentMessageExtension1 event emitted by the CrossDomainMessenger + vm.expectEmit(address(l1CrossDomainMessenger)); + emit SentMessageExtension1(address(l1StandardBridge), value); + + // deposit eth to bob + vm.prank(alice, alice); + } +} + +contract L1StandardBridge_InteropBase_Test is PreBridgeETH { + function setUp() public virtual override { + super.enableInterop(); + super.setUp(); + } + + function _preBridgeETH(bool isLegacy, uint256 value) internal virtual override { + if (!isForkTest()) { + assertEq(address(sharedLockbox).balance, 0, "SharedLockbox: balance should be 0"); + } + + super._preBridgeETH(isLegacy, value); + } +} + +contract L1StandardBridge_Receive_Test is CommonTest { + /// @dev Tests receive bridges ETH successfully. + function test_receive_succeeds() external { + uint256 balanceBefore = address(optimismPortal2).balance; + + // The legacy event must be emitted for backwards compatibility + vm.expectEmit(address(l1StandardBridge)); + emit ETHDepositInitiated(alice, alice, 100, hex""); + + vm.expectEmit(address(l1StandardBridge)); + emit ETHBridgeInitiated(alice, alice, 100, hex""); + + vm.expectCall( + address(l1CrossDomainMessenger), + abi.encodeCall( + ICrossDomainMessenger.sendMessage, + ( + address(l2StandardBridge), + abi.encodeCall(StandardBridge.finalizeBridgeETH, (alice, alice, 100, hex"")), + 200_000 + ) + ) + ); + + vm.prank(alice, alice); + (bool success,) = address(l1StandardBridge).call{ value: 100 }(hex""); + assertEq(success, true); + assertEq(address(optimismPortal2).balance, balanceBefore + 100); + } +} + +contract L1StandardBridge_Receive_Interop_Test is L1StandardBridge_InteropBase_Test { + /// @dev Tests receive bridges ETH successfully. + function test_receive_succeeds() external { + uint256 portalBalanceBefore = address(optimismPortal2).balance; + uint256 lockboxBalanceBefore = address(sharedLockbox).balance; + + // The legacy event must be emitted for backwards compatibility + vm.expectEmit(address(l1StandardBridge)); + emit ETHDepositInitiated(alice, alice, 100, hex""); + + vm.expectEmit(address(l1StandardBridge)); + emit ETHBridgeInitiated(alice, alice, 100, hex""); + + vm.expectCall( + address(l1CrossDomainMessenger), + abi.encodeCall( + ICrossDomainMessenger.sendMessage, + ( + address(l2StandardBridge), + abi.encodeCall(StandardBridge.finalizeBridgeETH, (alice, alice, 100, hex"")), + 200_000 + ) + ) + ); + + vm.prank(alice, alice); + (bool success,) = address(l1StandardBridge).call{ value: 100 }(hex""); + assertEq(success, true); + assertEq(address(optimismPortal2).balance, portalBalanceBefore); + assertEq(address(sharedLockbox).balance, lockboxBalanceBefore + 100); + } +} + +contract L1StandardBridge_Receive_TestFail is CommonTest { + /// @dev Tests receive function reverts with custom gas token. + function testFuzz_receive_customGasToken_reverts(uint256 _value) external { + // TODO(opcm upgrades): remove skip once upgrade path is implemented + skipIfForkTest("L1StandardBridge_Receive_TestFail: gas paying token functionality DNE on op mainnet"); + + vm.prank(alice, alice); + vm.mockCall( + address(systemConfig), abi.encodeCall(systemConfig.gasPayingToken, ()), abi.encode(address(1), uint8(18)) + ); + vm.deal(alice, _value); + (bool success, bytes memory data) = address(l1StandardBridge).call{ value: _value }(hex""); + assertFalse(success); + assembly { + data := add(data, 0x04) + } + assertEq(abi.decode(data, (string)), "StandardBridge: cannot bridge ETH with custom gas token"); + } } contract L1StandardBridge_DepositETH_Test is PreBridgeETH { + /// @dev Tests that depositing ETH succeeds. + /// Emits ETHDepositInitiated and ETHBridgeInitiated events. + /// Calls depositTransaction on the OptimismPortal. + /// Only EOA can call depositETH. + /// ETH ends up in the optimismPortal. + function test_depositETH_succeeds() external { + _preBridgeETH({ isLegacy: true, value: 500 }); + uint256 balanceBefore = address(optimismPortal2).balance; + + l1StandardBridge.depositETH{ value: 500 }(50000, hex"dead"); + + assertEq(address(optimismPortal2).balance, balanceBefore + 500); + } +} + +contract L1StandardBridge_DepositETH_Interop_Test is L1StandardBridge_InteropBase_Test { /// @dev Tests that depositing ETH succeeds. /// Emits ETHDepositInitiated and ETHBridgeInitiated events. /// Calls depositTransaction on the OptimismPortal. @@ -330,6 +455,22 @@ contract L1StandardBridge_DepositETH_TestFail is CommonTest { } contract L1StandardBridge_BridgeETH_Test is PreBridgeETH { + /// @dev Tests that bridging ETH succeeds. + /// Emits ETHDepositInitiated and ETHBridgeInitiated events. + /// Calls depositTransaction on the OptimismPortal. + /// Only EOA can call bridgeETH. + /// ETH ends up in the optimismPortal. + function test_bridgeETH_succeeds() external { + _preBridgeETH({ isLegacy: false, value: 500 }); + uint256 balanceBefore = address(optimismPortal2).balance; + + l1StandardBridge.bridgeETH{ value: 500 }(50000, hex"dead"); + + assertEq(address(optimismPortal2).balance, balanceBefore + 500); + } +} + +contract L1StandardBridge_BridgeETH_Interop_Test is L1StandardBridge_InteropBase_Test { /// @dev Tests that bridging ETH succeeds. /// Emits ETHDepositInitiated and ETHBridgeInitiated events. /// Calls depositTransaction on the OptimismPortal. @@ -363,73 +504,23 @@ contract L1StandardBridge_BridgeETH_TestFail is PreBridgeETH { } } -contract PreBridgeETHTo is CommonTest { - /// @dev Asserts the expected calls and events for bridging ETH to a different - /// address depending on whether the bridge call is legacy or not. - function _preBridgeETHTo(bool isLegacy, uint256 value) internal { - uint256 nonce = l1CrossDomainMessenger.messageNonce(); - uint256 version = 0; // Internal constant in the OptimismPortal: DEPOSIT_VERSION - address l1MessengerAliased = AddressAliasHelper.applyL1ToL2Alias(address(l1CrossDomainMessenger)); - - if (isLegacy) { - vm.expectCall( - address(l1StandardBridge), value, abi.encodeCall(l1StandardBridge.depositETHTo, (bob, 60000, hex"dead")) - ); - } else { - vm.expectCall( - address(l1StandardBridge), value, abi.encodeCall(l1StandardBridge.bridgeETHTo, (bob, 60000, hex"dead")) - ); - } - - bytes memory message = abi.encodeCall(StandardBridge.finalizeBridgeETH, (alice, bob, value, hex"dead")); - - // the L1 bridge should call - // L1CrossDomainMessenger.sendMessage - vm.expectCall( - address(l1CrossDomainMessenger), - abi.encodeCall(ICrossDomainMessenger.sendMessage, (address(l2StandardBridge), message, 60000)) - ); - - bytes memory innerMessage = abi.encodeCall( - ICrossDomainMessenger.relayMessage, - (nonce, address(l1StandardBridge), address(l2StandardBridge), value, 60000, message) - ); - - uint64 baseGas = l1CrossDomainMessenger.baseGas(message, 60000); - vm.expectCall( - address(optimismPortal2), - abi.encodeCall( - IOptimismPortal2.depositTransaction, - (address(l2CrossDomainMessenger), value, baseGas, false, innerMessage) - ) - ); - - bytes memory opaqueData = abi.encodePacked(uint256(value), uint256(value), baseGas, false, innerMessage); - - vm.expectEmit(address(l1StandardBridge)); - emit ETHDepositInitiated(alice, bob, value, hex"dead"); - - vm.expectEmit(address(l1StandardBridge)); - emit ETHBridgeInitiated(alice, bob, value, hex"dead"); - - // OptimismPortal emits a TransactionDeposited event on `depositTransaction` call - vm.expectEmit(address(optimismPortal2)); - emit TransactionDeposited(l1MessengerAliased, address(l2CrossDomainMessenger), version, opaqueData); - - // SentMessage event emitted by the CrossDomainMessenger - vm.expectEmit(address(l1CrossDomainMessenger)); - emit SentMessage(address(l2StandardBridge), address(l1StandardBridge), message, nonce, 60000); +contract L1StandardBridge_DepositETHTo_Test is PreBridgeETH { + /// @dev Tests that depositing ETH to a different address succeeds. + /// Emits ETHDepositInitiated event. + /// Calls depositTransaction on the OptimismPortal. + /// EOA or contract can call depositETHTo. + /// ETH ends up in the optimismPortal. + function test_depositETHTo_succeeds() external { + _preBridgeETHTo({ isLegacy: true, value: 600 }); + uint256 balanceBefore = address(optimismPortal2).balance; - // SentMessageExtension1 event emitted by the CrossDomainMessenger - vm.expectEmit(address(l1CrossDomainMessenger)); - emit SentMessageExtension1(address(l1StandardBridge), value); + l1StandardBridge.depositETHTo{ value: 600 }(bob, 60000, hex"dead"); - // deposit eth to bob - vm.prank(alice, alice); + assertEq(address(optimismPortal2).balance, balanceBefore + 600); } } -contract L1StandardBridge_DepositETHTo_Test is PreBridgeETHTo { +contract L1StandardBridge_DepositETHTo_Interop_Test is L1StandardBridge_InteropBase_Test { /// @dev Tests that depositing ETH to a different address succeeds. /// Emits ETHDepositInitiated event. /// Calls depositTransaction on the OptimismPortal. @@ -470,7 +561,23 @@ contract L1StandardBridge_DepositETHTo_TestFail is CommonTest { } } -contract L1StandardBridge_BridgeETHTo_Test is PreBridgeETHTo { +contract L1StandardBridge_BridgeETHTo_Test is PreBridgeETH { + /// @dev Tests that bridging ETH to a different address succeeds. + /// Emits ETHDepositInitiated and ETHBridgeInitiated events. + /// Calls depositTransaction on the OptimismPortal. + /// Only EOA can call bridgeETHTo. + /// ETH ends up in the optimismPortal. + function test_bridgeETHTo_succeeds() external { + _preBridgeETHTo({ isLegacy: false, value: 600 }); + uint256 balanceBefore = address(optimismPortal2).balance; + + l1StandardBridge.bridgeETHTo{ value: 600 }(bob, 60000, hex"dead"); + + assertEq(address(optimismPortal2).balance, balanceBefore + 600); + } +} + +contract L1StandardBridge_BridgeETHTo_Interop_Test is L1StandardBridge_InteropBase_Test { /// @dev Tests that bridging ETH to a different address succeeds. /// Emits ETHDepositInitiated and ETHBridgeInitiated events. /// Calls depositTransaction on the OptimismPortal. @@ -488,7 +595,7 @@ contract L1StandardBridge_BridgeETHTo_Test is PreBridgeETHTo { } } -contract L1StandardBridge_BridgeETHTo_TestFail is PreBridgeETHTo { +contract L1StandardBridge_BridgeETHTo_TestFail is PreBridgeETH { /// @dev Tests that bridging reverts with custom gas token. function testFuzz_bridgeETHTo_customGasToken_reverts( uint256 _value, diff --git a/packages/contracts-bedrock/test/L1/LiquidityMigrator.t.sol b/packages/contracts-bedrock/test/L1/LiquidityMigrator.t.sol deleted file mode 100644 index 44d2d7ea805..00000000000 --- a/packages/contracts-bedrock/test/L1/LiquidityMigrator.t.sol +++ /dev/null @@ -1,68 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.15; - -import { CommonTest } from "test/setup/CommonTest.sol"; -import { LiquidityMigrator } from "src/L1/LiquidityMigrator.sol"; -import { IProxyAdmin } from "interfaces/universal/IProxyAdmin.sol"; - -contract LiquidityMigratorTest is CommonTest { - event ETHMigrated(uint256 amount); - - function setUp() public virtual override { - super.enableInterop(); - super.setUp(); - } - - /// @notice Tests the migration of the contract's ETH balance to the SharedLockbox works properly. - function test_migrateETH_succeeds(uint256 _ethAmount) public { - vm.deal(address(liquidityMigrator), _ethAmount); - - // Get the balance of the migrator before the migration to compare later on the assertions - uint256 migratorEthBalance = address(liquidityMigrator).balance; - uint256 lockboxBalanceBefore = address(sharedLockbox).balance; - - // Set the migrator as an authorized portal so it can lock the ETH while migrating - vm.prank(address(superchainConfig)); - sharedLockbox.authorizePortal(address(liquidityMigrator)); - - // Look for the emit of the `ETHMigrated` event - vm.expectEmit(address(liquidityMigrator)); - emit ETHMigrated(migratorEthBalance); - - // Call the `migrateETH` function with the amount - liquidityMigrator.migrateETH(); - - // Assert the balances after the migration happened - assert(address(liquidityMigrator).balance == 0); - assert(address(sharedLockbox).balance == lockboxBalanceBefore + migratorEthBalance); - } - - /// @notice Tests the migration of the portal's ETH balance to the SharedLockbox works properly. - function test_portal_migrateETH_succeeds(uint256 _ethAmount) public { - vm.deal(address(optimismPortal2), _ethAmount); - - // Get the balance of the portal before the migration to compare later on the assertions - uint256 portalEthBalance = address(optimismPortal2).balance; - uint256 lockboxBalanceBefore = address(sharedLockbox).balance; - - // Get the proxy admin address and it's owner - IProxyAdmin proxyAdmin = IProxyAdmin(deploy.mustGetAddress("ProxyAdmin")); - address proxyAdminOwner = proxyAdmin.owner(); - - // Look for the emit of the `ETHMigrated` event - vm.expectEmit(address(optimismPortal2)); - emit ETHMigrated(portalEthBalance); - - // Update the portal proxy implementation to the LiquidityMigrator contract - vm.prank(proxyAdminOwner); - proxyAdmin.upgradeAndCall({ - _proxy: payable(optimismPortal2), - _implementation: address(liquidityMigrator), - _data: abi.encodeCall(LiquidityMigrator.migrateETH, ()) - }); - - // Assert the balances after the migration happened - assert(address(optimismPortal2).balance == 0); - assert(address(sharedLockbox).balance == lockboxBalanceBefore + portalEthBalance); - } -} diff --git a/packages/contracts-bedrock/test/L1/OptimismPortal2.t.sol b/packages/contracts-bedrock/test/L1/OptimismPortal2.t.sol index 12662484e15..ee73a0bf610 100644 --- a/packages/contracts-bedrock/test/L1/OptimismPortal2.t.sol +++ b/packages/contracts-bedrock/test/L1/OptimismPortal2.t.sol @@ -72,7 +72,6 @@ contract OptimismPortal2_Test is CommonTest { // This check is not valid on forked tests as the respectedGameType varies between OP Chains. assertEq(optimismPortal2.respectedGameType().raw(), deploy.cfg().respectedGameType()); - assertEq(address(optimismPortal2.sharedLockbox()), address(sharedLockbox)); } /// @dev Tests that `pause` successfully pauses @@ -136,26 +135,10 @@ contract OptimismPortal2_Test is CommonTest { assertEq(optimismPortal2.paused(), true); } - /// @dev Tests that `sharedLockbox` returns correctly - function testFuzz_sharedLockbox_succeeds(address _caller, address _lockbox) external { - // Mock and expect the SuperchainConfig's SharedLockbox - vm.mockCall( - address(superchainConfig), abi.encodeCall(superchainConfig.SHARED_LOCKBOX, ()), abi.encode(_lockbox) - ); - vm.expectCall(address(superchainConfig), 0, abi.encodeCall(superchainConfig.SHARED_LOCKBOX, ())); - - vm.prank(_caller); - address _result = address(optimismPortal2.sharedLockbox()); - - assertEq(_result, _lockbox); - } - /// @dev Tests that `receive` successdully deposits ETH. function testFuzz_receive_succeeds(uint256 _value) external { - uint256 portalBalanceBefore = address(optimismPortal2).balance; - uint256 lockboxBalanceBefore = address(sharedLockbox).balance; - - _value = bound(_value, 0, type(uint256).max - lockboxBalanceBefore); + uint256 balanceBefore = address(optimismPortal2).balance; + _value = bound(_value, 0, type(uint256).max - balanceBefore); vm.expectEmit(address(optimismPortal2)); emitTransactionDeposited({ @@ -168,17 +151,13 @@ contract OptimismPortal2_Test is CommonTest { _data: hex"" }); - // Expect call to the SharedLockbox to lock the funds - if (_value > 0) vm.expectCall(address(sharedLockbox), _value, abi.encodeCall(sharedLockbox.lockETH, ())); - // give alice money and send as an eoa vm.deal(alice, _value); vm.prank(alice, alice); (bool s,) = address(optimismPortal2).call{ value: _value }(hex""); assertTrue(s); - assertEq(address(optimismPortal2).balance, portalBalanceBefore); - assertEq(address(sharedLockbox).balance, lockboxBalanceBefore + _value); + assertEq(address(optimismPortal2).balance, balanceBefore + _value); } /// @dev Tests that `depositTransaction` reverts when the destination address is non-zero @@ -257,9 +236,8 @@ contract OptimismPortal2_Test is CommonTest { ); if (_isCreation) _to = address(0); - uint256 portalBalanceBefore = address(optimismPortal2).balance; - uint256 lockboxBalanceBefore = address(sharedLockbox).balance; - _mint = bound(_mint, 0, type(uint256).max - lockboxBalanceBefore); + uint256 balanceBefore = address(optimismPortal2).balance; + _mint = bound(_mint, 0, type(uint256).max - balanceBefore); // EOA emulation vm.expectEmit(address(optimismPortal2)); @@ -273,9 +251,6 @@ contract OptimismPortal2_Test is CommonTest { _data: _data }); - // Expect call to the SharedLockbox to lock the funds - if (_mint > 0) vm.expectCall(address(sharedLockbox), _mint, abi.encodeCall(sharedLockbox.lockETH, ())); - vm.deal(depositor, _mint); vm.prank(depositor, depositor); optimismPortal2.depositTransaction{ value: _mint }({ @@ -285,9 +260,7 @@ contract OptimismPortal2_Test is CommonTest { _isCreation: _isCreation, _data: _data }); - - assertEq(address(optimismPortal2).balance, portalBalanceBefore); - assertEq(address(sharedLockbox).balance, lockboxBalanceBefore + _mint); + assertEq(address(optimismPortal2).balance, balanceBefore + _mint); } /// @dev Tests that `depositTransaction` succeeds for a contract. @@ -310,9 +283,8 @@ contract OptimismPortal2_Test is CommonTest { ); if (_isCreation) _to = address(0); - uint256 portalBalanceBefore = address(optimismPortal2).balance; - uint256 lockboxBalanceBefore = address(sharedLockbox).balance; - _mint = bound(_mint, 0, type(uint256).max - lockboxBalanceBefore); + uint256 balanceBefore = address(optimismPortal2).balance; + _mint = bound(_mint, 0, type(uint256).max - balanceBefore); vm.expectEmit(address(optimismPortal2)); emitTransactionDeposited({ @@ -325,9 +297,6 @@ contract OptimismPortal2_Test is CommonTest { _data: _data }); - // Expect call to the SharedLockbox to lock the funds - if (_mint > 0) vm.expectCall(address(sharedLockbox), _mint, abi.encodeCall(sharedLockbox.lockETH, ())); - vm.deal(address(this), _mint); vm.prank(address(this)); optimismPortal2.depositTransaction{ value: _mint }({ @@ -337,9 +306,7 @@ contract OptimismPortal2_Test is CommonTest { _isCreation: _isCreation, _data: _data }); - - assertEq(address(optimismPortal2).balance, portalBalanceBefore); - assertEq(address(sharedLockbox).balance, lockboxBalanceBefore + _mint); + assertEq(address(optimismPortal2).balance, balanceBefore + _mint); } /// @dev Temporary test that checks that correct calls to setGasPayingToken when using a custom gas token revert @@ -623,8 +590,8 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { // Warp beyond the chess clocks and finalize the game. vm.warp(block.timestamp + game.maxClockDuration().raw() + 1 seconds); - // Fund the SharedLockbox so that we can withdraw ETH. - vm.deal(address(sharedLockbox), 0xFFFFFFFF); + // Fund the portal so that we can withdraw ETH. + vm.deal(address(optimismPortal2), 0xFFFFFFFF); } /// @dev Asserts that the reentrant call will revert. @@ -1008,10 +975,9 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { uint256 _proposedGameIndex_noData = disputeGameFactory.gameCount() - 1; // Warp beyond the chess clocks and finalize the game. vm.warp(block.timestamp + game_noData.maxClockDuration().raw() + 1 seconds); - - // Fund the SharedLockbox so that we can withdraw ETH. - vm.deal(address(sharedLockbox), _defaultTx_noData.value); - vm.expectCall(address(sharedLockbox), abi.encodeCall(sharedLockbox.unlockETH, (_defaultTx_noData.value))); + // Fund the portal so that we can withdraw ETH. + vm.store(address(optimismPortal2), bytes32(uint256(61)), bytes32(uint256(0xFFFFFFFF))); + vm.deal(address(optimismPortal2), 0xFFFFFFFF); uint256 bobBalanceBefore = bob.balance; @@ -1188,10 +1154,6 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { function test_finalizeWithdrawalTransaction_provenWithdrawalHashEther_succeeds() external { uint256 bobBalanceBefore = address(bob).balance; - // Fund the SharedLockbox so that we can withdraw ETH. - vm.deal(address(sharedLockbox), _defaultTx.value); - vm.expectCall(address(sharedLockbox), abi.encodeCall(sharedLockbox.unlockETH, (_defaultTx.value))); - vm.expectEmit(address(optimismPortal2)); emit WithdrawalProven(_withdrawalHash, alice, bob); vm.expectEmit(address(optimismPortal2)); @@ -1228,10 +1190,6 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { // Warp 1 second into the future so that the proof is submitted after the timestamp of game creation. vm.warp(block.timestamp + 1 seconds); - // Fund the SharedLockbox so that we can withdraw ETH. - vm.deal(address(sharedLockbox), _defaultTx.value); - vm.expectCall(address(sharedLockbox), abi.encodeCall(sharedLockbox.unlockETH, (_defaultTx.value))); - // Prove the withdrawal transaction against the invalid dispute game, as 0xb0b. vm.expectEmit(true, true, true, true); emit WithdrawalProven(_withdrawalHash, alice, bob); @@ -1411,10 +1369,6 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { uint256 bobBalanceBefore = address(bob).balance; vm.etch(bob, hex"fe"); // Contract with just the invalid opcode. - // Fund the SharedLockbox so that we can withdraw ETH. - vm.deal(address(sharedLockbox), _defaultTx.value); - vm.expectCall(address(sharedLockbox), abi.encodeCall(sharedLockbox.unlockETH, (_defaultTx.value))); - vm.expectEmit(true, true, true, true); emit WithdrawalProven(_withdrawalHash, alice, bob); vm.expectEmit(true, true, true, true); @@ -1441,10 +1395,6 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { /// @dev Tests that `finalizeWithdrawalTransaction` reverts if the withdrawal has already been /// finalized. function test_finalizeWithdrawalTransaction_onReplay_reverts() external { - // Fund the SharedLockbox so that we can withdraw ETH. - vm.deal(address(sharedLockbox), _defaultTx.value); - vm.expectCall(address(sharedLockbox), abi.encodeCall(sharedLockbox.unlockETH, (_defaultTx.value))); - vm.expectEmit(true, true, true, true); emit WithdrawalProven(_withdrawalHash, alice, bob); vm.expectEmit(true, true, true, true); @@ -1542,10 +1492,6 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { // Return a mock output root from the game. vm.mockCall(address(game), abi.encodeCall(game.rootClaim, ()), abi.encode(outputRoot)); - // Fund the SharedLockbox so that we can withdraw ETH. - vm.deal(address(sharedLockbox), _testTx.value); - vm.expectCall(address(sharedLockbox), abi.encodeCall(sharedLockbox.unlockETH, (_testTx.value))); - vm.expectEmit(true, true, true, true); emit WithdrawalProven(withdrawalHash, alice, address(this)); vm.expectEmit(true, true, true, true); @@ -1585,9 +1531,7 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { // Total ETH supply is currently about 120M ETH. uint256 value = bound(_value, 0, 200_000_000 ether); - - // Add ETH to the SharedLockbox for the portal to withdraw. - vm.deal(address(sharedLockbox), value); + vm.deal(address(optimismPortal2), value); uint256 gasLimit = bound(_gasLimit, 0, 50_000_000); uint256 nonce = l2ToL1MessagePasser.messageNonce(); @@ -1636,9 +1580,6 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { // Warp past the finalization period vm.warp(block.timestamp + optimismPortal2.proofMaturityDelaySeconds() + 1); - // Expect call to the SharedLockbox to unlock the funds - if (value > 0) vm.expectCall(address(sharedLockbox), abi.encodeCall(sharedLockbox.unlockETH, (value))); - // Finalize the withdrawal transaction vm.expectCallMinGas(_tx.target, _tx.value, uint64(_tx.gasLimit), _tx.data); optimismPortal2.finalizeWithdrawalTransaction(_tx); diff --git a/packages/contracts-bedrock/test/L1/OptimismPortalInterop.t.sol b/packages/contracts-bedrock/test/L1/OptimismPortalInterop.t.sol index d6b178b3fdb..3fb00ec0c3f 100644 --- a/packages/contracts-bedrock/test/L1/OptimismPortalInterop.t.sol +++ b/packages/contracts-bedrock/test/L1/OptimismPortalInterop.t.sol @@ -3,17 +3,28 @@ pragma solidity 0.8.15; // Testing import { CommonTest } from "test/setup/CommonTest.sol"; +import { VmSafe } from "forge-std/Vm.sol"; // Libraries import { Constants } from "src/libraries/Constants.sol"; import { Predeploys } from "src/libraries/Predeploys.sol"; import "src/libraries/PortalErrors.sol"; +import { AddressAliasHelper } from "src/vendor/AddressAliasHelper.sol"; +import { Types } from "src/libraries/Types.sol"; +import { Hashing } from "src/libraries/Hashing.sol"; // Interfaces +import { IResourceMetering } from "interfaces/L1/IResourceMetering.sol"; +import { ISuperchainConfigInterop } from "interfaces/L1/ISuperchainConfigInterop.sol"; import { IL1BlockInterop, ConfigType } from "interfaces/L2/IL1BlockInterop.sol"; import { IOptimismPortalInterop } from "interfaces/L1/IOptimismPortalInterop.sol"; +import { IFaultDisputeGame } from "interfaces/dispute/IFaultDisputeGame.sol"; +import { IDisputeGame } from "interfaces/dispute/IDisputeGame.sol"; +import "src/dispute/lib/Types.sol"; + +contract OptimismPortalInterop_Base_Test is CommonTest { + event ETHMigrated(uint256 amount); -contract OptimismPortalInterop_Test is CommonTest { /// @notice Marked virtual to be overridden in /// test/kontrol/deployment/DeploymentSummary.t.sol function setUp() public virtual override { @@ -21,9 +32,20 @@ contract OptimismPortalInterop_Test is CommonTest { super.setUp(); } + /// @dev Returns the OptimismPortalInterop instance. + function _optimismPortal() internal view returns (IOptimismPortalInterop) { + return IOptimismPortalInterop(payable(address(optimismPortal2))); + } + + function _superchainConfig() internal view returns (ISuperchainConfigInterop) { + return ISuperchainConfigInterop(payable(address(superchainConfig))); + } +} + +contract OptimismPortalInterop_Config_Test is OptimismPortalInterop_Base_Test { /// @dev Tests that the config for the gas paying token can be set. function testFuzz_setConfig_gasPayingToken_succeeds(bytes calldata _value) public { - vm.expectEmit(address(optimismPortal2)); + vm.expectEmit(address(_optimismPortal())); emitTransactionDeposited({ _from: Constants.DEPOSITOR_ACCOUNT, _to: Predeploys.L1_BLOCK_ATTRIBUTES, @@ -34,18 +56,1643 @@ contract OptimismPortalInterop_Test is CommonTest { _data: abi.encodeCall(IL1BlockInterop.setConfig, (ConfigType.SET_GAS_PAYING_TOKEN, _value)) }); - vm.prank(address(_optimismPortalInterop().systemConfig())); - _optimismPortalInterop().setConfig(ConfigType.SET_GAS_PAYING_TOKEN, _value); + vm.prank(address(_optimismPortal().systemConfig())); + _optimismPortal().setConfig(ConfigType.SET_GAS_PAYING_TOKEN, _value); } /// @dev Tests that setting the gas paying token config as not the system config reverts. function testFuzz_setConfig_gasPayingTokenButNotSystemConfig_reverts(bytes calldata _value) public { vm.expectRevert(Unauthorized.selector); - _optimismPortalInterop().setConfig(ConfigType.SET_GAS_PAYING_TOKEN, _value); + _optimismPortal().setConfig(ConfigType.SET_GAS_PAYING_TOKEN, _value); } +} - /// @dev Returns the OptimismPortalInterop instance. - function _optimismPortalInterop() internal view returns (IOptimismPortalInterop) { - return IOptimismPortalInterop(payable(address(optimismPortal2))); +contract OptimismPortalInterop_Test is OptimismPortalInterop_Base_Test { + address depositor; + + function setUp() public virtual override { + super.setUp(); + depositor = makeAddr("depositor"); + } + + /// @dev Tests that the initializer sets the correct values. + /// @notice Marked virtual to be overridden in + /// test/kontrol/deployment/DeploymentSummary.t.sol + function test_initializeSharedLockbox_succeeds() external virtual { + returnIfForkTest("OptimismPortalInterop_Test: Do not check sharedLockbox on forked networks"); + // This check is not valid on forked tests as the sharedLockbox is not live. + assertEq(address(_optimismPortal().sharedLockbox()), address(sharedLockbox)); + } + + /// @dev Tests that `sharedLockbox` returns correctly + function testFuzz_sharedLockbox_succeeds(address _caller, address _lockbox) external { + // Mock and expect the SuperchainConfig's SharedLockbox + vm.mockCall( + address(_superchainConfig()), abi.encodeCall(_superchainConfig().sharedLockbox, ()), abi.encode(_lockbox) + ); + vm.expectCall(address(_superchainConfig()), 0, abi.encodeCall(_superchainConfig().sharedLockbox, ())); + + vm.prank(_caller); + address _result = address(_optimismPortal().sharedLockbox()); + + assertEq(_result, _lockbox); + } + + /// @dev Tests that `receive` successdully deposits ETH. + function testFuzz_receive_succeeds(uint256 _value) external { + uint256 portalBalanceBefore = address(_optimismPortal()).balance; + uint256 lockboxBalanceBefore = address(sharedLockbox).balance; + + _value = bound(_value, 0, type(uint256).max - lockboxBalanceBefore); + + vm.expectEmit(address(_optimismPortal())); + emitTransactionDeposited({ + _from: alice, + _to: alice, + _value: _value, + _mint: _value, + _gasLimit: 100_000, + _isCreation: false, + _data: hex"" + }); + + // Expect call to the SharedLockbox to lock the funds + if (_value > 0) vm.expectCall(address(sharedLockbox), _value, abi.encodeCall(sharedLockbox.lockETH, ())); + + // give alice money and send as an eoa + vm.deal(alice, _value); + vm.prank(alice, alice); + (bool s,) = address(_optimismPortal()).call{ value: _value }(hex""); + + assertTrue(s); + assertEq(address(_optimismPortal()).balance, portalBalanceBefore); + assertEq(address(sharedLockbox).balance, lockboxBalanceBefore + _value); + } + + /// @dev Tests that `depositTransaction` reverts when the destination address is non-zero + /// for a contract creation deposit. + function test_depositTransaction_contractCreation_reverts() external { + // contract creation must have a target of address(0) + vm.expectRevert(BadTarget.selector); + _optimismPortal().depositTransaction(address(1), 1, 0, true, hex""); + } + + /// @dev Tests that `depositTransaction` reverts when the data is too large. + /// This places an upper bound on unsafe blocks sent over p2p. + function test_depositTransaction_largeData_reverts() external { + uint256 size = 120_001; + uint64 gasLimit = _optimismPortal().minimumGasLimit(uint64(size)); + vm.expectRevert(LargeCalldata.selector); + _optimismPortal().depositTransaction({ + _to: address(0), + _value: 0, + _gasLimit: gasLimit, + _isCreation: false, + _data: new bytes(size) + }); + } + + /// @dev Tests that `depositTransaction` reverts when the gas limit is too small. + function test_depositTransaction_smallGasLimit_reverts() external { + vm.expectRevert(SmallGasLimit.selector); + _optimismPortal().depositTransaction({ + _to: address(1), + _value: 0, + _gasLimit: 0, + _isCreation: false, + _data: hex"" + }); + } + + /// @dev Tests that `depositTransaction` succeeds for small, + /// but sufficient, gas limits. + function testFuzz_depositTransaction_smallGasLimit_succeeds(bytes memory _data, bool _shouldFail) external { + uint64 gasLimit = _optimismPortal().minimumGasLimit(uint64(_data.length)); + if (_shouldFail) { + gasLimit = uint64(bound(gasLimit, 0, gasLimit - 1)); + vm.expectRevert(SmallGasLimit.selector); + } + + _optimismPortal().depositTransaction({ + _to: address(0x40), + _value: 0, + _gasLimit: gasLimit, + _isCreation: false, + _data: _data + }); + } + + /// @dev Tests that `minimumGasLimit` succeeds for small calldata sizes. + /// The gas limit should be 21k for 0 calldata and increase linearly + /// for larger calldata sizes. + function test_minimumGasLimit_succeeds() external view { + assertEq(_optimismPortal().minimumGasLimit(0), 21_000); + assertTrue(_optimismPortal().minimumGasLimit(2) > _optimismPortal().minimumGasLimit(1)); + assertTrue(_optimismPortal().minimumGasLimit(3) > _optimismPortal().minimumGasLimit(2)); + } + + /// @dev Tests that `depositTransaction` succeeds for an EOA. + function testFuzz_depositTransaction_eoa_succeeds( + address _to, + uint64 _gasLimit, + uint256 _value, + uint256 _mint, + bool _isCreation, + bytes memory _data + ) + external + { + _gasLimit = uint64( + bound( + _gasLimit, + _optimismPortal().minimumGasLimit(uint64(_data.length)), + systemConfig.resourceConfig().maxResourceLimit + ) + ); + if (_isCreation) _to = address(0); + + uint256 portalBalanceBefore = address(_optimismPortal()).balance; + uint256 lockboxBalanceBefore = address(sharedLockbox).balance; + _mint = bound(_mint, 0, type(uint256).max - lockboxBalanceBefore); + + // EOA emulation + vm.expectEmit(address(_optimismPortal())); + emitTransactionDeposited({ + _from: depositor, + _to: _to, + _value: _value, + _mint: _mint, + _gasLimit: _gasLimit, + _isCreation: _isCreation, + _data: _data + }); + + // Expect call to the SharedLockbox to lock the funds + if (_mint > 0) vm.expectCall(address(sharedLockbox), _mint, abi.encodeCall(sharedLockbox.lockETH, ())); + + vm.deal(depositor, _mint); + vm.prank(depositor, depositor); + _optimismPortal().depositTransaction{ value: _mint }({ + _to: _to, + _value: _value, + _gasLimit: _gasLimit, + _isCreation: _isCreation, + _data: _data + }); + + assertEq(address(_optimismPortal()).balance, portalBalanceBefore); + assertEq(address(sharedLockbox).balance, lockboxBalanceBefore + _mint); + } + + /// @dev Tests that `depositTransaction` succeeds for a contract. + function testFuzz_depositTransaction_contract_succeeds( + address _to, + uint64 _gasLimit, + uint256 _value, + uint256 _mint, + bool _isCreation, + bytes memory _data + ) + external + { + _gasLimit = uint64( + bound( + _gasLimit, + _optimismPortal().minimumGasLimit(uint64(_data.length)), + systemConfig.resourceConfig().maxResourceLimit + ) + ); + if (_isCreation) _to = address(0); + + uint256 portalBalanceBefore = address(_optimismPortal()).balance; + uint256 lockboxBalanceBefore = address(sharedLockbox).balance; + _mint = bound(_mint, 0, type(uint256).max - lockboxBalanceBefore); + + vm.expectEmit(address(_optimismPortal())); + emitTransactionDeposited({ + _from: AddressAliasHelper.applyL1ToL2Alias(address(this)), + _to: _to, + _value: _value, + _mint: _mint, + _gasLimit: _gasLimit, + _isCreation: _isCreation, + _data: _data + }); + + // Expect call to the SharedLockbox to lock the funds + if (_mint > 0) vm.expectCall(address(sharedLockbox), _mint, abi.encodeCall(sharedLockbox.lockETH, ())); + + vm.deal(address(this), _mint); + vm.prank(address(this)); + _optimismPortal().depositTransaction{ value: _mint }({ + _to: _to, + _value: _value, + _gasLimit: _gasLimit, + _isCreation: _isCreation, + _data: _data + }); + + assertEq(address(_optimismPortal()).balance, portalBalanceBefore); + assertEq(address(sharedLockbox).balance, lockboxBalanceBefore + _mint); + } + + /// @dev Tests that `balance()` returns the correct balance when the gas paying token is ether. + function testFuzz_balance_ether_succeeds(uint256 _amount) external { + // TODO(opcm upgrades): remove skip once upgrade path is implemented + skipIfForkTest("OptimismPortalInterop_Test: gas paying token functionality DNE on op mainnet"); + // Check that the gas paying token is set to ether + (address token,) = systemConfig.gasPayingToken(); + assertEq(token, Constants.ETHER); + + // Increase the balance of the gas paying token + vm.deal(address(_optimismPortal()), _amount); + + // Check that the balance has been correctly updated + assertEq(_optimismPortal().balance(), address(_optimismPortal()).balance); + } + + /// @dev Tests that the donateETH function donates ETH and does no state read/write + function test_donateETH_succeeds(uint256 _amount) external { + vm.startPrank(alice); + vm.deal(alice, _amount); + + uint256 preBalance = address(_optimismPortal()).balance; + _amount = bound(_amount, 0, type(uint256).max - preBalance); + + vm.startStateDiffRecording(); + _optimismPortal().donateETH{ value: _amount }(); + VmSafe.AccountAccess[] memory accountAccesses = vm.stopAndReturnStateDiff(); + + // not necessary since it's checked below + assertEq(address(_optimismPortal()).balance, preBalance + _amount); + + // 0 for extcodesize of proxy before being called by this test, + // 1 for the call to the proxy by the pranked address + // 2 for the delegate call to the impl by the proxy + assertEq(accountAccesses.length, 3); + assertEq(uint8(accountAccesses[1].kind), uint8(VmSafe.AccountAccessKind.Call)); + assertEq(uint8(accountAccesses[2].kind), uint8(VmSafe.AccountAccessKind.DelegateCall)); + + // to of 1 is the optimism portal proxy + assertEq(accountAccesses[1].account, address(_optimismPortal())); + // accessor is the pranked address + assertEq(accountAccesses[1].accessor, alice); + // value is the amount of ETH donated + assertEq(accountAccesses[1].value, _amount); + // old balance is the balance of the optimism portal before the donation + assertEq(accountAccesses[1].oldBalance, preBalance); + // new balance is the balance of the optimism portal after the donation + assertEq(accountAccesses[1].newBalance, preBalance + _amount); + // data is the selector of the donateETH function + assertEq(accountAccesses[1].data, abi.encodePacked(_optimismPortal().donateETH.selector)); + // reverted of alice call to proxy is false + assertEq(accountAccesses[1].reverted, false); + // reverted of delegate call of proxy to impl is false + assertEq(accountAccesses[2].reverted, false); + // storage accesses of delegate call of proxy to impl is empty (No storage read or write!) + assertEq(accountAccesses[2].storageAccesses.length, 0); + } +} + +contract OptimismPortalInterop_FinalizeWithdrawal_Test is OptimismPortalInterop_Base_Test { + // Reusable default values for a test withdrawal + Types.WithdrawalTransaction _defaultTx; + + IFaultDisputeGame game; + uint256 _proposedGameIndex; + uint256 _proposedBlockNumber; + bytes32 _stateRoot; + bytes32 _storageRoot; + bytes32 _outputRoot; + bytes32 _withdrawalHash; + bytes[] _withdrawalProof; + Types.OutputRootProof internal _outputRootProof; + + // Use a constructor to set the storage vars above, so as to minimize the number of ffi calls. + constructor() { + super.setUp(); + + _defaultTx = Types.WithdrawalTransaction({ + nonce: 0, + sender: alice, + target: bob, + value: 100, + gasLimit: 100_000, + data: hex"aa" // includes calldata for ERC20 withdrawal test + }); + // Get withdrawal proof data we can use for testing. + (_stateRoot, _storageRoot, _outputRoot, _withdrawalHash, _withdrawalProof) = + ffi.getProveWithdrawalTransactionInputs(_defaultTx); + + // Setup a dummy output root proof for reuse. + _outputRootProof = Types.OutputRootProof({ + version: bytes32(uint256(0)), + stateRoot: _stateRoot, + messagePasserStorageRoot: _storageRoot, + latestBlockhash: bytes32(uint256(0)) + }); + } + + /// @dev Setup the system for a ready-to-use state. + function setUp() public virtual override { + _proposedBlockNumber = 0xFF; + GameType respectedGameType = _optimismPortal().respectedGameType(); + uint256 bondAmount = disputeGameFactory.initBonds(respectedGameType); + game = IFaultDisputeGame( + payable( + address( + disputeGameFactory.create{ value: bondAmount }( + _optimismPortal().respectedGameType(), Claim.wrap(_outputRoot), abi.encode(_proposedBlockNumber) + ) + ) + ) + ); + _proposedGameIndex = disputeGameFactory.gameCount() - 1; + + // Warp beyond the chess clocks and finalize the game. + vm.warp(block.timestamp + game.maxClockDuration().raw() + 1 seconds); + + // Fund the SharedLockbox so that we can withdraw ETH. + vm.deal(address(sharedLockbox), 0xFFFFFFFF); + } + + /// @dev Asserts that the reentrant call will revert. + function callPortalAndExpectRevert() external payable { + vm.expectRevert(NonReentrant.selector); + // Arguments here don't matter, as the require check is the first thing that happens. + // We assume that this has already been proven. + _optimismPortal().finalizeWithdrawalTransaction(_defaultTx); + // Assert that the withdrawal was not finalized. + assertFalse(_optimismPortal().finalizedWithdrawals(Hashing.hashWithdrawal(_defaultTx))); + } + + /// @dev Tests that `proveWithdrawalTransaction` reverts when paused. + function test_proveWithdrawalTransaction_paused_reverts() external { + vm.prank(_optimismPortal().guardian()); + _superchainConfig().pause("identifier"); + + vm.expectRevert(CallPaused.selector); + _optimismPortal().proveWithdrawalTransaction({ + _tx: _defaultTx, + _disputeGameIndex: _proposedGameIndex, + _outputRootProof: _outputRootProof, + _withdrawalProof: _withdrawalProof + }); + } + + /// @dev Tests that `proveWithdrawalTransaction` reverts when the target is the portal contract. + function test_proveWithdrawalTransaction_onSelfCall_reverts() external { + _defaultTx.target = address(_optimismPortal()); + vm.expectRevert(BadTarget.selector); + _optimismPortal().proveWithdrawalTransaction({ + _tx: _defaultTx, + _disputeGameIndex: _proposedGameIndex, + _outputRootProof: _outputRootProof, + _withdrawalProof: _withdrawalProof + }); + } + + /// @dev Tests that `proveWithdrawalTransaction` reverts when the outputRootProof does not match the output root + function test_proveWithdrawalTransaction_onInvalidOutputRootProof_reverts() external { + // Modify the version to invalidate the withdrawal proof. + _outputRootProof.version = bytes32(uint256(1)); + vm.expectRevert(InvalidProof.selector); + _optimismPortal().proveWithdrawalTransaction({ + _tx: _defaultTx, + _disputeGameIndex: _proposedGameIndex, + _outputRootProof: _outputRootProof, + _withdrawalProof: _withdrawalProof + }); + } + + /// @dev Tests that `proveWithdrawalTransaction` reverts when the withdrawal is missing. + function test_proveWithdrawalTransaction_onInvalidWithdrawalProof_reverts() external { + // modify the default test values to invalidate the proof. + _defaultTx.data = hex"abcd"; + vm.expectRevert("MerkleTrie: path remainder must share all nibbles with key"); + _optimismPortal().proveWithdrawalTransaction({ + _tx: _defaultTx, + _disputeGameIndex: _proposedGameIndex, + _outputRootProof: _outputRootProof, + _withdrawalProof: _withdrawalProof + }); + } + + /// @dev Tests that `proveWithdrawalTransaction` reverts when the withdrawal has already been proven, and the new + /// game has the `CHALLENGER_WINS` status. + function test_proveWithdrawalTransaction_replayProveDifferentGameChallengerWins_reverts() external { + vm.expectEmit(address(_optimismPortal())); + emit WithdrawalProven(_withdrawalHash, alice, bob); + vm.expectEmit(address(_optimismPortal())); + emit WithdrawalProvenExtension1(_withdrawalHash, address(this)); + _optimismPortal().proveWithdrawalTransaction({ + _tx: _defaultTx, + _disputeGameIndex: _proposedGameIndex, + _outputRootProof: _outputRootProof, + _withdrawalProof: _withdrawalProof + }); + + // Create a new dispute game, and mock both games to be CHALLENGER_WINS. + IDisputeGame game2 = disputeGameFactory.create( + _optimismPortal().respectedGameType(), Claim.wrap(_outputRoot), abi.encode(_proposedBlockNumber + 1) + ); + _proposedGameIndex = disputeGameFactory.gameCount() - 1; + vm.mockCall(address(game), abi.encodeCall(game.status, ()), abi.encode(GameStatus.CHALLENGER_WINS)); + vm.mockCall(address(game2), abi.encodeCall(game.status, ()), abi.encode(GameStatus.CHALLENGER_WINS)); + + vm.expectRevert(InvalidDisputeGame.selector); + _optimismPortal().proveWithdrawalTransaction({ + _tx: _defaultTx, + _disputeGameIndex: _proposedGameIndex, + _outputRootProof: _outputRootProof, + _withdrawalProof: _withdrawalProof + }); + } + + /// @dev Tests that `proveWithdrawalTransaction` reverts if the dispute game being proven against is not of the + /// respected game type. + function test_proveWithdrawalTransaction_badGameType_reverts() external { + vm.mockCall( + address(disputeGameFactory), + abi.encodeCall(disputeGameFactory.gameAtIndex, (_proposedGameIndex)), + abi.encode(GameType.wrap(0xFF), Timestamp.wrap(uint64(block.timestamp)), IDisputeGame(address(game))) + ); + + vm.expectRevert(InvalidGameType.selector); + _optimismPortal().proveWithdrawalTransaction({ + _tx: _defaultTx, + _disputeGameIndex: _proposedGameIndex, + _outputRootProof: _outputRootProof, + _withdrawalProof: _withdrawalProof + }); + } + + /// @dev Tests that `proveWithdrawalTransaction` can be re-executed if the dispute game proven against has been + /// blacklisted. + function test_proveWithdrawalTransaction_replayProveBlacklisted_succeeds() external { + vm.expectEmit(true, true, true, true); + emit WithdrawalProven(_withdrawalHash, alice, bob); + vm.expectEmit(true, true, true, true); + emit WithdrawalProvenExtension1(_withdrawalHash, address(this)); + _optimismPortal().proveWithdrawalTransaction({ + _tx: _defaultTx, + _disputeGameIndex: _proposedGameIndex, + _outputRootProof: _outputRootProof, + _withdrawalProof: _withdrawalProof + }); + + // Blacklist the dispute dispute game. + vm.prank(_optimismPortal().guardian()); + _optimismPortal().blacklistDisputeGame(IDisputeGame(address(game))); + + // Mock the status of the dispute game we just proved against to be CHALLENGER_WINS. + vm.mockCall(address(game), abi.encodeCall(game.status, ()), abi.encode(GameStatus.CHALLENGER_WINS)); + // Create a new game to re-prove against + disputeGameFactory.create( + _optimismPortal().respectedGameType(), Claim.wrap(_outputRoot), abi.encode(_proposedBlockNumber + 1) + ); + _proposedGameIndex = disputeGameFactory.gameCount() - 1; + + vm.expectEmit(true, true, true, true); + emit WithdrawalProven(_withdrawalHash, alice, bob); + vm.expectEmit(true, true, true, true); + emit WithdrawalProvenExtension1(_withdrawalHash, address(this)); + _optimismPortal().proveWithdrawalTransaction({ + _tx: _defaultTx, + _disputeGameIndex: _proposedGameIndex, + _outputRootProof: _outputRootProof, + _withdrawalProof: _withdrawalProof + }); + } + + /// @dev Tests that `proveWithdrawalTransaction` can be re-executed if the dispute game proven against has resolved + /// against the favor of the root claim. + function test_proveWithdrawalTransaction_replayProveBadProposal_succeeds() external { + vm.expectEmit(true, true, true, true); + emit WithdrawalProven(_withdrawalHash, alice, bob); + vm.expectEmit(true, true, true, true); + emit WithdrawalProvenExtension1(_withdrawalHash, address(this)); + _optimismPortal().proveWithdrawalTransaction({ + _tx: _defaultTx, + _disputeGameIndex: _proposedGameIndex, + _outputRootProof: _outputRootProof, + _withdrawalProof: _withdrawalProof + }); + + // Mock the status of the dispute game we just proved against to be CHALLENGER_WINS. + vm.mockCall(address(game), abi.encodeCall(game.status, ()), abi.encode(GameStatus.CHALLENGER_WINS)); + // Create a new game to re-prove against + disputeGameFactory.create( + _optimismPortal().respectedGameType(), Claim.wrap(_outputRoot), abi.encode(_proposedBlockNumber + 1) + ); + _proposedGameIndex = disputeGameFactory.gameCount() - 1; + + vm.expectEmit(true, true, true, true); + emit WithdrawalProven(_withdrawalHash, alice, bob); + vm.expectEmit(true, true, true, true); + emit WithdrawalProvenExtension1(_withdrawalHash, address(this)); + _optimismPortal().proveWithdrawalTransaction({ + _tx: _defaultTx, + _disputeGameIndex: _proposedGameIndex, + _outputRootProof: _outputRootProof, + _withdrawalProof: _withdrawalProof + }); + } + + /// @dev Tests that `proveWithdrawalTransaction` can be re-executed if the dispute game proven against is no longer + /// of the respected game type. + function test_proveWithdrawalTransaction_replayRespectedGameTypeChanged_succeeds() external { + // Prove the withdrawal against a game with the current respected game type. + vm.expectEmit(true, true, true, true); + emit WithdrawalProven(_withdrawalHash, alice, bob); + vm.expectEmit(true, true, true, true); + emit WithdrawalProvenExtension1(_withdrawalHash, address(this)); + _optimismPortal().proveWithdrawalTransaction({ + _tx: _defaultTx, + _disputeGameIndex: _proposedGameIndex, + _outputRootProof: _outputRootProof, + _withdrawalProof: _withdrawalProof + }); + + // Update the respected game type to 0xbeef. + vm.prank(_optimismPortal().guardian()); + _optimismPortal().setRespectedGameType(GameType.wrap(0xbeef)); + + // Create a new game and mock the game type as 0xbeef in the factory. + IDisputeGame newGame = + disputeGameFactory.create(GameType.wrap(0), Claim.wrap(_outputRoot), abi.encode(_proposedBlockNumber + 1)); + vm.mockCall( + address(disputeGameFactory), + abi.encodeCall(disputeGameFactory.gameAtIndex, (_proposedGameIndex + 1)), + abi.encode(GameType.wrap(0xbeef), Timestamp.wrap(uint64(block.timestamp)), IDisputeGame(address(newGame))) + ); + + // Re-proving should be successful against the new game. + vm.expectEmit(true, true, true, true); + emit WithdrawalProven(_withdrawalHash, alice, bob); + vm.expectEmit(true, true, true, true); + emit WithdrawalProvenExtension1(_withdrawalHash, address(this)); + _optimismPortal().proveWithdrawalTransaction({ + _tx: _defaultTx, + _disputeGameIndex: _proposedGameIndex + 1, + _outputRootProof: _outputRootProof, + _withdrawalProof: _withdrawalProof + }); + } + + /// @dev Tests that `proveWithdrawalTransaction` succeeds. + function test_proveWithdrawalTransaction_validWithdrawalProof_succeeds() external { + vm.expectEmit(true, true, true, true); + emit WithdrawalProven(_withdrawalHash, alice, bob); + vm.expectEmit(true, true, true, true); + emit WithdrawalProvenExtension1(_withdrawalHash, address(this)); + _optimismPortal().proveWithdrawalTransaction({ + _tx: _defaultTx, + _disputeGameIndex: _proposedGameIndex, + _outputRootProof: _outputRootProof, + _withdrawalProof: _withdrawalProof + }); + } + + /// @dev Tests that `finalizeWithdrawalTransaction` reverts when attempting to replay using a secondary proof + /// submitter. + function test_finalizeWithdrawalTransaction_secondProofReplay_reverts() external { + uint256 bobBalanceBefore = address(bob).balance; + + // Submit the first proof for the withdrawal hash. + vm.expectEmit(true, true, true, true); + emit WithdrawalProven(_withdrawalHash, alice, bob); + vm.expectEmit(true, true, true, true); + emit WithdrawalProvenExtension1(_withdrawalHash, address(this)); + _optimismPortal().proveWithdrawalTransaction({ + _tx: _defaultTx, + _disputeGameIndex: _proposedGameIndex, + _outputRootProof: _outputRootProof, + _withdrawalProof: _withdrawalProof + }); + + // Submit a second proof for the same withdrawal hash. + vm.expectEmit(true, true, true, true); + emit WithdrawalProven(_withdrawalHash, alice, bob); + vm.expectEmit(true, true, true, true); + emit WithdrawalProvenExtension1(_withdrawalHash, address(0xb0b)); + vm.prank(address(0xb0b)); + _optimismPortal().proveWithdrawalTransaction({ + _tx: _defaultTx, + _disputeGameIndex: _proposedGameIndex, + _outputRootProof: _outputRootProof, + _withdrawalProof: _withdrawalProof + }); + + // Warp and resolve the dispute game. + game.resolveClaim(0, 0); + game.resolve(); + vm.warp(block.timestamp + _optimismPortal().proofMaturityDelaySeconds() + 1 seconds); + + vm.expectEmit(true, true, false, true); + emit WithdrawalFinalized(_withdrawalHash, true); + _optimismPortal().finalizeWithdrawalTransactionExternalProof(_defaultTx, address(0xb0b)); + + vm.expectRevert(AlreadyFinalized.selector); + _optimismPortal().finalizeWithdrawalTransactionExternalProof(_defaultTx, address(this)); + + assert(address(bob).balance == bobBalanceBefore + 100); + } + + /// @dev Tests that `finalizeWithdrawalTransaction` reverts if the target reverts and caller is the + /// ESTIMATION_ADDRESS. + function test_finalizeWithdrawalTransaction_targetFailsAndCallerIsEstimationAddress_reverts() external { + vm.etch(bob, hex"fe"); // Contract with just the invalid opcode. + + vm.prank(alice); + vm.expectEmit(true, true, true, true); + emit WithdrawalProven(_withdrawalHash, alice, bob); + _optimismPortal().proveWithdrawalTransaction(_defaultTx, _proposedGameIndex, _outputRootProof, _withdrawalProof); + + // Warp and resolve the dispute game. + game.resolveClaim(0, 0); + game.resolve(); + vm.warp(block.timestamp + _optimismPortal().proofMaturityDelaySeconds() + 1 seconds); + + vm.startPrank(alice, Constants.ESTIMATION_ADDRESS); + vm.expectRevert(GasEstimation.selector); + _optimismPortal().finalizeWithdrawalTransaction(_defaultTx); + } + + /// @dev Tests that `finalizeWithdrawalTransaction` succeeds when _tx.data is empty. + function test_finalizeWithdrawalTransaction_noTxData_succeeds() external { + Types.WithdrawalTransaction memory _defaultTx_noData = Types.WithdrawalTransaction({ + nonce: 0, + sender: alice, + target: bob, + value: 100, + gasLimit: 100_000, + data: hex"" + }); + // Get withdrawal proof data we can use for testing. + ( + bytes32 _stateRoot_noData, + bytes32 _storageRoot_noData, + bytes32 _outputRoot_noData, + bytes32 _withdrawalHash_noData, + bytes[] memory _withdrawalProof_noData + ) = ffi.getProveWithdrawalTransactionInputs(_defaultTx_noData); + // Setup a dummy output root proof for reuse. + Types.OutputRootProof memory _outputRootProof_noData = Types.OutputRootProof({ + version: bytes32(uint256(0)), + stateRoot: _stateRoot_noData, + messagePasserStorageRoot: _storageRoot_noData, + latestBlockhash: bytes32(uint256(0)) + }); + uint256 _proposedBlockNumber_noData = 0xFF; + IFaultDisputeGame game_noData = IFaultDisputeGame( + payable( + address( + disputeGameFactory.create( + _optimismPortal().respectedGameType(), + Claim.wrap(_outputRoot_noData), + abi.encode(_proposedBlockNumber_noData) + ) + ) + ) + ); + uint256 _proposedGameIndex_noData = disputeGameFactory.gameCount() - 1; + // Warp beyond the chess clocks and finalize the game. + vm.warp(block.timestamp + game_noData.maxClockDuration().raw() + 1 seconds); + + // Fund the SharedLockbox so that we can withdraw ETH. + vm.deal(address(sharedLockbox), _defaultTx_noData.value); + vm.expectCall(address(sharedLockbox), abi.encodeCall(sharedLockbox.unlockETH, (_defaultTx_noData.value))); + + uint256 bobBalanceBefore = bob.balance; + + vm.expectEmit(address(_optimismPortal())); + emit WithdrawalProven(_withdrawalHash_noData, alice, bob); + vm.expectEmit(address(_optimismPortal())); + emit WithdrawalProvenExtension1(_withdrawalHash_noData, address(this)); + _optimismPortal().proveWithdrawalTransaction({ + _tx: _defaultTx_noData, + _disputeGameIndex: _proposedGameIndex_noData, + _outputRootProof: _outputRootProof_noData, + _withdrawalProof: _withdrawalProof_noData + }); + + // Warp and resolve the dispute game. + game_noData.resolveClaim(0, 0); + game_noData.resolve(); + vm.warp(block.timestamp + _optimismPortal().proofMaturityDelaySeconds() + 1 seconds); + + vm.expectEmit(true, true, false, true); + emit WithdrawalFinalized(_withdrawalHash_noData, true); + _optimismPortal().finalizeWithdrawalTransaction(_defaultTx_noData); + + assert(bob.balance == bobBalanceBefore + 100); + } + + /// @dev Tests that `finalizeWithdrawalTransaction` reverts when using a custom gas token. + /// @dev Should be removed when/if Custom Gas Token functionality is allowed again. + function test_finalizeWithdrawalTransaction_customGasToken_reverts() external { + Types.WithdrawalTransaction memory _defaultTx_noData = Types.WithdrawalTransaction({ + nonce: 0, + sender: alice, + target: bob, + value: 100, + gasLimit: 100_000, + data: hex"" + }); + // Get withdrawal proof data we can use for testing. + ( + bytes32 _stateRoot_noData, + bytes32 _storageRoot_noData, + bytes32 _outputRoot_noData, + bytes32 _withdrawalHash_noData, + bytes[] memory _withdrawalProof_noData + ) = ffi.getProveWithdrawalTransactionInputs(_defaultTx_noData); + // Setup a dummy output root proof for reuse. + Types.OutputRootProof memory _outputRootProof_noData = Types.OutputRootProof({ + version: bytes32(uint256(0)), + stateRoot: _stateRoot_noData, + messagePasserStorageRoot: _storageRoot_noData, + latestBlockhash: bytes32(uint256(0)) + }); + uint256 _proposedBlockNumber_noData = 0xFF; + IFaultDisputeGame game_noData = IFaultDisputeGame( + payable( + address( + disputeGameFactory.create( + _optimismPortal().respectedGameType(), + Claim.wrap(_outputRoot_noData), + abi.encode(_proposedBlockNumber_noData) + ) + ) + ) + ); + uint256 _proposedGameIndex_noData = disputeGameFactory.gameCount() - 1; + // Warp beyond the chess clocks and finalize the game. + vm.warp(block.timestamp + game_noData.maxClockDuration().raw() + 1 seconds); + // Fund the portal so that we can withdraw ETH. + vm.store(address(_optimismPortal()), bytes32(uint256(61)), bytes32(uint256(0xFFFFFFFF))); + deal(address(L1Token), address(_optimismPortal()), 0xFFFFFFFF); + + // modify the gas token to be non ether + vm.mockCall( + address(systemConfig), abi.encodeCall(systemConfig.gasPayingToken, ()), abi.encode(address(L1Token), 18) + ); + + vm.expectEmit(address(_optimismPortal())); + emit WithdrawalProven(_withdrawalHash_noData, alice, bob); + vm.expectEmit(address(_optimismPortal())); + emit WithdrawalProvenExtension1(_withdrawalHash_noData, address(this)); + _optimismPortal().proveWithdrawalTransaction({ + _tx: _defaultTx_noData, + _disputeGameIndex: _proposedGameIndex_noData, + _outputRootProof: _outputRootProof_noData, + _withdrawalProof: _withdrawalProof_noData + }); + + // Warp and resolve the dispute game. + game_noData.resolveClaim(0, 0); + game_noData.resolve(); + vm.warp(block.timestamp + _optimismPortal().proofMaturityDelaySeconds() + 1 seconds); + + vm.expectRevert(IOptimismPortalInterop.CustomGasTokenNotSupported.selector); + _optimismPortal().finalizeWithdrawalTransaction(_defaultTx_noData); + } + + /// @dev Tests that `finalizeWithdrawalTransaction` succeeds when _tx.data is empty and with a custom gas token. + function test_finalizeWithdrawalTransaction_noTxDataNonEtherGasToken_succeeds() external { + vm.skip(true, "Custom gas token not supported"); + + Types.WithdrawalTransaction memory _defaultTx_noData = Types.WithdrawalTransaction({ + nonce: 0, + sender: alice, + target: bob, + value: 100, + gasLimit: 100_000, + data: hex"" + }); + // Get withdrawal proof data we can use for testing. + ( + bytes32 _stateRoot_noData, + bytes32 _storageRoot_noData, + bytes32 _outputRoot_noData, + bytes32 _withdrawalHash_noData, + bytes[] memory _withdrawalProof_noData + ) = ffi.getProveWithdrawalTransactionInputs(_defaultTx_noData); + // Setup a dummy output root proof for reuse. + Types.OutputRootProof memory _outputRootProof_noData = Types.OutputRootProof({ + version: bytes32(uint256(0)), + stateRoot: _stateRoot_noData, + messagePasserStorageRoot: _storageRoot_noData, + latestBlockhash: bytes32(uint256(0)) + }); + uint256 _proposedBlockNumber_noData = 0xFF; + IFaultDisputeGame game_noData = IFaultDisputeGame( + payable( + address( + disputeGameFactory.create( + _optimismPortal().respectedGameType(), + Claim.wrap(_outputRoot_noData), + abi.encode(_proposedBlockNumber_noData) + ) + ) + ) + ); + uint256 _proposedGameIndex_noData = disputeGameFactory.gameCount() - 1; + // Warp beyond the chess clocks and finalize the game. + vm.warp(block.timestamp + game_noData.maxClockDuration().raw() + 1 seconds); + // Fund the portal so that we can withdraw ETH. + vm.store(address(_optimismPortal()), bytes32(uint256(61)), bytes32(uint256(0xFFFFFFFF))); + deal(address(L1Token), address(_optimismPortal()), 0xFFFFFFFF); + + // modify the gas token to be non ether + vm.mockCall( + address(systemConfig), abi.encodeCall(systemConfig.gasPayingToken, ()), abi.encode(address(L1Token), 18) + ); + + uint256 bobBalanceBefore = L1Token.balanceOf(bob); + + vm.expectEmit(address(_optimismPortal())); + emit WithdrawalProven(_withdrawalHash_noData, alice, bob); + vm.expectEmit(address(_optimismPortal())); + emit WithdrawalProvenExtension1(_withdrawalHash_noData, address(this)); + _optimismPortal().proveWithdrawalTransaction({ + _tx: _defaultTx_noData, + _disputeGameIndex: _proposedGameIndex_noData, + _outputRootProof: _outputRootProof_noData, + _withdrawalProof: _withdrawalProof_noData + }); + + // Warp and resolve the dispute game. + game_noData.resolveClaim(0, 0); + game_noData.resolve(); + vm.warp(block.timestamp + _optimismPortal().proofMaturityDelaySeconds() + 1 seconds); + + vm.expectEmit(true, true, false, true); + emit WithdrawalFinalized(_withdrawalHash_noData, true); + _optimismPortal().finalizeWithdrawalTransaction(_defaultTx_noData); + + assert(L1Token.balanceOf(bob) == bobBalanceBefore + 100); + } + + /// @dev Tests that `finalizeWithdrawalTransaction` succeeds. + function test_finalizeWithdrawalTransaction_provenWithdrawalHashEther_succeeds() external { + uint256 bobBalanceBefore = address(bob).balance; + + // Fund the SharedLockbox so that we can withdraw ETH. + vm.deal(address(sharedLockbox), _defaultTx.value); + vm.expectCall(address(sharedLockbox), abi.encodeCall(sharedLockbox.unlockETH, (_defaultTx.value))); + + vm.expectEmit(address(_optimismPortal())); + emit WithdrawalProven(_withdrawalHash, alice, bob); + vm.expectEmit(address(_optimismPortal())); + emit WithdrawalProvenExtension1(_withdrawalHash, address(this)); + _optimismPortal().proveWithdrawalTransaction({ + _tx: _defaultTx, + _disputeGameIndex: _proposedGameIndex, + _outputRootProof: _outputRootProof, + _withdrawalProof: _withdrawalProof + }); + + // Warp and resolve the dispute game. + game.resolveClaim(0, 0); + game.resolve(); + vm.warp(block.timestamp + _optimismPortal().proofMaturityDelaySeconds() + 1 seconds); + + vm.expectEmit(true, true, false, true); + emit WithdrawalFinalized(_withdrawalHash, true); + _optimismPortal().finalizeWithdrawalTransaction(_defaultTx); + + assert(address(bob).balance == bobBalanceBefore + 100); + } + + /// @dev Tests that `finalizeWithdrawalTransaction` succeeds using a different proof than an earlier one by another + /// party. + function test_finalizeWithdrawalTransaction_secondaryProof_succeeds() external { + uint256 bobBalanceBefore = address(bob).balance; + + // Create a secondary dispute game. + IDisputeGame secondGame = disputeGameFactory.create( + _optimismPortal().respectedGameType(), Claim.wrap(_outputRoot), abi.encode(_proposedBlockNumber + 1) + ); + + // Warp 1 second into the future so that the proof is submitted after the timestamp of game creation. + vm.warp(block.timestamp + 1 seconds); + + // Fund the SharedLockbox so that we can withdraw ETH. + vm.deal(address(sharedLockbox), _defaultTx.value); + vm.expectCall(address(sharedLockbox), abi.encodeCall(sharedLockbox.unlockETH, (_defaultTx.value))); + + // Prove the withdrawal transaction against the invalid dispute game, as 0xb0b. + vm.expectEmit(true, true, true, true); + emit WithdrawalProven(_withdrawalHash, alice, bob); + vm.expectEmit(true, true, true, true); + emit WithdrawalProvenExtension1(_withdrawalHash, address(0xb0b)); + vm.prank(address(0xb0b)); + _optimismPortal().proveWithdrawalTransaction({ + _tx: _defaultTx, + _disputeGameIndex: _proposedGameIndex + 1, + _outputRootProof: _outputRootProof, + _withdrawalProof: _withdrawalProof + }); + + // Mock the status of the dispute game 0xb0b proves against to be CHALLENGER_WINS. + vm.mockCall(address(secondGame), abi.encodeCall(game.status, ()), abi.encode(GameStatus.CHALLENGER_WINS)); + + // Prove the withdrawal transaction against the invalid dispute game, as the test contract, against the original + // game. + vm.expectEmit(true, true, true, true); + emit WithdrawalProven(_withdrawalHash, alice, bob); + vm.expectEmit(true, true, true, true); + emit WithdrawalProvenExtension1(_withdrawalHash, address(this)); + _optimismPortal().proveWithdrawalTransaction({ + _tx: _defaultTx, + _disputeGameIndex: _proposedGameIndex, + _outputRootProof: _outputRootProof, + _withdrawalProof: _withdrawalProof + }); + + // Warp and resolve the original dispute game. + game.resolveClaim(0, 0); + game.resolve(); + vm.warp(block.timestamp + _optimismPortal().proofMaturityDelaySeconds() + 1 seconds); + + // Ensure both proofs are registered successfully. + assertEq(_optimismPortal().numProofSubmitters(_withdrawalHash), 2); + + vm.expectRevert(ProposalNotValidated.selector); + vm.prank(address(0xb0b)); + _optimismPortal().finalizeWithdrawalTransaction(_defaultTx); + + vm.expectEmit(true, true, false, true); + emit WithdrawalFinalized(_withdrawalHash, true); + _optimismPortal().finalizeWithdrawalTransaction(_defaultTx); + + assert(address(bob).balance == bobBalanceBefore + 100); + } + + /// @dev Tests that `finalizeWithdrawalTransaction` succeeds. + function test_finalizeWithdrawalTransaction_provenWithdrawalHashNonEtherTargetToken_reverts() external { + vm.skip(true, "Custom gas token not supported"); + + vm.mockCall( + address(systemConfig), + abi.encodeCall(systemConfig.gasPayingToken, ()), + abi.encode(address(_defaultTx.target), 18) + ); + + _optimismPortal().proveWithdrawalTransaction({ + _tx: _defaultTx, + _disputeGameIndex: _proposedGameIndex, + _outputRootProof: _outputRootProof, + _withdrawalProof: _withdrawalProof + }); + + // Warp to after the finalization period + game.resolveClaim(0, 0); + game.resolve(); + vm.warp(block.timestamp + _optimismPortal().proofMaturityDelaySeconds() + 1); + + vm.expectRevert(BadTarget.selector); + _optimismPortal().finalizeWithdrawalTransaction(_defaultTx); + } + + /// @dev Tests that `finalizeWithdrawalTransaction` reverts if the contract is paused. + function test_finalizeWithdrawalTransaction_paused_reverts() external { + vm.prank(_optimismPortal().guardian()); + _superchainConfig().pause("identifier"); + + vm.expectRevert(CallPaused.selector); + _optimismPortal().finalizeWithdrawalTransaction(_defaultTx); + } + + /// @dev Tests that `finalizeWithdrawalTransaction` reverts if the withdrawal has not been + function test_finalizeWithdrawalTransaction_ifWithdrawalNotProven_reverts() external { + uint256 bobBalanceBefore = address(bob).balance; + + vm.expectRevert(Unproven.selector); + _optimismPortal().finalizeWithdrawalTransaction(_defaultTx); + + assert(address(bob).balance == bobBalanceBefore); + } + + /// @dev Tests that `finalizeWithdrawalTransaction` reverts if the withdrawal has not been + /// proven long enough ago. + function test_finalizeWithdrawalTransaction_ifWithdrawalProofNotOldEnough_reverts() external { + uint256 bobBalanceBefore = address(bob).balance; + + vm.expectEmit(address(_optimismPortal())); + emit WithdrawalProven(_withdrawalHash, alice, bob); + vm.expectEmit(address(_optimismPortal())); + emit WithdrawalProvenExtension1(_withdrawalHash, address(this)); + _optimismPortal().proveWithdrawalTransaction({ + _tx: _defaultTx, + _disputeGameIndex: _proposedGameIndex, + _outputRootProof: _outputRootProof, + _withdrawalProof: _withdrawalProof + }); + + vm.expectRevert("OptimismPortal: proven withdrawal has not matured yet"); + _optimismPortal().finalizeWithdrawalTransaction(_defaultTx); + + assert(address(bob).balance == bobBalanceBefore); + } + + /// @dev Tests that `finalizeWithdrawalTransaction` reverts if the provenWithdrawal's timestamp + /// is less than the dispute game's creation timestamp. + function test_finalizeWithdrawalTransaction_timestampLessThanGameCreation_reverts() external { + uint256 bobBalanceBefore = address(bob).balance; + + // Prove our withdrawal + vm.expectEmit(true, true, true, true); + emit WithdrawalProven(_withdrawalHash, alice, bob); + vm.expectEmit(true, true, true, true); + emit WithdrawalProvenExtension1(_withdrawalHash, address(this)); + _optimismPortal().proveWithdrawalTransaction({ + _tx: _defaultTx, + _disputeGameIndex: _proposedGameIndex, + _outputRootProof: _outputRootProof, + _withdrawalProof: _withdrawalProof + }); + + // Warp to after the finalization period + vm.warp(block.timestamp + _optimismPortal().proofMaturityDelaySeconds() + 1); + + // Mock a createdAt change in the dispute game. + vm.mockCall(address(game), abi.encodeCall(game.createdAt, ()), abi.encode(block.timestamp + 1)); + + // Attempt to finalize the withdrawal + vm.expectRevert("OptimismPortal: withdrawal timestamp less than dispute game creation timestamp"); + _optimismPortal().finalizeWithdrawalTransaction(_defaultTx); + + // Ensure that bob's balance has remained the same + assertEq(bobBalanceBefore, address(bob).balance); + } + + /// @dev Tests that `finalizeWithdrawalTransaction` reverts if the dispute game has not resolved in favor of the + /// root claim. + function test_finalizeWithdrawalTransaction_ifDisputeGameNotResolved_reverts() external { + uint256 bobBalanceBefore = address(bob).balance; + + // Prove our withdrawal + vm.expectEmit(true, true, true, true); + emit WithdrawalProven(_withdrawalHash, alice, bob); + vm.expectEmit(true, true, true, true); + emit WithdrawalProvenExtension1(_withdrawalHash, address(this)); + _optimismPortal().proveWithdrawalTransaction({ + _tx: _defaultTx, + _disputeGameIndex: _proposedGameIndex, + _outputRootProof: _outputRootProof, + _withdrawalProof: _withdrawalProof + }); + + // Warp to after the finalization period + vm.warp(block.timestamp + _optimismPortal().proofMaturityDelaySeconds() + 1); + + // Attempt to finalize the withdrawal + vm.expectRevert(ProposalNotValidated.selector); + _optimismPortal().finalizeWithdrawalTransaction(_defaultTx); + + // Ensure that bob's balance has remained the same + assertEq(bobBalanceBefore, address(bob).balance); + } + + /// @dev Tests that `finalizeWithdrawalTransaction` reverts if the target reverts. + function test_finalizeWithdrawalTransaction_targetFails_fails() external { + uint256 bobBalanceBefore = address(bob).balance; + vm.etch(bob, hex"fe"); // Contract with just the invalid opcode. + + // Fund the SharedLockbox so that we can withdraw ETH. + vm.deal(address(sharedLockbox), _defaultTx.value); + vm.expectCall(address(sharedLockbox), abi.encodeCall(sharedLockbox.unlockETH, (_defaultTx.value))); + + vm.expectEmit(true, true, true, true); + emit WithdrawalProven(_withdrawalHash, alice, bob); + vm.expectEmit(true, true, true, true); + emit WithdrawalProvenExtension1(_withdrawalHash, address(this)); + _optimismPortal().proveWithdrawalTransaction({ + _tx: _defaultTx, + _disputeGameIndex: _proposedGameIndex, + _outputRootProof: _outputRootProof, + _withdrawalProof: _withdrawalProof + }); + + // Resolve the dispute game. + game.resolveClaim(0, 0); + game.resolve(); + + vm.warp(block.timestamp + _optimismPortal().proofMaturityDelaySeconds() + 1); + vm.expectEmit(true, true, true, true); + emit WithdrawalFinalized(_withdrawalHash, false); + _optimismPortal().finalizeWithdrawalTransaction(_defaultTx); + + assert(address(bob).balance == bobBalanceBefore); + } + + /// @dev Tests that `finalizeWithdrawalTransaction` reverts if the withdrawal has already been + /// finalized. + function test_finalizeWithdrawalTransaction_onReplay_reverts() external { + // Fund the SharedLockbox so that we can withdraw ETH. + vm.deal(address(sharedLockbox), _defaultTx.value); + vm.expectCall(address(sharedLockbox), abi.encodeCall(sharedLockbox.unlockETH, (_defaultTx.value))); + + vm.expectEmit(true, true, true, true); + emit WithdrawalProven(_withdrawalHash, alice, bob); + vm.expectEmit(true, true, true, true); + emit WithdrawalProvenExtension1(_withdrawalHash, address(this)); + _optimismPortal().proveWithdrawalTransaction({ + _tx: _defaultTx, + _disputeGameIndex: _proposedGameIndex, + _outputRootProof: _outputRootProof, + _withdrawalProof: _withdrawalProof + }); + + // Resolve the dispute game. + game.resolveClaim(0, 0); + game.resolve(); + + vm.warp(block.timestamp + _optimismPortal().proofMaturityDelaySeconds() + 1); + vm.expectEmit(true, true, true, true); + emit WithdrawalFinalized(_withdrawalHash, true); + _optimismPortal().finalizeWithdrawalTransaction(_defaultTx); + + vm.expectRevert(AlreadyFinalized.selector); + _optimismPortal().finalizeWithdrawalTransaction(_defaultTx); + } + + /// @dev Tests that `finalizeWithdrawalTransaction` reverts if the withdrawal transaction + /// does not have enough gas to execute. + function test_finalizeWithdrawalTransaction_onInsufficientGas_reverts() external { + // This number was identified through trial and error. + uint256 gasLimit = 150_000; + Types.WithdrawalTransaction memory insufficientGasTx = Types.WithdrawalTransaction({ + nonce: 0, + sender: alice, + target: bob, + value: 100, + gasLimit: gasLimit, + data: hex"" + }); + + // Get updated proof inputs. + (bytes32 stateRoot, bytes32 storageRoot,,, bytes[] memory withdrawalProof) = + ffi.getProveWithdrawalTransactionInputs(insufficientGasTx); + Types.OutputRootProof memory outputRootProof = Types.OutputRootProof({ + version: bytes32(0), + stateRoot: stateRoot, + messagePasserStorageRoot: storageRoot, + latestBlockhash: bytes32(0) + }); + + vm.mockCall( + address(game), abi.encodeCall(game.rootClaim, ()), abi.encode(Hashing.hashOutputRootProof(outputRootProof)) + ); + + _optimismPortal().proveWithdrawalTransaction({ + _tx: insufficientGasTx, + _disputeGameIndex: _proposedGameIndex, + _outputRootProof: outputRootProof, + _withdrawalProof: withdrawalProof + }); + + // Resolve the dispute game. + game.resolveClaim(0, 0); + game.resolve(); + + vm.warp(block.timestamp + _optimismPortal().proofMaturityDelaySeconds() + 1); + vm.expectRevert("SafeCall: Not enough gas"); + _optimismPortal().finalizeWithdrawalTransaction{ gas: gasLimit }(insufficientGasTx); + } + + /// @dev Tests that `finalizeWithdrawalTransaction` reverts if a sub-call attempts to finalize + /// another withdrawal. + function test_finalizeWithdrawalTransaction_onReentrancy_reverts() external { + uint256 bobBalanceBefore = address(bob).balance; + + // Copy and modify the default test values to attempt a reentrant call by first calling to + // this contract's callPortalAndExpectRevert() function above. + Types.WithdrawalTransaction memory _testTx = _defaultTx; + _testTx.target = address(this); + _testTx.data = abi.encodeCall(this.callPortalAndExpectRevert, ()); + + // Get modified proof inputs. + ( + bytes32 stateRoot, + bytes32 storageRoot, + bytes32 outputRoot, + bytes32 withdrawalHash, + bytes[] memory withdrawalProof + ) = ffi.getProveWithdrawalTransactionInputs(_testTx); + Types.OutputRootProof memory outputRootProof = Types.OutputRootProof({ + version: bytes32(0), + stateRoot: stateRoot, + messagePasserStorageRoot: storageRoot, + latestBlockhash: bytes32(0) + }); + + // Return a mock output root from the game. + vm.mockCall(address(game), abi.encodeCall(game.rootClaim, ()), abi.encode(outputRoot)); + + // Fund the SharedLockbox so that we can withdraw ETH. + vm.deal(address(sharedLockbox), _testTx.value); + vm.expectCall(address(sharedLockbox), abi.encodeCall(sharedLockbox.unlockETH, (_testTx.value))); + + vm.expectEmit(true, true, true, true); + emit WithdrawalProven(withdrawalHash, alice, address(this)); + vm.expectEmit(true, true, true, true); + emit WithdrawalProvenExtension1(withdrawalHash, address(this)); + _optimismPortal().proveWithdrawalTransaction(_testTx, _proposedGameIndex, outputRootProof, withdrawalProof); + + // Resolve the dispute game. + game.resolveClaim(0, 0); + game.resolve(); + + vm.warp(block.timestamp + _optimismPortal().proofMaturityDelaySeconds() + 1); + vm.expectCall(address(this), _testTx.data); + vm.expectEmit(true, true, true, true); + emit WithdrawalFinalized(withdrawalHash, true); + _optimismPortal().finalizeWithdrawalTransaction(_testTx); + + // Ensure that bob's balance was not changed by the reentrant call. + assert(address(bob).balance == bobBalanceBefore); + } + + /// @dev Tests that `finalizeWithdrawalTransaction` succeeds. + function testDiff_finalizeWithdrawalTransaction_succeeds( + address _sender, + address _target, + uint256 _value, + uint256 _gasLimit, + bytes memory _data + ) + external + { + vm.assume( + _target != address(_optimismPortal()) // 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) + ); + + // Total ETH supply is currently about 120M ETH. + uint256 value = bound(_value, 0, 200_000_000 ether); + + // Add ETH to the SharedLockbox for the portal to withdraw. + vm.deal(address(sharedLockbox), value); + + 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: value, + 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 + _optimismPortal().proveWithdrawalTransaction(_tx, _proposedGameIndex, proof, withdrawalProof); + (IDisputeGame _game,) = _optimismPortal().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 + _optimismPortal().proofMaturityDelaySeconds() + 1); + + // Expect call to the SharedLockbox to unlock the funds + if (value > 0) vm.expectCall(address(sharedLockbox), abi.encodeCall(sharedLockbox.unlockETH, (value))); + + // Finalize the withdrawal transaction + vm.expectCallMinGas(_tx.target, _tx.value, uint64(_tx.gasLimit), _tx.data); + _optimismPortal().finalizeWithdrawalTransaction(_tx); + assertTrue(_optimismPortal().finalizedWithdrawals(withdrawalHash)); + } + + /// @dev Tests that `finalizeWithdrawalTransaction` reverts if the withdrawal's dispute game has been blacklisted. + function test_finalizeWithdrawalTransaction_blacklisted_reverts() external { + vm.expectEmit(true, true, true, true); + emit WithdrawalProven(_withdrawalHash, alice, bob); + vm.expectEmit(true, true, true, true); + emit WithdrawalProvenExtension1(_withdrawalHash, address(this)); + _optimismPortal().proveWithdrawalTransaction({ + _tx: _defaultTx, + _disputeGameIndex: _proposedGameIndex, + _outputRootProof: _outputRootProof, + _withdrawalProof: _withdrawalProof + }); + + // Resolve the dispute game. + game.resolveClaim(0, 0); + game.resolve(); + + vm.prank(_optimismPortal().guardian()); + _optimismPortal().blacklistDisputeGame(IDisputeGame(address(game))); + + vm.warp(block.timestamp + _optimismPortal().proofMaturityDelaySeconds() + 1); + + vm.expectRevert(Blacklisted.selector); + _optimismPortal().finalizeWithdrawalTransaction(_defaultTx); + } + + /// @dev Tests that `finalizeWithdrawalTransaction` reverts if the withdrawal's dispute game is still in the air + /// gap. + function test_finalizeWithdrawalTransaction_gameInAirGap_reverts() external { + vm.expectEmit(true, true, true, true); + emit WithdrawalProven(_withdrawalHash, alice, bob); + vm.expectEmit(true, true, true, true); + emit WithdrawalProvenExtension1(_withdrawalHash, address(this)); + _optimismPortal().proveWithdrawalTransaction({ + _tx: _defaultTx, + _disputeGameIndex: _proposedGameIndex, + _outputRootProof: _outputRootProof, + _withdrawalProof: _withdrawalProof + }); + + // Warp past the finalization period. + vm.warp(block.timestamp + _optimismPortal().proofMaturityDelaySeconds() + 1); + + // Resolve the dispute game. + game.resolveClaim(0, 0); + game.resolve(); + + // Attempt to finalize the withdrawal directly after the game resolves. This should fail. + vm.expectRevert("OptimismPortal: output proposal in air-gap"); + _optimismPortal().finalizeWithdrawalTransaction(_defaultTx); + + // Finalize the withdrawal transaction. This should succeed. + vm.warp(block.timestamp + _optimismPortal().disputeGameFinalityDelaySeconds() + 1); + _optimismPortal().finalizeWithdrawalTransaction(_defaultTx); + assertTrue(_optimismPortal().finalizedWithdrawals(_withdrawalHash)); + } + + /// @dev Tests that `finalizeWithdrawalTransaction` reverts if the respected game type has changed since the + /// withdrawal was proven. + function test_finalizeWithdrawalTransaction_respectedTypeChangedSinceProving_reverts() external { + vm.expectEmit(true, true, true, true); + emit WithdrawalProven(_withdrawalHash, alice, bob); + vm.expectEmit(true, true, true, true); + emit WithdrawalProvenExtension1(_withdrawalHash, address(this)); + _optimismPortal().proveWithdrawalTransaction({ + _tx: _defaultTx, + _disputeGameIndex: _proposedGameIndex, + _outputRootProof: _outputRootProof, + _withdrawalProof: _withdrawalProof + }); + + // Warp past the finalization period. + vm.warp(block.timestamp + _optimismPortal().proofMaturityDelaySeconds() + 1); + + // Resolve the dispute game. + game.resolveClaim(0, 0); + game.resolve(); + + // Change the respected game type in the portal. + vm.prank(_optimismPortal().guardian()); + _optimismPortal().setRespectedGameType(GameType.wrap(0xFF)); + + vm.expectRevert(InvalidGameType.selector); + _optimismPortal().finalizeWithdrawalTransaction(_defaultTx); + } + + /// @dev Tests that `finalizeWithdrawalTransaction` reverts if the respected game type was updated after the + /// dispute game was created. + function test_finalizeWithdrawalTransaction_gameOlderThanRespectedGameTypeUpdate_reverts() external { + vm.expectEmit(address(_optimismPortal())); + emit WithdrawalProven(_withdrawalHash, alice, bob); + vm.expectEmit(address(_optimismPortal())); + emit WithdrawalProvenExtension1(_withdrawalHash, address(this)); + _optimismPortal().proveWithdrawalTransaction({ + _tx: _defaultTx, + _disputeGameIndex: _proposedGameIndex, + _outputRootProof: _outputRootProof, + _withdrawalProof: _withdrawalProof + }); + + // Warp past the finalization period. + vm.warp(block.timestamp + _optimismPortal().proofMaturityDelaySeconds() + 1); + + // Resolve the dispute game. + game.resolveClaim(0, 0); + game.resolve(); + + // Change the respected game type in the portal. + vm.prank(_optimismPortal().guardian()); + _optimismPortal().setRespectedGameType(GameType.wrap(0xFF)); + + // Mock the game's type so that we pass the correct game type check. + vm.mockCall(address(game), abi.encodeCall(game.gameType, ()), abi.encode(GameType.wrap(0xFF))); + + vm.expectRevert("OptimismPortal: dispute game created before respected game type was updated"); + _optimismPortal().finalizeWithdrawalTransaction(_defaultTx); + } + + /// @dev Tests an e2e prove -> finalize path, checking the edges of each delay for correctness. + function test_finalizeWithdrawalTransaction_delayEdges_succeeds() external { + // Prove the withdrawal transaction. + vm.expectEmit(true, true, true, true); + emit WithdrawalProven(_withdrawalHash, alice, bob); + vm.expectEmit(true, true, true, true); + emit WithdrawalProvenExtension1(_withdrawalHash, address(this)); + _optimismPortal().proveWithdrawalTransaction({ + _tx: _defaultTx, + _disputeGameIndex: _proposedGameIndex, + _outputRootProof: _outputRootProof, + _withdrawalProof: _withdrawalProof + }); + + // Attempt to finalize the withdrawal transaction 1 second before the proof has matured. This should fail. + vm.warp(block.timestamp + _optimismPortal().proofMaturityDelaySeconds()); + vm.expectRevert("OptimismPortal: proven withdrawal has not matured yet"); + _optimismPortal().finalizeWithdrawalTransaction(_defaultTx); + + // Warp 1 second in the future, past the proof maturity delay, and attempt to finalize the withdrawal. + // This should also fail, since the dispute game has not resolved yet. + vm.warp(block.timestamp + 1 seconds); + vm.expectRevert(ProposalNotValidated.selector); + _optimismPortal().finalizeWithdrawalTransaction(_defaultTx); + + // Finalize the dispute game and attempt to finalize the withdrawal again. This should also fail, since the + // air gap dispute game delay has not elapsed. + game.resolveClaim(0, 0); + game.resolve(); + vm.warp(block.timestamp + _optimismPortal().disputeGameFinalityDelaySeconds()); + vm.expectRevert("OptimismPortal: output proposal in air-gap"); + _optimismPortal().finalizeWithdrawalTransaction(_defaultTx); + + // Warp 1 second in the future, past the air gap dispute game delay, and attempt to finalize the withdrawal. + // This should succeed. + vm.warp(block.timestamp + 1 seconds); + _optimismPortal().finalizeWithdrawalTransaction(_defaultTx); + assertTrue(_optimismPortal().finalizedWithdrawals(_withdrawalHash)); + } +} + +/// @title OptimismPortalInterop_ResourceFuzz_Test +/// @dev Test various values of the resource metering config to ensure that deposits cannot be +/// broken by changing the config. +contract OptimismPortalInterop_ResourceFuzz_Test is OptimismPortalInterop_Base_Test { + /// @dev The max gas limit observed throughout this test. Setting this too high can cause + /// the test to take too long to run. + uint256 constant MAX_GAS_LIMIT = 30_000_000; + + function setUp() public override { + super.setUp(); + } + + /// @dev Test that various values of the resource metering config will not break deposits. + function testFuzz_systemConfigDeposit_succeeds( + uint32 _maxResourceLimit, + uint8 _elasticityMultiplier, + uint8 _baseFeeMaxChangeDenominator, + uint32 _minimumBaseFee, + uint32 _systemTxMaxGas, + uint128 _maximumBaseFee, + uint64 _gasLimit, + uint64 _prevBoughtGas, + uint128 _prevBaseFee, + uint8 _blockDiff + ) + external + { + // Get the set system gas limit + uint64 gasLimit = systemConfig.gasLimit(); + + // Bound resource config + _systemTxMaxGas = uint32(bound(_systemTxMaxGas, 0, gasLimit - 21000)); + _maxResourceLimit = uint32(bound(_maxResourceLimit, 21000, MAX_GAS_LIMIT / 8)); + _maxResourceLimit = uint32(bound(_maxResourceLimit, 21000, gasLimit - _systemTxMaxGas)); + _maximumBaseFee = uint128(bound(_maximumBaseFee, 1, type(uint128).max)); + _minimumBaseFee = uint32(bound(_minimumBaseFee, 0, _maximumBaseFee - 1)); + _gasLimit = uint64(bound(_gasLimit, 21000, _maxResourceLimit)); + _gasLimit = uint64(bound(_gasLimit, 0, gasLimit)); + _prevBaseFee = uint128(bound(_prevBaseFee, 0, 3 gwei)); + _prevBoughtGas = uint64(bound(_prevBoughtGas, 0, _maxResourceLimit - _gasLimit)); + _blockDiff = uint8(bound(_blockDiff, 0, 3)); + _baseFeeMaxChangeDenominator = uint8(bound(_baseFeeMaxChangeDenominator, 2, type(uint8).max)); + _elasticityMultiplier = uint8(bound(_elasticityMultiplier, 1, type(uint8).max)); + + // Prevent values that would cause reverts + vm.assume(uint256(_maxResourceLimit) + uint256(_systemTxMaxGas) <= gasLimit); + vm.assume(((_maxResourceLimit / _elasticityMultiplier) * _elasticityMultiplier) == _maxResourceLimit); + + // Although we typically want to limit the usage of vm.assume, we've constructed the above + // bounds to satisfy the assumptions listed in this specific section. These assumptions + // serve only to act as an additional sanity check on top of the bounds and should not + // result in an unnecessary number of test rejections. + vm.assume(gasLimit >= _gasLimit); + vm.assume(_minimumBaseFee < _maximumBaseFee); + + // Base fee can increase quickly and mean that we can't buy the amount of gas we want. + // Here we add a VM assumption to bound the potential increase. + // Compute the maximum possible increase in base fee. + uint256 maxPercentIncrease = uint256(_elasticityMultiplier - 1) * 100 / uint256(_baseFeeMaxChangeDenominator); + // Assume that we have enough gas to burn. + // Compute the maximum amount of gas we'd need to burn. + // Assume we need 1/5 of our gas to do other stuff. + vm.assume(_prevBaseFee * maxPercentIncrease * _gasLimit / 100 < MAX_GAS_LIMIT * 4 / 5); + + // Pick a pseudorandom block number + vm.roll(uint256(keccak256(abi.encode(_blockDiff))) % uint256(type(uint16).max) + uint256(_blockDiff)); + + // Create a resource config to mock the call to the system config with + IResourceMetering.ResourceConfig memory rcfg = IResourceMetering.ResourceConfig({ + maxResourceLimit: _maxResourceLimit, + elasticityMultiplier: _elasticityMultiplier, + baseFeeMaxChangeDenominator: _baseFeeMaxChangeDenominator, + minimumBaseFee: _minimumBaseFee, + systemTxMaxGas: _systemTxMaxGas, + maximumBaseFee: _maximumBaseFee + }); + vm.mockCall(address(systemConfig), abi.encodeCall(systemConfig.resourceConfig, ()), abi.encode(rcfg)); + + // Set the resource params + uint256 _prevBlockNum = block.number - _blockDiff; + vm.store( + address(_optimismPortal()), + bytes32(uint256(1)), + bytes32((_prevBlockNum << 192) | (uint256(_prevBoughtGas) << 128) | _prevBaseFee) + ); + // Ensure that the storage setting is correct + (uint128 prevBaseFee, uint64 prevBoughtGas, uint64 prevBlockNum) = _optimismPortal().params(); + assertEq(prevBaseFee, _prevBaseFee); + assertEq(prevBoughtGas, _prevBoughtGas); + assertEq(prevBlockNum, _prevBlockNum); + + // Do a deposit, should not revert + _optimismPortal().depositTransaction{ gas: MAX_GAS_LIMIT }({ + _to: address(0x20), + _value: 0x40, + _gasLimit: _gasLimit, + _isCreation: false, + _data: hex"" + }); + } +} + +contract OptimismPortalInterop_MigrateLiquidity_Test is OptimismPortalInterop_Base_Test { + /// @notice Test that the `migrateLiquidity` function reverts if the caller is not the superchain config. + function test_migrateLiquidity_notSuperchainConfig_reverts(address _caller) external { + vm.assume(_caller != address(_superchainConfig())); + vm.expectRevert(Unauthorized.selector); + _optimismPortal().migrateLiquidity(); + } + + /// @notice Test that the `migrateLiquidity` function succeeds. + function test_migrateLiquidity_succeeds(uint256 _value) external { + vm.deal(address(_optimismPortal()), _value); + + // Ensure that the contracts has the correct balance + assertEq(address(_optimismPortal()).balance, _value); + assertEq(address(sharedLockbox).balance, 0); + + // TODO: Use new portal that is not migrated + // Assert the migrated flag is not set + // assertFalse(_optimismPortal().migrated()); + + // Expect call to the shared lockbox to lock the ETH + vm.expectCall(address(sharedLockbox), _value, abi.encodeCall(sharedLockbox.lockETH, ())); + + // Expect emit ETHMigrated event + vm.expectEmit(address(_optimismPortal())); + emit ETHMigrated(_value); + + // Migrate the liquidity + vm.prank(address(_superchainConfig())); + _optimismPortal().migrateLiquidity(); + + // Assert the migrated flag is set + assertTrue(_optimismPortal().migrated()); + + // Ensure that the contracts has the correct balance + assertEq(address(_optimismPortal()).balance, 0); + assertEq(address(sharedLockbox).balance, _value); } } diff --git a/packages/contracts-bedrock/test/L1/SharedLockbox.t.sol b/packages/contracts-bedrock/test/L1/SharedLockbox.t.sol index c1ce08846d1..282272682ec 100644 --- a/packages/contracts-bedrock/test/L1/SharedLockbox.t.sol +++ b/packages/contracts-bedrock/test/L1/SharedLockbox.t.sol @@ -7,6 +7,7 @@ import { Unauthorized, Paused as PausedError } from "src/libraries/errors/Common // Interfaces import { IOptimismPortal2 as IOptimismPortal } from "interfaces/L1/IOptimismPortal2.sol"; +import { ISuperchainConfigInterop } from "interfaces/L1/ISuperchainConfigInterop.sol"; contract SharedLockboxTest is CommonTest { event ETHLocked(address indexed portal, uint256 amount); @@ -20,9 +21,13 @@ contract SharedLockboxTest is CommonTest { super.setUp(); } + function _superchainConfig() internal view returns (ISuperchainConfigInterop) { + return ISuperchainConfigInterop(address(superchainConfig)); + } + /// @notice Tests it reverts when the caller is not an authorized portal. function test_lockETH_unauthorizedPortal_reverts(address _caller) public { - vm.assume(!sharedLockbox.authorizedPortals(_caller)); + vm.assume(!_superchainConfig().authorizedPortals(_caller)); // Expect the revert with `Unauthorized` selector vm.expectRevert(Unauthorized.selector); @@ -33,10 +38,35 @@ contract SharedLockboxTest is CommonTest { } /// @notice Tests the ETH is correctly locked when the caller is an authorized portal. - function test_lockETH_succeeds(address _portal, uint256 _amount) public { - // Set the caller as an authorized portal - vm.prank(address(superchainConfig)); - sharedLockbox.authorizePortal(_portal); + function test_lockETH_succeeds(uint256 _amount) public { + // Deal the ETH amount to the portal + vm.deal(address(optimismPortal2), _amount); + + // Get the balance of the portal and lockbox before the lock to compare later on the assertions + uint256 _portalBalanceBefore = address(optimismPortal2).balance; + uint256 _lockboxBalanceBefore = address(sharedLockbox).balance; + + // Look for the emit of the `ETHLocked` event + vm.expectEmit(address(sharedLockbox)); + emit ETHLocked(address(optimismPortal2), _amount); + + // Call the `lockETH` function with the portal + vm.prank(address(optimismPortal2)); + sharedLockbox.lockETH{ value: _amount }(); + + // Assert the portal's balance decreased and the lockbox's balance increased by the amount locked + assertEq(address(optimismPortal2).balance, _portalBalanceBefore - _amount); + assertEq(address(sharedLockbox).balance, _lockboxBalanceBefore + _amount); + } + + /// @notice Tests the ETH is correctly locked when the caller is an authorized portal with different portals. + function test_lockETHWithDifferentPortal_succeeds(address _portal, uint256 _amount) public { + // Mock the portal as an authorized portal + vm.mockCall( + address(superchainConfig), + abi.encodeCall(ISuperchainConfigInterop.authorizedPortals, (_portal)), + abi.encode(true) + ); // Deal the ETH amount to the portal vm.deal(_portal, _amount); @@ -74,7 +104,7 @@ contract SharedLockboxTest is CommonTest { /// @notice Tests it reverts when the caller is not an authorized portal. function test_unlockETH_unauthorizedPortal_reverts(address _caller, uint256 _value) public { - vm.assume(!sharedLockbox.authorizedPortals(_caller)); + vm.assume(!_superchainConfig().authorizedPortals(_caller)); // Expect the revert with `Unauthorized` selector vm.expectRevert(Unauthorized.selector); @@ -86,10 +116,6 @@ contract SharedLockboxTest is CommonTest { /// @notice Tests the ETH is correctly unlocked when the caller is an authorized portal. function test_unlockETH_succeeds(uint256 _value) public { - // Set the caller as an authorized portal - vm.prank(address(superchainConfig)); - sharedLockbox.authorizePortal(address(optimismPortal2)); - // Deal the ETH amount to the lockbox vm.deal(address(sharedLockbox), _value); @@ -113,50 +139,6 @@ contract SharedLockboxTest is CommonTest { assertEq(address(sharedLockbox).balance, _lockboxBalanceBefore - _value); } - /// @notice Tests `authorizePortal` reverts when the contract is paused. - function test_authorizePortal_paused_reverts(address _caller, address _portal) public { - // Set the paused status to true - vm.prank(superchainConfig.guardian()); - superchainConfig.pause("test"); - - // Expect the revert with `Paused` selector - vm.expectRevert(PausedError.selector); - - // Call the `authorizePortal` function with the caller - vm.prank(_caller); - sharedLockbox.authorizePortal(_portal); - } - - /// @notice Tests it reverts when the caller is not the SuperchainConfig. - function test_authorizePortal_notSuperchainConfig_reverts(address _caller) public { - vm.assume(_caller != address(superchainConfig)); - - // Expect the revert with `Unauthorized` selector - vm.expectRevert(Unauthorized.selector); - - // Call the `authorizePortal` function with a non-SuperchainConfig caller - vm.prank(_caller); - sharedLockbox.authorizePortal(_caller); - } - - /// @notice Tests the portal is correctly authorized when the caller is the SuperchainConfig. - function test_authorizePortal_succeeds(address _portal) public { - // Check the portal's authorized status before the authorization to compare later on the assertions. - // Adding this check to make it more future proof in case something changes on the setup. - vm.assume(sharedLockbox.authorizedPortals(_portal) == false); - - // Look for the emit of the `PortalAuthorized` event - vm.expectEmit(address(sharedLockbox)); - emit PortalAuthorized(_portal); - - // Call the `authorizePortal` function with the SuperchainConfig - vm.prank(address(superchainConfig)); - sharedLockbox.authorizePortal(_portal); - - // Assert the portal's authorized status was updated correctly - assertEq(sharedLockbox.authorizedPortals(_portal), true); - } - /// @notice Tests the paused status is correctly returned. function test_paused_succeeds() public { // Assert the paused status is false diff --git a/packages/contracts-bedrock/test/L1/SuperchainConfig.t.sol b/packages/contracts-bedrock/test/L1/SuperchainConfig.t.sol index f2f419e3e9b..072ab3ff194 100644 --- a/packages/contracts-bedrock/test/L1/SuperchainConfig.t.sol +++ b/packages/contracts-bedrock/test/L1/SuperchainConfig.t.sol @@ -5,14 +5,12 @@ import { CommonTest } from "test/setup/CommonTest.sol"; // Target contract dependencies import { IProxy } from "interfaces/universal/IProxy.sol"; -import { Unauthorized } from "src/libraries/errors/CommonErrors.sol"; // Target contract import { ISuperchainConfig } from "interfaces/L1/ISuperchainConfig.sol"; -import { SuperchainConfig, ISharedLockbox, ISystemConfig } from "src/L1/SuperchainConfig.sol"; +import { SuperchainConfig } from "src/L1/SuperchainConfig.sol"; import { DeployUtils } from "scripts/libraries/DeployUtils.sol"; -import { EnumerableSet } from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; contract SuperchainConfig_Init_Test is CommonTest { function setUp() public virtual override { @@ -24,7 +22,6 @@ contract SuperchainConfig_Init_Test is CommonTest { function test_initialize_succeeds() external view { assertFalse(superchainConfig.paused()); assertEq(superchainConfig.guardian(), deploy.cfg().superchainConfigGuardian()); - assertEq(superchainConfig.dependencyManager(), deploy.cfg().finalSystemOwner()); } /// @dev Tests that it can be intialized as paused. @@ -38,24 +35,18 @@ contract SuperchainConfig_Init_Test is CommonTest { ISuperchainConfig newImpl = ISuperchainConfig( DeployUtils.create1({ _name: "SuperchainConfig", - _args: DeployUtils.encodeConstructor( - abi.encodeCall(ISuperchainConfig.__constructor__, (address(sharedLockbox))) - ) + _args: DeployUtils.encodeConstructor(abi.encodeCall(ISuperchainConfig.__constructor__, ())) }) ); vm.startPrank(alice); newProxy.upgradeToAndCall( address(newImpl), - abi.encodeCall( - ISuperchainConfig.initialize, - (deploy.cfg().superchainConfigGuardian(), deploy.cfg().finalSystemOwner(), true) - ) + abi.encodeCall(ISuperchainConfig.initialize, (deploy.cfg().superchainConfigGuardian(), true)) ); assertTrue(ISuperchainConfig(address(newProxy)).paused()); assertEq(ISuperchainConfig(address(newProxy)).guardian(), deploy.cfg().superchainConfigGuardian()); - assertEq(ISuperchainConfig(address(newProxy)).dependencyManager(), deploy.cfg().finalSystemOwner()); } } @@ -120,164 +111,3 @@ contract SuperchainConfig_Unpause_Test is CommonTest { assertFalse(superchainConfig.paused()); } } - -contract SuperchainConfig_AddDependency_Test is CommonTest { - event DependencyAdded(uint256 indexed chainId, address indexed systemConfig, address indexed portal); - - function setUp() public virtual override { - super.enableInterop(); - super.setUp(); - } - - function _mockAndExpect(address _target, bytes memory _calldata, bytes memory _returnData) internal { - vm.mockCall(_target, _calldata, _returnData); - vm.expectCall(_target, _calldata); - } - - /// @notice Tests that `addDependency` reverts when called by an unauthorized address. - function test_addDependency_unauthorized_reverts( - address _caller, - uint256 _chainId, - address _systemConfig - ) - external - { - vm.assume(_caller != superchainConfig.dependencyManager()); - - vm.expectRevert(Unauthorized.selector); - vm.prank(_caller); - superchainConfig.addDependency(_chainId, _systemConfig); - } - - /// @notice Tests that `addDependency` reverts when the dependency set is too large. - function test_addDependency_dependencySetTooLarge_reverts() external { - vm.startPrank(superchainConfig.dependencyManager()); - - // Add the maximum number of dependencies to the dependency set - uint256 i; - for (i; i < type(uint8).max; i++) { - superchainConfig.addDependency(i, address(systemConfig)); - } - - // Check that the dependency set is full and that expect the next call to revert - assertEq(superchainConfig.dependencySetSize(), type(uint8).max); - vm.expectRevert(SuperchainConfig.DependencySetTooLarge.selector); - - // Try to add another dependency to the dependency set - uint256 chainId = i + 1; - superchainConfig.addDependency(chainId, address(systemConfig)); - - vm.stopPrank(); - } - - /// @notice Tests that `addDependency` reverts when the chain ID is the same as the current chain ID. - function test_addDependency_sameChainID_reverts() external { - vm.prank(superchainConfig.dependencyManager()); - vm.expectRevert(SuperchainConfig.InvalidChainID.selector); - superchainConfig.addDependency(block.chainid, address(systemConfig)); - } - - /// @notice Tests that `addDependency` reverts when the chain is already in the dependency set. - function test_addDependency_chainAlreadyExists_reverts(uint256 _chainId) external { - vm.assume(_chainId != block.chainid); - - vm.startPrank(superchainConfig.dependencyManager()); - superchainConfig.addDependency(_chainId, address(systemConfig)); - - vm.expectRevert(SuperchainConfig.DependencyAlreadyAdded.selector); - superchainConfig.addDependency(_chainId, address(systemConfig)); - vm.stopPrank(); - } - - /// @notice Tests that `addDependency` successfully adds a chain to the dependency set when it is empty. - function test_addDependency_onEmptyDependencySet_succeeds(uint256 _chainId, address _portal) external { - vm.assume(!superchainConfig.isInDependencySet(_chainId)); - - // Store the PORTAL address we expect to be used in a call in the SystemConfig OptimsimPortal slot, and expect - // it to be called - vm.store( - address(systemConfig), - bytes32(uint256(keccak256("systemconfig.optimismportal")) - 1), - bytes32(uint256(uint160(_portal))) - ); - vm.expectCall(address(systemConfig), abi.encodeCall(ISystemConfig.optimismPortal, ())); - - // Mock and expect the call to authorize the portal on the SharedLockbox with the `_portal` address - vm.expectCall(address(sharedLockbox), abi.encodeCall(ISharedLockbox.authorizePortal, (_portal))); - - // Expect the DependencyAdded event to be emitted - vm.expectEmit(address(superchainConfig)); - emit DependencyAdded(_chainId, address(systemConfig), _portal); - - // Add the new chain to the dependency set - vm.prank(superchainConfig.dependencyManager()); - superchainConfig.addDependency(_chainId, address(systemConfig)); - - // Check that the new chain is in the dependency set - assertTrue(superchainConfig.isInDependencySet(_chainId)); - assertEq(superchainConfig.dependencySetSize(), 1); - } -} - -contract SuperchainConfig_IsInDependencySet_Test is CommonTest { - /// @dev Tests that `isInDependencySet` returns false when the chain is not in the dependency set. Checking if empty - /// to ensure that should always be false. - function test_isInDependencySet_false_succeeds(uint256 _chainId) external view { - assert(superchainConfig.dependencySet().length == 0); - assertFalse(superchainConfig.isInDependencySet(_chainId)); - } - - /// @dev Tests that `isInDependencySet` returns true when the chain is in the dependency set. - function test_isInDependencySet_true_succeeds(uint256 _chainId) external { - vm.assume(_chainId != block.chainid); - vm.prank(superchainConfig.dependencyManager()); - superchainConfig.addDependency(_chainId, address(systemConfig)); - assertTrue(superchainConfig.isInDependencySet(_chainId)); - } -} - -contract SuperchainConfig_DependencySet_Test is CommonTest { - using EnumerableSet for EnumerableSet.UintSet; - - EnumerableSet.UintSet internal chainIds; - - function _addDependencies(uint256[] calldata _chainIdsArray) internal { - vm.assume(_chainIdsArray.length <= type(uint8).max); - - // Ensure there are no repeated values on the input array - for (uint256 i; i < _chainIdsArray.length; i++) { - if (_chainIdsArray[i] != block.chainid) chainIds.add(_chainIdsArray[i]); - } - - vm.startPrank(superchainConfig.dependencyManager()); - - // Add the dependencies to the dependency set - for (uint256 i; i < chainIds.length(); i++) { - superchainConfig.addDependency(chainIds.at(i), address(systemConfig)); - } - - vm.stopPrank(); - } - - /// @notice Tests that the dependency set returns properly the dependencies added. - function test_dependencySet_succeeds(uint256[] calldata _chainIdsArray) public { - _addDependencies(_chainIdsArray); - - // Check that the dependency set has the same length as the dependencies - uint256[] memory dependencySet = superchainConfig.dependencySet(); - assertEq(dependencySet.length, chainIds.length()); - - // Check that the dependency set has the same chain IDs as the dependencies - for (uint256 i; i < chainIds.length(); i++) { - assertEq(dependencySet[i], chainIds.at(i)); - } - } - - /// @notice Tests that the dependency set size returns properly the number of dependencies added. - function test_dependencySetSize_succeeds(uint256[] calldata _chainIdsArray) public { - _addDependencies(_chainIdsArray); - - // Check that the dependency set has the same length as the dependencies - assertEq(superchainConfig.dependencySetSize(), chainIds.length()); - } -} diff --git a/packages/contracts-bedrock/test/L1/SuperchainConfigInterop.t.sol b/packages/contracts-bedrock/test/L1/SuperchainConfigInterop.t.sol new file mode 100644 index 00000000000..28c768bb1f8 --- /dev/null +++ b/packages/contracts-bedrock/test/L1/SuperchainConfigInterop.t.sol @@ -0,0 +1,355 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.15; + +import { CommonTest } from "test/setup/CommonTest.sol"; + +// Target contract dependencies +import { IProxy } from "interfaces/universal/IProxy.sol"; +import { Unauthorized } from "src/libraries/errors/CommonErrors.sol"; + +// Target contract +import { ISuperchainConfigInterop } from "interfaces/L1/ISuperchainConfigInterop.sol"; +import { SuperchainConfigInterop, ISystemConfig, IOptimismPortalInterop } from "src/L1/SuperchainConfigInterop.sol"; + +import { DeployUtils } from "scripts/libraries/DeployUtils.sol"; +import { EnumerableSet } from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; +import { Predeploys } from "src/libraries/Predeploys.sol"; + +contract SuperchainConfigInterop_Base_Test is CommonTest { + function setUp() public virtual override { + super.enableInterop(); + super.setUp(); + } + + function _superchainConfigInterop() internal view returns (SuperchainConfigInterop) { + return SuperchainConfigInterop(address(superchainConfig)); + } + + function _mockAndExpect(address _target, bytes memory _calldata, bytes memory _returnData) internal { + vm.mockCall(_target, _calldata, _returnData); + vm.expectCall(_target, _calldata); + } + + function _setUpPortal(uint256 _chainId) internal returns (address portal_) { + portal_ = address(bytes20(keccak256(abi.encodePacked(_chainId)))); + + // Mock the portal to return the correct superchain config address + _mockAndExpect( + portal_, abi.encodeCall(IOptimismPortalInterop.superchainConfig, ()), abi.encode(address(superchainConfig)) + ); + _mockAndExpect(portal_, abi.encodeCall(IOptimismPortalInterop.migrateLiquidity, ()), abi.encode()); + + // Store the PORTAL address we expect to be used in a call in the SystemConfig OptimsimPortal slot, and expect + // it to be called + vm.store( + address(systemConfig), + bytes32(uint256(keccak256("systemconfig.optimismportal")) - 1), + bytes32(uint256(uint160(portal_))) + ); + vm.expectCall(address(systemConfig), abi.encodeCall(ISystemConfig.optimismPortal, ())); + } +} + +contract SuperchainConfigInterop_Init_Test is SuperchainConfigInterop_Base_Test { + function setUp() public virtual override { + super.setUp(); + skipIfForkTest("SuperchainConfig_Init_Test: cannot test initialization on forked network"); + } + + /// @dev Tests that initialization sets the correct values. These are defined in CommonTest.sol. + function test_initialize_succeeds() external view { + assertFalse(_superchainConfigInterop().paused()); + assertEq(_superchainConfigInterop().guardian(), deploy.cfg().superchainConfigGuardian()); + assertEq(_superchainConfigInterop().clusterManager(), deploy.cfg().finalSystemOwner()); + assertEq(address(_superchainConfigInterop().sharedLockbox()), address(sharedLockbox)); + } + + /// @dev Tests that it can be intialized as paused. + function test_initialize_paused_succeeds() external { + IProxy newProxy = IProxy( + DeployUtils.create1({ + _name: "Proxy", + _args: DeployUtils.encodeConstructor(abi.encodeCall(IProxy.__constructor__, (alice))) + }) + ); + ISuperchainConfigInterop newImpl = ISuperchainConfigInterop( + DeployUtils.create1({ + _name: "SuperchainConfigInterop", + _args: DeployUtils.encodeConstructor(abi.encodeCall(ISuperchainConfigInterop.__constructor__, ())) + }) + ); + + vm.startPrank(alice); + newProxy.upgradeToAndCall( + address(newImpl), + abi.encodeCall( + ISuperchainConfigInterop.initialize, + (deploy.cfg().superchainConfigGuardian(), true, deploy.cfg().finalSystemOwner(), address(sharedLockbox)) + ) + ); + + assertTrue(ISuperchainConfigInterop(address(newProxy)).paused()); + assertEq(ISuperchainConfigInterop(address(newProxy)).guardian(), deploy.cfg().superchainConfigGuardian()); + assertEq(ISuperchainConfigInterop(address(newProxy)).clusterManager(), deploy.cfg().finalSystemOwner()); + assertEq(address(ISuperchainConfigInterop(address(newProxy)).sharedLockbox()), address(sharedLockbox)); + } +} + +contract SuperchainConfigInterop_AddDependency_Test is SuperchainConfigInterop_Base_Test { + event DependencyAdded(uint256 indexed chainId, address indexed systemConfig, address indexed portal); + + /// @notice Tests that `addDependency` reverts when called by an unauthorized address. + function test_addDependency_unauthorized_reverts( + address _caller, + uint256 _chainId, + address _systemConfig + ) + external + { + vm.assume(_caller != _superchainConfigInterop().clusterManager()); + + vm.expectRevert(Unauthorized.selector); + vm.prank(_caller); + _superchainConfigInterop().addDependency(_chainId, _systemConfig); + } + + /// @notice Tests that `addDependency` reverts when the dependency set is too large. + function test_addDependency_dependencySetTooLarge_reverts() external { + vm.startPrank(_superchainConfigInterop().clusterManager()); + uint256 currentSize = _superchainConfigInterop().dependencySetSize(); + + // Add the maximum number of dependencies to the dependency set + uint256 i; + for (i; i < type(uint8).max - currentSize; i++) { + _setUpPortal(i); + _superchainConfigInterop().addDependency(i, address(systemConfig)); + } + + // Check that the dependency set is full and that expect the next call to revert + assertEq(_superchainConfigInterop().dependencySetSize(), type(uint8).max); + vm.expectRevert(SuperchainConfigInterop.DependencySetTooLarge.selector); + + // Try to add another dependency to the dependency set + uint256 chainId = i + 1; + _superchainConfigInterop().addDependency(chainId, address(systemConfig)); + + vm.stopPrank(); + } + + /// @notice Tests that `addDependency` reverts when the chain is already in the dependency set. + function test_addDependency_chainAlreadyExists_reverts(uint256 _chainId) external { + vm.assume(_chainId != block.chainid); + + // Mock the portal + _setUpPortal(_chainId); + + vm.startPrank(_superchainConfigInterop().clusterManager()); + _superchainConfigInterop().addDependency(_chainId, address(systemConfig)); + + vm.expectRevert(SuperchainConfigInterop.DependencyAlreadyAdded.selector); + _superchainConfigInterop().addDependency(_chainId, address(systemConfig)); + vm.stopPrank(); + } + + /// @notice Tests that `addDependency` successfully adds a chain to the dependency set calling it from the cluster + /// manager. + function test_addDependencyFromClusterManager_succeeds(uint256 _chainId) external { + vm.assume(!_superchainConfigInterop().isInDependencySet(_chainId)); + uint256 currentSize = _superchainConfigInterop().dependencySetSize(); + + address portal = _setUpPortal(_chainId); + + // Expect the DependencyAdded event to be emitted + vm.expectEmit(address(superchainConfig)); + emit DependencyAdded(_chainId, address(systemConfig), portal); + + // Add the new chain to the dependency set + vm.prank(_superchainConfigInterop().clusterManager()); + _superchainConfigInterop().addDependency(_chainId, address(systemConfig)); + + // Check that the new chain is in the dependency set + assertTrue(_superchainConfigInterop().isInDependencySet(_chainId)); + assertEq(_superchainConfigInterop().dependencySetSize(), currentSize + 1); + assertTrue(_superchainConfigInterop().authorizedPortals(portal)); + } + + /// @notice Tests that `addDependency` successfully adds a chain to the dependency set through a portal call. + function test_addDependencyFromPortal_succeeds(uint256 _chainId, uint256 _chainId2) external { + vm.assume(!_superchainConfigInterop().isInDependencySet(_chainId)); + vm.assume(!_superchainConfigInterop().isInDependencySet(_chainId2)); + uint256 currentSize = _superchainConfigInterop().dependencySetSize(); + + // Add first an authorized portal + address authorizedPortal = _setUpPortal(_chainId); + vm.prank(_superchainConfigInterop().clusterManager()); + _superchainConfigInterop().addDependency(_chainId, address(systemConfig)); + + address portal2 = _setUpPortal(_chainId2); + + // Expect the DependencyAdded event to be emitted + vm.expectEmit(address(superchainConfig)); + emit DependencyAdded(_chainId2, address(systemConfig), portal2); + + // Mock the `authorizedPortal` to return the dependency manager predeploy as l2Sender + _mockAndExpect( + authorizedPortal, + abi.encodeCall(IOptimismPortalInterop.l2Sender, ()), + abi.encode(Predeploys.DEPENDENCY_MANAGER) + ); + + // Add the new chain to the dependency set from the `authorizedPortal` + vm.prank(authorizedPortal); + _superchainConfigInterop().addDependency(_chainId2, address(systemConfig)); + + // Check that the new chain is in the dependency set + assertTrue(_superchainConfigInterop().isInDependencySet(_chainId2)); + assertEq(_superchainConfigInterop().dependencySetSize(), currentSize + 2); + assertTrue(_superchainConfigInterop().authorizedPortals(portal2)); + } + + /// @notice Tests that `addDependency` reverts when the caller is not the cluster manager or an authorized portal. + function test_addDependency_notClusterManagerOrPortal_reverts(address _caller, uint256 _chainId) external { + vm.assume(_caller != _superchainConfigInterop().clusterManager()); + vm.expectRevert(Unauthorized.selector); + _superchainConfigInterop().addDependency(_chainId, address(systemConfig)); + } + + /// @notice Tests that `addDependency` reverts when the caller is an authorized portal but not the correct L2 + /// sender. + function test_addDependency_notCorrectL2Sender_reverts(uint256 _chainId, address _l2sender) external { + vm.assume(_chainId != block.chainid); + vm.assume(_l2sender != Predeploys.DEPENDENCY_MANAGER); + + address portal = _setUpPortal(_chainId); + + // Mock the `authorizedPortal` to return the dependency manager predeploy as l2Sender + _mockAndExpect(portal, abi.encodeCall(IOptimismPortalInterop.l2Sender, ()), abi.encode(_l2sender)); + + // Add first an authorized portal + vm.prank(_superchainConfigInterop().clusterManager()); + _superchainConfigInterop().addDependency(_chainId, address(systemConfig)); + + vm.expectRevert(Unauthorized.selector); + vm.prank(portal); + _superchainConfigInterop().addDependency(_chainId, address(systemConfig)); + } + + /// @notice Tests that `addDependency` reverts when the portal has an invalid superchain config address. + function test_addDependency_portalInvalidSuperchainConfig_reverts( + uint256 _chainId, + address _superchainConfig + ) + external + { + vm.assume(_chainId != block.chainid); + vm.assume(_superchainConfig != address(superchainConfig)); + + address portal = address(bytes20(keccak256(abi.encodePacked(_chainId)))); + + // Store the PORTAL address we expect to be used in a call in the SystemConfig OptimsimPortal slot, and expect + // it to be called + vm.store( + address(systemConfig), + bytes32(uint256(keccak256("systemconfig.optimismportal")) - 1), + bytes32(uint256(uint160(portal))) + ); + + // Mock the portal to return a different superchain config address + _mockAndExpect( + portal, abi.encodeCall(IOptimismPortalInterop.superchainConfig, ()), abi.encode(_superchainConfig) + ); + + vm.prank(_superchainConfigInterop().clusterManager()); + vm.expectRevert(SuperchainConfigInterop.InvalidSuperchainConfig.selector); + _superchainConfigInterop().addDependency(_chainId, address(systemConfig)); + } + + /// @notice Tests that `addDependency` reverts when the portal is already authorized. + function test_addDependency_portalAlreadyAuthorized_reverts(uint256 _chainId) external { + vm.assume(_chainId != block.chainid); + vm.assume(_chainId <= type(uint128).max); + + _setUpPortal(_chainId); + + // Add first an authorized portal + vm.prank(_superchainConfigInterop().clusterManager()); + _superchainConfigInterop().addDependency(_chainId, address(systemConfig)); + + vm.prank(_superchainConfigInterop().clusterManager()); + vm.expectRevert(SuperchainConfigInterop.PortalAlreadyAuthorized.selector); + _superchainConfigInterop().addDependency(_chainId + 1, address(systemConfig)); + } +} + +contract SuperchainConfigInterop_IsInDependencySet_Test is SuperchainConfigInterop_Base_Test { + /// @dev Tests that `isInDependencySet` returns false when the chain is not in the dependency set. Checking if empty + /// to ensure that should always be false. + function test_isInDependencySet_false_succeeds(uint256 _chainId) external view { + vm.assume(_chainId != deploy.cfg().l2ChainID()); + assertFalse(_superchainConfigInterop().isInDependencySet(_chainId)); + } + + /// @dev Tests that `isInDependencySet` returns true when the chain is in the dependency set. + function test_isInDependencySet_true_succeeds(uint256 _chainId) external { + vm.assume(_chainId != block.chainid); + _setUpPortal(_chainId); + + vm.prank(_superchainConfigInterop().clusterManager()); + _superchainConfigInterop().addDependency(_chainId, address(systemConfig)); + + assertTrue(_superchainConfigInterop().isInDependencySet(_chainId)); + } +} + +contract SuperchainConfigInterop_DependencySet_Test is SuperchainConfigInterop_Base_Test { + using EnumerableSet for EnumerableSet.UintSet; + + EnumerableSet.UintSet internal chainIds; + uint256 currentSize; + + function setUp() public virtual override { + super.setUp(); + currentSize = _superchainConfigInterop().dependencySetSize(); + } + + function _addDependencies(uint256[] calldata _chainIdsArray) internal { + vm.assume(_chainIdsArray.length <= type(uint8).max - currentSize); + + // Ensure there are no repeated values on the input array + for (uint256 i; i < _chainIdsArray.length; i++) { + if (_chainIdsArray[i] != block.chainid) chainIds.add(_chainIdsArray[i]); + } + + vm.startPrank(_superchainConfigInterop().clusterManager()); + + // Add the dependencies to the dependency set + for (uint256 i; i < chainIds.length(); i++) { + _setUpPortal(i); + _superchainConfigInterop().addDependency(chainIds.at(i), address(systemConfig)); + } + + vm.stopPrank(); + } + + /// @notice Tests that the dependency set returns properly the dependencies added. + function test_dependencySet_succeeds(uint256[] calldata _chainIdsArray) public { + _addDependencies(_chainIdsArray); + + // Check that the dependency set has the same length as the dependencies + uint256[] memory dependencySet = _superchainConfigInterop().dependencySet(); + assertEq(dependencySet.length, chainIds.length() + currentSize); + + // Check that the dependency set has the same chain IDs as the dependencies + for (uint256 i; i < chainIds.length(); i++) { + assertEq(dependencySet[i + currentSize], chainIds.at(i)); + } + } + + /// @notice Tests that the dependency set size returns properly the number of dependencies added. + function test_dependencySetSize_succeeds(uint256[] calldata _chainIdsArray) public { + _addDependencies(_chainIdsArray); + + // Check that the dependency set has the same length as the dependencies + assertEq(_superchainConfigInterop().dependencySetSize(), chainIds.length() + currentSize); + } +} diff --git a/packages/contracts-bedrock/test/L2/CrossL2Inbox.t.sol b/packages/contracts-bedrock/test/L2/CrossL2Inbox.t.sol index a4e9d706f1b..26556a4eed8 100644 --- a/packages/contracts-bedrock/test/L2/CrossL2Inbox.t.sol +++ b/packages/contracts-bedrock/test/L2/CrossL2Inbox.t.sol @@ -1,164 +1,35 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.25; +pragma solidity 0.8.15; // Testing utilities -import { Test } from "forge-std/Test.sol"; +import { CommonTest } from "test/setup/CommonTest.sol"; // Libraries import { Predeploys } from "src/libraries/Predeploys.sol"; -import { TransientContext } from "src/libraries/TransientContext.sol"; // Target contracts -import { - CrossL2Inbox, - Identifier, - NotEntered, - NoExecutingDeposits, - InvalidTimestamp, - TargetCallFailed, - NotDepositor, - InteropStartAlreadySet -} from "src/L2/CrossL2Inbox.sol"; +import { ICrossL2Inbox, Identifier } from "interfaces/L2/ICrossL2Inbox.sol"; import { IL1BlockInterop } from "interfaces/L2/IL1BlockInterop.sol"; -/// @title CrossL2InboxWithModifiableTransientStorage -/// @dev CrossL2Inbox contract with methods to modify the transient storage. -/// This is used to test the transient storage of CrossL2Inbox. -contract CrossL2InboxWithModifiableTransientStorage is CrossL2Inbox { - /// @dev Increments call depth in transient storage. - function increment() external { - TransientContext.increment(); - } - - /// @dev Sets origin in transient storage. - /// @param _origin Origin to set. - function setOrigin(address _origin) external { - TransientContext.set(ORIGIN_SLOT, uint160(_origin)); - } - - /// @dev Sets block number in transient storage. - /// @param _blockNumber Block number to set. - function setBlockNumber(uint256 _blockNumber) external { - TransientContext.set(BLOCK_NUMBER_SLOT, _blockNumber); - } - - /// @dev Sets log index in transient storage. - /// @param _logIndex Log index to set. - function setLogIndex(uint256 _logIndex) external { - TransientContext.set(LOG_INDEX_SLOT, _logIndex); - } - - /// @dev Sets timestamp in transient storage. - /// @param _timestamp Timestamp to set. - function setTimestamp(uint256 _timestamp) external { - TransientContext.set(TIMESTAMP_SLOT, _timestamp); - } - - /// @dev Sets chain ID in transient storage. - /// @param _chainId Chain ID to set. - function setChainId(uint256 _chainId) external { - TransientContext.set(CHAINID_SLOT, _chainId); - } -} - /// @title CrossL2InboxTest /// @dev Contract for testing the CrossL2Inbox contract. -contract CrossL2InboxTest is Test { - /// @dev Selector for the `isInDependencySet` method of the L1Block contract. - bytes4 constant L1BlockIsInDependencySetSelector = bytes4(keccak256("isInDependencySet(uint256)")); +contract CrossL2InboxTest is CommonTest { + error NoExecutingDeposits(); - /// @dev Storage slot that the interop start timestamp is stored at. - /// Equal to bytes32(uint256(keccak256("crossl2inbox.interopstart")) - 1) - bytes32 internal constant INTEROP_START_SLOT = bytes32(uint256(keccak256("crossl2inbox.interopstart")) - 1); + event ExecutingMessage(bytes32 indexed msgHash, Identifier id); /// @dev CrossL2Inbox contract instance. - CrossL2Inbox crossL2Inbox; - - // interop start timestamp - uint256 interopStartTime = 420; - - /// @dev The address that represents the system caller responsible for L1 attributes - /// transactions. - address internal constant DEPOSITOR_ACCOUNT = 0xDeaDDEaDDeAdDeAdDEAdDEaddeAddEAdDEAd0001; + ICrossL2Inbox crossL2Inbox; /// @dev Sets up the test suite. - function setUp() public { - // Deploy the L2ToL2CrossDomainMessenger contract - vm.etch(Predeploys.CROSS_L2_INBOX, address(new CrossL2InboxWithModifiableTransientStorage()).code); - crossL2Inbox = CrossL2Inbox(Predeploys.CROSS_L2_INBOX); - } - - modifier setInteropStart() { - // Set interop start - vm.store(address(crossL2Inbox), INTEROP_START_SLOT, bytes32(interopStartTime)); - - // Set timestamp to be after interop start - vm.warp(interopStartTime + 1 hours); - - _; - } - - /// @dev Tests that the setInteropStart function updates the INTEROP_START_SLOT storage slot correctly - function testFuzz_setInteropStart_succeeds(uint256 time) external { - // Jump to time. - vm.warp(time); - - // Impersonate the depositor account. - vm.prank(DEPOSITOR_ACCOUNT); + function setUp() public virtual override { + super.enableInterop(); + super.setUp(); - // Set interop start. - crossL2Inbox.setInteropStart(); - - // Check that the storage slot was set correctly and the public getter function returns the right value. - assertEq(crossL2Inbox.interopStart(), time); - assertEq(uint256(vm.load(address(crossL2Inbox), INTEROP_START_SLOT)), time); - } - - /// @dev Tests that the setInteropStart function reverts when the caller is not the DEPOSITOR_ACCOUNT. - function test_setInteropStart_notDepositorAccount_reverts() external { - // Expect revert with OnlyDepositorAccount selector - vm.expectRevert(NotDepositor.selector); - - // Call setInteropStart function - crossL2Inbox.setInteropStart(); + crossL2Inbox = ICrossL2Inbox(Predeploys.CROSS_L2_INBOX); } - /// @dev Tests that the setInteropStart function reverts if called when already set - function test_setInteropStart_interopStartAlreadySet_reverts() external { - // Impersonate the depositor account. - vm.startPrank(DEPOSITOR_ACCOUNT); - - // Call setInteropStart function - crossL2Inbox.setInteropStart(); - - // Expect revert with InteropStartAlreadySet selector if called a second time - vm.expectRevert(InteropStartAlreadySet.selector); - - // Call setInteropStart function again - crossL2Inbox.setInteropStart(); - } - - /// @dev Tests that the `executeMessage` function succeeds. - function testFuzz_executeMessage_succeeds( - Identifier memory _id, - address _target, - bytes calldata _message, - uint256 _value - ) - external - payable - setInteropStart - { - // Ensure that the id's timestamp is valid (less than or equal to the current block timestamp and greater than - // interop start time) - _id.timestamp = bound(_id.timestamp, interopStartTime + 1, block.timestamp); - - // Ensure that the target call is payable if value is sent - if (_value > 0) assumePayable(_target); - - // Ensure target is not a forge address. - assumeNotForgeAddress(_target); - + function testFuzz_validateMessage_succeeds(Identifier memory _id, bytes32 _messageHash) external { // Ensure is not a deposit transaction vm.mockCall({ callee: Predeploys.L1_BLOCK_ATTRIBUTES, @@ -166,236 +37,9 @@ contract CrossL2InboxTest is Test { returnData: abi.encode(false) }); - // Ensure that the target call does not revert - vm.mockCall({ callee: _target, msgValue: _value, data: _message, returnData: abi.encode(true) }); - - // Ensure that the contract has enough balance to send with value - vm.deal(address(this), _value); - - // Look for the call to the target contract - vm.expectCall({ callee: _target, msgValue: _value, data: _message }); - // Look for the emit ExecutingMessage event vm.expectEmit(Predeploys.CROSS_L2_INBOX); - emit CrossL2Inbox.ExecutingMessage(keccak256(_message), _id); - - // Call the executeMessage function - crossL2Inbox.executeMessage{ value: _value }({ _id: _id, _target: _target, _message: _message }); - - // Check that the Identifier was stored correctly, but first we have to increment. This is because - // `executeMessage` increments + decrements the transient call depth, so we need to increment to have the - // getters use the right call depth. - CrossL2InboxWithModifiableTransientStorage(Predeploys.CROSS_L2_INBOX).increment(); - assertEq(crossL2Inbox.origin(), _id.origin); - assertEq(crossL2Inbox.blockNumber(), _id.blockNumber); - assertEq(crossL2Inbox.logIndex(), _id.logIndex); - assertEq(crossL2Inbox.timestamp(), _id.timestamp); - assertEq(crossL2Inbox.chainId(), _id.chainId); - } - - /// @dev Mock reentrant function that calls the `executeMessage` function. - /// @param _id Identifier to pass to the `executeMessage` function. - function mockReentrant(Identifier calldata _id) external payable { - crossL2Inbox.executeMessage({ _id: _id, _target: address(0), _message: "" }); - } - - /// @dev Tests that the `executeMessage` function successfully handles reentrant calls. - function testFuzz_executeMessage_reentrant_succeeds( - Identifier memory _id1, // identifier passed to `executeMessage` by the initial call. - Identifier memory _id2, // identifier passed to `executeMessage` by the reentrant call. - uint256 _value - ) - external - payable - setInteropStart - { - // Ensure that the ids' timestamp are valid (less than or equal to the current block timestamp and greater than - // interop start time) - _id1.timestamp = bound(_id1.timestamp, interopStartTime + 1, block.timestamp); - _id2.timestamp = bound(_id2.timestamp, interopStartTime + 1, block.timestamp); - - // Ensure is not a deposit transaction - vm.mockCall({ - callee: Predeploys.L1_BLOCK_ATTRIBUTES, - data: abi.encodeCall(IL1BlockInterop.isDeposit, ()), - returnData: abi.encode(false) - }); - - // Set the target and message for the reentrant call - address target = address(this); - bytes memory message = abi.encodeCall(this.mockReentrant, (_id2)); - - // Ensure that the contract has enough balance to send with value - vm.deal(address(this), _value); - - // Look for the call to the target contract - vm.expectCall({ callee: target, msgValue: _value, data: message }); - - // Call the executeMessage function - crossL2Inbox.executeMessage{ value: _value }({ _id: _id1, _target: target, _message: message }); - - // Check that the reentrant function didn't update Identifier in transient storage at first call's call depth - CrossL2InboxWithModifiableTransientStorage(Predeploys.CROSS_L2_INBOX).increment(); - assertEq(crossL2Inbox.origin(), _id1.origin); - assertEq(crossL2Inbox.blockNumber(), _id1.blockNumber); - assertEq(crossL2Inbox.logIndex(), _id1.logIndex); - assertEq(crossL2Inbox.timestamp(), _id1.timestamp); - assertEq(crossL2Inbox.chainId(), _id1.chainId); - - // Check that the reentrant function updated the Identifier at deeper call depth - CrossL2InboxWithModifiableTransientStorage(Predeploys.CROSS_L2_INBOX).increment(); - assertEq(crossL2Inbox.origin(), _id2.origin); - assertEq(crossL2Inbox.blockNumber(), _id2.blockNumber); - assertEq(crossL2Inbox.logIndex(), _id2.logIndex); - assertEq(crossL2Inbox.timestamp(), _id2.timestamp); - assertEq(crossL2Inbox.chainId(), _id2.chainId); - } - - /// @dev Tests that the `executeMessage` function reverts if the transaction comes from a deposit. - function testFuzz_executeMessage_isDeposit_reverts( - Identifier calldata _id, - address _target, - bytes calldata _message, - uint256 _value - ) - external - { - // Ensure it is a deposit transaction - vm.mockCall({ - callee: Predeploys.L1_BLOCK_ATTRIBUTES, - data: abi.encodeCall(IL1BlockInterop.isDeposit, ()), - returnData: abi.encode(true) - }); - - // Ensure that the contract has enough balance to send with value - vm.deal(address(this), _value); - - // Expect a revert with the NoExecutingDeposits selector - vm.expectRevert(NoExecutingDeposits.selector); - - // Call the executeMessage function - crossL2Inbox.executeMessage{ value: _value }({ _id: _id, _target: _target, _message: _message }); - } - - /// @dev Tests that the `executeMessage` function reverts when called with an identifier with an invalid timestamp. - function testFuzz_executeMessage_invalidTimestamp_reverts( - Identifier memory _id, - address _target, - bytes calldata _message, - uint256 _value - ) - external - setInteropStart - { - // Ensure that the id's timestamp is invalid (greater than the current block timestamp) - _id.timestamp = bound(_id.timestamp, block.timestamp + 1, type(uint256).max); - - // Ensure is not a deposit transaction - vm.mockCall({ - callee: Predeploys.L1_BLOCK_ATTRIBUTES, - data: abi.encodeCall(IL1BlockInterop.isDeposit, ()), - returnData: abi.encode(false) - }); - - // Ensure that the contract has enough balance to send with value - vm.deal(address(this), _value); - - // Expect a revert with the InvalidTimestamp selector - vm.expectRevert(InvalidTimestamp.selector); - - // Call the executeMessage function - crossL2Inbox.executeMessage{ value: _value }({ _id: _id, _target: _target, _message: _message }); - } - - /// @dev Tests that the `executeMessage` function reverts when called with an identifier with a timestamp earlier - /// than INTEROP_START timestamp - function testFuzz_executeMessage_invalidTimestampInteropStart_reverts( - Identifier memory _id, - address _target, - bytes calldata _message, - uint256 _value - ) - external - setInteropStart - { - // Ensure that the id's timestamp is invalid (less than or equal to interopStartTime) - _id.timestamp = bound(_id.timestamp, 0, crossL2Inbox.interopStart()); - - // Ensure that the contract has enough balance to send with value - vm.deal(address(this), _value); - - // Ensure is not a deposit transaction - vm.mockCall({ - callee: Predeploys.L1_BLOCK_ATTRIBUTES, - data: abi.encodeCall(IL1BlockInterop.isDeposit, ()), - returnData: abi.encode(false) - }); - - // Expect a revert with the InvalidTimestamp selector - vm.expectRevert(InvalidTimestamp.selector); - - // Call the executeMessage function - crossL2Inbox.executeMessage{ value: _value }({ _id: _id, _target: _target, _message: _message }); - } - - /// @dev Tests that the `executeMessage` function reverts when the target call fails. - function testFuzz_executeMessage_targetCallFailed_reverts( - Identifier memory _id, - address _target, - bytes calldata _message, - uint256 _value - ) - external - setInteropStart - { - // Ensure that the id's timestamp is valid (less than or equal to the current block timestamp and greater than - // interop start time) - _id.timestamp = bound(_id.timestamp, interopStartTime + 1, block.timestamp); - - // Ensure that the target call is payable if value is sent - if (_value > 0) assumePayable(_target); - - // Ensure target is not a forge address. - assumeNotForgeAddress(_target); - - // Ensure that the target call reverts - vm.mockCallRevert({ callee: _target, msgValue: _value, data: _message, revertData: abi.encode(false) }); - - // Ensure is not a deposit transaction - vm.mockCall({ - callee: Predeploys.L1_BLOCK_ATTRIBUTES, - data: abi.encodeCall(IL1BlockInterop.isDeposit, ()), - returnData: abi.encode(false) - }); - - // Ensure that the contract has enough balance to send with value - vm.deal(address(this), _value); - - // Look for the call to the target contract - vm.expectCall({ callee: _target, msgValue: _value, data: _message }); - - // Expect a revert with the TargetCallFailed selector - vm.expectRevert(TargetCallFailed.selector); - - // Call the executeMessage function - crossL2Inbox.executeMessage{ value: _value }({ _id: _id, _target: _target, _message: _message }); - } - - function testFuzz_validateMessage_succeeds(Identifier memory _id, bytes32 _messageHash) external setInteropStart { - // Ensure that the id's timestamp is valid (less than or equal to the current block timestamp and greater than - // interop start time) - _id.timestamp = bound(_id.timestamp, interopStartTime + 1, block.timestamp); - - // Ensure is not a deposit transaction - vm.mockCall({ - callee: Predeploys.L1_BLOCK_ATTRIBUTES, - data: abi.encodeCall(IL1BlockInterop.isDeposit, ()), - returnData: abi.encode(false) - }); - - // Look for the emit ExecutingMessage event - vm.expectEmit(Predeploys.CROSS_L2_INBOX); - emit CrossL2Inbox.ExecutingMessage(_messageHash, _id); + emit ExecutingMessage(_messageHash, _id); // Call the validateMessage function crossL2Inbox.validateMessage(_id, _messageHash); @@ -415,146 +59,4 @@ contract CrossL2InboxTest is Test { // Call the executeMessage function crossL2Inbox.validateMessage(_id, _messageHash); } - - /// @dev Tests that the `validateMessage` function reverts when called with an identifier with a timestamp later - /// than current block.timestamp. - function testFuzz_validateMessage_invalidTimestamp_reverts( - Identifier memory _id, - bytes32 _messageHash - ) - external - setInteropStart - { - // Ensure is not a deposit transaction - vm.mockCall({ - callee: Predeploys.L1_BLOCK_ATTRIBUTES, - data: abi.encodeCall(IL1BlockInterop.isDeposit, ()), - returnData: abi.encode(false) - }); - - // Ensure that the id's timestamp is invalid (greater than the current block timestamp) - _id.timestamp = bound(_id.timestamp, block.timestamp + 1, type(uint256).max); - - // Expect a revert with the InvalidTimestamp selector - vm.expectRevert(InvalidTimestamp.selector); - - // Call the validateMessage function - crossL2Inbox.validateMessage(_id, _messageHash); - } - - /// @dev Tests that the `validateMessage` function reverts when called with an identifier with a timestamp earlier - /// than INTEROP_START timestamp - function testFuzz_validateMessage_invalidTimestampInteropStart_reverts( - Identifier memory _id, - bytes32 _messageHash - ) - external - setInteropStart - { - // Ensure that the id's timestamp is invalid (less than or equal to interopStartTime) - _id.timestamp = bound(_id.timestamp, 0, crossL2Inbox.interopStart()); - - // Ensure is not a deposit transaction - vm.mockCall({ - callee: Predeploys.L1_BLOCK_ATTRIBUTES, - data: abi.encodeCall(IL1BlockInterop.isDeposit, ()), - returnData: abi.encode(false) - }); - - // Expect a revert with the InvalidTimestamp selector - vm.expectRevert(InvalidTimestamp.selector); - - // Call the validateMessage function - crossL2Inbox.validateMessage(_id, _messageHash); - } - - /// @dev Tests that the `origin` function returns the correct value. - function testFuzz_origin_succeeds(address _origin) external { - // Increment the call depth to prevent NotEntered revert - CrossL2InboxWithModifiableTransientStorage(Predeploys.CROSS_L2_INBOX).increment(); - // Set origin in the transient storage - CrossL2InboxWithModifiableTransientStorage(Predeploys.CROSS_L2_INBOX).setOrigin(_origin); - // Check that the `origin` function returns the correct value - assertEq(crossL2Inbox.origin(), _origin); - } - - /// @dev Tests that the `origin` function reverts when not entered. - function test_origin_notEntered_reverts() external { - // Expect a revert with the NotEntered selector - vm.expectRevert(NotEntered.selector); - // Call the `origin` function - crossL2Inbox.origin(); - } - - /// @dev Tests that the `blockNumber` function returns the correct value. - function testFuzz_blockNumber_succeeds(uint256 _blockNumber) external { - // Increment the call depth to prevent NotEntered revert - CrossL2InboxWithModifiableTransientStorage(Predeploys.CROSS_L2_INBOX).increment(); - // Set blockNumber in the transient storage - CrossL2InboxWithModifiableTransientStorage(Predeploys.CROSS_L2_INBOX).setBlockNumber(_blockNumber); - // Check that the `blockNumber` function returns the correct value - assertEq(crossL2Inbox.blockNumber(), _blockNumber); - } - - /// @dev Tests that the `blockNumber` function reverts when not entered. - function test_blockNumber_notEntered_reverts() external { - // Expect a revert with the NotEntered selector - vm.expectRevert(NotEntered.selector); - // Call the `blockNumber` function - crossL2Inbox.blockNumber(); - } - - /// @dev Tests that the `logIndex` function returns the correct value. - function testFuzz_logIndex_succeeds(uint256 _logIndex) external { - // Increment the call depth to prevent NotEntered revert - CrossL2InboxWithModifiableTransientStorage(Predeploys.CROSS_L2_INBOX).increment(); - // Set logIndex in the transient storage - CrossL2InboxWithModifiableTransientStorage(Predeploys.CROSS_L2_INBOX).setLogIndex(_logIndex); - // Check that the `logIndex` function returns the correct value - assertEq(crossL2Inbox.logIndex(), _logIndex); - } - - /// @dev Tests that the `logIndex` function reverts when not entered. - function test_logIndex_notEntered_reverts() external { - // Expect a revert with the NotEntered selector - vm.expectRevert(NotEntered.selector); - // Call the `logIndex` function - crossL2Inbox.logIndex(); - } - - /// @dev Tests that the `timestamp` function returns the correct value. - function testFuzz_timestamp_succeeds(uint256 _timestamp) external { - // Increment the call depth to prevent NotEntered revert - CrossL2InboxWithModifiableTransientStorage(Predeploys.CROSS_L2_INBOX).increment(); - // Set timestamp in the transient storage - CrossL2InboxWithModifiableTransientStorage(Predeploys.CROSS_L2_INBOX).setTimestamp(_timestamp); - // Check that the `timestamp` function returns the correct value - assertEq(crossL2Inbox.timestamp(), _timestamp); - } - - /// @dev Tests that the `timestamp` function reverts when not entered. - function test_timestamp_notEntered_reverts() external { - // Expect a revert with the NotEntered selector - vm.expectRevert(NotEntered.selector); - // Call the `timestamp` function - crossL2Inbox.timestamp(); - } - - /// @dev Tests that the `chainId` function returns the correct value. - function testFuzz_chainId_succeeds(uint256 _chainId) external { - // Increment the call depth to prevent NotEntered revert - CrossL2InboxWithModifiableTransientStorage(Predeploys.CROSS_L2_INBOX).increment(); - // Set chainId in the transient storage - CrossL2InboxWithModifiableTransientStorage(Predeploys.CROSS_L2_INBOX).setChainId(_chainId); - // Check that the `chainId` function returns the correct value - assertEq(crossL2Inbox.chainId(), _chainId); - } - - /// @dev Tests that the `chainId` function reverts when not entered. - function test_chainId_notEntered_reverts() external { - // Expect a revert with the NotEntered selector - vm.expectRevert(NotEntered.selector); - // Call the `chainId` function - crossL2Inbox.chainId(); - } } diff --git a/packages/contracts-bedrock/test/L2/L2Genesis.t.sol b/packages/contracts-bedrock/test/L2/L2Genesis.t.sol index 2398ac332df..3320be59cde 100644 --- a/packages/contracts-bedrock/test/L2/L2Genesis.t.sol +++ b/packages/contracts-bedrock/test/L2/L2Genesis.t.sol @@ -140,7 +140,7 @@ contract L2GenesisTest is Test { assertEq(getCodeCount(_path, "Proxy.sol:Proxy"), Predeploys.PREDEPLOY_COUNT - 2); // 24 proxies have the implementation set if useInterop is true and 17 if useInterop is false - assertEq(getPredeployCountWithSlotSet(_path, Constants.PROXY_IMPLEMENTATION_ADDRESS), _useInterop ? 24 : 17); + assertEq(getPredeployCountWithSlotSet(_path, Constants.PROXY_IMPLEMENTATION_ADDRESS), _useInterop ? 25 : 17); // All proxies except 2 have the proxy 1967 admin slot set to the proxy admin assertEq( diff --git a/packages/contracts-bedrock/test/L2/L2ToL2CrossDomainMessenger.t.sol b/packages/contracts-bedrock/test/L2/L2ToL2CrossDomainMessenger.t.sol index 55e43a4f782..510d5d529fa 100644 --- a/packages/contracts-bedrock/test/L2/L2ToL2CrossDomainMessenger.t.sol +++ b/packages/contracts-bedrock/test/L2/L2ToL2CrossDomainMessenger.t.sol @@ -26,6 +26,7 @@ import { // Interfaces import { ICrossL2Inbox, Identifier } from "interfaces/L2/ICrossL2Inbox.sol"; +import { IDependencySet } from "interfaces/L2/IDependencySet.sol"; /// @title L2ToL2CrossDomainMessengerWithModifiableTransientStorage /// @dev L2ToL2CrossDomainMessenger contract with methods to modify the transient storage. @@ -89,6 +90,13 @@ contract L2ToL2CrossDomainMessengerTest is Test { // Ensure that the target contract is not CrossL2Inbox or L2ToL2CrossDomainMessenger vm.assume(_target != Predeploys.CROSS_L2_INBOX && _target != Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER); + // Mock the call over the `isInDependencySet` function to return true + vm.mockCall( + Predeploys.DEPENDENCY_MANAGER, + abi.encodeCall(IDependencySet.isInDependencySet, (_destination)), + abi.encode(true) + ); + // Get the current message nonce uint256 messageNonce = l2ToL2CrossDomainMessenger.messageNonce(); diff --git a/packages/contracts-bedrock/test/invariants/OptimismPortal2.t.sol b/packages/contracts-bedrock/test/invariants/OptimismPortal2.t.sol index 2d8934da406..9cfb014a03b 100644 --- a/packages/contracts-bedrock/test/invariants/OptimismPortal2.t.sol +++ b/packages/contracts-bedrock/test/invariants/OptimismPortal2.t.sol @@ -136,15 +136,19 @@ contract OptimismPortal2_Invariant_Harness is CommonTest { game.resolveClaim(0, 0); game.resolve(); - // Fund the SharedLockbox so that we can withdraw ETH. - vm.deal(address(sharedLockbox), 0xFFFFFFFF); + // Fund the system so that we can withdraw ETH. + _fundSystem(); + } + + function _fundSystem() internal virtual { + vm.deal(address(optimismPortal2), 0xFFFFFFFF); } } contract OptimismPortal2_Deposit_Invariant is CommonTest { OptimismPortal2_Depositor internal actor; - function setUp() public override { + function setUp() public virtual override { super.setUp(); // Create a deposit actor. actor = new OptimismPortal2_Depositor(vm, optimismPortal2); @@ -167,8 +171,15 @@ contract OptimismPortal2_Deposit_Invariant is CommonTest { } } +contract OptimismPortalInterop_Deposit_Invariant is OptimismPortal2_Deposit_Invariant { + function setUp() public virtual override { + super.enableInterop(); + super.setUp(); + } +} + contract OptimismPortal2_CannotTimeTravel is OptimismPortal2_Invariant_Harness { - function setUp() public override { + function setUp() public virtual override { super.setUp(); // Prove the withdrawal transaction @@ -190,8 +201,19 @@ contract OptimismPortal2_CannotTimeTravel is OptimismPortal2_Invariant_Harness { } } +contract OptimismPortalInterop_CannotTimeTravel is OptimismPortal2_CannotTimeTravel { + function setUp() public virtual override { + super.enableInterop(); + super.setUp(); + } + + function _fundSystem() internal virtual override { + vm.deal(address(sharedLockbox), 0xFFFFFFFF); + } +} + contract OptimismPortal2_CannotFinalizeTwice is OptimismPortal2_Invariant_Harness { - function setUp() public override { + function setUp() public virtual override { super.setUp(); // Prove the withdrawal transaction @@ -219,8 +241,19 @@ contract OptimismPortal2_CannotFinalizeTwice is OptimismPortal2_Invariant_Harnes } } +contract OptimismPortalInterop_CannotFinalizeTwice is OptimismPortal2_CannotFinalizeTwice { + function setUp() public virtual override { + super.enableInterop(); + super.setUp(); + } + + function _fundSystem() internal virtual override { + vm.deal(address(sharedLockbox), 0xFFFFFFFF); + } +} + contract OptimismPortal_CanAlwaysFinalizeAfterWindow is OptimismPortal2_Invariant_Harness { - function setUp() public override { + function setUp() public virtual override { super.setUp(); // Prove the withdrawal transaction @@ -249,3 +282,14 @@ contract OptimismPortal_CanAlwaysFinalizeAfterWindow is OptimismPortal2_Invarian assertEq(address(bob).balance, bobBalanceBefore + _defaultTx.value); } } + +contract OptimismPortalInterop_CanAlwaysFinalizeAfterWindow is OptimismPortal_CanAlwaysFinalizeAfterWindow { + function setUp() public virtual override { + super.enableInterop(); + super.setUp(); + } + + function _fundSystem() internal virtual override { + vm.deal(address(sharedLockbox), 0xFFFFFFFF); + } +} diff --git a/packages/contracts-bedrock/test/opcm/DeployImplementations.t.sol b/packages/contracts-bedrock/test/opcm/DeployImplementations.t.sol index ab0599e4754..584aa59a9c2 100644 --- a/packages/contracts-bedrock/test/opcm/DeployImplementations.t.sol +++ b/packages/contracts-bedrock/test/opcm/DeployImplementations.t.sol @@ -11,7 +11,6 @@ import { IDisputeGameFactory } from "interfaces/dispute/IDisputeGameFactory.sol" import { ISuperchainConfig } from "interfaces/L1/ISuperchainConfig.sol"; import { IProtocolVersions } from "interfaces/L1/IProtocolVersions.sol"; -import { ISharedLockbox } from "interfaces/L1/ISharedLockbox.sol"; import { OPContractsManager } from "src/L1/OPContractsManager.sol"; import { IOptimismPortal2 } from "interfaces/L1/IOptimismPortal2.sol"; import { ISystemConfig } from "interfaces/L1/ISystemConfig.sol"; @@ -40,7 +39,6 @@ contract DeployImplementationsInput_Test is Test { string release = "dev-release"; // this means implementation contracts will be deployed ISuperchainConfig superchainConfigProxy = ISuperchainConfig(makeAddr("superchainConfigProxy")); IProtocolVersions protocolVersionsProxy = IProtocolVersions(makeAddr("protocolVersionsProxy")); - ISharedLockbox sharedLockboxProxy = ISharedLockbox(makeAddr("sharedLockboxProxy")); function setUp() public { dii = new DeployImplementationsInput(); @@ -73,9 +71,6 @@ contract DeployImplementationsInput_Test is Test { vm.expectRevert("DeployImplementationsInput: not set"); dii.standardVersionsToml(); - - vm.expectRevert("DeployImplementationsInput: not set"); - dii.sharedLockboxProxy(); } } @@ -228,7 +223,6 @@ contract DeployImplementations_Test is Test { uint256 disputeGameFinalityDelaySeconds = 500; ISuperchainConfig superchainConfigProxy = ISuperchainConfig(makeAddr("superchainConfigProxy")); IProtocolVersions protocolVersionsProxy = IProtocolVersions(makeAddr("protocolVersionsProxy")); - ISharedLockbox sharedLockboxProxy = ISharedLockbox(makeAddr("sharedLockboxProxy")); function setUp() public virtual { deployImplementations = new DeployImplementations(); @@ -348,7 +342,6 @@ contract DeployImplementations_Test is Test { vm.etch(address(superchainProxyAdmin), address(superchainProxyAdmin).code); vm.etch(address(superchainConfigProxy), address(superchainConfigProxy).code); vm.etch(address(protocolVersionsProxy), hex"01"); - vm.etch(address(sharedLockboxProxy), hex"01"); dii.set(dii.withdrawalDelaySeconds.selector, withdrawalDelaySeconds); dii.set(dii.minProposalSizeBytes.selector, minProposalSizeBytes); @@ -359,7 +352,6 @@ contract DeployImplementations_Test is Test { dii.set(dii.l1ContractsRelease.selector, release); dii.set(dii.superchainConfigProxy.selector, address(superchainConfigProxy)); dii.set(dii.protocolVersionsProxy.selector, address(protocolVersionsProxy)); - dii.set(dii.sharedLockboxProxy.selector, address(sharedLockboxProxy)); deployImplementations.run(dii, dio); @@ -373,7 +365,6 @@ contract DeployImplementations_Test is Test { assertEq(release, dii.l1ContractsRelease(), "525"); assertEq(address(superchainConfigProxy), address(dii.superchainConfigProxy()), "550"); assertEq(address(protocolVersionsProxy), address(dii.protocolVersionsProxy()), "575"); - assertEq(address(sharedLockboxProxy), address(dii.sharedLockboxProxy()), "577"); // Architecture assertions. assertEq(address(dio.mipsSingleton().oracle()), address(dio.preimageOracleSingleton()), "600"); @@ -395,7 +386,6 @@ contract DeployImplementations_Test is Test { dii.set(dii.l1ContractsRelease.selector, release); dii.set(dii.superchainConfigProxy.selector, address(superchainConfigProxy)); dii.set(dii.protocolVersionsProxy.selector, address(protocolVersionsProxy)); - dii.set(dii.sharedLockboxProxy.selector, address(sharedLockboxProxy)); // Set the challenge period to a value that is too large, using vm.store because the setter // method won't allow it. diff --git a/packages/contracts-bedrock/test/opcm/DeployOPChain.t.sol b/packages/contracts-bedrock/test/opcm/DeployOPChain.t.sol index b7ae94d27fc..38e78ff8e29 100644 --- a/packages/contracts-bedrock/test/opcm/DeployOPChain.t.sol +++ b/packages/contracts-bedrock/test/opcm/DeployOPChain.t.sol @@ -23,7 +23,6 @@ import { IL1ChugSplashProxy } from "interfaces/legacy/IL1ChugSplashProxy.sol"; import { IResolvedDelegateProxy } from "interfaces/legacy/IResolvedDelegateProxy.sol"; import { ISuperchainConfig } from "interfaces/L1/ISuperchainConfig.sol"; -import { ISharedLockbox } from "interfaces/L1/ISharedLockbox.sol"; import { IProtocolVersions, ProtocolVersion } from "interfaces/L1/IProtocolVersions.sol"; import { OPContractsManager } from "src/L1/OPContractsManager.sol"; import { IProxy } from "interfaces/universal/IProxy.sol"; @@ -315,7 +314,7 @@ contract DeployOPChain_TestBase is Test { ProtocolVersion recommendedProtocolVersion = ProtocolVersion.wrap(2); // Define default inputs for DeployImplementations. - // `superchainConfigProxy`, `protocolVersionsProxy` and `sharedLockboxProxy` are set during `setUp` since they are + // `superchainConfigProxy` and `protocolVersionsProxy` are set during `setUp` since they are // outputs of the previous step. uint256 withdrawalDelaySeconds = 100; uint256 minProposalSizeBytes = 200; @@ -325,7 +324,6 @@ contract DeployOPChain_TestBase is Test { string release = "dev-release"; // this means implementation contracts will be deployed ISuperchainConfig superchainConfigProxy; IProtocolVersions protocolVersionsProxy; - ISharedLockbox sharedLockboxProxy; // Define default inputs for DeployOPChain. // `opcm` is set during `setUp` since it is an output of the previous step. @@ -386,7 +384,6 @@ contract DeployOPChain_TestBase is Test { // Populate the inputs for DeployImplementations based on the output of DeploySuperchain. superchainConfigProxy = dso.superchainConfigProxy(); protocolVersionsProxy = dso.protocolVersionsProxy(); - sharedLockboxProxy = dso.sharedLockboxProxy(); // Configure and deploy Implementation contracts DeployImplementations deployImplementations = createDeployImplementationsContract(); @@ -401,7 +398,6 @@ contract DeployOPChain_TestBase is Test { dii.set(dii.l1ContractsRelease.selector, release); dii.set(dii.superchainConfigProxy.selector, address(superchainConfigProxy)); dii.set(dii.protocolVersionsProxy.selector, address(protocolVersionsProxy)); - dii.set(dii.sharedLockboxProxy.selector, address(sharedLockboxProxy)); // End users of the DeployImplementations contract will need to set the `standardVersionsToml`. string memory standardVersionsTomlPath = string.concat(vm.projectRoot(), "/test/fixtures/standard-versions.toml"); diff --git a/packages/contracts-bedrock/test/opcm/DeploySuperchain.t.sol b/packages/contracts-bedrock/test/opcm/DeploySuperchain.t.sol index cd1d8d5ebf8..b647f9edf96 100644 --- a/packages/contracts-bedrock/test/opcm/DeploySuperchain.t.sol +++ b/packages/contracts-bedrock/test/opcm/DeploySuperchain.t.sol @@ -7,10 +7,15 @@ import { stdToml } from "forge-std/StdToml.sol"; import { ProxyAdmin } from "src/universal/ProxyAdmin.sol"; import { Proxy } from "src/universal/Proxy.sol"; import { SuperchainConfig } from "src/L1/SuperchainConfig.sol"; -import { SharedLockbox } from "src/L1/SharedLockbox.sol"; -import { LiquidityMigrator } from "src/L1/LiquidityMigrator.sol"; +import { ISuperchainConfigInterop } from "interfaces/L1/ISuperchainConfigInterop.sol"; +import { ISharedLockbox } from "interfaces/L1/ISharedLockbox.sol"; import { IProtocolVersions, ProtocolVersion } from "interfaces/L1/IProtocolVersions.sol"; -import { DeploySuperchainInput, DeploySuperchain, DeploySuperchainOutput } from "scripts/deploy/DeploySuperchain.s.sol"; +import { + DeploySuperchainInput, + DeploySuperchain, + DeploySuperchainInterop, + DeploySuperchainOutput +} from "scripts/deploy/DeploySuperchain.s.sol"; contract DeploySuperchainInput_Test is Test { DeploySuperchainInput dsi; @@ -62,9 +67,8 @@ contract DeploySuperchainOutput_Test is Test { SuperchainConfig superchainConfigProxy = SuperchainConfig(makeAddr("superchainConfigProxy")); IProtocolVersions protocolVersionsImpl = IProtocolVersions(makeAddr("protocolVersionsImpl")); IProtocolVersions protocolVersionsProxy = IProtocolVersions(makeAddr("protocolVersionsProxy")); - SharedLockbox sharedLockboxImpl = SharedLockbox(makeAddr("sharedLockboxImpl")); - SharedLockbox sharedLockboxProxy = SharedLockbox(makeAddr("sharedLockboxProxy")); - LiquidityMigrator liquidityMigratorImpl = LiquidityMigrator(makeAddr("liquidityMigratorImpl")); + ISharedLockbox sharedLockboxImpl = ISharedLockbox(makeAddr("sharedLockboxImpl")); + ISharedLockbox sharedLockboxProxy = ISharedLockbox(makeAddr("sharedLockboxProxy")); // Ensure each address has code, since these are expected to be contracts. vm.etch(address(superchainProxyAdmin), hex"01"); @@ -74,7 +78,6 @@ contract DeploySuperchainOutput_Test is Test { vm.etch(address(protocolVersionsProxy), hex"01"); vm.etch(address(sharedLockboxImpl), hex"01"); vm.etch(address(sharedLockboxProxy), hex"01"); - vm.etch(address(liquidityMigratorImpl), hex"01"); // Set the output data. dso.set(dso.superchainProxyAdmin.selector, address(superchainProxyAdmin)); @@ -84,7 +87,6 @@ contract DeploySuperchainOutput_Test is Test { dso.set(dso.protocolVersionsProxy.selector, address(protocolVersionsProxy)); dso.set(dso.sharedLockboxImpl.selector, address(sharedLockboxImpl)); dso.set(dso.sharedLockboxProxy.selector, address(sharedLockboxProxy)); - dso.set(dso.liquidityMigratorImpl.selector, address(liquidityMigratorImpl)); // Compare the test data to the getter methods. assertEq(address(superchainProxyAdmin), address(dso.superchainProxyAdmin()), "100"); @@ -94,7 +96,6 @@ contract DeploySuperchainOutput_Test is Test { assertEq(address(protocolVersionsProxy), address(dso.protocolVersionsProxy()), "500"); assertEq(address(sharedLockboxImpl), address(dso.sharedLockboxImpl()), "600"); assertEq(address(sharedLockboxProxy), address(dso.sharedLockboxProxy()), "700"); - assertEq(address(liquidityMigratorImpl), address(dso.liquidityMigratorImpl()), "800"); } function test_getters_whenNotSet_reverts() public { @@ -115,9 +116,6 @@ contract DeploySuperchainOutput_Test is Test { vm.expectRevert("DeployUtils: zero address"); dso.sharedLockboxProxy(); - - vm.expectRevert("DeployUtils: zero address"); - dso.liquidityMigratorImpl(); } function test_getters_whenAddrHasNoCode_reverts() public { @@ -147,10 +145,6 @@ contract DeploySuperchainOutput_Test is Test { dso.set(dso.sharedLockboxProxy.selector, emptyAddr); vm.expectRevert(expectedErr); dso.sharedLockboxProxy(); - - dso.set(dso.liquidityMigratorImpl.selector, emptyAddr); - vm.expectRevert(expectedErr); - dso.liquidityMigratorImpl(); } } @@ -168,8 +162,9 @@ contract DeploySuperchain_Test is Test { bool defaultPaused = false; ProtocolVersion defaultRequiredProtocolVersion = ProtocolVersion.wrap(1); ProtocolVersion defaultRecommendedProtocolVersion = ProtocolVersion.wrap(2); + bool defaultIsInterop = false; - function setUp() public { + function setUp() public virtual { deploySuperchain = new DeploySuperchain(); (dsi, dso) = deploySuperchain.etchIOContracts(); } @@ -200,36 +195,39 @@ contract DeploySuperchain_Test is Test { dsi.set(dsi.paused.selector, paused); dsi.set(dsi.requiredProtocolVersion.selector, requiredProtocolVersion); dsi.set(dsi.recommendedProtocolVersion.selector, recommendedProtocolVersion); + dsi.set(dsi.isInterop.selector, defaultIsInterop); // Run the deployment script. deploySuperchain.run(dsi, dso); + // Assert the output values. + _outputAsserts(); + + // Ensure that `checkOutput` passes. This is called by the `run` function during execution, + // so this just acts as a sanity check. It reverts on failure. + dso.checkOutput(dsi); + } + + function _outputAsserts() internal virtual { // Assert inputs were properly passed through to the contract initializers. - assertEq(address(dso.superchainProxyAdmin().owner()), superchainProxyAdminOwner, "100"); - assertEq(address(dso.protocolVersionsProxy().owner()), protocolVersionsOwner, "200"); - assertEq(address(dso.superchainConfigProxy().guardian()), guardian, "300"); - assertEq(dso.superchainConfigProxy().paused(), paused, "400"); - assertEq(unwrap(dso.protocolVersionsProxy().required()), unwrap(requiredProtocolVersion), "500"); - assertEq(unwrap(dso.protocolVersionsProxy().recommended()), unwrap(recommendedProtocolVersion), "600"); - assertEq(address(dso.sharedLockboxProxy().SUPERCHAIN_CONFIG()), address(dso.superchainConfigProxy()), "700"); + assertEq(address(dso.superchainProxyAdmin().owner()), dsi.superchainProxyAdminOwner(), "100"); + assertEq(address(dso.protocolVersionsProxy().owner()), dsi.protocolVersionsOwner(), "200"); + assertEq(address(dso.superchainConfigProxy().guardian()), dsi.guardian(), "300"); + assertEq(dso.superchainConfigProxy().paused(), dsi.paused(), "400"); + assertEq(unwrap(dso.protocolVersionsProxy().required()), unwrap(dsi.requiredProtocolVersion()), "500"); + assertEq(unwrap(dso.protocolVersionsProxy().recommended()), unwrap(dsi.recommendedProtocolVersion()), "600"); // Architecture assertions. // We prank as the zero address due to the Proxy's `proxyCallIfNotAdmin` modifier. Proxy superchainConfigProxy = Proxy(payable(address(dso.superchainConfigProxy()))); Proxy protocolVersionsProxy = Proxy(payable(address(dso.protocolVersionsProxy()))); - Proxy sharedLockboxProxy = Proxy(payable(address(dso.sharedLockboxProxy()))); vm.startPrank(address(0)); assertEq(superchainConfigProxy.implementation(), address(dso.superchainConfigImpl()), "700"); assertEq(protocolVersionsProxy.implementation(), address(dso.protocolVersionsImpl()), "800"); assertEq(superchainConfigProxy.admin(), protocolVersionsProxy.admin(), "900"); assertEq(superchainConfigProxy.admin(), address(dso.superchainProxyAdmin()), "1000"); - assertEq(sharedLockboxProxy.implementation(), address(dso.sharedLockboxImpl()), "1100"); vm.stopPrank(); - - // Ensure that `checkOutput` passes. This is called by the `run` function during execution, - // so this just acts as a sanity check. It reverts on failure. - dso.checkOutput(dsi); } function test_run_nullInput_reverts() public { @@ -277,3 +275,33 @@ contract DeploySuperchain_Test is Test { vm.store(address(dsi), bytes32(slot_), bytes32(0)); } } + +contract DeploySuperchainInterop_Test is DeploySuperchain_Test { + function setUp() public virtual override { + super.setUp(); + defaultIsInterop = true; + deploySuperchain = new DeploySuperchainInterop(); + } + + function _superchainConfig() internal view returns (ISuperchainConfigInterop) { + return ISuperchainConfigInterop(address(dso.superchainConfigProxy())); + } + + function _outputAsserts() internal virtual override { + super._outputAsserts(); + + // Assert inputs were properly passed through to the contract initializers. + assertEq(address(_superchainConfig().clusterManager()), dsi.superchainProxyAdminOwner(), "1100"); + assertEq(address(_superchainConfig().sharedLockbox()), address(dso.sharedLockboxProxy()), "1200"); + assertEq(address(dso.sharedLockboxProxy().superchainConfig()), address(dso.superchainConfigProxy()), "1300"); + + // Architecture assertions. + // We prank as the zero address due to the Proxy's `proxyCallIfNotAdmin` modifier. + Proxy sharedLockboxProxy = Proxy(payable(address(dso.sharedLockboxProxy()))); + + vm.startPrank(address(0)); + assertEq(sharedLockboxProxy.implementation(), address(dso.sharedLockboxImpl()), "1400"); + assertEq(sharedLockboxProxy.admin(), address(dso.superchainProxyAdmin()), "1500"); + vm.stopPrank(); + } +} diff --git a/packages/contracts-bedrock/test/opcm/SetDisputeGameImpl.t.sol b/packages/contracts-bedrock/test/opcm/SetDisputeGameImpl.t.sol index 72f7fbdaab9..2dca24b486b 100644 --- a/packages/contracts-bedrock/test/opcm/SetDisputeGameImpl.t.sol +++ b/packages/contracts-bedrock/test/opcm/SetDisputeGameImpl.t.sol @@ -79,12 +79,12 @@ contract SetDisputeGameImpl_Test is Test { input = new SetDisputeGameImplInput(); DisputeGameFactory dgfImpl = new DisputeGameFactory(); OptimismPortal2 portalImpl = new OptimismPortal2(0, 0); - SuperchainConfig supConfigImpl = new SuperchainConfig(address(0)); + SuperchainConfig supConfigImpl = new SuperchainConfig(); Proxy supConfigProxy = new Proxy(address(1)); vm.prank(address(1)); supConfigProxy.upgradeToAndCall( - address(supConfigImpl), abi.encodeCall(supConfigImpl.initialize, (address(this), address(this), false)) + address(supConfigImpl), abi.encodeCall(supConfigImpl.initialize, (address(this), false)) ); Proxy factoryProxy = new Proxy(address(1)); diff --git a/packages/contracts-bedrock/test/setup/CommonTest.sol b/packages/contracts-bedrock/test/setup/CommonTest.sol index ba2ccbb7753..13997234087 100644 --- a/packages/contracts-bedrock/test/setup/CommonTest.sol +++ b/packages/contracts-bedrock/test/setup/CommonTest.sol @@ -22,6 +22,7 @@ import { console } from "forge-std/console.sol"; // Interfaces import { IOptimismMintableERC20Full } from "interfaces/universal/IOptimismMintableERC20Full.sol"; import { ILegacyMintableERC20Full } from "interfaces/legacy/ILegacyMintableERC20Full.sol"; +import { ISuperchainConfigInterop } from "interfaces/L1/ISuperchainConfigInterop.sol"; /// @title CommonTest /// @dev An extenstion to `Test` that sets up the optimism smart contracts. @@ -92,14 +93,25 @@ contract CommonTest is Test, Setup, Events { // Deploy L2 Setup.L2(); - // Authorize portals to interact with the SharedLockbox. - vm.prank(address(superchainConfig)); - sharedLockbox.authorizePortal(address(optimismPortal2)); + // Add L2 chain as cluster dependency + if (useInteropOverride) _addDependency(); // Call bridge initializer setup function bridgeInitializerSetUp(); } + function _addDependency() internal { + vm.chainId(deploy.cfg().l1ChainID()); + uint256 l2ChainID = deploy.cfg().l2ChainID(); + + ISuperchainConfigInterop superchainConfigInterop = ISuperchainConfigInterop(address(superchainConfig)); + + vm.prank(superchainConfigInterop.clusterManager()); + superchainConfigInterop.addDependency(l2ChainID, address(systemConfig)); + + vm.chainId(l2ChainID); + } + function bridgeInitializerSetUp() public { L1Token = new ERC20("Native L1 Token", "L1T"); diff --git a/packages/contracts-bedrock/test/setup/Setup.sol b/packages/contracts-bedrock/test/setup/Setup.sol index 1d8e01dd4d4..8a55b3229f6 100644 --- a/packages/contracts-bedrock/test/setup/Setup.sol +++ b/packages/contracts-bedrock/test/setup/Setup.sol @@ -24,7 +24,6 @@ import { IL1CrossDomainMessenger } from "interfaces/L1/IL1CrossDomainMessenger.s import { ISystemConfig } from "interfaces/L1/ISystemConfig.sol"; import { ISuperchainConfig } from "interfaces/L1/ISuperchainConfig.sol"; import { ISharedLockbox } from "interfaces/L1/ISharedLockbox.sol"; -import { ILiquidityMigrator } from "interfaces/L1/ILiquidityMigrator.sol"; import { IDataAvailabilityChallenge } from "interfaces/L1/IDataAvailabilityChallenge.sol"; import { IL1StandardBridge } from "interfaces/L1/IL1StandardBridge.sol"; import { IProtocolVersions } from "interfaces/L1/IProtocolVersions.sol"; @@ -51,6 +50,7 @@ import { IWETH98 } from "interfaces/universal/IWETH98.sol"; import { IGovernanceToken } from "interfaces/governance/IGovernanceToken.sol"; import { ILegacyMessagePasser } from "interfaces/legacy/ILegacyMessagePasser.sol"; import { ISuperchainTokenBridge } from "interfaces/L2/ISuperchainTokenBridge.sol"; +import { IDependencyManager } from "interfaces/L2/IDependencyManager.sol"; /// @title Setup /// @dev This contact is responsible for setting up the contracts in state. It currently @@ -91,7 +91,6 @@ contract Setup { ISuperchainConfig superchainConfig; IDataAvailabilityChallenge dataAvailabilityChallenge; ISharedLockbox sharedLockbox; - ILiquidityMigrator liquidityMigrator; // L2 contracts IL2CrossDomainMessenger l2CrossDomainMessenger = @@ -116,6 +115,7 @@ contract Setup { ISuperchainTokenBridge superchainTokenBridge = ISuperchainTokenBridge(Predeploys.SUPERCHAIN_TOKEN_BRIDGE); IOptimismSuperchainERC20Factory l2OptimismSuperchainERC20Factory = IOptimismSuperchainERC20Factory(Predeploys.OPTIMISM_SUPERCHAIN_ERC20_FACTORY); + IDependencyManager dependencyManager = IDependencyManager(Predeploys.DEPENDENCY_MANAGER); /// @notice Indicates whether a test is running against a forked production network. function isForkTest() public view returns (bool) { @@ -215,11 +215,13 @@ contract Setup { protocolVersions = IProtocolVersions(deploy.mustGetAddress("ProtocolVersionsProxy")); superchainConfig = ISuperchainConfig(deploy.mustGetAddress("SuperchainConfigProxy")); anchorStateRegistry = IAnchorStateRegistry(deploy.mustGetAddress("AnchorStateRegistryProxy")); - sharedLockbox = ISharedLockbox(deploy.mustGetAddress("SharedLockboxProxy")); - liquidityMigrator = ILiquidityMigrator(deploy.mustGetAddress("LiquidityMigrator")); disputeGameFactory = IDisputeGameFactory(deploy.mustGetAddress("DisputeGameFactoryProxy")); delayedWeth = IDelayedWETH(deploy.mustGetAddress("DelayedWETHProxy")); + if (deploy.cfg().useInterop()) { + sharedLockbox = ISharedLockbox(deploy.mustGetAddress("SharedLockboxProxy")); + } + if (deploy.cfg().useAltDA()) { dataAvailabilityChallenge = IDataAvailabilityChallenge(deploy.mustGetAddress("DataAvailabilityChallengeProxy")); @@ -273,6 +275,7 @@ contract Setup { labelPredeploy(Predeploys.OPTIMISM_SUPERCHAIN_ERC20_FACTORY); labelPredeploy(Predeploys.OPTIMISM_SUPERCHAIN_ERC20_BEACON); labelPredeploy(Predeploys.SUPERCHAIN_TOKEN_BRIDGE); + labelPredeploy(Predeploys.DEPENDENCY_MANAGER); // L2 Preinstalls labelPreinstall(Preinstalls.MultiCall3); diff --git a/packages/contracts-bedrock/test/universal/Specs.t.sol b/packages/contracts-bedrock/test/universal/Specs.t.sol index c642a039dd8..bb6254cba24 100644 --- a/packages/contracts-bedrock/test/universal/Specs.t.sol +++ b/packages/contracts-bedrock/test/universal/Specs.t.sol @@ -45,7 +45,7 @@ contract Specification_Test is CommonTest { COUNCILSAFEOWNER, PORTAL, SUPERCHAINCONFIG, - DEPENDENCYMANAGER + CLUSTERMANAGER } /// @notice Represents the specification of a function. @@ -275,6 +275,8 @@ contract Specification_Test is CommonTest { _auth: Role.SYSTEMCONFIGOWNER }); _addSpec({ _name: "OptimismPortalInterop", _sel: _getSel("sharedLockbox()") }); + _addSpec({ _name: "OptimismPortalInterop", _sel: _getSel("migrateLiquidity()"), _auth: Role.SUPERCHAINCONFIG }); + _addSpec({ _name: "OptimismPortalInterop", _sel: _getSel("migrated()") }); // OptimismPortal2 _addSpec({ _name: "OptimismPortal2", _sel: _getSel("depositTransaction(address,uint256,uint64,bool,bytes)") }); @@ -322,7 +324,6 @@ contract Specification_Test is CommonTest { _sel: _getSel("depositERC20Transaction(address,uint256,uint256,uint64,bool,bytes)") }); _addSpec({ _name: "OptimismPortal2", _sel: _getSel("setGasPayingToken(address,uint8,bytes32,bytes32)") }); - _addSpec({ _name: "OptimismPortal2", _sel: _getSel("sharedLockbox()") }); // ProtocolVersions _addSpec({ _name: "ProtocolVersions", _sel: _getSel("RECOMMENDED_SLOT()") }); @@ -351,33 +352,44 @@ contract Specification_Test is CommonTest { // SuperchainConfig _addSpec({ _name: "SuperchainConfig", _sel: _getSel("GUARDIAN_SLOT()") }); - _addSpec({ _name: "SuperchainConfig", _sel: _getSel("DEPENDENCY_MANAGER_SLOT()") }); _addSpec({ _name: "SuperchainConfig", _sel: _getSel("PAUSED_SLOT()") }); - _addSpec({ _name: "SuperchainConfig", _sel: _getSel("SHARED_LOCKBOX()") }); _addSpec({ _name: "SuperchainConfig", _sel: _getSel("guardian()") }); - _addSpec({ _name: "SuperchainConfig", _sel: _getSel("dependencyManager()") }); - _addSpec({ _name: "SuperchainConfig", _sel: _getSel("initialize(address,address,bool)") }); + _addSpec({ _name: "SuperchainConfig", _sel: _getSel("initialize(address,bool)") }); _addSpec({ _name: "SuperchainConfig", _sel: _getSel("pause(string)"), _auth: Role.GUARDIAN }); _addSpec({ _name: "SuperchainConfig", _sel: _getSel("paused()") }); _addSpec({ _name: "SuperchainConfig", _sel: _getSel("unpause()"), _auth: Role.GUARDIAN }); _addSpec({ _name: "SuperchainConfig", _sel: _getSel("version()") }); - _addSpec({ - _name: "SuperchainConfig", + + // SuperchainConfigInterop + _addSpec({ _name: "SuperchainConfigInterop", _sel: _getSel("GUARDIAN_SLOT()") }); + _addSpec({ _name: "SuperchainConfigInterop", _sel: _getSel("CLUSTER_MANAGER_SLOT()") }); + _addSpec({ _name: "SuperchainConfigInterop", _sel: _getSel("PAUSED_SLOT()") }); + _addSpec({ _name: "SuperchainConfigInterop", _sel: _getSel("sharedLockbox()") }); + _addSpec({ _name: "SuperchainConfigInterop", _sel: _getSel("guardian()") }); + _addSpec({ _name: "SuperchainConfigInterop", _sel: _getSel("clusterManager()") }); + _addSpec({ _name: "SuperchainConfigInterop", _sel: _getSel("initialize(address,bool)") }); + _addSpec({ _name: "SuperchainConfigInterop", _sel: _getSel("initialize(address,bool,address,address)") }); + _addSpec({ _name: "SuperchainConfigInterop", _sel: _getSel("pause(string)"), _auth: Role.GUARDIAN }); + _addSpec({ _name: "SuperchainConfigInterop", _sel: _getSel("paused()") }); + _addSpec({ _name: "SuperchainConfigInterop", _sel: _getSel("unpause()"), _auth: Role.GUARDIAN }); + _addSpec({ _name: "SuperchainConfigInterop", _sel: _getSel("version()") }); + _addSpec({ + _name: "SuperchainConfigInterop", _sel: _getSel("addDependency(uint256,address)"), - _auth: Role.DEPENDENCYMANAGER + _auth: Role.CLUSTERMANAGER }); - _addSpec({ _name: "SuperchainConfig", _sel: _getSel("isInDependencySet(uint256)") }); - _addSpec({ _name: "SuperchainConfig", _sel: _getSel("dependencySet()") }); - _addSpec({ _name: "SuperchainConfig", _sel: _getSel("dependencySetSize()") }); + _addSpec({ _name: "SuperchainConfigInterop", _sel: _getSel("isInDependencySet(uint256)") }); + _addSpec({ _name: "SuperchainConfigInterop", _sel: _getSel("dependencySet()") }); + _addSpec({ _name: "SuperchainConfigInterop", _sel: _getSel("dependencySetSize()") }); + _addSpec({ _name: "SuperchainConfigInterop", _sel: _getSel("authorizedPortals(address)") }); // SharedLockbox - _addSpec({ _name: "SharedLockbox", _sel: _getSel("SUPERCHAIN_CONFIG()") }); - _addSpec({ _name: "SharedLockbox", _sel: _getSel("authorizedPortals(address)") }); + _addSpec({ _name: "SharedLockbox", _sel: _getSel("superchainConfig()") }); _addSpec({ _name: "SharedLockbox", _sel: _getSel("lockETH()"), _auth: Role.PORTAL }); _addSpec({ _name: "SharedLockbox", _sel: _getSel("unlockETH(uint256)"), _auth: Role.PORTAL }); - _addSpec({ _name: "SharedLockbox", _sel: _getSel("authorizePortal(address)"), _auth: Role.SUPERCHAINCONFIG }); _addSpec({ _name: "SharedLockbox", _sel: _getSel("version()") }); _addSpec({ _name: "SharedLockbox", _sel: _getSel("paused()") }); + _addSpec({ _name: "SharedLockbox", _sel: _getSel("initialize(address)") }); // SystemConfig _addSpec({ _name: "SystemConfig", _sel: _getSel("UNSAFE_BLOCK_SIGNER_SLOT()") }); @@ -865,11 +877,6 @@ contract Specification_Test is CommonTest { _addSpec({ _name: "LivenessModule", _sel: _getSel("safe()") }); _addSpec({ _name: "LivenessModule", _sel: _getSel("thresholdPercentage()") }); _addSpec({ _name: "LivenessModule", _sel: _getSel("version()") }); - - // LiquidityMigrator - _addSpec({ _name: "LiquidityMigrator", _sel: _getSel("migrateETH()") }); - _addSpec({ _name: "LiquidityMigrator", _sel: _getSel("SHARED_LOCKBOX()") }); - _addSpec({ _name: "LiquidityMigrator", _sel: _getSel("version()") }); } /// @dev Computes the selector from a function signature. @@ -953,9 +960,9 @@ contract Specification_Test is CommonTest { /// @notice Ensures that the DeputyGuardian is authorized to take all Guardian actions. function test_deputyGuardianAuth_works() public view { - // Additional 2 roles for the DeputyPauseModule. - assertEq(specsByRole[Role.GUARDIAN].length, 5); - assertEq(specsByRole[Role.DEPUTYGUARDIAN].length, specsByRole[Role.GUARDIAN].length + 2); + // Additional 2 roles for the DeputyPauseModule. Plus 2 for the SuperchainConfigInterop (remove when unified). + assertEq(specsByRole[Role.GUARDIAN].length, 5 + 2); + assertEq(specsByRole[Role.DEPUTYGUARDIAN].length, specsByRole[Role.GUARDIAN].length); mapping(bytes4 => Spec) storage dgmFuncSpecs = specs["DeputyGuardianModule"]; mapping(bytes4 => Spec) storage superchainConfigFuncSpecs = specs["SuperchainConfig"]; diff --git a/packages/contracts-bedrock/test/vendor/Initializable.t.sol b/packages/contracts-bedrock/test/vendor/Initializable.t.sol index 0a5c29dabf2..3a7573f0593 100644 --- a/packages/contracts-bedrock/test/vendor/Initializable.t.sol +++ b/packages/contracts-bedrock/test/vendor/Initializable.t.sol @@ -58,7 +58,7 @@ contract Initializer_Test is CommonTest { InitializeableContract({ name: "SuperchainConfigImpl", target: deploy.mustGetAddress("SuperchainConfigImpl"), - initCalldata: abi.encodeCall(superchainConfig.initialize, (address(0), address(0), false)) + initCalldata: abi.encodeCall(superchainConfig.initialize, (address(0), false)) }) ); // SuperchainConfigProxy @@ -66,7 +66,7 @@ contract Initializer_Test is CommonTest { InitializeableContract({ name: "SuperchainConfigProxy", target: address(superchainConfig), - initCalldata: abi.encodeCall(superchainConfig.initialize, (address(0), address(0), false)) + initCalldata: abi.encodeCall(superchainConfig.initialize, (address(0), false)) }) ); // L1CrossDomainMessengerImpl @@ -343,27 +343,30 @@ contract Initializer_Test is CommonTest { /// 3. The `initialize()` function of each contract cannot be called again. function test_cannotReinitialize_succeeds() public { // Collect exclusions. - string[] memory excludes = new string[](9); + string[] memory excludes = new string[](11); // TODO: Neither of these contracts are labeled properly in the deployment script. Both are // currently being labeled as their non-interop versions. Remove these exclusions once // the deployment script is fixed. excludes[0] = "src/L1/SystemConfigInterop.sol"; excludes[1] = "src/L1/OptimismPortalInterop.sol"; + excludes[2] = "src/L1/SuperchainConfigInterop.sol"; // Contract is currently not being deployed as part of the standard deployment script. - excludes[2] = "src/L2/OptimismSuperchainERC20.sol"; + excludes[3] = "src/L2/OptimismSuperchainERC20.sol"; // Periphery contracts don't get deployed as part of the standard deployment script. - excludes[3] = "src/periphery/*"; + excludes[4] = "src/periphery/*"; // TODO: Deployment script is currently "broken" in the sense that it doesn't properly // label the FaultDisputeGame and PermissionedDisputeGame contracts and instead // simply deploys them anonymously. Means that functions like "getInitializedSlot" // don't work properly. Remove these exclusions once the deployment script is fixed. - excludes[4] = "src/dispute/FaultDisputeGame.sol"; - excludes[5] = "src/dispute/PermissionedDisputeGame.sol"; + excludes[5] = "src/dispute/FaultDisputeGame.sol"; + excludes[6] = "src/dispute/PermissionedDisputeGame.sol"; // TODO: Eventually remove this exclusion. Same reason as above dispute contracts. - excludes[6] = "src/L1/OPContractsManager.sol"; - excludes[7] = "src/L1/OPContractsManagerInterop.sol"; + excludes[7] = "src/L1/OPContractsManager.sol"; + excludes[8] = "src/L1/OPContractsManagerInterop.sol"; // L2 contract initialization is tested in Predeploys.t.sol - excludes[8] = "src/L2/*"; + excludes[9] = "src/L2/*"; + // Exclude SharedLockbox since using OZv5 initializer + excludes[10] = "src/L1/SharedLockbox.sol"; // Get all contract names in the src directory, minus the excluded contracts. string[] memory contractNames = ForgeArtifacts.getContractNames("src/*", excludes); diff --git a/packages/contracts-bedrock/test/vendor/InitializableOZv5.t.sol b/packages/contracts-bedrock/test/vendor/InitializableOZv5.t.sol index 51c2fce2667..393a3b44e6a 100644 --- a/packages/contracts-bedrock/test/vendor/InitializableOZv5.t.sol +++ b/packages/contracts-bedrock/test/vendor/InitializableOZv5.t.sol @@ -1,22 +1,28 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.25; +pragma solidity 0.8.15; -import { Test } from "forge-std/Test.sol"; +// Testing +import { CommonTest } from "test/setup/CommonTest.sol"; + +// Libraries import { IOptimismSuperchainERC20 } from "interfaces/L2/IOptimismSuperchainERC20.sol"; -import { Initializable } from "@openzeppelin/contracts-v5/proxy/utils/Initializable.sol"; import { DeployUtils } from "scripts/libraries/DeployUtils.sol"; + /// @title InitializerOZv5_Test /// @dev Ensures that the `initialize()` function on contracts cannot be called more than /// once. Tests the contracts inheriting from `Initializable` from OpenZeppelin Contracts v5. -contract InitializerOZv5_Test is Test { +contract InitializerOZv5_Test is CommonTest { /// @notice The storage slot of the `initialized` flag in the `Initializable` contract from OZ v5. /// keccak256(abi.encode(uint256(keccak256("openzeppelin.storage.Initializable")) - 1)) & ~bytes32(uint256(0xff)) bytes32 private constant INITIALIZABLE_STORAGE = 0xf0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a00; + error InvalidInitialization(); + /// @notice Contains the address of an `Initializable` contract and the calldata /// used to initialize it. struct InitializeableContract { + string name; address target; bytes initCalldata; } @@ -25,13 +31,18 @@ contract InitializerOZv5_Test is Test { /// used to initialize them. InitializeableContract[] contracts; - function setUp() public { + function setUp() public override { + super.enableInterop(); + super.enableAltDA(); + super.setUp(); + // Initialize the `contracts` array with the addresses of the contracts to test and the // calldata used to initialize them // OptimismSuperchainERC20 contracts.push( InitializeableContract({ + name: "OptimismSuperchainERC20", target: address( DeployUtils.create1({ _name: "OptimismSuperchainERC20", @@ -41,6 +52,24 @@ contract InitializerOZv5_Test is Test { initCalldata: abi.encodeCall(IOptimismSuperchainERC20.initialize, (address(0), "", "", 18)) }) ); + + // ShareLockboxImpl + contracts.push( + InitializeableContract({ + name: "SharedLockboxImpl", + target: deploy.mustGetAddress("SharedLockboxImpl"), + initCalldata: abi.encodeCall(sharedLockbox.initialize, (address(0))) + }) + ); + + // SharedLockboxProxy + contracts.push( + InitializeableContract({ + name: "SharedLockboxProxy", + target: address(sharedLockbox), + initCalldata: abi.encodeCall(sharedLockbox.initialize, (address(0))) + }) + ); } /// @notice Tests that: @@ -60,13 +89,17 @@ contract InitializerOZv5_Test is Test { // Assert that the contract is already initialized. bytes32 slotVal = vm.load(_contract.target, INITIALIZABLE_STORAGE); - uint64 initialized = uint64(uint256(slotVal)); - assertEq(initialized, type(uint64).max); + uint64 initialized = uint64(uint256(slotVal) & 0xFFFFFFFFFFFFFFFF); + assertTrue( + // Either 1 for initialized or type(uint64).max for initializer disabled. + initialized == 1 || initialized == type(uint64).max, + "Initializable: contract is not initialized" + ); // Then, attempt to re-initialize the contract. This should fail. (bool success, bytes memory returnData) = _contract.target.call(_contract.initCalldata); assertFalse(success); - assertEq(bytes4(returnData), Initializable.InvalidInitialization.selector); + assertEq(bytes4(returnData), InvalidInitialization.selector); } } }