From 159fb052760337300061600af6a72e066a75b358 Mon Sep 17 00:00:00 2001 From: Kelvin Fichter Date: Thu, 16 Jan 2025 18:01:15 -0600 Subject: [PATCH 01/50] feat: AnchorStateRegistry as source of truth Updates the OptimismPortal to use the AnchorStateRegistry as the source of truth for the validity of Dispute Game contracts. --- .../deployer/integration_test/apply_test.go | 2 +- .../pkg/deployer/opcm/dispute_game_factory.go | 1 - .../pkg/deployer/pipeline/dispute_games.go | 9 +- op-e2e/actions/helpers/user_test.go | 6 + op-e2e/system/bridge/validity_test.go | 6 + op-e2e/system/helpers/withdrawal_helper.go | 7 + packages/contracts-bedrock/foundry.toml | 8 + .../interfaces/L1/IOptimismPortal2.sol | 48 +- .../interfaces/L1/IOptimismPortalInterop.sol | 47 +- .../dispute/IAnchorStateRegistry.sol | 26 +- .../interfaces/safe/IDeputyGuardianModule.sol | 13 +- .../scripts/deploy/ChainAssertions.sol | 3 +- .../scripts/deploy/Deploy.s.sol | 3 +- .../deploy/DeployImplementations.s.sol | 28 +- .../scripts/deploy/DeployOPChain.s.sol | 15 +- .../scripts/deploy/SetDisputeGameImpl.s.sol | 27 +- .../snapshots/abi/AnchorStateRegistry.json | 136 ++++- .../snapshots/abi/DeputyGuardianModule.json | 54 +- .../snapshots/abi/OptimismPortal2.json | 150 ++---- .../snapshots/abi/OptimismPortalInterop.json | 151 ++---- .../snapshots/semver-lock.json | 28 +- .../storageLayout/AnchorStateRegistry.json | 32 +- .../storageLayout/OptimismPortal2.json | 19 +- .../storageLayout/OptimismPortalInterop.json | 19 +- .../src/L1/OPContractsManager.sol | 73 ++- .../src/L1/OPContractsManagerInterop.sol | 4 +- .../src/L1/OPPrestateUpdater.sol | 4 +- .../src/L1/OptimismPortal2.sol | 467 +++++++++--------- .../src/L1/OptimismPortalInterop.sol | 20 +- .../src/dispute/AnchorStateRegistry.sol | 106 +++- .../src/libraries/PortalErrors.sol | 40 -- .../src/safe/DeputyGuardianModule.sol | 67 +-- .../test/L1/OPContractsManager.t.sol | 4 +- .../test/L1/OPPrestateUpdater.t.sol | 4 +- .../test/L1/OptimismPortal2.t.sol | 376 ++++++++------ .../test/L1/OptimismPortalInterop.t.sol | 5 +- .../test/dispute/AnchorStateRegistry.t.sol | 362 +++++++++++--- .../test/dispute/FaultDisputeGame.t.sol | 6 + .../test/dispute/SuperFaultDisputeGame.t.sol | 7 +- .../test/invariants/OptimismPortal2.t.sol | 7 +- .../test/kontrol/proofs/OptimismPortal2.k.sol | 5 +- .../test/opcm/DeployImplementations.t.sol | 4 +- .../test/opcm/SetDisputeGameImpl.t.sol | 29 +- .../test/safe/DeputyGuardianModule.t.sol | 165 ++++--- .../test/safe/DeputyPauseModule.t.sol | 3 +- .../test/universal/Specs.t.sol | 53 +- .../test/vendor/Initializable.t.sol | 25 +- 47 files changed, 1504 insertions(+), 1170 deletions(-) delete mode 100644 packages/contracts-bedrock/src/libraries/PortalErrors.sol diff --git a/op-deployer/pkg/deployer/integration_test/apply_test.go b/op-deployer/pkg/deployer/integration_test/apply_test.go index 025b57f288eb5..f070de2fc3ce8 100644 --- a/op-deployer/pkg/deployer/integration_test/apply_test.go +++ b/op-deployer/pkg/deployer/integration_test/apply_test.go @@ -293,7 +293,7 @@ func TestProofParamOverrides(t *testing.T) { { "disputeGameFinalityDelaySeconds", uint64Caster, - st.ImplementationsDeployment.OptimismPortalImplAddress, + st.ImplementationsDeployment.AnchorStateRegistryImplAddress, }, { "faultGameAbsolutePrestate", diff --git a/op-deployer/pkg/deployer/opcm/dispute_game_factory.go b/op-deployer/pkg/deployer/opcm/dispute_game_factory.go index 6e79f0683a945..61480523510c4 100644 --- a/op-deployer/pkg/deployer/opcm/dispute_game_factory.go +++ b/op-deployer/pkg/deployer/opcm/dispute_game_factory.go @@ -8,7 +8,6 @@ import ( type SetDisputeGameImplInput struct { Factory common.Address Impl common.Address - Portal common.Address AnchorStateRegistry common.Address GameType uint32 } diff --git a/op-deployer/pkg/deployer/pipeline/dispute_games.go b/op-deployer/pkg/deployer/pipeline/dispute_games.go index dd95398556cdc..5363ba7c49796 100644 --- a/op-deployer/pkg/deployer/pipeline/dispute_games.go +++ b/op-deployer/pkg/deployer/pipeline/dispute_games.go @@ -127,13 +127,12 @@ func deployDisputeGame( lgr.Info("setting dispute game impl on factory", "respected", game.MakeRespected) sdgiInput := opcm.SetDisputeGameImplInput{ - Factory: thisState.DisputeGameFactoryProxyAddress, - Impl: out.DisputeGameImpl, - GameType: game.DisputeGameType, - AnchorStateRegistry: thisState.AnchorStateRegistryProxyAddress, + Factory: thisState.DisputeGameFactoryProxyAddress, + Impl: out.DisputeGameImpl, + GameType: game.DisputeGameType, } if game.MakeRespected { - sdgiInput.Portal = thisState.OptimismPortalProxyAddress + sdgiInput.AnchorStateRegistry = thisState.AnchorStateRegistryProxyAddress } if err := opcm.SetDisputeGameImpl( env.L1ScriptHost, diff --git a/op-e2e/actions/helpers/user_test.go b/op-e2e/actions/helpers/user_test.go index 00b27dea1fe47..2c5822f988942 100644 --- a/op-e2e/actions/helpers/user_test.go +++ b/op-e2e/actions/helpers/user_test.go @@ -308,6 +308,12 @@ func runCrossLayerUserTest(gt *testing.T, test hardforkScheduledTest) { require.Equal(t, types.ReceiptStatusSuccessful, receipt.Status, "proposal failed") } + // Mine an empty block so that the timestamp is updated. Otherwise ActProveWithdrawal will fail + // because it tries to estimate gas based on the current timestamp, which is the same timestamp + // as the dispute game creation timestamp, which causes proveWithdrawalTransaction to revert. + miner.ActL1StartBlock(12)(t) + miner.ActL1EndBlock(t) + // prove our withdrawal on L1 alice.ActProveWithdrawal(t) // include proved withdrawal in new L1 block diff --git a/op-e2e/system/bridge/validity_test.go b/op-e2e/system/bridge/validity_test.go index 4c4ceff07805f..ebc141436a4de 100644 --- a/op-e2e/system/bridge/validity_test.go +++ b/op-e2e/system/bridge/validity_test.go @@ -449,6 +449,12 @@ func testMixedWithdrawalValidity(t *testing.T, allocType config.AllocType) { receiptCl := ethclient.NewClient(rpcClient) blockCl := ethclient.NewClient(rpcClient) + // Mine an empty block so that the timestamp is updated. Otherwise, + // proveWithdrawalTransaction gas estimation may fail because the current timestamp is + // the same as the dispute game creation timestamp. + err = wait.ForNextBlock(ctx, l1Client) + require.NoError(t, err) + // Now create the withdrawal params, err := helpers.ProveWithdrawalParameters(context.Background(), proofCl, receiptCl, blockCl, tx.Hash(), header, l2OutputOracle, disputeGameFactory, optimismPortal2, cfg.AllocType) require.Nil(t, err) diff --git a/op-e2e/system/helpers/withdrawal_helper.go b/op-e2e/system/helpers/withdrawal_helper.go index a2a57c9e3bb66..787495e4aa579 100644 --- a/op-e2e/system/helpers/withdrawal_helper.go +++ b/op-e2e/system/helpers/withdrawal_helper.go @@ -127,12 +127,19 @@ func ProveWithdrawal(t *testing.T, cfg e2esys.SystemConfig, clients ClientProvid require.NoError(t, err) } + // Wait for another block to be mined so that the timestamp increases. Otherwise, + // proveWithdrawalTransaction gas estimation may fail because the current timestamp is the same + // as the dispute game creation timestamp. + err = wait.ForNextBlock(ctx, l1Client) + require.NoError(t, err) + receiptCl := clients.NodeClient(l2NodeName) headerCl := clients.NodeClient(l2NodeName) proofCl := gethclient.New(receiptCl.Client()) ctx, cancel = context.WithTimeout(context.Background(), 30*time.Second) defer cancel() + // Get the latest header header, err := receiptCl.HeaderByNumber(ctx, new(big.Int).SetUint64(blockNumber)) require.NoError(t, err) diff --git a/packages/contracts-bedrock/foundry.toml b/packages/contracts-bedrock/foundry.toml index aafcc6c22b3d6..782d9ec587bc4 100644 --- a/packages/contracts-bedrock/foundry.toml +++ b/packages/contracts-bedrock/foundry.toml @@ -27,7 +27,10 @@ compilation_restrictions = [ { paths = "src/dispute/SuperFaultDisputeGame.sol", optimizer_runs = 5000 }, { paths = "src/dispute/SuperPermissionedDisputeGame.sol", optimizer_runs = 5000 }, { paths = "src/L1/OPContractsManager.sol", optimizer_runs = 5000 }, + { paths = "src/L1/OPContractsManagerInterop.sol", optimizer_runs = 5000 }, { paths = "src/L1/StandardValidator.sol", optimizer_runs = 5000 }, + { paths = "src/L1/OptimismPortal2.sol", optimizer_runs = 5000 }, + { paths = "src/L1/OptimismPortalInterop.sol", optimizer_runs = 5000 }, ] extra_output = ['devdoc', 'userdoc', 'metadata', 'storageLayout'] @@ -138,8 +141,13 @@ additional_compiler_profiles = [ compilation_restrictions = [ { paths = "src/dispute/FaultDisputeGame.sol", optimizer_runs = 0 }, { paths = "src/dispute/PermissionedDisputeGame.sol", optimizer_runs = 0 }, + { paths = "src/dispute/SuperFaultDisputeGame.sol", optimizer_runs = 0 }, + { paths = "src/dispute/SuperPermissionedDisputeGame.sol", optimizer_runs = 0 }, { paths = "src/L1/OPContractsManager.sol", optimizer_runs = 0 }, + { paths = "src/L1/OPContractsManagerInterop.sol", optimizer_runs = 0 }, { paths = "src/L1/StandardValidator.sol", optimizer_runs = 0 }, + { paths = "src/L1/OptimismPortal2.sol", optimizer_runs = 0 }, + { paths = "src/L1/OptimismPortalInterop.sol", optimizer_runs = 0 }, ] ################################################################ diff --git a/packages/contracts-bedrock/interfaces/L1/IOptimismPortal2.sol b/packages/contracts-bedrock/interfaces/L1/IOptimismPortal2.sol index bbf12c3fce4dc..76b0111f171e2 100644 --- a/packages/contracts-bedrock/interfaces/L1/IOptimismPortal2.sol +++ b/packages/contracts-bedrock/interfaces/L1/IOptimismPortal2.sol @@ -2,40 +2,38 @@ pragma solidity ^0.8.0; import { Types } from "src/libraries/Types.sol"; -import { GameType, Timestamp } from "src/dispute/lib/LibUDT.sol"; +import { GameType } from "src/dispute/lib/LibUDT.sol"; 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 { IAnchorStateRegistry } from "interfaces/dispute/IAnchorStateRegistry.sol"; interface IOptimismPortal2 { - error AlreadyFinalized(); - error BadTarget(); - error Blacklisted(); - error CallPaused(); error ContentLengthMismatch(); error EmptyItem(); - error GasEstimation(); error InvalidDataRemainder(); - error InvalidDisputeGame(); - error InvalidGameType(); error InvalidHeader(); - error InvalidMerkleProof(); - error InvalidProof(); - error LargeCalldata(); - error NonReentrant(); + error OptimismPortal_AlreadyFinalized(); + error OptimismPortal_BadTarget(); + error OptimismPortal_CallPaused(); + error OptimismPortal_CalldataTooLarge(); + error OptimismPortal_GasEstimation(); + error OptimismPortal_GasLimitTooLow(); + error OptimismPortal_ImproperDisputeGame(); + error OptimismPortal_InvalidDisputeGame(); + error OptimismPortal_InvalidMerkleProof(); + error OptimismPortal_InvalidOutputRootProof(); + error OptimismPortal_InvalidProofTimestamp(); + error OptimismPortal_InvalidRootClaim(); + error OptimismPortal_NoReentrancy(); + error OptimismPortal_ProofNotOldEnough(); + error OptimismPortal_Unproven(); error OutOfGas(); - error ProposalNotValidated(); - error SmallGasLimit(); - error Unauthorized(); error UnexpectedList(); error UnexpectedString(); - error Unproven(); - error LegacyGame(); - event DisputeGameBlacklisted(IDisputeGame indexed disputeGame); event Initialized(uint8 version); - event RespectedGameTypeSet(GameType indexed newGameType, Timestamp indexed updatedAt); event TransactionDeposited(address indexed from, address indexed to, uint256 indexed version, bytes opaqueData); event WithdrawalFinalized(bytes32 indexed withdrawalHash, bool success); event WithdrawalProven(bytes32 indexed withdrawalHash, address indexed from, address indexed to); @@ -43,7 +41,7 @@ interface IOptimismPortal2 { receive() external payable; - function blacklistDisputeGame(IDisputeGame _disputeGame) external; + function anchorStateRegistry() external view returns (IAnchorStateRegistry); function checkWithdrawal(bytes32 _withdrawalHash, address _proofSubmitter) external view; function depositTransaction( address _to, @@ -54,7 +52,6 @@ interface IOptimismPortal2 { ) external payable; - function disputeGameBlacklist(IDisputeGame) external view returns (bool); function disputeGameFactory() external view returns (IDisputeGameFactory); function disputeGameFinalityDelaySeconds() external view returns (uint256); function donateETH() external payable; @@ -67,10 +64,9 @@ interface IOptimismPortal2 { function finalizedWithdrawals(bytes32) external view returns (bool); function guardian() external view returns (address); function initialize( - IDisputeGameFactory _disputeGameFactory, ISystemConfig _systemConfig, ISuperchainConfig _superchainConfig, - GameType _initialRespectedGameType + IAnchorStateRegistry _anchorStateRegistry ) external; function l2Sender() external view returns (address); @@ -93,13 +89,13 @@ interface IOptimismPortal2 { ) external view - returns (IDisputeGame disputeGameProxy, uint64 timestamp); // nosemgrep + returns (IDisputeGame disputeGameProxy, uint64 timestamp); function respectedGameType() external view returns (GameType); function respectedGameTypeUpdatedAt() external view returns (uint64); - function setRespectedGameType(GameType _gameType) external; function superchainConfig() external view returns (ISuperchainConfig); function systemConfig() external view returns (ISystemConfig); + function upgrade(IAnchorStateRegistry _anchorStateRegistry) external; function version() external pure returns (string memory); - function __constructor__(uint256 _proofMaturityDelaySeconds, uint256 _disputeGameFinalityDelaySeconds) external; + function __constructor__(uint256 _proofMaturityDelaySeconds) external; } diff --git a/packages/contracts-bedrock/interfaces/L1/IOptimismPortalInterop.sol b/packages/contracts-bedrock/interfaces/L1/IOptimismPortalInterop.sol index 6dec13a8335c9..716b42f41985b 100644 --- a/packages/contracts-bedrock/interfaces/L1/IOptimismPortalInterop.sol +++ b/packages/contracts-bedrock/interfaces/L1/IOptimismPortalInterop.sol @@ -2,41 +2,40 @@ pragma solidity ^0.8.0; import { Types } from "src/libraries/Types.sol"; -import { GameType, Timestamp } from "src/dispute/lib/LibUDT.sol"; +import { GameType } from "src/dispute/lib/LibUDT.sol"; 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 { ConfigType } from "interfaces/L2/IL1BlockInterop.sol"; +import { IAnchorStateRegistry } from "interfaces/dispute/IAnchorStateRegistry.sol"; interface IOptimismPortalInterop { - error AlreadyFinalized(); - error BadTarget(); - error Blacklisted(); - error CallPaused(); error ContentLengthMismatch(); error EmptyItem(); - error GasEstimation(); error InvalidDataRemainder(); - error InvalidDisputeGame(); - error InvalidGameType(); error InvalidHeader(); - error InvalidMerkleProof(); - error InvalidProof(); - error LargeCalldata(); - error NonReentrant(); + error OptimismPortal_AlreadyFinalized(); + error OptimismPortal_BadTarget(); + error OptimismPortal_CallPaused(); + error OptimismPortal_CalldataTooLarge(); + error OptimismPortal_GasEstimation(); + error OptimismPortal_GasLimitTooLow(); + error OptimismPortal_ImproperDisputeGame(); + error OptimismPortal_InvalidDisputeGame(); + error OptimismPortal_InvalidMerkleProof(); + error OptimismPortal_InvalidOutputRootProof(); + error OptimismPortal_InvalidProofTimestamp(); + error OptimismPortal_InvalidRootClaim(); + error OptimismPortal_NoReentrancy(); + error OptimismPortal_ProofNotOldEnough(); + error OptimismPortal_Unauthorized(); + error OptimismPortal_Unproven(); error OutOfGas(); - error ProposalNotValidated(); - error SmallGasLimit(); - error Unauthorized(); error UnexpectedList(); error UnexpectedString(); - error Unproven(); - error LegacyGame(); - event DisputeGameBlacklisted(IDisputeGame indexed disputeGame); event Initialized(uint8 version); - event RespectedGameTypeSet(GameType indexed newGameType, Timestamp indexed updatedAt); event TransactionDeposited(address indexed from, address indexed to, uint256 indexed version, bytes opaqueData); event WithdrawalFinalized(bytes32 indexed withdrawalHash, bool success); event WithdrawalProven(bytes32 indexed withdrawalHash, address indexed from, address indexed to); @@ -44,7 +43,7 @@ interface IOptimismPortalInterop { receive() external payable; - function blacklistDisputeGame(IDisputeGame _disputeGame) external; + function anchorStateRegistry() external view returns (IAnchorStateRegistry); function checkWithdrawal(bytes32 _withdrawalHash, address _proofSubmitter) external view; function depositTransaction( address _to, @@ -55,7 +54,6 @@ interface IOptimismPortalInterop { ) external payable; - function disputeGameBlacklist(IDisputeGame) external view returns (bool); function disputeGameFactory() external view returns (IDisputeGameFactory); function disputeGameFinalityDelaySeconds() external view returns (uint256); function donateETH() external payable; @@ -68,10 +66,9 @@ interface IOptimismPortalInterop { function finalizedWithdrawals(bytes32) external view returns (bool); function guardian() external view returns (address); function initialize( - IDisputeGameFactory _disputeGameFactory, ISystemConfig _systemConfig, ISuperchainConfig _superchainConfig, - GameType _initialRespectedGameType + IAnchorStateRegistry _anchorStateRegistry ) external; function l2Sender() external view returns (address); @@ -98,10 +95,10 @@ interface IOptimismPortalInterop { function respectedGameType() external view returns (GameType); function respectedGameTypeUpdatedAt() external view returns (uint64); function setConfig(ConfigType _type, bytes memory _value) external; - function setRespectedGameType(GameType _gameType) external; function superchainConfig() external view returns (ISuperchainConfig); function systemConfig() external view returns (ISystemConfig); + function upgrade(IAnchorStateRegistry _anchorStateRegistry) external; function version() external pure returns (string memory); - function __constructor__(uint256 _proofMaturityDelaySeconds, uint256 _disputeGameFinalityDelaySeconds) external; + function __constructor__(uint256 _proofMaturityDelaySeconds) external; } diff --git a/packages/contracts-bedrock/interfaces/dispute/IAnchorStateRegistry.sol b/packages/contracts-bedrock/interfaces/dispute/IAnchorStateRegistry.sol index a6f7d3a45c430..d6c96648e63d1 100644 --- a/packages/contracts-bedrock/interfaces/dispute/IAnchorStateRegistry.sol +++ b/packages/contracts-bedrock/interfaces/dispute/IAnchorStateRegistry.sol @@ -5,30 +5,33 @@ import { IDisputeGame } from "interfaces/dispute/IDisputeGame.sol"; import { IFaultDisputeGame } from "interfaces/dispute/IFaultDisputeGame.sol"; import { IDisputeGameFactory } from "interfaces/dispute/IDisputeGameFactory.sol"; import { ISuperchainConfig } from "interfaces/L1/ISuperchainConfig.sol"; -import { IOptimismPortal2 } from "interfaces/L1/IOptimismPortal2.sol"; import { GameType, Hash, OutputRoot } from "src/dispute/lib/Types.sol"; interface IAnchorStateRegistry { - error AnchorStateRegistry_Unauthorized(); - error AnchorStateRegistry_InvalidAnchorGame(); error AnchorStateRegistry_AnchorGameBlacklisted(); + error AnchorStateRegistry_InvalidAnchorGame(); + error AnchorStateRegistry_Unauthorized(); - event AnchorNotUpdated(IFaultDisputeGame indexed game); event AnchorUpdated(IFaultDisputeGame indexed game); + event DisputeGameBlacklisted(IDisputeGame indexed disputeGame); event Initialized(uint8 version); + event RespectedGameTypeSet(GameType gameType); + event RetirementTimestampSet(uint256 timestamp); function anchorGame() external view returns (IFaultDisputeGame); function anchors(GameType) external view returns (Hash, uint256); + function blacklistDisputeGame(IDisputeGame _disputeGame) external; + function disputeGameBlacklist(IDisputeGame) external view returns (bool); function getAnchorRoot() external view returns (Hash, uint256); + function disputeGameFinalityDelaySeconds() external view returns (uint256); function disputeGameFactory() external view returns (IDisputeGameFactory); function initialize( ISuperchainConfig _superchainConfig, IDisputeGameFactory _disputeGameFactory, - IOptimismPortal2 _portal, - OutputRoot memory _startingAnchorRoot + OutputRoot memory _startingAnchorRoot, + GameType _startingRespectedGameType ) external; - function isGameBlacklisted(IDisputeGame _game) external view returns (bool); function isGameProper(IDisputeGame _game) external view returns (bool); function isGameRegistered(IDisputeGame _game) external view returns (bool); @@ -37,11 +40,16 @@ interface IAnchorStateRegistry { function isGameRetired(IDisputeGame _game) external view returns (bool); function isGameFinalized(IDisputeGame _game) external view returns (bool); function isGameClaimValid(IDisputeGame _game) external view returns (bool); - function portal() external view returns (IOptimismPortal2); + function paused() external view returns (bool); function respectedGameType() external view returns (GameType); + function retirementTimestamp() external view returns (uint64); function setAnchorState(IDisputeGame _game) external; + function setRespectedGameType(GameType _gameType) external; function superchainConfig() external view returns (ISuperchainConfig); + function updateRetirementTimestamp() external; function version() external view returns (string memory); - function __constructor__() external; + function __constructor__( + uint256 _disputeGameFinalityDelaySeconds + ) external; } diff --git a/packages/contracts-bedrock/interfaces/safe/IDeputyGuardianModule.sol b/packages/contracts-bedrock/interfaces/safe/IDeputyGuardianModule.sol index a5c0e33130279..d4d76649a50c7 100644 --- a/packages/contracts-bedrock/interfaces/safe/IDeputyGuardianModule.sol +++ b/packages/contracts-bedrock/interfaces/safe/IDeputyGuardianModule.sol @@ -2,22 +2,21 @@ pragma solidity ^0.8.0; import { IAnchorStateRegistry } from "interfaces/dispute/IAnchorStateRegistry.sol"; -import { IFaultDisputeGame } from "interfaces/dispute/IFaultDisputeGame.sol"; import { ISuperchainConfig } from "interfaces/L1/ISuperchainConfig.sol"; -import { IOptimismPortal2 } from "interfaces/L1/IOptimismPortal2.sol"; import { IDisputeGame } from "interfaces/dispute/IDisputeGame.sol"; import { ISemver } from "interfaces/universal/ISemver.sol"; import { GameType, Timestamp } from "src/dispute/lib/Types.sol"; import { GnosisSafe as Safe } from "safe-contracts/GnosisSafe.sol"; interface IDeputyGuardianModule is ISemver { - error ExecutionFailed(string); - error Unauthorized(); + error DeputyGuardianModule_ExecutionFailed(string); + error DeputyGuardianModule_Unauthorized(); event Paused(string identifier); event Unpaused(); event DisputeGameBlacklisted(IDisputeGame indexed game); event RespectedGameTypeSet(GameType indexed gameType, Timestamp indexed updatedAt); + event RetirementTimestampUpdated(Timestamp indexed updatedAt); function version() external view returns (string memory); function __constructor__(Safe _safe, ISuperchainConfig _superchainConfig, address _deputyGuardian) external; @@ -26,7 +25,7 @@ interface IDeputyGuardianModule is ISemver { function deputyGuardian() external view returns (address deputyGuardian_); function pause() external; function unpause() external; - function setAnchorState(IAnchorStateRegistry _registry, IFaultDisputeGame _game) external; - function blacklistDisputeGame(IOptimismPortal2 _portal, IDisputeGame _game) external; - function setRespectedGameType(IOptimismPortal2 _portal, GameType _gameType) external; + function blacklistDisputeGame(IAnchorStateRegistry _anchorStateRegistry, IDisputeGame _game) external; + function setRespectedGameType(IAnchorStateRegistry _anchorStateRegistry, GameType _gameType) external; + function updateRetirementTimestamp(IAnchorStateRegistry _anchorStateRegistry) external; } diff --git a/packages/contracts-bedrock/scripts/deploy/ChainAssertions.sol b/packages/contracts-bedrock/scripts/deploy/ChainAssertions.sol index c4734d89a0d6c..f79e58bf45cf2 100644 --- a/packages/contracts-bedrock/scripts/deploy/ChainAssertions.sol +++ b/packages/contracts-bedrock/scripts/deploy/ChainAssertions.sol @@ -385,13 +385,14 @@ library ChainAssertions { if (_isProxy) { require(address(portal.disputeGameFactory()) == _contracts.DisputeGameFactory, "CHECK-OP2-20"); + require(address(portal.anchorStateRegistry()) == _contracts.AnchorStateRegistry, "CHECK-OP2-25"); require(address(portal.systemConfig()) == _contracts.SystemConfig, "CHECK-OP2-30"); require(portal.guardian() == guardian, "CHECK-OP2-40"); require(address(portal.superchainConfig()) == address(_contracts.SuperchainConfig), "CHECK-OP2-50"); require(portal.paused() == ISuperchainConfig(_contracts.SuperchainConfig).paused(), "CHECK-OP2-60"); require(portal.l2Sender() == Constants.DEFAULT_L2_SENDER, "CHECK-OP2-70"); } else { - require(address(portal.disputeGameFactory()) == address(0), "CHECK-OP2-80"); + require(address(portal.anchorStateRegistry()) == address(0), "CHECK-OP2-80"); require(address(portal.systemConfig()) == address(0), "CHECK-OP2-90"); require(address(portal.superchainConfig()) == address(0), "CHECK-OP2-100"); require(portal.l2Sender() == address(0), "CHECK-OP2-110"); diff --git a/packages/contracts-bedrock/scripts/deploy/Deploy.s.sol b/packages/contracts-bedrock/scripts/deploy/Deploy.s.sol index ca927105ad92c..1d556b0f6db83 100644 --- a/packages/contracts-bedrock/scripts/deploy/Deploy.s.sol +++ b/packages/contracts-bedrock/scripts/deploy/Deploy.s.sol @@ -35,7 +35,6 @@ import { GameType, Claim, GameTypes, OutputRoot, Hash } from "src/dispute/lib/Ty import { IOPContractsManager } from "interfaces/L1/IOPContractsManager.sol"; import { IProxy } from "interfaces/universal/IProxy.sol"; import { IProxyAdmin } from "interfaces/universal/IProxyAdmin.sol"; -import { IOptimismPortal2 } from "interfaces/L1/IOptimismPortal2.sol"; import { ISuperchainConfig } from "interfaces/L1/ISuperchainConfig.sol"; import { ISystemConfig } from "interfaces/L1/ISystemConfig.sol"; import { IDataAvailabilityChallenge } from "interfaces/L1/IDataAvailabilityChallenge.sol"; @@ -206,7 +205,7 @@ contract Deploy is Deployer { // Set the respected game type according to the deploy config vm.startPrank(ISuperchainConfig(artifacts.mustGetAddress("SuperchainConfigProxy")).guardian()); - IOptimismPortal2(artifacts.mustGetAddress("OptimismPortalProxy")).setRespectedGameType( + IAnchorStateRegistry(artifacts.mustGetAddress("AnchorStateRegistryProxy")).setRespectedGameType( GameType.wrap(uint32(cfg.respectedGameType())) ); vm.stopPrank(); diff --git a/packages/contracts-bedrock/scripts/deploy/DeployImplementations.s.sol b/packages/contracts-bedrock/scripts/deploy/DeployImplementations.s.sol index e94a4c1859392..8e38f08e9b6aa 100644 --- a/packages/contracts-bedrock/scripts/deploy/DeployImplementations.s.sol +++ b/packages/contracts-bedrock/scripts/deploy/DeployImplementations.s.sol @@ -310,7 +310,7 @@ contract DeployImplementationsOutput is BaseDeployIO { DeployUtils.assertInitialized({ _contractAddress: address(portal), _isProxy: false, _slot: 0, _offset: 0 }); - require(address(portal.disputeGameFactory()) == address(0), "PORTAL-10"); + require(address(portal.anchorStateRegistry()) == address(0), "PORTAL-10"); require(address(portal.systemConfig()) == address(0), "PORTAL-20"); require(address(portal.superchainConfig()) == address(0), "PORTAL-30"); require(portal.l2Sender() == address(0), "PORTAL-40"); @@ -456,7 +456,7 @@ contract DeployImplementations is Script { deployPreimageOracleSingleton(_dii, _dio); deployMipsSingleton(_dii, _dio); deployDisputeGameFactoryImpl(_dio); - deployAnchorStateRegistryImpl(_dio); + deployAnchorStateRegistryImpl(_dii, _dio); // Deploy the OP Contracts Manager with the new implementations set. deployOPContractsManager(_dii, _dio); @@ -691,14 +691,11 @@ contract DeployImplementations is Script { virtual { uint256 proofMaturityDelaySeconds = _dii.proofMaturityDelaySeconds(); - uint256 disputeGameFinalityDelaySeconds = _dii.disputeGameFinalityDelaySeconds(); IOptimismPortal2 impl = IOptimismPortal2( DeployUtils.createDeterministic({ _name: "OptimismPortal2", _args: DeployUtils.encodeConstructor( - abi.encodeCall( - IOptimismPortal2.__constructor__, (proofMaturityDelaySeconds, disputeGameFinalityDelaySeconds) - ) + abi.encodeCall(IOptimismPortal2.__constructor__, (proofMaturityDelaySeconds)) ), _salt: _salt }) @@ -776,11 +773,20 @@ contract DeployImplementations is Script { _dio.set(_dio.disputeGameFactoryImpl.selector, address(impl)); } - function deployAnchorStateRegistryImpl(DeployImplementationsOutput _dio) public virtual { + function deployAnchorStateRegistryImpl( + DeployImplementationsInput _dii, + DeployImplementationsOutput _dio + ) + public + virtual + { + uint256 disputeGameFinalityDelaySeconds = _dii.disputeGameFinalityDelaySeconds(); IAnchorStateRegistry impl = IAnchorStateRegistry( DeployUtils.createDeterministic({ _name: "AnchorStateRegistry", - _args: DeployUtils.encodeConstructor(abi.encodeCall(IAnchorStateRegistry.__constructor__, ())), + _args: DeployUtils.encodeConstructor( + abi.encodeCall(IAnchorStateRegistry.__constructor__, (disputeGameFinalityDelaySeconds)) + ), _salt: _salt }) ); @@ -909,19 +915,15 @@ contract DeployImplementationsInterop is DeployImplementations { override { uint256 proofMaturityDelaySeconds = _dii.proofMaturityDelaySeconds(); - uint256 disputeGameFinalityDelaySeconds = _dii.disputeGameFinalityDelaySeconds(); IOptimismPortalInterop impl = IOptimismPortalInterop( DeployUtils.createDeterministic({ _name: "OptimismPortalInterop", _args: DeployUtils.encodeConstructor( - abi.encodeCall( - IOptimismPortalInterop.__constructor__, (proofMaturityDelaySeconds, disputeGameFinalityDelaySeconds) - ) + abi.encodeCall(IOptimismPortalInterop.__constructor__, (proofMaturityDelaySeconds)) ), _salt: _salt }) ); - vm.label(address(impl), "OptimismPortalImpl"); _dio.set(_dio.optimismPortalImpl.selector, address(impl)); } diff --git a/packages/contracts-bedrock/scripts/deploy/DeployOPChain.s.sol b/packages/contracts-bedrock/scripts/deploy/DeployOPChain.s.sol index 2112d614251ae..ba384992d44db 100644 --- a/packages/contracts-bedrock/scripts/deploy/DeployOPChain.s.sol +++ b/packages/contracts-bedrock/scripts/deploy/DeployOPChain.s.sol @@ -597,16 +597,17 @@ contract DeployOPChain is Script { IOptimismPortal2 portal = _doo.optimismPortalProxy(); ISuperchainConfig superchainConfig = ISuperchainConfig(address(_doi.opcm().superchainConfig())); - require(address(portal.disputeGameFactory()) == address(_doo.disputeGameFactoryProxy()), "PORTAL-10"); - require(address(portal.systemConfig()) == address(_doo.systemConfigProxy()), "PORTAL-20"); - require(address(portal.superchainConfig()) == address(superchainConfig), "PORTAL-30"); - require(portal.guardian() == superchainConfig.guardian(), "PORTAL-40"); - require(portal.paused() == superchainConfig.paused(), "PORTAL-50"); - require(portal.l2Sender() == Constants.DEFAULT_L2_SENDER, "PORTAL-60"); + require(address(portal.anchorStateRegistry()) == address(_doo.anchorStateRegistryProxy()), "PORTAL-10"); + require(address(portal.disputeGameFactory()) == address(_doo.disputeGameFactoryProxy()), "PORTAL-20"); + require(address(portal.systemConfig()) == address(_doo.systemConfigProxy()), "PORTAL-30"); + require(address(portal.superchainConfig()) == address(superchainConfig), "PORTAL-40"); + require(portal.guardian() == superchainConfig.guardian(), "PORTAL-50"); + require(portal.paused() == superchainConfig.paused(), "PORTAL-60"); + require(portal.l2Sender() == Constants.DEFAULT_L2_SENDER, "PORTAL-70"); // This slot is the custom gas token _balance and this check ensures // that it stays unset for forwards compatibility with custom gas token. - require(vm.load(address(portal), bytes32(uint256(61))) == bytes32(0), "PORTAL-70"); + require(vm.load(address(portal), bytes32(uint256(61))) == bytes32(0), "PORTAL-80"); } function assertValidDisputeGameFactory(DeployOPChainInput _doi, DeployOPChainOutput _doo) internal { diff --git a/packages/contracts-bedrock/scripts/deploy/SetDisputeGameImpl.s.sol b/packages/contracts-bedrock/scripts/deploy/SetDisputeGameImpl.s.sol index 17e3d5a99dce2..77d4880f02aa5 100644 --- a/packages/contracts-bedrock/scripts/deploy/SetDisputeGameImpl.s.sol +++ b/packages/contracts-bedrock/scripts/deploy/SetDisputeGameImpl.s.sol @@ -7,11 +7,11 @@ import { IFaultDisputeGame } from "interfaces/dispute/IFaultDisputeGame.sol"; import { IDisputeGameFactory } from "interfaces/dispute/IDisputeGameFactory.sol"; import { BaseDeployIO } from "scripts/deploy/BaseDeployIO.sol"; import { GameType } from "src/dispute/lib/Types.sol"; -import { IOptimismPortal2 } from "interfaces/L1/IOptimismPortal2.sol"; +import { IAnchorStateRegistry } from "interfaces/dispute/IAnchorStateRegistry.sol"; contract SetDisputeGameImplInput is BaseDeployIO { IDisputeGameFactory internal _factory; - IOptimismPortal2 internal _portal; + IAnchorStateRegistry internal _anchorStateRegistry; IFaultDisputeGame internal _impl; uint32 internal _gameType; @@ -20,7 +20,7 @@ contract SetDisputeGameImplInput is BaseDeployIO { require(_addr != address(0), "SetDisputeGameImplInput: cannot set zero address"); if (_sel == this.factory.selector) _factory = IDisputeGameFactory(_addr); - else if (_sel == this.portal.selector) _portal = IOptimismPortal2(payable(_addr)); + else if (_sel == this.anchorStateRegistry.selector) _anchorStateRegistry = IAnchorStateRegistry(_addr); else if (_sel == this.impl.selector) _impl = IFaultDisputeGame(_addr); else revert("SetDisputeGameImplInput: unknown selector"); } @@ -36,8 +36,8 @@ contract SetDisputeGameImplInput is BaseDeployIO { return _factory; } - function portal() public view returns (IOptimismPortal2) { - return _portal; + function anchorStateRegistry() public view returns (IAnchorStateRegistry) { + return _anchorStateRegistry; } function impl() public view returns (IFaultDisputeGame) { @@ -57,15 +57,15 @@ contract SetDisputeGameImpl is Script { require(address(factory.gameImpls(gameType)) == address(0), "SDGI-10"); IFaultDisputeGame impl = _input.impl(); - IOptimismPortal2 portal = _input.portal(); + IAnchorStateRegistry anchorStateRegistry = _input.anchorStateRegistry(); vm.broadcast(msg.sender); factory.setImplementation(gameType, impl); - if (address(portal) != address(0)) { - require(address(portal.disputeGameFactory()) == address(factory), "SDGI-20"); + if (address(anchorStateRegistry) != address(0)) { + require(address(anchorStateRegistry.disputeGameFactory()) == address(factory), "SDGI-20"); vm.broadcast(msg.sender); - portal.setRespectedGameType(gameType); + anchorStateRegistry.setRespectedGameType(gameType); } assertValid(_input); @@ -75,9 +75,12 @@ contract SetDisputeGameImpl is Script { GameType gameType = GameType.wrap(_input.gameType()); require(address(_input.factory().gameImpls(gameType)) == address(_input.impl()), "SDGI-30"); - if (address(_input.portal()) != address(0)) { - require(address(_input.portal().disputeGameFactory()) == address(_input.factory()), "SDGI-40"); - require(GameType.unwrap(_input.portal().respectedGameType()) == GameType.unwrap(gameType), "SDGI-50"); + if (address(_input.anchorStateRegistry()) != address(0)) { + require(address(_input.anchorStateRegistry().disputeGameFactory()) == address(_input.factory()), "SDGI-40"); + require( + GameType.unwrap(_input.anchorStateRegistry().respectedGameType()) == GameType.unwrap(gameType), + "SDGI-50" + ); } } } diff --git a/packages/contracts-bedrock/snapshots/abi/AnchorStateRegistry.json b/packages/contracts-bedrock/snapshots/abi/AnchorStateRegistry.json index 25919d1d4bfb1..3aa3c26096c60 100644 --- a/packages/contracts-bedrock/snapshots/abi/AnchorStateRegistry.json +++ b/packages/contracts-bedrock/snapshots/abi/AnchorStateRegistry.json @@ -1,6 +1,12 @@ [ { - "inputs": [], + "inputs": [ + { + "internalType": "uint256", + "name": "_disputeGameFinalityDelaySeconds", + "type": "uint256" + } + ], "stateMutability": "nonpayable", "type": "constructor" }, @@ -41,6 +47,38 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [ + { + "internalType": "contract IDisputeGame", + "name": "_disputeGame", + "type": "address" + } + ], + "name": "blacklistDisputeGame", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract IDisputeGame", + "name": "", + "type": "address" + } + ], + "name": "disputeGameBlacklist", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [], "name": "disputeGameFactory", @@ -54,6 +92,19 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [], + "name": "disputeGameFinalityDelaySeconds", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [], "name": "getAnchorRoot", @@ -84,11 +135,6 @@ "name": "_disputeGameFactory", "type": "address" }, - { - "internalType": "contract IOptimismPortal2", - "name": "_portal", - "type": "address" - }, { "components": [ { @@ -105,6 +151,11 @@ "internalType": "struct OutputRoot", "name": "_startingAnchorRoot", "type": "tuple" + }, + { + "internalType": "GameType", + "name": "_startingRespectedGameType", + "type": "uint32" } ], "name": "initialize", @@ -266,12 +317,12 @@ }, { "inputs": [], - "name": "portal", + "name": "paused", "outputs": [ { - "internalType": "contract IOptimismPortal2", + "internalType": "bool", "name": "", - "type": "address" + "type": "bool" } ], "stateMutability": "view", @@ -290,6 +341,19 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [], + "name": "retirementTimestamp", + "outputs": [ + { + "internalType": "uint64", + "name": "", + "type": "uint64" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [ { @@ -303,6 +367,19 @@ "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [ + { + "internalType": "GameType", + "name": "_gameType", + "type": "uint32" + } + ], + "name": "setRespectedGameType", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [], "name": "superchainConfig", @@ -316,6 +393,13 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [], + "name": "updateRetirementTimestamp", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [], "name": "version", @@ -339,7 +423,7 @@ "type": "address" } ], - "name": "AnchorNotUpdated", + "name": "AnchorUpdated", "type": "event" }, { @@ -347,12 +431,12 @@ "inputs": [ { "indexed": true, - "internalType": "contract IFaultDisputeGame", - "name": "game", + "internalType": "contract IDisputeGame", + "name": "disputeGame", "type": "address" } ], - "name": "AnchorUpdated", + "name": "DisputeGameBlacklisted", "type": "event" }, { @@ -368,6 +452,32 @@ "name": "Initialized", "type": "event" }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "GameType", + "name": "gameType", + "type": "uint32" + } + ], + "name": "RespectedGameTypeSet", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "timestamp", + "type": "uint256" + } + ], + "name": "RetirementTimestampSet", + "type": "event" + }, { "inputs": [], "name": "AnchorStateRegistry_AnchorGameBlacklisted", diff --git a/packages/contracts-bedrock/snapshots/abi/DeputyGuardianModule.json b/packages/contracts-bedrock/snapshots/abi/DeputyGuardianModule.json index f1006749351ea..ab662e4ee49fc 100644 --- a/packages/contracts-bedrock/snapshots/abi/DeputyGuardianModule.json +++ b/packages/contracts-bedrock/snapshots/abi/DeputyGuardianModule.json @@ -23,8 +23,8 @@ { "inputs": [ { - "internalType": "contract IOptimismPortal2", - "name": "_portal", + "internalType": "contract IAnchorStateRegistry", + "name": "_anchorStateRegistry", "type": "address" }, { @@ -75,25 +75,7 @@ "inputs": [ { "internalType": "contract IAnchorStateRegistry", - "name": "_registry", - "type": "address" - }, - { - "internalType": "contract IFaultDisputeGame", - "name": "_game", - "type": "address" - } - ], - "name": "setAnchorState", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "contract IOptimismPortal2", - "name": "_portal", + "name": "_anchorStateRegistry", "type": "address" }, { @@ -127,6 +109,19 @@ "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [ + { + "internalType": "contract IAnchorStateRegistry", + "name": "_anchorStateRegistry", + "type": "address" + } + ], + "name": "updateRetirementTimestamp", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [], "name": "version", @@ -185,6 +180,19 @@ "name": "RespectedGameTypeSet", "type": "event" }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "Timestamp", + "name": "updatedAt", + "type": "uint64" + } + ], + "name": "RetirementTimestampUpdated", + "type": "event" + }, { "anonymous": false, "inputs": [], @@ -199,12 +207,12 @@ "type": "string" } ], - "name": "ExecutionFailed", + "name": "DeputyGuardianModule_ExecutionFailed", "type": "error" }, { "inputs": [], - "name": "Unauthorized", + "name": "DeputyGuardianModule_Unauthorized", "type": "error" } ] \ 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 71b8677f7dd85..ebc1b9132de40 100644 --- a/packages/contracts-bedrock/snapshots/abi/OptimismPortal2.json +++ b/packages/contracts-bedrock/snapshots/abi/OptimismPortal2.json @@ -5,11 +5,6 @@ "internalType": "uint256", "name": "_proofMaturityDelaySeconds", "type": "uint256" - }, - { - "internalType": "uint256", - "name": "_disputeGameFinalityDelaySeconds", - "type": "uint256" } ], "stateMutability": "nonpayable", @@ -20,16 +15,16 @@ "type": "receive" }, { - "inputs": [ + "inputs": [], + "name": "anchorStateRegistry", + "outputs": [ { - "internalType": "contract IDisputeGame", - "name": "_disputeGame", + "internalType": "contract IAnchorStateRegistry", + "name": "", "type": "address" } ], - "name": "blacklistDisputeGame", - "outputs": [], - "stateMutability": "nonpayable", + "stateMutability": "view", "type": "function" }, { @@ -83,25 +78,6 @@ "stateMutability": "payable", "type": "function" }, - { - "inputs": [ - { - "internalType": "contract IDisputeGame", - "name": "", - "type": "address" - } - ], - "name": "disputeGameBlacklist", - "outputs": [ - { - "internalType": "bool", - "name": "", - "type": "bool" - } - ], - "stateMutability": "view", - "type": "function" - }, { "inputs": [], "name": "disputeGameFactory", @@ -264,11 +240,6 @@ }, { "inputs": [ - { - "internalType": "contract IDisputeGameFactory", - "name": "_disputeGameFactory", - "type": "address" - }, { "internalType": "contract ISystemConfig", "name": "_systemConfig", @@ -280,9 +251,9 @@ "type": "address" }, { - "internalType": "GameType", - "name": "_initialRespectedGameType", - "type": "uint32" + "internalType": "contract IAnchorStateRegistry", + "name": "_anchorStateRegistry", + "type": "address" } ], "name": "initialize", @@ -551,19 +522,6 @@ "stateMutability": "view", "type": "function" }, - { - "inputs": [ - { - "internalType": "GameType", - "name": "_gameType", - "type": "uint32" - } - ], - "name": "setRespectedGameType", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, { "inputs": [], "name": "superchainConfig", @@ -590,6 +548,19 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [ + { + "internalType": "contract IAnchorStateRegistry", + "name": "_anchorStateRegistry", + "type": "address" + } + ], + "name": "upgrade", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [], "name": "version", @@ -603,19 +574,6 @@ "stateMutability": "pure", "type": "function" }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "contract IDisputeGame", - "name": "disputeGame", - "type": "address" - } - ], - "name": "DisputeGameBlacklisted", - "type": "event" - }, { "anonymous": false, "inputs": [ @@ -629,25 +587,6 @@ "name": "Initialized", "type": "event" }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "GameType", - "name": "newGameType", - "type": "uint32" - }, - { - "indexed": true, - "internalType": "Timestamp", - "name": "updatedAt", - "type": "uint64" - } - ], - "name": "RespectedGameTypeSet", - "type": "event" - }, { "anonymous": false, "inputs": [ @@ -744,102 +683,102 @@ }, { "inputs": [], - "name": "AlreadyFinalized", + "name": "ContentLengthMismatch", "type": "error" }, { "inputs": [], - "name": "BadTarget", + "name": "EmptyItem", "type": "error" }, { "inputs": [], - "name": "Blacklisted", + "name": "InvalidDataRemainder", "type": "error" }, { "inputs": [], - "name": "CallPaused", + "name": "InvalidHeader", "type": "error" }, { "inputs": [], - "name": "ContentLengthMismatch", + "name": "OptimismPortal_AlreadyFinalized", "type": "error" }, { "inputs": [], - "name": "EmptyItem", + "name": "OptimismPortal_BadTarget", "type": "error" }, { "inputs": [], - "name": "GasEstimation", + "name": "OptimismPortal_CallPaused", "type": "error" }, { "inputs": [], - "name": "InvalidDataRemainder", + "name": "OptimismPortal_CalldataTooLarge", "type": "error" }, { "inputs": [], - "name": "InvalidDisputeGame", + "name": "OptimismPortal_GasEstimation", "type": "error" }, { "inputs": [], - "name": "InvalidGameType", + "name": "OptimismPortal_GasLimitTooLow", "type": "error" }, { "inputs": [], - "name": "InvalidHeader", + "name": "OptimismPortal_ImproperDisputeGame", "type": "error" }, { "inputs": [], - "name": "InvalidMerkleProof", + "name": "OptimismPortal_InvalidDisputeGame", "type": "error" }, { "inputs": [], - "name": "InvalidProof", + "name": "OptimismPortal_InvalidMerkleProof", "type": "error" }, { "inputs": [], - "name": "LargeCalldata", + "name": "OptimismPortal_InvalidOutputRootProof", "type": "error" }, { "inputs": [], - "name": "LegacyGame", + "name": "OptimismPortal_InvalidProofTimestamp", "type": "error" }, { "inputs": [], - "name": "NonReentrant", + "name": "OptimismPortal_InvalidRootClaim", "type": "error" }, { "inputs": [], - "name": "OutOfGas", + "name": "OptimismPortal_NoReentrancy", "type": "error" }, { "inputs": [], - "name": "ProposalNotValidated", + "name": "OptimismPortal_ProofNotOldEnough", "type": "error" }, { "inputs": [], - "name": "SmallGasLimit", + "name": "OptimismPortal_Unproven", "type": "error" }, { "inputs": [], - "name": "Unauthorized", + "name": "OutOfGas", "type": "error" }, { @@ -851,10 +790,5 @@ "inputs": [], "name": "UnexpectedString", "type": "error" - }, - { - "inputs": [], - "name": "Unproven", - "type": "error" } ] \ No newline at end of file diff --git a/packages/contracts-bedrock/snapshots/abi/OptimismPortalInterop.json b/packages/contracts-bedrock/snapshots/abi/OptimismPortalInterop.json index 2dd27e68eaff9..4a6c342105f2c 100644 --- a/packages/contracts-bedrock/snapshots/abi/OptimismPortalInterop.json +++ b/packages/contracts-bedrock/snapshots/abi/OptimismPortalInterop.json @@ -5,11 +5,6 @@ "internalType": "uint256", "name": "_proofMaturityDelaySeconds", "type": "uint256" - }, - { - "internalType": "uint256", - "name": "_disputeGameFinalityDelaySeconds", - "type": "uint256" } ], "stateMutability": "nonpayable", @@ -20,16 +15,16 @@ "type": "receive" }, { - "inputs": [ + "inputs": [], + "name": "anchorStateRegistry", + "outputs": [ { - "internalType": "contract IDisputeGame", - "name": "_disputeGame", + "internalType": "contract IAnchorStateRegistry", + "name": "", "type": "address" } ], - "name": "blacklistDisputeGame", - "outputs": [], - "stateMutability": "nonpayable", + "stateMutability": "view", "type": "function" }, { @@ -83,25 +78,6 @@ "stateMutability": "payable", "type": "function" }, - { - "inputs": [ - { - "internalType": "contract IDisputeGame", - "name": "", - "type": "address" - } - ], - "name": "disputeGameBlacklist", - "outputs": [ - { - "internalType": "bool", - "name": "", - "type": "bool" - } - ], - "stateMutability": "view", - "type": "function" - }, { "inputs": [], "name": "disputeGameFactory", @@ -264,11 +240,6 @@ }, { "inputs": [ - { - "internalType": "contract IDisputeGameFactory", - "name": "_disputeGameFactory", - "type": "address" - }, { "internalType": "contract ISystemConfig", "name": "_systemConfig", @@ -280,9 +251,9 @@ "type": "address" }, { - "internalType": "GameType", - "name": "_initialRespectedGameType", - "type": "uint32" + "internalType": "contract IAnchorStateRegistry", + "name": "_anchorStateRegistry", + "type": "address" } ], "name": "initialize", @@ -569,19 +540,6 @@ "stateMutability": "nonpayable", "type": "function" }, - { - "inputs": [ - { - "internalType": "GameType", - "name": "_gameType", - "type": "uint32" - } - ], - "name": "setRespectedGameType", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, { "inputs": [], "name": "superchainConfig", @@ -608,6 +566,19 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [ + { + "internalType": "contract IAnchorStateRegistry", + "name": "_anchorStateRegistry", + "type": "address" + } + ], + "name": "upgrade", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [], "name": "version", @@ -621,19 +592,6 @@ "stateMutability": "pure", "type": "function" }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "contract IDisputeGame", - "name": "disputeGame", - "type": "address" - } - ], - "name": "DisputeGameBlacklisted", - "type": "event" - }, { "anonymous": false, "inputs": [ @@ -647,25 +605,6 @@ "name": "Initialized", "type": "event" }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "GameType", - "name": "newGameType", - "type": "uint32" - }, - { - "indexed": true, - "internalType": "Timestamp", - "name": "updatedAt", - "type": "uint64" - } - ], - "name": "RespectedGameTypeSet", - "type": "event" - }, { "anonymous": false, "inputs": [ @@ -762,117 +701,117 @@ }, { "inputs": [], - "name": "AlreadyFinalized", + "name": "ContentLengthMismatch", "type": "error" }, { "inputs": [], - "name": "BadTarget", + "name": "EmptyItem", "type": "error" }, { "inputs": [], - "name": "Blacklisted", + "name": "InvalidDataRemainder", "type": "error" }, { "inputs": [], - "name": "CallPaused", + "name": "InvalidHeader", "type": "error" }, { "inputs": [], - "name": "ContentLengthMismatch", + "name": "OptimismPortal_AlreadyFinalized", "type": "error" }, { "inputs": [], - "name": "EmptyItem", + "name": "OptimismPortal_BadTarget", "type": "error" }, { "inputs": [], - "name": "GasEstimation", + "name": "OptimismPortal_CallPaused", "type": "error" }, { "inputs": [], - "name": "InvalidDataRemainder", + "name": "OptimismPortal_CalldataTooLarge", "type": "error" }, { "inputs": [], - "name": "InvalidDisputeGame", + "name": "OptimismPortal_GasEstimation", "type": "error" }, { "inputs": [], - "name": "InvalidGameType", + "name": "OptimismPortal_GasLimitTooLow", "type": "error" }, { "inputs": [], - "name": "InvalidHeader", + "name": "OptimismPortal_ImproperDisputeGame", "type": "error" }, { "inputs": [], - "name": "InvalidMerkleProof", + "name": "OptimismPortal_InvalidDisputeGame", "type": "error" }, { "inputs": [], - "name": "InvalidProof", + "name": "OptimismPortal_InvalidMerkleProof", "type": "error" }, { "inputs": [], - "name": "LargeCalldata", + "name": "OptimismPortal_InvalidOutputRootProof", "type": "error" }, { "inputs": [], - "name": "LegacyGame", + "name": "OptimismPortal_InvalidProofTimestamp", "type": "error" }, { "inputs": [], - "name": "NonReentrant", + "name": "OptimismPortal_InvalidRootClaim", "type": "error" }, { "inputs": [], - "name": "OutOfGas", + "name": "OptimismPortal_NoReentrancy", "type": "error" }, { "inputs": [], - "name": "ProposalNotValidated", + "name": "OptimismPortal_ProofNotOldEnough", "type": "error" }, { "inputs": [], - "name": "SmallGasLimit", + "name": "OptimismPortal_Unauthorized", "type": "error" }, { "inputs": [], - "name": "Unauthorized", + "name": "OptimismPortal_Unproven", "type": "error" }, { "inputs": [], - "name": "UnexpectedList", + "name": "OutOfGas", "type": "error" }, { "inputs": [], - "name": "UnexpectedString", + "name": "UnexpectedList", "type": "error" }, { "inputs": [], - "name": "Unproven", + "name": "UnexpectedString", "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 42eacc880d131..752e8131e6ca9 100644 --- a/packages/contracts-bedrock/snapshots/semver-lock.json +++ b/packages/contracts-bedrock/snapshots/semver-lock.json @@ -16,24 +16,24 @@ "sourceCodeHash": "0x7f614d0c5251365580c02dd898900a583ece2aa28579adea74a3808b575949b4" }, "src/L1/OPContractsManager.sol": { - "initCodeHash": "0xfc2b4c9a4b589d3bd5267a9335b0471d2adb13af3f9ae4c01a74fe7289dc5e48", - "sourceCodeHash": "0xa288e8bbe3cdc5361c904a57c55c0b2a658c1b7148df6592cd2e002ae2e957a0" + "initCodeHash": "0x3da482f3107d23948d9535eac40f2d09861605570f0d7e1c2ca4766e1ae71943", + "sourceCodeHash": "0x8cbad8c307c0b56514ce52cba4c3519e2e70cbd19957cd50a65eac208a09cf7d" }, "src/L1/OPContractsManagerInterop.sol": { - "initCodeHash": "0x8adc6744a20019ebc1ea77b4a3227d1644e334a6bf3ae5796dd84be10f81d8cf", - "sourceCodeHash": "0x504f8b1ef3fa5920ebab1ca438b1fe6e69587d3502da2b68502b4587f3abb1a4" + "initCodeHash": "0xff7784d6851f3dc751e3c8ebe1d0046484c512c765802a16685c8f7d107e3c88", + "sourceCodeHash": "0xd04bb5babc02fd93618397678506e9bcce5f1881b008785da0767bda74d8447c" }, "src/L1/OPPrestateUpdater.sol": { - "initCodeHash": "0x687553a705b47a57aa77de1af612913ed73f96b1929a84585b0022a0113203f1", - "sourceCodeHash": "0x7d2ec5f151a244e83f483a9a99598bee4915d0624a7af6c07cc82d82bf0dbb93" + "initCodeHash": "0x2d55d1a4826bcb5c65e5b66300fe470ea02398fcdd0a436755024855b372360d", + "sourceCodeHash": "0x985c33cc1933d68de38d6c13afc1ac42ef7e455cb06bb255c1fed22b06b97a62" }, "src/L1/OptimismPortal2.sol": { - "initCodeHash": "0x93c5e8ff52ff8b1cedd985b4a8890c12b56d5959832405c7622615c3541908f5", - "sourceCodeHash": "0x753465f9317d89f8ea64bcd3fc3fb1164d040b9be2d5ba3d401d951f2ceff023" + "initCodeHash": "0xcc7b2a3f10786983f995f557306944eb27c77f7a502bcebfac111ded62a08344", + "sourceCodeHash": "0x016cf228b63fedd9fafbeed5b804786235bcc07ceb499f4919dfad40cdeda322" }, "src/L1/OptimismPortalInterop.sol": { - "initCodeHash": "0x12ecac882d3a45901993f93661a5e8ada1fb964d25e7843cfb9f56b2a30aa7b4", - "sourceCodeHash": "0xa031a40aa6f722466a76a36871989942f34351a327227a62266bb9a26c439fe2" + "initCodeHash": "0x561140ae220a1cd3f8fabd35e435a128fb9d9f2d83686b89494a90cee8d8707f", + "sourceCodeHash": "0x1b2ce1f2e3125bd449501b9d228b207d31f2bc39c7f23262c674e0d1f47c0d0b" }, "src/L1/ProtocolVersions.sol": { "initCodeHash": "0x5a76c8530cb24cf23d3baacc6eefaac226382af13f1e2a35535d2ec2b0573b29", @@ -160,8 +160,8 @@ "sourceCodeHash": "0x03c160168986ffc8d26a90c37366e7ad6da03f49d83449e1f8b3de0f4b590f6f" }, "src/dispute/AnchorStateRegistry.sol": { - "initCodeHash": "0x08cc5a5e41eadb6c411fa6387ddc0cf12be360855599dd622cce84c0ba081e77", - "sourceCodeHash": "0xe0aaa79f7184724ff0fba2e92e85f652f936fecd099288edb0a0f6b0e0240f34" + "initCodeHash": "0xb905a31a816dc7354e9153a6cbf08d968c6d631e5383bd64c5ff1825bf284825", + "sourceCodeHash": "0xf785b369133782f614ed792766f5ec05d79b639db29abc2af2a4074f8600fb36" }, "src/dispute/DelayedWETH.sol": { "initCodeHash": "0xdd0b5e523f3b53563fe0b6e6165fb73605b14910ffa32a7cbed855cdebab47c6", @@ -200,8 +200,8 @@ "sourceCodeHash": "0x62c9a6182d82692fb9c173ddb0d7978bcff2d1d4dc8cd2f10625e1e65bda6888" }, "src/safe/DeputyGuardianModule.sol": { - "initCodeHash": "0x5eaf823d81995ce1f703f26e31049c54c1d4902dd9873a0b4645d470f2f459a2", - "sourceCodeHash": "0x17236a91c4171ae9525eae0e59fa65bb2dc320d62677cfc7d7eb942f182619fb" + "initCodeHash": "0xf0ccc9997a120b2642bf4b15b5c12b041219c5e748c0c5e24af36f9cde62f175", + "sourceCodeHash": "0xa7dc83c6278118b9a7b4633633eeaa7e5b466bf24fdd184925ab620555caf0aa" }, "src/safe/DeputyPauseModule.sol": { "initCodeHash": "0xa3b7bf0c93b41f39ebc18a81322b90127a633d684ae9f86c2f2a1c48fe7f1372", diff --git a/packages/contracts-bedrock/snapshots/storageLayout/AnchorStateRegistry.json b/packages/contracts-bedrock/snapshots/storageLayout/AnchorStateRegistry.json index fac376a90d126..4d981001f8fba 100644 --- a/packages/contracts-bedrock/snapshots/storageLayout/AnchorStateRegistry.json +++ b/packages/contracts-bedrock/snapshots/storageLayout/AnchorStateRegistry.json @@ -27,25 +27,39 @@ "slot": "1", "type": "contract IDisputeGameFactory" }, - { - "bytes": "20", - "label": "portal", - "offset": 0, - "slot": "2", - "type": "contract IOptimismPortal2" - }, { "bytes": "20", "label": "anchorGame", "offset": 0, - "slot": "3", + "slot": "2", "type": "contract IFaultDisputeGame" }, { "bytes": "64", "label": "startingAnchorRoot", "offset": 0, - "slot": "4", + "slot": "3", "type": "struct OutputRoot" + }, + { + "bytes": "32", + "label": "disputeGameBlacklist", + "offset": 0, + "slot": "5", + "type": "mapping(contract IDisputeGame => bool)" + }, + { + "bytes": "4", + "label": "respectedGameType", + "offset": 0, + "slot": "6", + "type": "GameType" + }, + { + "bytes": "8", + "label": "retirementTimestamp", + "offset": 4, + "slot": "6", + "type": "uint64" } ] \ No newline at end of file diff --git a/packages/contracts-bedrock/snapshots/storageLayout/OptimismPortal2.json b/packages/contracts-bedrock/snapshots/storageLayout/OptimismPortal2.json index 654695522e049..ccb5d970d342e 100644 --- a/packages/contracts-bedrock/snapshots/storageLayout/OptimismPortal2.json +++ b/packages/contracts-bedrock/snapshots/storageLayout/OptimismPortal2.json @@ -78,10 +78,10 @@ }, { "bytes": "20", - "label": "disputeGameFactory", + "label": "spacer_56_0_20", "offset": 0, "slot": "56", - "type": "contract IDisputeGameFactory" + "type": "address" }, { "bytes": "32", @@ -92,21 +92,21 @@ }, { "bytes": "32", - "label": "disputeGameBlacklist", + "label": "spacer_58_0_32", "offset": 0, "slot": "58", - "type": "mapping(contract IDisputeGame => bool)" + "type": "bytes32" }, { "bytes": "4", - "label": "respectedGameType", + "label": "spacer_59_0_4", "offset": 0, "slot": "59", "type": "GameType" }, { "bytes": "8", - "label": "respectedGameTypeUpdatedAt", + "label": "spacer_59_4_8", "offset": 4, "slot": "59", "type": "uint64" @@ -124,5 +124,12 @@ "offset": 0, "slot": "61", "type": "uint256" + }, + { + "bytes": "20", + "label": "anchorStateRegistry", + "offset": 0, + "slot": "62", + "type": "contract IAnchorStateRegistry" } ] \ No newline at end of file diff --git a/packages/contracts-bedrock/snapshots/storageLayout/OptimismPortalInterop.json b/packages/contracts-bedrock/snapshots/storageLayout/OptimismPortalInterop.json index 654695522e049..ccb5d970d342e 100644 --- a/packages/contracts-bedrock/snapshots/storageLayout/OptimismPortalInterop.json +++ b/packages/contracts-bedrock/snapshots/storageLayout/OptimismPortalInterop.json @@ -78,10 +78,10 @@ }, { "bytes": "20", - "label": "disputeGameFactory", + "label": "spacer_56_0_20", "offset": 0, "slot": "56", - "type": "contract IDisputeGameFactory" + "type": "address" }, { "bytes": "32", @@ -92,21 +92,21 @@ }, { "bytes": "32", - "label": "disputeGameBlacklist", + "label": "spacer_58_0_32", "offset": 0, "slot": "58", - "type": "mapping(contract IDisputeGame => bool)" + "type": "bytes32" }, { "bytes": "4", - "label": "respectedGameType", + "label": "spacer_59_0_4", "offset": 0, "slot": "59", "type": "GameType" }, { "bytes": "8", - "label": "respectedGameTypeUpdatedAt", + "label": "spacer_59_4_8", "offset": 4, "slot": "59", "type": "uint64" @@ -124,5 +124,12 @@ "offset": 0, "slot": "61", "type": "uint256" + }, + { + "bytes": "20", + "label": "anchorStateRegistry", + "offset": 0, + "slot": "62", + "type": "contract IAnchorStateRegistry" } ] \ No newline at end of file diff --git a/packages/contracts-bedrock/src/L1/OPContractsManager.sol b/packages/contracts-bedrock/src/L1/OPContractsManager.sol index 420532132e433..272b611c4b918 100644 --- a/packages/contracts-bedrock/src/L1/OPContractsManager.sol +++ b/packages/contracts-bedrock/src/L1/OPContractsManager.sol @@ -146,9 +146,9 @@ contract OPContractsManager is ISemver { // -------- Constants and Variables -------- - /// @custom:semver 1.6.0 + /// @custom:semver 1.7.0 function version() public pure virtual returns (string memory) { - return "1.6.0"; + return "1.7.0"; } /// @notice Address of the SuperchainConfig contract shared by all chains. @@ -328,18 +328,19 @@ contract OPContractsManager is ISemver { ) ); output.opChainProxyAdmin.setProxyType(address(output.l1StandardBridgeProxy), IProxyAdmin.ProxyType.CHUGSPLASH); - string memory contractName = "OVM_L1CrossDomainMessenger"; output.l1CrossDomainMessengerProxy = IL1CrossDomainMessenger( Blueprint.deployFrom( blueprint.resolvedDelegateProxy, computeSalt(l2ChainId, saltMixer, "L1CrossDomainMessenger"), - abi.encode(output.addressManager, contractName) + abi.encode(output.addressManager, "OVM_L1CrossDomainMessenger") ) ); output.opChainProxyAdmin.setProxyType( address(output.l1CrossDomainMessengerProxy), IProxyAdmin.ProxyType.RESOLVED ); - output.opChainProxyAdmin.setImplementationName(address(output.l1CrossDomainMessengerProxy), contractName); + output.opChainProxyAdmin.setImplementationName( + address(output.l1CrossDomainMessengerProxy), "OVM_L1CrossDomainMessenger" + ); // Eventually we will switch from DelayedWETHPermissionedGameProxy to DelayedWETHPermissionlessGameProxy. output.delayedWETHPermissionedGameProxy = IDelayedWETH( @@ -374,6 +375,14 @@ contract OPContractsManager is ISemver { // -------- Set and Initialize Proxy Implementations -------- bytes memory data; + data = encodeAnchorStateRegistryInitializer(_input, output); + upgradeToAndCall( + output.opChainProxyAdmin, + address(output.anchorStateRegistryProxy), + implementation.anchorStateRegistryImpl, + data + ); + data = encodeL1ERC721BridgeInitializer(output); upgradeToAndCall( output.opChainProxyAdmin, address(output.l1ERC721BridgeProxy), implementation.l1ERC721BridgeImpl, data @@ -435,14 +444,6 @@ contract OPContractsManager is ISemver { transferOwnership(address(output.disputeGameFactoryProxy), address(_input.roles.opChainProxyAdminOwner)); - data = encodeAnchorStateRegistryInitializer(_input, output); - upgradeToAndCall( - output.opChainProxyAdmin, - address(output.anchorStateRegistryProxy), - implementation.anchorStateRegistryImpl, - data - ); - // -------- Finalize Deployment -------- // Transfer ownership of the ProxyAdmin from this contract to the specified owner. transferOwnership(address(output.opChainProxyAdmin), _input.roles.opChainProxyAdminOwner); @@ -507,6 +508,11 @@ contract OPContractsManager is ISemver { revert SuperchainConfigMismatch(_opChainConfigs[i].systemConfigProxy); } + // Grab the respected game type BEFORE upgrading the OptimismPortal or the function + // won't work because the new implementation tries to get it from the + // AnchorStateRegistry which doesn't have it yet. + GameType respectedGameType = IOptimismPortal2(payable(opChainAddrs.optimismPortal)).respectedGameType(); + // -------- Upgrade Contracts Stored in SystemConfig -------- upgradeTo( _opChainConfigs[i].proxyAdmin, address(_opChainConfigs[i].systemConfigProxy), impls.systemConfigImpl @@ -563,9 +569,9 @@ contract OPContractsManager is ISemver { // 2. getting the respected game type from the OptimismPortal. // 3. getting the anchor root for the respected game type from the Anchor State Registry. { - GameType gameType = IOptimismPortal2(payable(opChainAddrs.optimismPortal)).respectedGameType(); - (Hash root, uint256 l2BlockNumber) = - getAnchorStateRegistry(IFaultDisputeGame(address(permissionedDisputeGame))).anchors(gameType); + (Hash root, uint256 l2BlockNumber) = getAnchorStateRegistry( + IFaultDisputeGame(address(permissionedDisputeGame)) + ).anchors(respectedGameType); OutputRoot memory startingAnchorRoot = OutputRoot({ root: root, l2BlockNumber: l2BlockNumber }); upgradeToAndCall( @@ -577,15 +583,17 @@ contract OPContractsManager is ISemver { ( superchainConfig, IDisputeGameFactory(opChainAddrs.disputeGameFactory), - IOptimismPortal2(payable(opChainAddrs.optimismPortal)), - startingAnchorRoot + startingAnchorRoot, + respectedGameType ) ) ); + + // Upgrade the OptimismPortal to have a reference to the new AnchorStateRegistry. + IOptimismPortal2(payable(opChainAddrs.optimismPortal)).upgrade(newAnchorStateRegistryProxy); } // Deploy and set a new permissioned game to update its prestate - deployAndSetNewGameImpl({ _l2ChainId: l2ChainId, _disputeGame: IDisputeGame(address(permissionedDisputeGame)), @@ -744,17 +752,12 @@ contract OPContractsManager is ISemver { setDGFImplementation(dgf, gameConfig.disputeGameType, IDisputeGame(address(outputs[i].faultDisputeGame))); dgf.setInitBond(gameConfig.disputeGameType, gameConfig.initialBond); - if (gameConfig.permissioned) { - // Emit event for the newly added game type with the old permissioned dispute game - emit GameTypeAdded( - l2ChainId, gameConfig.disputeGameType, outputs[i].faultDisputeGame, IDisputeGame(address(pdg)) - ); - } else { - // Emit event for the newly added game type with the old fault dispute game - emit GameTypeAdded( - l2ChainId, gameConfig.disputeGameType, outputs[i].faultDisputeGame, IDisputeGame(address(fdg)) - ); - } + emit GameTypeAdded( + l2ChainId, + gameConfig.disputeGameType, + outputs[i].faultDisputeGame, + IDisputeGame(gameConfig.permissioned ? address(pdg) : address(fdg)) + ); } return outputs; @@ -849,13 +852,7 @@ contract OPContractsManager is ISemver { returns (bytes memory) { return abi.encodeCall( - IOptimismPortal2.initialize, - ( - _output.disputeGameFactoryProxy, - _output.systemConfigProxy, - superchainConfig, - GameTypes.PERMISSIONED_CANNON - ) + IOptimismPortal2.initialize, (_output.systemConfigProxy, superchainConfig, _output.anchorStateRegistryProxy) ); } @@ -936,7 +933,7 @@ contract OPContractsManager is ISemver { OutputRoot memory startingAnchorRoot = abi.decode(_input.startingAnchorRoot, (OutputRoot)); return abi.encodeCall( IAnchorStateRegistry.initialize, - (superchainConfig, _output.disputeGameFactoryProxy, _output.optimismPortalProxy, startingAnchorRoot) + (superchainConfig, _output.disputeGameFactoryProxy, startingAnchorRoot, GameTypes.PERMISSIONED_CANNON) ); } diff --git a/packages/contracts-bedrock/src/L1/OPContractsManagerInterop.sol b/packages/contracts-bedrock/src/L1/OPContractsManagerInterop.sol index cd4fbcf9f5387..dcd98a3ed3d01 100644 --- a/packages/contracts-bedrock/src/L1/OPContractsManagerInterop.sol +++ b/packages/contracts-bedrock/src/L1/OPContractsManagerInterop.sol @@ -13,9 +13,9 @@ import { ISystemConfigInterop } from "interfaces/L1/ISystemConfigInterop.sol"; import { IProxyAdmin } from "interfaces/universal/IProxyAdmin.sol"; contract OPContractsManagerInterop is OPContractsManager { - /// @custom:semver +interop.10 + /// @custom:semver +interop.11 function version() public pure override returns (string memory) { - return string.concat(super.version(), "+interop.10"); + return string.concat(super.version(), "+interop.11"); } constructor( diff --git a/packages/contracts-bedrock/src/L1/OPPrestateUpdater.sol b/packages/contracts-bedrock/src/L1/OPPrestateUpdater.sol index 849e8454e68b8..dd6f50989fec0 100644 --- a/packages/contracts-bedrock/src/L1/OPPrestateUpdater.sol +++ b/packages/contracts-bedrock/src/L1/OPPrestateUpdater.sol @@ -27,9 +27,9 @@ contract OPPrestateUpdater is OPContractsManager { error PrestateRequired(); // @return Version string - /// @custom:semver 1.7.0 + /// @custom:semver 1.7.1 function version() public pure override returns (string memory) { - return "1.7.0"; + return "1.7.1"; } // @notice Constructs the OPPrestateUpdater contract diff --git a/packages/contracts-bedrock/src/L1/OptimismPortal2.sol b/packages/contracts-bedrock/src/L1/OptimismPortal2.sol index 0573afdf46f3b..b567caf8eacc0 100644 --- a/packages/contracts-bedrock/src/L1/OptimismPortal2.sol +++ b/packages/contracts-bedrock/src/L1/OptimismPortal2.sol @@ -14,25 +14,7 @@ import { Types } from "src/libraries/Types.sol"; import { Hashing } from "src/libraries/Hashing.sol"; import { SecureMerkleTrie } from "src/libraries/trie/SecureMerkleTrie.sol"; import { AddressAliasHelper } from "src/vendor/AddressAliasHelper.sol"; -import { - BadTarget, - LargeCalldata, - SmallGasLimit, - Unauthorized, - CallPaused, - GasEstimation, - NonReentrant, - InvalidProof, - InvalidGameType, - InvalidDisputeGame, - InvalidMerkleProof, - Blacklisted, - Unproven, - ProposalNotValidated, - AlreadyFinalized, - LegacyGame -} from "src/libraries/PortalErrors.sol"; -import { GameStatus, GameType, Claim, Timestamp } from "src/dispute/lib/Types.sol"; +import { GameStatus, GameType } from "src/dispute/lib/Types.sol"; // Interfaces import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; @@ -42,6 +24,7 @@ import { IResourceMetering } from "interfaces/L1/IResourceMetering.sol"; import { ISuperchainConfig } from "interfaces/L1/ISuperchainConfig.sol"; import { IDisputeGameFactory } from "interfaces/dispute/IDisputeGameFactory.sol"; import { IDisputeGame } from "interfaces/dispute/IDisputeGame.sol"; +import { IAnchorStateRegistry } from "interfaces/dispute/IAnchorStateRegistry.sol"; /// @custom:proxied true /// @title OptimismPortal2 @@ -53,20 +36,16 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ISemver { using SafeERC20 for IERC20; /// @notice Represents a proven withdrawal. - /// @custom:field disputeGameProxy The address of the dispute game proxy that the withdrawal was proven against. + /// @custom:field disputeGameProxy Game that the withdrawal was proven against. /// @custom:field timestamp Timestamp at which the withdrawal was proven. struct ProvenWithdrawal { IDisputeGame disputeGameProxy; uint64 timestamp; } - /// @notice The delay between when a withdrawal transaction is proven and when it may be finalized. + /// @notice The delay between when a withdrawal is proven and when it may be finalized. uint256 internal immutable PROOF_MATURITY_DELAY_SECONDS; - /// @notice The delay between when a dispute game is resolved and when a withdrawal proven against it may be - /// finalized. - uint256 internal immutable DISPUTE_GAME_FINALITY_DELAY_SECONDS; - /// @notice Version of the deposit event. uint256 internal constant DEPOSIT_VERSION = 0; @@ -94,7 +73,7 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ISemver { /// @notice Spacer for backwards compatibility. bool private spacer_53_0_1; - /// @notice Contract of the Superchain Config. + /// @notice Address of the SuperchainConfig contract. ISuperchainConfig public superchainConfig; /// @custom:legacy @@ -102,25 +81,30 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ISemver { /// @notice Spacer taking up the legacy `l2Oracle` address slot. address private spacer_54_0_20; - /// @notice Contract of the SystemConfig. + /// @notice Address of the SystemConfig contract. /// @custom:network-specific ISystemConfig public systemConfig; - /// @notice Address of the DisputeGameFactory. /// @custom:network-specific - IDisputeGameFactory public disputeGameFactory; + /// @custom:legacy + /// @custom:spacer disputeGameFactory + /// @notice Spacer taking up the legacy `disputeGameFactory` address slot. + address private spacer_56_0_20; - /// @notice A mapping of withdrawal hashes to proof submitters to `ProvenWithdrawal` data. + /// @notice A mapping of withdrawal hashes to proof submitters to ProvenWithdrawal data. mapping(bytes32 => mapping(address => ProvenWithdrawal)) public provenWithdrawals; - /// @notice A mapping of dispute game addresses to whether or not they are blacklisted. - mapping(IDisputeGame => bool) public disputeGameBlacklist; + /// @custom:legacy + /// @custom:spacer disputeGameBlacklist + bytes32 private spacer_58_0_32; - /// @notice The game type that the OptimismPortal consults for output proposals. - GameType public respectedGameType; + /// @custom:legacy + /// @custom:spacer respectedGameType + GameType private spacer_59_0_4; - /// @notice The timestamp at which the respected game type was last updated. - uint64 public respectedGameTypeUpdatedAt; + /// @custom:legacy + /// @custom:spacer respectedGameTypeUpdatedAt + uint64 private spacer_59_4_8; /// @notice Mapping of withdrawal hashes to addresses that have submitted a proof for the /// withdrawal. Original OptimismPortal contract only allowed one proof to be submitted @@ -133,12 +117,13 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ISemver { /// @custom:legacy /// @custom:spacer _balance - /// @notice Spacer taking up the legacy `_balance` slot. uint256 private spacer_61_0_32; - /// @notice Emitted when a transaction is deposited from L1 to L2. - /// The parameters of this event are read by the rollup node and used to derive deposit - /// transactions on L2. + /// @notice Address of the AnchorStateRegistry contract. + IAnchorStateRegistry public anchorStateRegistry; + + /// @notice Emitted when a transaction is deposited from L1 to L2. The parameters of this event + /// are read by the rollup node and used to derive deposit transactions on L2. /// @param from Address that triggered the deposit transaction. /// @param to Address that the deposit transaction is directed to. /// @param version Version of this deposit transaction event. @@ -151,8 +136,9 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ISemver { /// @param to Address that the withdrawal transaction is directed to. event WithdrawalProven(bytes32 indexed withdrawalHash, address indexed from, address indexed to); - /// @notice Emitted when a withdrawal transaction is proven. Exists as a separate event to allow for backwards - /// compatibility for tooling that observes the `WithdrawalProven` event. + /// @notice Emitted when a withdrawal transaction is proven. Exists as a separate event to + /// allow for backwards compatibility for tooling that observes the WithdrawalProven + /// event. /// @param withdrawalHash Hash of the withdrawal transaction. /// @param proofSubmitter Address of the proof submitter. event WithdrawalProvenExtension1(bytes32 indexed withdrawalHash, address indexed proofSubmitter); @@ -162,74 +148,98 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ISemver { /// @param success Whether the withdrawal transaction was successful. event WithdrawalFinalized(bytes32 indexed withdrawalHash, bool success); - /// @notice Emitted when a dispute game is blacklisted by the Guardian. - /// @param disputeGame Address of the dispute game that was blacklisted. - event DisputeGameBlacklisted(IDisputeGame indexed disputeGame); + /// @notice Thrown when a withdrawal has already been finalized. + error OptimismPortal_AlreadyFinalized(); + + /// @notice Thrown when the target of a withdrawal is unsafe. + error OptimismPortal_BadTarget(); + + /// @notice Thrown when the calldata for a deposit is too large. + error OptimismPortal_CalldataTooLarge(); + + /// @notice Thrown when the portal is paused. + error OptimismPortal_CallPaused(); + + /// @notice Thrown when a gas estimation transaction is being executed. + error OptimismPortal_GasEstimation(); + + /// @notice Thrown when the gas limit for a deposit is too low. + error OptimismPortal_GasLimitTooLow(); + + /// @notice Thrown when the target of a withdrawal is not a proper dispute game. + error OptimismPortal_ImproperDisputeGame(); + + /// @notice Thrown when a withdrawal has not been proven against a valid dispute game. + error OptimismPortal_InvalidDisputeGame(); + + /// @notice Thrown when a withdrawal has not been proven against a valid merkle proof. + error OptimismPortal_InvalidMerkleProof(); + + /// @notice Thrown when a withdrawal has not been proven against a valid output root proof. + error OptimismPortal_InvalidOutputRootProof(); + + /// @notice Thrown when a withdrawal's timestamp is not greater than the dispute game's creation timestamp. + error OptimismPortal_InvalidProofTimestamp(); + + /// @notice Thrown when the root claim of a dispute game is invalid. + error OptimismPortal_InvalidRootClaim(); + + /// @notice Thrown when a withdrawal is being finalized by a reentrant call. + error OptimismPortal_NoReentrancy(); - /// @notice Emitted when the Guardian changes the respected game type in the portal. - /// @param newGameType The new respected game type. - /// @param updatedAt The timestamp at which the respected game type was updated. - event RespectedGameTypeSet(GameType indexed newGameType, Timestamp indexed updatedAt); + /// @notice Thrown when a withdrawal has not been proven for long enough. + error OptimismPortal_ProofNotOldEnough(); + + /// @notice Thrown when a withdrawal has not been proven. + error OptimismPortal_Unproven(); /// @notice Reverts when paused. modifier whenNotPaused() { - if (paused()) revert CallPaused(); + if (paused()) revert OptimismPortal_CallPaused(); _; } /// @notice Semantic version. - /// @custom:semver 3.13.0 + /// @custom:semver 4.0.0 function version() public pure virtual returns (string memory) { - return "3.13.0"; + return "4.0.0"; } - /// @notice Constructs the OptimismPortal contract. - constructor(uint256 _proofMaturityDelaySeconds, uint256 _disputeGameFinalityDelaySeconds) { + /// @param _proofMaturityDelaySeconds The proof maturity delay in seconds. + constructor(uint256 _proofMaturityDelaySeconds) { PROOF_MATURITY_DELAY_SECONDS = _proofMaturityDelaySeconds; - DISPUTE_GAME_FINALITY_DELAY_SECONDS = _disputeGameFinalityDelaySeconds; - _disableInitializers(); } /// @notice Initializer. - /// @param _disputeGameFactory Contract of the DisputeGameFactory. - /// @param _systemConfig Contract of the SystemConfig. - /// @param _superchainConfig Contract of the SuperchainConfig. + /// @param _systemConfig Address of the SystemConfig. + /// @param _superchainConfig Address of the SuperchainConfig. + /// @param _anchorStateRegistry Address of the AnchorStateRegistry. function initialize( - IDisputeGameFactory _disputeGameFactory, ISystemConfig _systemConfig, ISuperchainConfig _superchainConfig, - GameType _initialRespectedGameType + IAnchorStateRegistry _anchorStateRegistry ) external - initializer + reinitializer(2) { - disputeGameFactory = _disputeGameFactory; systemConfig = _systemConfig; superchainConfig = _superchainConfig; + anchorStateRegistry = _anchorStateRegistry; - // Set the `l2Sender` slot, only if it is currently empty. This signals the first initialization of the - // contract. + // Set the l2Sender slot, only if it is currently empty. This signals the first + // initialization of the contract. if (l2Sender == address(0)) { l2Sender = Constants.DEFAULT_L2_SENDER; - - // Set the `respectedGameTypeUpdatedAt` timestamp, to ignore all games of the respected type prior - // to this operation. - respectedGameTypeUpdatedAt = uint64(block.timestamp); - - // Set the initial respected game type - respectedGameType = _initialRespectedGameType; } __ResourceMetering_init(); } - /// @notice Getter function for the address of the guardian. - /// Public getter is legacy and will be removed in the future. Use `SuperchainConfig.guardian()` instead. - /// @return Address of the guardian. - /// @custom:legacy - function guardian() public view returns (address) { - return superchainConfig.guardian(); + /// @notice Upgrades the OptimismPortal contract to have a reference to the AnchorStateRegistry. + /// @param _anchorStateRegistry AnchorStateRegistry contract. + function upgrade(IAnchorStateRegistry _anchorStateRegistry) external reinitializer(2) { + anchorStateRegistry = _anchorStateRegistry; } /// @notice Getter for the current paused status. @@ -242,9 +252,33 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ISemver { return PROOF_MATURITY_DELAY_SECONDS; } + /// @notice Getter for the address of the DisputeGameFactory contract. + function disputeGameFactory() public view returns (IDisputeGameFactory) { + return anchorStateRegistry.disputeGameFactory(); + } + + /// @custom:legacy + /// @notice Getter function for the address of the guardian. + function guardian() public view returns (address) { + return superchainConfig.guardian(); + } + + /// @custom:legacy /// @notice Getter for the dispute game finality delay. - function disputeGameFinalityDelaySeconds() public view returns (uint256) { - return DISPUTE_GAME_FINALITY_DELAY_SECONDS; + function disputeGameFinalityDelaySeconds() external view returns (uint256) { + return anchorStateRegistry.disputeGameFinalityDelaySeconds(); + } + + /// @custom:legacy + /// @notice Getter for the respected game type. + function respectedGameType() external view returns (GameType) { + return anchorStateRegistry.respectedGameType(); + } + + /// @custom:legacy + /// @notice Getter for the timestamp at which the respected game type was updated. + function respectedGameTypeUpdatedAt() external view returns (uint64) { + return anchorStateRegistry.retirementTimestamp(); } /// @notice Computes the minimum gas limit for a deposit. @@ -273,22 +307,11 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ISemver { // Intentionally empty. } - /// @notice Getter for the resource config. - /// Used internally by the ResourceMetering contract. - /// The SystemConfig is the source of truth for the resource config. - /// @return config_ ResourceMetering ResourceConfig - function _resourceConfig() internal view override returns (ResourceMetering.ResourceConfig memory config_) { - IResourceMetering.ResourceConfig memory config = systemConfig.resourceConfig(); - assembly ("memory-safe") { - config_ := config - } - } - /// @notice Proves a withdrawal transaction. /// @param _tx Withdrawal transaction to finalize. /// @param _disputeGameIndex Index of the dispute game to prove the withdrawal against. - /// @param _outputRootProof Inclusion proof of the L2ToL1MessagePasser contract's storage root. - /// @param _withdrawalProof Inclusion proof of the withdrawal in L2ToL1MessagePasser contract. + /// @param _outputRootProof Inclusion proof of the L2ToL1MessagePasser storage root. + /// @param _withdrawalProof Inclusion proof of the withdrawal within the L2ToL1MessagePasser. function proveWithdrawalTransaction( Types.WithdrawalTransaction memory _tx, uint256 _disputeGameIndex, @@ -298,47 +321,45 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ISemver { external whenNotPaused { - // Prevent users from creating a deposit transaction where this address is the message - // sender on L2. Because this is checked here, we do not need to check again in - // `finalizeWithdrawalTransaction`. - if (_tx.target == address(this)) revert BadTarget(); + // Make sure that the target address is safe. + if (_isUnsafeTarget(_tx.target)) { + revert OptimismPortal_BadTarget(); + } // Fetch the dispute game proxy from the `DisputeGameFactory` contract. - (GameType gameType,, IDisputeGame gameProxy) = disputeGameFactory.gameAtIndex(_disputeGameIndex); - Claim outputRoot = gameProxy.rootClaim(); - - // The game type of the dispute game must be the respected game type. - if (gameType.raw() != respectedGameType.raw()) revert InvalidGameType(); - - // The game type of the DisputeGame must have been the respected game type at creation. - // eip150-safe - try gameProxy.wasRespectedGameTypeWhenCreated() returns (bool wasRespected_) { - if (!wasRespected_) revert InvalidGameType(); - } catch { - revert LegacyGame(); + (,, IDisputeGame gameProxy) = disputeGameFactory().gameAtIndex(_disputeGameIndex); + + // Game must be a Proper Game. + if (!anchorStateRegistry.isGameProper(gameProxy)) { + revert OptimismPortal_ImproperDisputeGame(); } - // Game must have been created after the respected game type was updated. This check is a - // strict inequality because we want to prevent users from being able to prove or finalize - // withdrawals against games that were created in the same block that the retirement - // timestamp was set. If the retirement timestamp and game type are changed in the same - // block, such games could still be considered valid even if they used the old game type - // that we intended to invalidate. - require( - gameProxy.createdAt().raw() > respectedGameTypeUpdatedAt, - "OptimismPortal: dispute game created before respected game type was updated" - ); + // Game must have been respected game type when created. + if (!anchorStateRegistry.isGameRespected(gameProxy)) { + revert OptimismPortal_InvalidDisputeGame(); + } + + // Game must not have resolved in favor of the Challenger (invalid root claim). + if (gameProxy.status() == GameStatus.CHALLENGER_WINS) { + revert OptimismPortal_InvalidDisputeGame(); + } // Verify that the output root can be generated with the elements in the proof. - if (outputRoot.raw() != Hashing.hashOutputRootProof(_outputRootProof)) revert InvalidProof(); + if (gameProxy.rootClaim().raw() != Hashing.hashOutputRootProof(_outputRootProof)) { + revert OptimismPortal_InvalidOutputRootProof(); + } + + // As a sanity check, we make sure that the current timestamp is not less than or equal to + // the dispute game's creation timestamp. Not strictly necessary but extra layer of + // safety against weird bugs. Note that this blocks withdrawals from being proven in the + // same block that a dispute game is created. + if (block.timestamp <= gameProxy.createdAt().raw()) { + revert OptimismPortal_InvalidProofTimestamp(); + } // Load the ProvenWithdrawal into memory, using the withdrawal hash as a unique identifier. bytes32 withdrawalHash = Hashing.hashWithdrawal(_tx); - // We do not allow for proving withdrawals against dispute games that have resolved against the favor - // of the root claim. - if (gameProxy.status() == GameStatus.CHALLENGER_WINS) revert InvalidDisputeGame(); - // Compute the storage slot of the withdrawal hash in the L2ToL1MessagePasser contract. // Refer to the Solidity documentation for more information on how storage layouts are // computed for mappings. @@ -360,21 +381,22 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ISemver { _proof: _withdrawalProof, _root: _outputRootProof.messagePasserStorageRoot }) == false - ) revert InvalidMerkleProof(); + ) { + revert OptimismPortal_InvalidMerkleProof(); + } - // Designate the withdrawalHash as proven by storing the `disputeGameProxy` & `timestamp` in the - // `provenWithdrawals` mapping. A `withdrawalHash` can only be proven once unless the dispute game it proved - // against resolves against the favor of the root claim. + // Designate the withdrawalHash as proven by storing the disputeGameProxy and timestamp in + // the provenWithdrawals mapping. A given user may re-prove a withdrawalHash multiple + // times, but each proof will reset the proof timer. provenWithdrawals[withdrawalHash][msg.sender] = ProvenWithdrawal({ disputeGameProxy: gameProxy, timestamp: uint64(block.timestamp) }); - // Emit a `WithdrawalProven` event. - emit WithdrawalProven(withdrawalHash, _tx.sender, _tx.target); - // Emit a `WithdrawalProvenExtension1` event. - emit WithdrawalProvenExtension1(withdrawalHash, msg.sender); - // Add the proof submitter to the list of proof submitters for this withdrawal hash. proofSubmitters[withdrawalHash].push(msg.sender); + + // Emit a WithdrawalProven events. + emit WithdrawalProven(withdrawalHash, _tx.sender, _tx.target); + emit WithdrawalProvenExtension1(withdrawalHash, msg.sender); } /// @notice Finalizes a withdrawal transaction. @@ -396,9 +418,16 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ISemver { // Make sure that the l2Sender has not yet been set. The l2Sender is set to a value other // than the default value when a withdrawal transaction is being finalized. This check is // a defacto reentrancy guard. - if (l2Sender != Constants.DEFAULT_L2_SENDER) revert NonReentrant(); + if (l2Sender != Constants.DEFAULT_L2_SENDER) { + revert OptimismPortal_NoReentrancy(); + } + + // Make sure that the target address is safe. + if (_isUnsafeTarget(_tx.target)) { + revert OptimismPortal_BadTarget(); + } - // Compute the withdrawal hash. + // Grab the withdrawal. bytes32 withdrawalHash = Hashing.hashWithdrawal(_tx); // Check that the withdrawal can be finalized. @@ -430,7 +459,46 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ISemver { // sub call to the target contract if the minimum gas limit specified by the user would not // be sufficient to execute the sub call. if (!success && tx.origin == Constants.ESTIMATION_ADDRESS) { - revert GasEstimation(); + revert OptimismPortal_GasEstimation(); + } + } + + /// @notice Checks that a withdrawal has been proven and is ready to be finalized. + /// @param _withdrawalHash Hash of the withdrawal. + /// @param _proofSubmitter Address of the proof submitter. + function checkWithdrawal(bytes32 _withdrawalHash, address _proofSubmitter) public view { + // Grab the withdrawal and dispute game proxy. + ProvenWithdrawal memory provenWithdrawal = provenWithdrawals[_withdrawalHash][_proofSubmitter]; + IDisputeGame disputeGameProxy = provenWithdrawal.disputeGameProxy; + + // Check that this withdrawal has not already been finalized, this is replay protection. + if (finalizedWithdrawals[_withdrawalHash]) { + revert OptimismPortal_AlreadyFinalized(); + } + + // A withdrawal can only be finalized if it has been proven. We know that a withdrawal has + // been proven at least once when its timestamp is non-zero. Unproven withdrawals will have + // a timestamp of zero. + if (provenWithdrawal.timestamp == 0) { + revert OptimismPortal_Unproven(); + } + + // As a sanity check, we make sure that the proven withdrawal's timestamp is greater than + // starting timestamp inside the Dispute Game. Not strictly necessary but extra layer of + // safety against weird bugs in the proving step. Note that this blocks withdrawals that + // are proven in the same block that a dispute game is created. + if (provenWithdrawal.timestamp <= disputeGameProxy.createdAt().raw()) { + revert OptimismPortal_InvalidProofTimestamp(); + } + + // A proven withdrawal must wait at least `PROOF_MATURITY_DELAY_SECONDS` before finalizing. + if (block.timestamp - provenWithdrawal.timestamp <= PROOF_MATURITY_DELAY_SECONDS) { + revert OptimismPortal_ProofNotOldEnough(); + } + + // Check that the root claim is valid. + if (!anchorStateRegistry.isGameClaimValid(disputeGameProxy)) { + revert OptimismPortal_InvalidRootClaim(); } } @@ -456,17 +524,23 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ISemver { { // Just to be safe, make sure that people specify address(0) as the target when doing // contract creations. - if (_isCreation && _to != address(0)) revert BadTarget(); + if (_isCreation && _to != address(0)) { + revert OptimismPortal_BadTarget(); + } // Prevent depositing transactions that have too small of a gas limit. Users should pay // more for more resource usage. - if (_gasLimit < minimumGasLimit(uint64(_data.length))) revert SmallGasLimit(); + if (_gasLimit < minimumGasLimit(uint64(_data.length))) { + revert OptimismPortal_GasLimitTooLow(); + } // Prevent the creation of deposit transactions that have too much calldata. This gives an // upper limit on the size of unsafe blocks over the p2p network. 120kb is chosen to ensure // that the transaction can fit into the p2p network policy of 128kb even though deposit // transactions are not gossipped over the p2p network. - if (_data.length > 120_000) revert LargeCalldata(); + if (_data.length > 120_000) { + revert OptimismPortal_CalldataTooLarge(); + } // Transform the from-address to its alias if the caller is a contract. address from = msg.sender; @@ -484,108 +558,27 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ISemver { emit TransactionDeposited(from, _to, DEPOSIT_VERSION, opaqueData); } - /// @notice Blacklists a dispute game. Should only be used in the event that a dispute game resolves incorrectly. - /// @param _disputeGame Dispute game to blacklist. - function blacklistDisputeGame(IDisputeGame _disputeGame) external { - if (msg.sender != guardian()) revert Unauthorized(); - disputeGameBlacklist[_disputeGame] = true; - emit DisputeGameBlacklisted(_disputeGame); - } - - /// @notice Sets the respected game type. Changing this value can alter the security properties of the system, - /// depending on the new game's behavior. - /// @param _gameType The game type to consult for output proposals. - function setRespectedGameType(GameType _gameType) external { - if (msg.sender != guardian()) revert Unauthorized(); - // respectedGameTypeUpdatedAt is now no longer set by default. We want to avoid modifying - // this function's signature as that would result in changes to the DeputyGuardianModule. - // We use type(uint32).max as a temporary solution to allow us to update the - // respectedGameTypeUpdatedAt timestamp without modifying this function's signature. - if (_gameType.raw() == type(uint32).max) { - respectedGameTypeUpdatedAt = uint64(block.timestamp); - } else { - respectedGameType = _gameType; - } - emit RespectedGameTypeSet(respectedGameType, Timestamp.wrap(respectedGameTypeUpdatedAt)); - } - - /// @notice Checks if a withdrawal can be finalized. This function will revert if the withdrawal cannot be - /// finalized, and otherwise has no side-effects. - /// @param _withdrawalHash Hash of the withdrawal to check. - /// @param _proofSubmitter The submitter of the proof for the withdrawal hash - function checkWithdrawal(bytes32 _withdrawalHash, address _proofSubmitter) public view { - ProvenWithdrawal memory provenWithdrawal = provenWithdrawals[_withdrawalHash][_proofSubmitter]; - IDisputeGame disputeGameProxy = provenWithdrawal.disputeGameProxy; - - // The dispute game must not be blacklisted. - if (disputeGameBlacklist[disputeGameProxy]) revert Blacklisted(); - - // A withdrawal can only be finalized if it has been proven. We know that a withdrawal has - // been proven at least once when its timestamp is non-zero. Unproven withdrawals will have - // a timestamp of zero. - if (provenWithdrawal.timestamp == 0) revert Unproven(); - - // Grab the createdAt timestamp once. - uint64 createdAt = disputeGameProxy.createdAt().raw(); - - // As a sanity check, we make sure that the proven withdrawal's timestamp is greater than - // starting timestamp inside the Dispute Game. Not strictly necessary but extra layer of - // safety against weird bugs in the proving step. - require( - provenWithdrawal.timestamp > createdAt, - "OptimismPortal: withdrawal timestamp less than dispute game creation timestamp" - ); - - // A proven withdrawal must wait at least `PROOF_MATURITY_DELAY_SECONDS` before finalizing. - require( - block.timestamp - provenWithdrawal.timestamp > PROOF_MATURITY_DELAY_SECONDS, - "OptimismPortal: proven withdrawal has not matured yet" - ); - - // A proven withdrawal must wait until the dispute game it was proven against has been - // resolved in favor of the root claim (the output proposal). This is to prevent users - // from finalizing withdrawals proven against non-finalized output roots. - if (disputeGameProxy.status() != GameStatus.DEFENDER_WINS) revert ProposalNotValidated(); - - // The game type of the dispute game must have been the respected game type at creation - // time. We check that the game type is the respected game type at proving time, but it's - // possible that the respected game type has since changed. Users can still use this game - // to finalize a withdrawal as long as it has not been otherwise invalidated. - // The game type of the DisputeGame must have been the respected game type at creation. - // eip150-safe - try disputeGameProxy.wasRespectedGameTypeWhenCreated() returns (bool wasRespected_) { - if (!wasRespected_) revert InvalidGameType(); - } catch { - revert LegacyGame(); - } - - // Game must have been created after the respected game type was updated. This check is a - // strict inequality because we want to prevent users from being able to prove or finalize - // withdrawals against games that were created in the same block that the retirement - // timestamp was set. If the retirement timestamp and game type are changed in the same - // block, such games could still be considered valid even if they used the old game type - // that we intended to invalidate. - require( - createdAt > respectedGameTypeUpdatedAt, - "OptimismPortal: dispute game created before respected game type was updated" - ); - - // Before a withdrawal can be finalized, the dispute game it was proven against must have been - // resolved for at least `DISPUTE_GAME_FINALITY_DELAY_SECONDS`. This is to allow for manual - // intervention in the event that a dispute game is resolved incorrectly. - require( - block.timestamp - disputeGameProxy.resolvedAt().raw() > DISPUTE_GAME_FINALITY_DELAY_SECONDS, - "OptimismPortal: output proposal in air-gap" - ); - - // Check that this withdrawal has not already been finalized, this is replay protection. - if (finalizedWithdrawals[_withdrawalHash]) revert AlreadyFinalized(); - } - /// @notice External getter for the number of proof submitters for a withdrawal hash. /// @param _withdrawalHash Hash of the withdrawal. /// @return The number of proof submitters for the withdrawal hash. function numProofSubmitters(bytes32 _withdrawalHash) external view returns (uint256) { return proofSubmitters[_withdrawalHash].length; } + + /// @notice Checks if a target address is unsafe. + function _isUnsafeTarget(address _target) internal view virtual returns (bool) { + // Prevent users from creating a deposit transaction where this address is the message + // sender on L2. + return _target == address(this); + } + + /// @notice Getter for the resource config. Used internally by the ResourceMetering contract. + /// The SystemConfig is the source of truth for the resource config. + /// @return config_ ResourceMetering ResourceConfig + function _resourceConfig() internal view override returns (ResourceMetering.ResourceConfig memory config_) { + IResourceMetering.ResourceConfig memory config = systemConfig.resourceConfig(); + assembly ("memory-safe") { + config_ := config + } + } } diff --git a/packages/contracts-bedrock/src/L1/OptimismPortalInterop.sol b/packages/contracts-bedrock/src/L1/OptimismPortalInterop.sol index 5993b7f27dc5b..532f67ac0be5a 100644 --- a/packages/contracts-bedrock/src/L1/OptimismPortalInterop.sol +++ b/packages/contracts-bedrock/src/L1/OptimismPortalInterop.sol @@ -7,7 +7,6 @@ import { OptimismPortal2 } from "src/L1/OptimismPortal2.sol"; // Libraries import { Predeploys } from "src/libraries/Predeploys.sol"; import { Constants } from "src/libraries/Constants.sol"; -import { Unauthorized } from "src/libraries/PortalErrors.sol"; // Interfaces import { IL1BlockInterop, ConfigType } from "interfaces/L2/IL1BlockInterop.sol"; @@ -18,23 +17,22 @@ 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 { - constructor( - uint256 _proofMaturityDelaySeconds, - uint256 _disputeGameFinalityDelaySeconds - ) - OptimismPortal2(_proofMaturityDelaySeconds, _disputeGameFinalityDelaySeconds) - { } - - /// @custom:semver +interop.2 + /// @notice Thrown when the caller is not the system config. + error OptimismPortal_Unauthorized(); + + /// @param _proofMaturityDelaySeconds The proof maturity delay in seconds. + constructor(uint256 _proofMaturityDelaySeconds) OptimismPortal2(_proofMaturityDelaySeconds) { } + + /// @custom:semver +interop.3 function version() public pure override returns (string memory) { - return string.concat(super.version(), "+interop.2"); + return string.concat(super.version(), "+interop.3"); } /// @notice Sets static configuration options for the L2 system. /// @param _type Type of configuration to set. /// @param _value Encoded value of the configuration. function setConfig(ConfigType _type, bytes memory _value) external { - if (msg.sender != address(systemConfig)) revert Unauthorized(); + if (msg.sender != address(systemConfig)) revert OptimismPortal_Unauthorized(); // Set L2 deposit gas as used without paying burning gas. Ensures that deposits cannot use too much L2 gas. // This value must be large enough to cover the cost of calling `L1Block.setConfig`. diff --git a/packages/contracts-bedrock/src/dispute/AnchorStateRegistry.sol b/packages/contracts-bedrock/src/dispute/AnchorStateRegistry.sol index 60cd9ba31e12a..35b9d2736fbc7 100644 --- a/packages/contracts-bedrock/src/dispute/AnchorStateRegistry.sol +++ b/packages/contracts-bedrock/src/dispute/AnchorStateRegistry.sol @@ -13,7 +13,6 @@ import { IFaultDisputeGame } from "interfaces/dispute/IFaultDisputeGame.sol"; import { IDisputeGame } from "interfaces/dispute/IDisputeGame.sol"; import { IDisputeGameFactory } from "interfaces/dispute/IDisputeGameFactory.sol"; import { ISuperchainConfig } from "interfaces/L1/ISuperchainConfig.sol"; -import { IOptimismPortal2 } from "interfaces/L1/IOptimismPortal2.sol"; /// @custom:proxied true /// @title AnchorStateRegistry @@ -23,8 +22,11 @@ import { IOptimismPortal2 } from "interfaces/L1/IOptimismPortal2.sol"; /// be initialized with a more recent starting state which reduces the amount of required offchain computation. contract AnchorStateRegistry is Initializable, ISemver { /// @notice Semantic version. - /// @custom:semver 2.2.2 - string public constant version = "2.2.2"; + /// @custom:semver 3.0.0 + string public constant version = "3.0.0"; + + /// @notice The dispute game finality delay in seconds. + uint256 internal immutable DISPUTE_GAME_FINALITY_DELAY_SECONDS; /// @notice Address of the SuperchainConfig contract. ISuperchainConfig public superchainConfig; @@ -32,61 +34,105 @@ contract AnchorStateRegistry is Initializable, ISemver { /// @notice Address of the DisputeGameFactory contract. IDisputeGameFactory public disputeGameFactory; - /// @notice Address of the OptimismPortal contract. - IOptimismPortal2 public portal; - /// @notice The game whose claim is currently being used as the anchor state. IFaultDisputeGame public anchorGame; /// @notice The starting anchor root. OutputRoot internal startingAnchorRoot; - /// @notice Emitted when an anchor state is not updated. - /// @param game Game that was not used as the new anchor game. - event AnchorNotUpdated(IFaultDisputeGame indexed game); + /// @notice Mapping of blacklisted dispute games. + mapping(IDisputeGame => bool) public disputeGameBlacklist; + + /// @notice The respected game type. + GameType public respectedGameType; + + /// @notice The retirement timestamp. All games created before or at this timestamp are + /// considered retired and are therefore not valid games. Retirement is used as a + /// blanket invalidation mechanism if games resolve incorrectly. + uint64 public retirementTimestamp; /// @notice Emitted when an anchor state is updated. /// @param game Game that was used as the new anchor game. event AnchorUpdated(IFaultDisputeGame indexed game); - /// @notice Thrown when an unauthorized caller attempts to set the anchor state. - error AnchorStateRegistry_Unauthorized(); + /// @notice Emitted when the respected game type is set. + /// @param gameType The new respected game type. + event RespectedGameTypeSet(GameType gameType); - /// @notice Thrown when an invalid anchor game is provided. - error AnchorStateRegistry_InvalidAnchorGame(); + /// @notice Emitted when the retirement timestamp is set. + /// @param timestamp The new retirement timestamp. + event RetirementTimestampSet(uint256 timestamp); + + /// @notice Emitted when a dispute game is blacklisted. + /// @param disputeGame The dispute game that was blacklisted. + event DisputeGameBlacklisted(IDisputeGame indexed disputeGame); /// @notice Thrown when the anchor root is requested, but the anchor game is blacklisted. error AnchorStateRegistry_AnchorGameBlacklisted(); - /// @notice Constructor to disable initializers. - constructor() { + /// @notice Thrown when an invalid anchor game is provided. + error AnchorStateRegistry_InvalidAnchorGame(); + + /// @notice Thrown when an unauthorized caller attempts to set the anchor state. + error AnchorStateRegistry_Unauthorized(); + + /// @param _disputeGameFinalityDelaySeconds The dispute game finality delay in seconds. + constructor(uint256 _disputeGameFinalityDelaySeconds) { + DISPUTE_GAME_FINALITY_DELAY_SECONDS = _disputeGameFinalityDelaySeconds; _disableInitializers(); } /// @notice Initializes the contract. /// @param _superchainConfig The address of the SuperchainConfig contract. /// @param _disputeGameFactory The address of the DisputeGameFactory contract. - /// @param _portal The address of the OptimismPortal contract. /// @param _startingAnchorRoot The starting anchor root. function initialize( ISuperchainConfig _superchainConfig, IDisputeGameFactory _disputeGameFactory, - IOptimismPortal2 _portal, - OutputRoot memory _startingAnchorRoot + OutputRoot memory _startingAnchorRoot, + GameType _startingRespectedGameType ) external initializer { superchainConfig = _superchainConfig; disputeGameFactory = _disputeGameFactory; - portal = _portal; startingAnchorRoot = _startingAnchorRoot; + respectedGameType = _startingRespectedGameType; + retirementTimestamp = uint64(block.timestamp); + } + + /// @notice Returns whether the contract is paused. + function paused() public view returns (bool) { + return superchainConfig.paused(); } - /// @notice Returns the respected game type. - /// @return The respected game type. - function respectedGameType() public view returns (GameType) { - return portal.respectedGameType(); + /// @notice Returns the dispute game finality delay in seconds. + function disputeGameFinalityDelaySeconds() external view returns (uint256) { + return DISPUTE_GAME_FINALITY_DELAY_SECONDS; + } + + /// @notice Allows the Guardian to set the respected game type. + /// @param _gameType The new respected game type. + function setRespectedGameType(GameType _gameType) external { + if (msg.sender != superchainConfig.guardian()) revert AnchorStateRegistry_Unauthorized(); + respectedGameType = _gameType; + emit RespectedGameTypeSet(_gameType); + } + + /// @notice Allows the Guardian to update the retirement timestamp. + function updateRetirementTimestamp() external { + if (msg.sender != superchainConfig.guardian()) revert AnchorStateRegistry_Unauthorized(); + retirementTimestamp = uint64(block.timestamp); + emit RetirementTimestampSet(block.timestamp); + } + + /// @notice Allows the Guardian to blacklist a dispute game. + /// @param _disputeGame Dispute game to blacklist. + function blacklistDisputeGame(IDisputeGame _disputeGame) external { + if (msg.sender != superchainConfig.guardian()) revert AnchorStateRegistry_Unauthorized(); + disputeGameBlacklist[_disputeGame] = true; + emit DisputeGameBlacklisted(_disputeGame); } /// @custom:legacy @@ -129,6 +175,9 @@ contract AnchorStateRegistry is Initializable, ISemver { /// @param _game The game to check. /// @return Whether the game is of a respected game type. function isGameRespected(IDisputeGame _game) public view returns (bool) { + // We don't do a try/catch here for legacy games because by the time this code is live on + // mainnet, users won't be using legacy games anymore. Avoiding the try/catch simplifies + // the logic. return _game.wasRespectedGameTypeWhenCreated(); } @@ -136,7 +185,7 @@ contract AnchorStateRegistry is Initializable, ISemver { /// @param _game The game to check. /// @return Whether the game is blacklisted. function isGameBlacklisted(IDisputeGame _game) public view returns (bool) { - return portal.disputeGameBlacklist(_game); + return disputeGameBlacklist[_game]; } /// @notice Determines whether a game is retired. @@ -146,7 +195,7 @@ contract AnchorStateRegistry is Initializable, ISemver { // Must be created after the respectedGameTypeUpdatedAt timestamp. Note that this means all // games created in the same block as the respectedGameTypeUpdatedAt timestamp are // considered retired. - return _game.createdAt().raw() <= portal.respectedGameTypeUpdatedAt(); + return _game.createdAt().raw() <= retirementTimestamp; } /// @notice Returns whether a game is resolved. @@ -186,6 +235,11 @@ contract AnchorStateRegistry is Initializable, ISemver { return false; } + // Must not be paused, temporarily causes game to be considered improper. + if (paused()) { + return false; + } + return true; } @@ -200,7 +254,7 @@ contract AnchorStateRegistry is Initializable, ISemver { // Game must be beyond the "airgap period" - time since resolution must be at least // "dispute game finality delay" seconds in the past. - if (block.timestamp - _game.resolvedAt().raw() <= portal.disputeGameFinalityDelaySeconds()) { + if (block.timestamp - _game.resolvedAt().raw() <= DISPUTE_GAME_FINALITY_DELAY_SECONDS) { return false; } diff --git a/packages/contracts-bedrock/src/libraries/PortalErrors.sol b/packages/contracts-bedrock/src/libraries/PortalErrors.sol deleted file mode 100644 index 9096a2938fdc1..0000000000000 --- a/packages/contracts-bedrock/src/libraries/PortalErrors.sol +++ /dev/null @@ -1,40 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; - -/// @notice Error for when a deposit or withdrawal is to a bad target. -error BadTarget(); -/// @notice Error for when a deposit has too much calldata. -error LargeCalldata(); -/// @notice Error for when a deposit has too small of a gas limit. -error SmallGasLimit(); -/// @notice Error for when a withdrawal transfer fails. -error TransferFailed(); -/// @notice Error for when a method cannot be called with non zero CALLVALUE. -error NoValue(); -/// @notice Error for an unauthorized CALLER. -error Unauthorized(); -/// @notice Error for when a method cannot be called when paused. This could be renamed -/// to `Paused` in the future, but it collides with the `Paused` event. -error CallPaused(); -/// @notice Error for special gas estimation. -error GasEstimation(); -/// @notice Error for when a method is being reentered. -error NonReentrant(); -/// @notice Error for invalid proof. -error InvalidProof(); -/// @notice Error for invalid game type. -error InvalidGameType(); -/// @notice Error for an invalid dispute game. -error InvalidDisputeGame(); -/// @notice Error for an invalid merkle proof. -error InvalidMerkleProof(); -/// @notice Error for when a dispute game has been blacklisted. -error Blacklisted(); -/// @notice Error for when trying to withdrawal without first proven. -error Unproven(); -/// @notice Error for when a proposal is not validated. -error ProposalNotValidated(); -/// @notice Error for when a withdrawal has already been finalized. -error AlreadyFinalized(); -/// @notice Error for when a game is a legacy game. -error LegacyGame(); diff --git a/packages/contracts-bedrock/src/safe/DeputyGuardianModule.sol b/packages/contracts-bedrock/src/safe/DeputyGuardianModule.sol index a742c452ef0ba..2a0be015c0f8b 100644 --- a/packages/contracts-bedrock/src/safe/DeputyGuardianModule.sol +++ b/packages/contracts-bedrock/src/safe/DeputyGuardianModule.sol @@ -6,14 +6,11 @@ import { GnosisSafe as Safe } from "safe-contracts/GnosisSafe.sol"; import { Enum } from "safe-contracts/common/Enum.sol"; // Libraries -import { Unauthorized } from "src/libraries/PortalErrors.sol"; import { GameType, Timestamp } from "src/dispute/lib/Types.sol"; // Interfaces import { IAnchorStateRegistry } from "interfaces/dispute/IAnchorStateRegistry.sol"; -import { IFaultDisputeGame } from "interfaces/dispute/IFaultDisputeGame.sol"; import { ISuperchainConfig } from "interfaces/L1/ISuperchainConfig.sol"; -import { IOptimismPortal2 } from "interfaces/L1/IOptimismPortal2.sol"; import { IDisputeGame } from "interfaces/dispute/IDisputeGame.sol"; import { ISemver } from "interfaces/universal/ISemver.sol"; @@ -24,7 +21,10 @@ import { ISemver } from "interfaces/universal/ISemver.sol"; /// authorization at any time by disabling this module. contract DeputyGuardianModule is ISemver { /// @notice Error message for failed transaction execution - error ExecutionFailed(string); + error DeputyGuardianModule_ExecutionFailed(string); + + /// @notice Thrown when the caller is not the deputy guardian. + error DeputyGuardianModule_Unauthorized(); /// @notice Emitted when the SuperchainConfig is paused event Paused(string identifier); @@ -38,6 +38,9 @@ contract DeputyGuardianModule is ISemver { /// @notice Emitted when the respected game type is set event RespectedGameTypeSet(GameType indexed gameType, Timestamp indexed updatedAt); + /// @notice Emitted when the retirement timestamp is updated + event RetirementTimestampUpdated(Timestamp indexed updatedAt); + /// @notice The Safe contract instance Safe internal immutable SAFE; @@ -48,8 +51,8 @@ contract DeputyGuardianModule is ISemver { address internal immutable DEPUTY_GUARDIAN; /// @notice Semantic version. - /// @custom:semver 2.0.1-beta.5 - string public constant version = "2.0.1-beta.5"; + /// @custom:semver 3.0.0 + string public constant version = "3.0.0"; // Constructor to initialize the Safe and baseModule instances constructor(Safe _safe, ISuperchainConfig _superchainConfig, address _deputyGuardian) { @@ -79,7 +82,7 @@ contract DeputyGuardianModule is ISemver { /// @notice Internal function to ensure that only the deputy guardian can call certain functions. function _onlyDeputyGuardian() internal view { if (msg.sender != DEPUTY_GUARDIAN) { - revert Unauthorized(); + revert DeputyGuardianModule_Unauthorized(); } } @@ -93,7 +96,7 @@ contract DeputyGuardianModule is ISemver { (bool success, bytes memory returnData) = SAFE.execTransactionFromModuleReturnData(address(SUPERCHAIN_CONFIG), 0, data, Enum.Operation.Call); if (!success) { - revert ExecutionFailed(string(returnData)); + revert DeputyGuardianModule_ExecutionFailed(string(returnData)); } emit Paused("Deputy Guardian"); } @@ -108,58 +111,58 @@ contract DeputyGuardianModule is ISemver { (bool success, bytes memory returnData) = SAFE.execTransactionFromModuleReturnData(address(SUPERCHAIN_CONFIG), 0, data, Enum.Operation.Call); if (!success) { - revert ExecutionFailed(string(returnData)); + revert DeputyGuardianModule_ExecutionFailed(string(returnData)); } emit Unpaused(); } /// @notice Calls the Security Council Safe's `execTransactionFromModuleReturnData()`, with the arguments - /// necessary to call `setAnchorState()` on the `AnchorStateRegistry` contract. + /// necessary to call `blacklistDisputeGame()` on the `AnchorStateRegistry` contract. /// Only the deputy guardian can call this function. - /// @param _registry The `IAnchorStateRegistry` contract instance. - /// @param _game The `IFaultDisputeGame` contract instance. - function setAnchorState(IAnchorStateRegistry _registry, IFaultDisputeGame _game) external { + /// @param _anchorStateRegistry The `AnchorStateRegistry` contract instance. + /// @param _game The `IDisputeGame` contract instance. + function blacklistDisputeGame(IAnchorStateRegistry _anchorStateRegistry, IDisputeGame _game) external { _onlyDeputyGuardian(); - bytes memory data = abi.encodeCall(IAnchorStateRegistry.setAnchorState, (_game)); + bytes memory data = abi.encodeCall(IAnchorStateRegistry.blacklistDisputeGame, (_game)); (bool success, bytes memory returnData) = - SAFE.execTransactionFromModuleReturnData(address(_registry), 0, data, Enum.Operation.Call); + SAFE.execTransactionFromModuleReturnData(address(_anchorStateRegistry), 0, data, Enum.Operation.Call); if (!success) { - revert ExecutionFailed(string(returnData)); + revert DeputyGuardianModule_ExecutionFailed(string(returnData)); } + emit DisputeGameBlacklisted(_game); } /// @notice Calls the Security Council Safe's `execTransactionFromModuleReturnData()`, with the arguments - /// necessary to call `blacklistDisputeGame()` on the `OptimismPortal2` contract. + /// necessary to call `setRespectedGameType()` on the `AnchorStateRegistry` contract. /// Only the deputy guardian can call this function. - /// @param _portal The `OptimismPortal2` contract instance. - /// @param _game The `IDisputeGame` contract instance. - function blacklistDisputeGame(IOptimismPortal2 _portal, IDisputeGame _game) external { + /// @param _anchorStateRegistry The `AnchorStateRegistry` contract instance. + /// @param _gameType The `GameType` to set as the respected game type. + function setRespectedGameType(IAnchorStateRegistry _anchorStateRegistry, GameType _gameType) external { _onlyDeputyGuardian(); - bytes memory data = abi.encodeCall(IOptimismPortal2.blacklistDisputeGame, (_game)); + bytes memory data = abi.encodeCall(IAnchorStateRegistry.setRespectedGameType, (_gameType)); (bool success, bytes memory returnData) = - SAFE.execTransactionFromModuleReturnData(address(_portal), 0, data, Enum.Operation.Call); + SAFE.execTransactionFromModuleReturnData(address(_anchorStateRegistry), 0, data, Enum.Operation.Call); if (!success) { - revert ExecutionFailed(string(returnData)); + revert DeputyGuardianModule_ExecutionFailed(string(returnData)); } - emit DisputeGameBlacklisted(_game); + emit RespectedGameTypeSet(_gameType, Timestamp.wrap(uint64(block.timestamp))); } /// @notice Calls the Security Council Safe's `execTransactionFromModuleReturnData()`, with the arguments - /// necessary to call `setRespectedGameType()` on the `OptimismPortal2` contract. + /// necessary to call `updateRetirementTimestamp()` on the `AnchorStateRegistry` contract. /// Only the deputy guardian can call this function. - /// @param _portal The `OptimismPortal2` contract instance. - /// @param _gameType The `GameType` to set as the respected game type. - function setRespectedGameType(IOptimismPortal2 _portal, GameType _gameType) external { + /// @param _anchorStateRegistry The `AnchorStateRegistry` contract instance. + function updateRetirementTimestamp(IAnchorStateRegistry _anchorStateRegistry) external { _onlyDeputyGuardian(); - bytes memory data = abi.encodeCall(IOptimismPortal2.setRespectedGameType, (_gameType)); + bytes memory data = abi.encodeCall(IAnchorStateRegistry.updateRetirementTimestamp, ()); (bool success, bytes memory returnData) = - SAFE.execTransactionFromModuleReturnData(address(_portal), 0, data, Enum.Operation.Call); + SAFE.execTransactionFromModuleReturnData(address(_anchorStateRegistry), 0, data, Enum.Operation.Call); if (!success) { - revert ExecutionFailed(string(returnData)); + revert DeputyGuardianModule_ExecutionFailed(string(returnData)); } - emit RespectedGameTypeSet(_gameType, Timestamp.wrap(uint64(block.timestamp))); + emit RetirementTimestampUpdated(Timestamp.wrap(uint64(block.timestamp))); } } diff --git a/packages/contracts-bedrock/test/L1/OPContractsManager.t.sol b/packages/contracts-bedrock/test/L1/OPContractsManager.t.sol index 0cf5ba4df889b..b597cd52497e8 100644 --- a/packages/contracts-bedrock/test/L1/OPContractsManager.t.sol +++ b/packages/contracts-bedrock/test/L1/OPContractsManager.t.sol @@ -587,7 +587,7 @@ contract OPContractsManager_AddGameType_Test is Test { }), optimismPortalImpl: DeployUtils.create1({ _name: "OptimismPortal2", - _args: DeployUtils.encodeConstructor(abi.encodeCall(IOptimismPortal2.__constructor__, (1, 1))) + _args: DeployUtils.encodeConstructor(abi.encodeCall(IOptimismPortal2.__constructor__, (1))) }), systemConfigImpl: DeployUtils.create1({ _name: "SystemConfig", @@ -611,7 +611,7 @@ contract OPContractsManager_AddGameType_Test is Test { }), anchorStateRegistryImpl: DeployUtils.create1({ _name: "AnchorStateRegistry", - _args: DeployUtils.encodeConstructor(abi.encodeCall(IAnchorStateRegistry.__constructor__, ())) + _args: DeployUtils.encodeConstructor(abi.encodeCall(IAnchorStateRegistry.__constructor__, (1))) }), delayedWETHImpl: DeployUtils.create1({ _name: "DelayedWETH", diff --git a/packages/contracts-bedrock/test/L1/OPPrestateUpdater.t.sol b/packages/contracts-bedrock/test/L1/OPPrestateUpdater.t.sol index d700efa24e42b..56edac1f8fa2b 100644 --- a/packages/contracts-bedrock/test/L1/OPPrestateUpdater.t.sol +++ b/packages/contracts-bedrock/test/L1/OPPrestateUpdater.t.sol @@ -86,7 +86,7 @@ contract OPPrestateUpdater_Test is Test { }), optimismPortalImpl: DeployUtils.create1({ _name: "OptimismPortal2", - _args: DeployUtils.encodeConstructor(abi.encodeCall(IOptimismPortal2.__constructor__, (1, 1))) + _args: DeployUtils.encodeConstructor(abi.encodeCall(IOptimismPortal2.__constructor__, (1))) }), systemConfigImpl: DeployUtils.create1({ _name: "SystemConfig", @@ -110,7 +110,7 @@ contract OPPrestateUpdater_Test is Test { }), anchorStateRegistryImpl: DeployUtils.create1({ _name: "AnchorStateRegistry", - _args: DeployUtils.encodeConstructor(abi.encodeCall(IAnchorStateRegistry.__constructor__, ())) + _args: DeployUtils.encodeConstructor(abi.encodeCall(IAnchorStateRegistry.__constructor__, (1))) }), delayedWETHImpl: DeployUtils.create1({ _name: "DelayedWETH", diff --git a/packages/contracts-bedrock/test/L1/OptimismPortal2.t.sol b/packages/contracts-bedrock/test/L1/OptimismPortal2.t.sol index 337adcdc8ebc6..a2b55af457bc9 100644 --- a/packages/contracts-bedrock/test/L1/OptimismPortal2.t.sol +++ b/packages/contracts-bedrock/test/L1/OptimismPortal2.t.sol @@ -17,7 +17,6 @@ import { Constants } from "src/libraries/Constants.sol"; import { AddressAliasHelper } from "src/vendor/AddressAliasHelper.sol"; import { EIP1967Helper } from "test/mocks/EIP1967Helper.sol"; import "src/dispute/lib/Types.sol"; -import "src/libraries/PortalErrors.sol"; // Interfaces import { IResourceMetering } from "interfaces/L1/IResourceMetering.sol"; @@ -45,20 +44,24 @@ contract OptimismPortal2_Test is CommonTest { /// test/kontrol/deployment/DeploymentSummary.t.sol function test_constructor_succeeds() external virtual { IOptimismPortal2 opImpl = IOptimismPortal2(payable(EIP1967Helper.getImplementation(address(optimismPortal2)))); - assertEq(address(opImpl.disputeGameFactory()), address(0)); + assertEq(address(opImpl.anchorStateRegistry()), address(0)); assertEq(address(opImpl.systemConfig()), address(0)); assertEq(address(opImpl.superchainConfig()), address(0)); - assertEq(opImpl.respectedGameType().raw(), deploy.cfg().respectedGameType()); // TODO(opcm upgrades): remove skip once upgrade path is implemented returnIfForkTest("OptimismPortal2_Test: l2Sender is nonzero on OP mainnet"); assertEq(opImpl.l2Sender(), address(0)); + + // TODO(opcm upgrades): remove skip once upgrade path is implemented + returnIfForkTest("OptimismPortal2_Test: anchorStateRegistry is nonzero on OP mainnet"); + assertEq(address(opImpl.anchorStateRegistry()), address(0)); } /// @dev Tests that the initializer sets the correct values. /// @notice Marked virtual to be overridden in /// test/kontrol/deployment/DeploymentSummary.t.sol function test_initialize_succeeds() external virtual { + assertEq(address(optimismPortal2.anchorStateRegistry()), address(anchorStateRegistry)); assertEq(address(optimismPortal2.disputeGameFactory()), address(disputeGameFactory)); assertEq(address(optimismPortal2.superchainConfig()), address(superchainConfig)); assertEq(optimismPortal2.l2Sender(), Constants.DEFAULT_L2_SENDER); @@ -165,17 +168,21 @@ contract OptimismPortal2_Test is CommonTest { /// @dev Tests that `depositTransaction` reverts when the destination address is non-zero /// for a contract creation deposit. function test_depositTransaction_contractCreation_reverts() external { + // TODO(opcm upgrades): remove skip once upgrade path is implemented + skipIfForkTest("OptimismPortal2_Test: error is different on OP Mainnet"); // contract creation must have a target of address(0) - vm.expectRevert(BadTarget.selector); + vm.expectRevert(IOptimismPortal2.OptimismPortal_BadTarget.selector); optimismPortal2.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 { + // TODO(opcm upgrades): remove skip once upgrade path is implemented + skipIfForkTest("OptimismPortal2_Test: error is different on OP Mainnet"); uint256 size = 120_001; uint64 gasLimit = optimismPortal2.minimumGasLimit(uint64(size)); - vm.expectRevert(LargeCalldata.selector); + vm.expectRevert(IOptimismPortal2.OptimismPortal_CalldataTooLarge.selector); optimismPortal2.depositTransaction({ _to: address(0), _value: 0, @@ -187,7 +194,9 @@ contract OptimismPortal2_Test is CommonTest { /// @dev Tests that `depositTransaction` reverts when the gas limit is too small. function test_depositTransaction_smallGasLimit_reverts() external { - vm.expectRevert(SmallGasLimit.selector); + // TODO(opcm upgrades): remove skip once upgrade path is implemented + skipIfForkTest("OptimismPortal2_Test: error is different on OP Mainnet"); + vm.expectRevert(IOptimismPortal2.OptimismPortal_GasLimitTooLow.selector); optimismPortal2.depositTransaction({ _to: address(1), _value: 0, _gasLimit: 0, _isCreation: false, _data: hex"" }); } @@ -197,7 +206,9 @@ contract OptimismPortal2_Test is CommonTest { uint64 gasLimit = optimismPortal2.minimumGasLimit(uint64(_data.length)); if (_shouldFail) { gasLimit = uint64(bound(gasLimit, 0, gasLimit - 1)); - vm.expectRevert(SmallGasLimit.selector); + // TODO(opcm upgrades): remove skip once upgrade path is implemented + skipIfForkTest("OptimismPortal2_Test: error is different on OP Mainnet"); + vm.expectRevert(IOptimismPortal2.OptimismPortal_GasLimitTooLow.selector); } optimismPortal2.depositTransaction({ @@ -408,7 +419,6 @@ contract OptimismPortal2_Test is CommonTest { contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { // Reusable default values for a test withdrawal Types.WithdrawalTransaction _defaultTx; - IFaultDisputeGame game; uint256 _proposedGameIndex; uint256 _proposedBlockNumber; @@ -431,6 +441,7 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { 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); @@ -458,12 +469,13 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { address(disputeGameFactory), keccak256(abi.encode(GameType.wrap(0), uint256(102))), bytes32(uint256(0)) ); } else { - // Warp forward in time to ensure that the game is created after the retirement timestamp. - vm.warp(optimismPortal2.respectedGameTypeUpdatedAt() + 1 seconds); - // Set up the dummy game. _proposedBlockNumber = 0xFF; } + + // Warp forward in time to ensure that the game is created after the retirement timestamp. + vm.warp(anchorStateRegistry.retirementTimestamp() + 1); + GameType respectedGameType = optimismPortal2.respectedGameType(); game = IFaultDisputeGame( payable( @@ -487,7 +499,7 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { /// @dev Asserts that the reentrant call will revert. function callPortalAndExpectRevert() external payable { - vm.expectRevert(NonReentrant.selector); + vm.expectRevert(IOptimismPortal2.OptimismPortal_NoReentrancy.selector); // Arguments here don't matter, as the require check is the first thing that happens. // We assume that this has already been proven. optimismPortal2.finalizeWithdrawalTransaction(_defaultTx); @@ -495,70 +507,12 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { assertFalse(optimismPortal2.finalizedWithdrawals(Hashing.hashWithdrawal(_defaultTx))); } - /// @dev Tests that `blacklistDisputeGame` reverts when called by a non-guardian. - function testFuzz_blacklist_onlyGuardian_reverts(address _act) external { - vm.assume(_act != address(optimismPortal2.guardian())); - - vm.expectRevert(Unauthorized.selector); - optimismPortal2.blacklistDisputeGame(IDisputeGame(address(0xdead))); - } - - /// @dev Tests that the guardian role can blacklist any dispute game. - function testFuzz_blacklist_guardian_succeeds(IDisputeGame _addr) external { - vm.expectEmit(address(optimismPortal2)); - emit DisputeGameBlacklisted(_addr); - - vm.prank(optimismPortal2.guardian()); - optimismPortal2.blacklistDisputeGame(_addr); - - assertTrue(optimismPortal2.disputeGameBlacklist(_addr)); - } - - /// @dev Tests that `setRespectedGameType` reverts when called by a non-guardian. - function testFuzz_setRespectedGameType_onlyGuardian_reverts(address _act, GameType _ty) external { - vm.assume(_act != address(optimismPortal2.guardian())); - - vm.prank(_act); - vm.expectRevert(Unauthorized.selector); - optimismPortal2.setRespectedGameType(_ty); - } - - /// @dev Tests that the guardian role can set the respected game type to anything they want. - function testFuzz_setRespectedGameType_guardianCanSetRespectedGameType_succeeds(GameType _ty) external { - vm.assume(_ty.raw() != type(uint32).max); - uint64 respectedGameTypeUpdatedAt = optimismPortal2.respectedGameTypeUpdatedAt(); - vm.expectEmit(address(optimismPortal2)); - emit RespectedGameTypeSet(_ty, Timestamp.wrap(respectedGameTypeUpdatedAt)); - vm.prank(optimismPortal2.guardian()); - optimismPortal2.setRespectedGameType(_ty); - // GameType changes, but the timestamp doesn't. - assertEq(optimismPortal2.respectedGameType().raw(), _ty.raw()); - assertEq(optimismPortal2.respectedGameTypeUpdatedAt(), respectedGameTypeUpdatedAt); - } - - /// @dev Tests that the guardian can set the `respectedGameTypeUpdatedAt` timestamp to current timestamp. - function testFuzz_setRespectedGameType_guardianCanSetRespectedGameTypeUpdatedAt_succeeds(uint64 _elapsed) - external - { - _elapsed = uint64(bound(_elapsed, 0, type(uint64).max - uint64(block.timestamp))); - GameType _ty = GameType.wrap(type(uint32).max); - uint64 _newRespectedGameTypeUpdatedAt = uint64(block.timestamp) + _elapsed; - GameType _existingGameType = optimismPortal2.respectedGameType(); - vm.warp(_newRespectedGameTypeUpdatedAt); - emit RespectedGameTypeSet(_existingGameType, Timestamp.wrap(_newRespectedGameTypeUpdatedAt)); - vm.prank(optimismPortal2.guardian()); - optimismPortal2.setRespectedGameType(_ty); - // GameType doesn't change, but the timestamp does. - assertEq(optimismPortal2.respectedGameType().raw(), _existingGameType.raw()); - assertEq(optimismPortal2.respectedGameTypeUpdatedAt(), _newRespectedGameTypeUpdatedAt); - } - /// @dev Tests that `proveWithdrawalTransaction` reverts when paused. function test_proveWithdrawalTransaction_paused_reverts() external { vm.prank(optimismPortal2.guardian()); superchainConfig.pause("identifier"); - vm.expectRevert(CallPaused.selector); + vm.expectRevert(IOptimismPortal2.OptimismPortal_CallPaused.selector); optimismPortal2.proveWithdrawalTransaction({ _tx: _defaultTx, _disputeGameIndex: _proposedGameIndex, @@ -570,7 +524,26 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { /// @dev Tests that `proveWithdrawalTransaction` reverts when the target is the portal contract. function test_proveWithdrawalTransaction_onSelfCall_reverts() external { _defaultTx.target = address(optimismPortal2); - vm.expectRevert(BadTarget.selector); + vm.expectRevert(IOptimismPortal2.OptimismPortal_BadTarget.selector); + optimismPortal2.proveWithdrawalTransaction({ + _tx: _defaultTx, + _disputeGameIndex: _proposedGameIndex, + _outputRootProof: _outputRootProof, + _withdrawalProof: _withdrawalProof + }); + } + + /// @dev Tests that `proveWithdrawalTransaction` reverts when the current timestamp is less + /// than or equal to the creation timestamp of the dispute game. + function testFuzz_proveWithdrawalTransaction_timestampLessThanOrEqualToCreation_reverts(uint64 _timestamp) + external + { + // Set the timestamp to be less than or equal to the creation timestamp of the dispute game. + _timestamp = uint64(bound(_timestamp, 0, game.createdAt().raw())); + vm.warp(_timestamp); + + // Should revert. + vm.expectRevert(IOptimismPortal2.OptimismPortal_InvalidProofTimestamp.selector); optimismPortal2.proveWithdrawalTransaction({ _tx: _defaultTx, _disputeGameIndex: _proposedGameIndex, @@ -583,7 +556,7 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { function test_proveWithdrawalTransaction_onInvalidOutputRootProof_reverts() external { // Modify the version to invalidate the withdrawal proof. _outputRootProof.version = bytes32(uint256(1)); - vm.expectRevert(InvalidProof.selector); + vm.expectRevert(IOptimismPortal2.OptimismPortal_InvalidOutputRootProof.selector); optimismPortal2.proveWithdrawalTransaction({ _tx: _defaultTx, _disputeGameIndex: _proposedGameIndex, @@ -627,25 +600,7 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { 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); - optimismPortal2.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); + vm.expectRevert(IOptimismPortal2.OptimismPortal_InvalidDisputeGame.selector); optimismPortal2.proveWithdrawalTransaction({ _tx: _defaultTx, _disputeGameIndex: _proposedGameIndex, @@ -657,7 +612,7 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { /// @dev Tests that `proveWithdrawalTransaction` reverts if the game was not the respected game type when created. function test_proveWithdrawalTransaction_wasNotRespectedGameTypeWhenCreated_reverts() external { vm.mockCall(address(game), abi.encodeCall(game.wasRespectedGameTypeWhenCreated, ()), abi.encode(false)); - vm.expectRevert(InvalidGameType.selector); + vm.expectRevert(IOptimismPortal2.OptimismPortal_InvalidDisputeGame.selector); optimismPortal2.proveWithdrawalTransaction({ _tx: _defaultTx, _disputeGameIndex: _proposedGameIndex, @@ -670,7 +625,7 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { /// `wasRespectedGameTypeWhenCreated`. function test_proveWithdrawalTransaction_legacyGame_reverts() external { vm.mockCallRevert(address(game), abi.encodeCall(game.wasRespectedGameTypeWhenCreated, ()), ""); - vm.expectRevert(LegacyGame.selector); + vm.expectRevert(); // nosemgrep: sol-safety-expectrevert-no-args optimismPortal2.proveWithdrawalTransaction({ _tx: _defaultTx, _disputeGameIndex: _proposedGameIndex, @@ -682,7 +637,8 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { /// @dev Tests that `proveWithdrawalTransaction` succeeds if the game was created after the /// game retirement timestamp. function testFuzz_proveWithdrawalTransaction_createdAfterRetirementTimestamp_succeeds(uint64 _createdAt) external { - _createdAt = uint64(bound(_createdAt, optimismPortal2.respectedGameTypeUpdatedAt() + 1, type(uint64).max)); + _createdAt = uint64(bound(_createdAt, optimismPortal2.respectedGameTypeUpdatedAt() + 1, type(uint64).max - 1)); + vm.warp(_createdAt + 1); vm.mockCall(address(game), abi.encodeCall(game.createdAt, ()), abi.encode(uint64(_createdAt))); optimismPortal2.proveWithdrawalTransaction({ _tx: _defaultTx, @@ -699,45 +655,7 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { { _createdAt = uint64(bound(_createdAt, 0, optimismPortal2.respectedGameTypeUpdatedAt())); vm.mockCall(address(game), abi.encodeCall(game.createdAt, ()), abi.encode(uint64(_createdAt))); - vm.expectRevert("OptimismPortal: dispute game created before respected game type was updated"); - optimismPortal2.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)); - optimismPortal2.proveWithdrawalTransaction({ - _tx: _defaultTx, - _disputeGameIndex: _proposedGameIndex, - _outputRootProof: _outputRootProof, - _withdrawalProof: _withdrawalProof - }); - - // Blacklist the dispute dispute game. - vm.prank(optimismPortal2.guardian()); - optimismPortal2.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( - optimismPortal2.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)); + vm.expectRevert(IOptimismPortal2.OptimismPortal_ImproperDisputeGame.selector); optimismPortal2.proveWithdrawalTransaction({ _tx: _defaultTx, _disputeGameIndex: _proposedGameIndex, @@ -762,12 +680,16 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { // 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( optimismPortal2.respectedGameType(), Claim.wrap(_outputRoot), abi.encode(_proposedBlockNumber + 1) ); _proposedGameIndex = disputeGameFactory.gameCount() - 1; + // Warp 1 second into the future so we're not in the same block as the dispute game. + vm.warp(block.timestamp + 1 seconds); + vm.expectEmit(true, true, true, true); emit WithdrawalProven(_withdrawalHash, alice, bob); vm.expectEmit(true, true, true, true); @@ -801,7 +723,7 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { // Update the respected game type to 0xbeef. vm.prank(optimismPortal2.guardian()); - optimismPortal2.setRespectedGameType(GameType.wrap(0xbeef)); + anchorStateRegistry.setRespectedGameType(GameType.wrap(0xbeef)); // Create a new game and mock the game type as 0xbeef in the factory. vm.mockCall( @@ -810,6 +732,9 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { abi.encode(GameType.wrap(0xbeef), Timestamp.wrap(uint64(block.timestamp)), IDisputeGame(address(newGame))) ); + // Warp 1 second into the future so we're not in the same block as the dispute game. + vm.warp(block.timestamp + 1 seconds); + // Re-proving should be successful against the new game. vm.expectEmit(true, true, true, true); emit WithdrawalProven(_withdrawalHash, alice, bob); @@ -876,7 +801,7 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { emit WithdrawalFinalized(_withdrawalHash, true); optimismPortal2.finalizeWithdrawalTransactionExternalProof(_defaultTx, address(0xb0b)); - vm.expectRevert(AlreadyFinalized.selector); + vm.expectRevert(IOptimismPortal2.OptimismPortal_AlreadyFinalized.selector); optimismPortal2.finalizeWithdrawalTransactionExternalProof(_defaultTx, address(this)); assert(address(bob).balance == bobBalanceBefore + 100); @@ -898,7 +823,7 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { vm.warp(block.timestamp + optimismPortal2.proofMaturityDelaySeconds() + 1 seconds); vm.startPrank(alice, Constants.ESTIMATION_ADDRESS); - vm.expectRevert(GasEstimation.selector); + vm.expectRevert(IOptimismPortal2.OptimismPortal_GasEstimation.selector); optimismPortal2.finalizeWithdrawalTransaction(_defaultTx); } @@ -1008,7 +933,7 @@ 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); + vm.warp(block.timestamp + 1); // Prove the withdrawal transaction against the invalid dispute game, as 0xb0b. vm.expectEmit(true, true, true, true); @@ -1047,7 +972,7 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { // Ensure both proofs are registered successfully. assertEq(optimismPortal2.numProofSubmitters(_withdrawalHash), 2); - vm.expectRevert(ProposalNotValidated.selector); + vm.expectRevert(IOptimismPortal2.OptimismPortal_InvalidRootClaim.selector); vm.prank(address(0xb0b)); optimismPortal2.finalizeWithdrawalTransaction(_defaultTx); @@ -1063,7 +988,7 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { vm.prank(optimismPortal2.guardian()); superchainConfig.pause("identifier"); - vm.expectRevert(CallPaused.selector); + vm.expectRevert(IOptimismPortal2.OptimismPortal_CallPaused.selector); optimismPortal2.finalizeWithdrawalTransaction(_defaultTx); } @@ -1071,7 +996,7 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { function test_finalizeWithdrawalTransaction_ifWithdrawalNotProven_reverts() external { uint256 bobBalanceBefore = address(bob).balance; - vm.expectRevert(Unproven.selector); + vm.expectRevert(IOptimismPortal2.OptimismPortal_Unproven.selector); optimismPortal2.finalizeWithdrawalTransaction(_defaultTx); assert(address(bob).balance == bobBalanceBefore); @@ -1093,7 +1018,7 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { _withdrawalProof: _withdrawalProof }); - vm.expectRevert("OptimismPortal: proven withdrawal has not matured yet"); + vm.expectRevert(IOptimismPortal2.OptimismPortal_ProofNotOldEnough.selector); optimismPortal2.finalizeWithdrawalTransaction(_defaultTx); assert(address(bob).balance == bobBalanceBefore); @@ -1123,7 +1048,7 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { 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"); + vm.expectRevert(IOptimismPortal2.OptimismPortal_InvalidProofTimestamp.selector); optimismPortal2.finalizeWithdrawalTransaction(_defaultTx); // Ensure that bob's balance has remained the same @@ -1151,7 +1076,7 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { vm.warp(block.timestamp + optimismPortal2.proofMaturityDelaySeconds() + 1); // Attempt to finalize the withdrawal - vm.expectRevert(ProposalNotValidated.selector); + vm.expectRevert(IOptimismPortal2.OptimismPortal_InvalidRootClaim.selector); optimismPortal2.finalizeWithdrawalTransaction(_defaultTx); // Ensure that bob's balance has remained the same @@ -1209,7 +1134,7 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { emit WithdrawalFinalized(_withdrawalHash, true); optimismPortal2.finalizeWithdrawalTransaction(_defaultTx); - vm.expectRevert(AlreadyFinalized.selector); + vm.expectRevert(IOptimismPortal2.OptimismPortal_AlreadyFinalized.selector); optimismPortal2.finalizeWithdrawalTransaction(_defaultTx); } @@ -1458,7 +1383,7 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { // Change the respectedGameType vm.prank(optimismPortal2.guardian()); - optimismPortal2.setRespectedGameType(_newGameType); + anchorStateRegistry.setRespectedGameType(_newGameType); // Withdrawal transaction still finalizable vm.expectCallMinGas(_tx.target, _tx.value, uint64(_tx.gasLimit), _tx.data); @@ -1484,11 +1409,11 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { game.resolve(); vm.prank(optimismPortal2.guardian()); - optimismPortal2.blacklistDisputeGame(IDisputeGame(address(game))); + anchorStateRegistry.blacklistDisputeGame(IDisputeGame(address(game))); vm.warp(block.timestamp + optimismPortal2.proofMaturityDelaySeconds() + 1); - vm.expectRevert(Blacklisted.selector); + vm.expectRevert(IOptimismPortal2.OptimismPortal_InvalidRootClaim.selector); optimismPortal2.finalizeWithdrawalTransaction(_defaultTx); } @@ -1514,7 +1439,7 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { game.resolve(); // Attempt to finalize the withdrawal directly after the game resolves. This should fail. - vm.expectRevert("OptimismPortal: output proposal in air-gap"); + vm.expectRevert(IOptimismPortal2.OptimismPortal_InvalidRootClaim.selector); optimismPortal2.finalizeWithdrawalTransaction(_defaultTx); // Finalize the withdrawal transaction. This should succeed. @@ -1549,9 +1474,10 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { // Set respectedGameTypeUpdatedAt. vm.prank(optimismPortal2.guardian()); - optimismPortal2.setRespectedGameType(GameType.wrap(type(uint32).max)); + anchorStateRegistry.updateRetirementTimestamp(); - vm.expectRevert("OptimismPortal: dispute game created before respected game type was updated"); + // Should revert. + vm.expectRevert(IOptimismPortal2.OptimismPortal_InvalidRootClaim.selector); optimismPortal2.finalizeWithdrawalTransaction(_defaultTx); } @@ -1581,7 +1507,7 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { vm.mockCall(address(game), abi.encodeCall(game.wasRespectedGameTypeWhenCreated, ()), abi.encode(false)); - vm.expectRevert(InvalidGameType.selector); + vm.expectRevert(IOptimismPortal2.OptimismPortal_InvalidRootClaim.selector); optimismPortal2.finalizeWithdrawalTransaction(_defaultTx); } @@ -1610,9 +1536,11 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { // Warp past the dispute game finality delay. vm.warp(block.timestamp + optimismPortal2.disputeGameFinalityDelaySeconds() + 1); + // Mock the wasRespectedGameTypeWhenCreated call to revert. vm.mockCallRevert(address(game), abi.encodeCall(game.wasRespectedGameTypeWhenCreated, ()), ""); - vm.expectRevert(LegacyGame.selector); + // Should revert. + vm.expectRevert(); // nosemgrep: sol-safety-expectrevert-no-args optimismPortal2.finalizeWithdrawalTransaction(_defaultTx); } @@ -1632,13 +1560,13 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { // Attempt to finalize the withdrawal transaction 1 second before the proof has matured. This should fail. vm.warp(block.timestamp + optimismPortal2.proofMaturityDelaySeconds()); - vm.expectRevert("OptimismPortal: proven withdrawal has not matured yet"); + vm.expectRevert(IOptimismPortal2.OptimismPortal_ProofNotOldEnough.selector); optimismPortal2.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); + vm.expectRevert(IOptimismPortal2.OptimismPortal_InvalidRootClaim.selector); optimismPortal2.finalizeWithdrawalTransaction(_defaultTx); // Finalize the dispute game and attempt to finalize the withdrawal again. This should also fail, since the @@ -1646,7 +1574,7 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { game.resolveClaim(0, 0); game.resolve(); vm.warp(block.timestamp + optimismPortal2.disputeGameFinalityDelaySeconds()); - vm.expectRevert("OptimismPortal: output proposal in air-gap"); + vm.expectRevert(IOptimismPortal2.OptimismPortal_InvalidRootClaim.selector); optimismPortal2.finalizeWithdrawalTransaction(_defaultTx); // Warp 1 second in the future, past the air gap dispute game delay, and attempt to finalize the withdrawal. @@ -1655,6 +1583,140 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { optimismPortal2.finalizeWithdrawalTransaction(_defaultTx); assertTrue(optimismPortal2.finalizedWithdrawals(_withdrawalHash)); } + + /// @notice Tests that checkWithdrawal succeeds if the withdrawal has been proven, the dispute + /// game has been finalized, and the root claim is valid. + function test_checkWithdrawal_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)); + optimismPortal2.proveWithdrawalTransaction({ + _tx: _defaultTx, + _disputeGameIndex: _proposedGameIndex, + _outputRootProof: _outputRootProof, + _withdrawalProof: _withdrawalProof + }); + + // Warp past the finalization period. + vm.warp(block.timestamp + optimismPortal2.proofMaturityDelaySeconds() + 1); + + // Mark the dispute game as CHALLENGER_WINS. + vm.mockCall(address(game), abi.encodeCall(game.status, ()), abi.encode(GameStatus.CHALLENGER_WINS)); + + // Mock isGameClaimValid to return true. + vm.mockCall( + address(anchorStateRegistry), abi.encodeCall(anchorStateRegistry.isGameClaimValid, (game)), abi.encode(true) + ); + + // Should succeed. + optimismPortal2.checkWithdrawal(_withdrawalHash, address(this)); + } + + /// @notice Tests that checkWithdrawal reverts if the withdrawal has already been finalized. + function test_checkWithdrawal_ifAlreadyFinalized_reverts() 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)); + optimismPortal2.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 + optimismPortal2.proofMaturityDelaySeconds() + 1); + + // Finalize the withdrawal. + optimismPortal2.finalizeWithdrawalTransaction(_defaultTx); + + // Should revert. + vm.expectRevert(IOptimismPortal2.OptimismPortal_AlreadyFinalized.selector); + optimismPortal2.checkWithdrawal(_withdrawalHash, address(this)); + } + + /// @notice Tests that checkWithdrawal reverts if the withdrawal has not been proven. + function test_checkWithdrawal_ifUnproven_reverts() external { + // Don't prove the withdrawal transaction. + // Should revert. + vm.expectRevert(IOptimismPortal2.OptimismPortal_Unproven.selector); + optimismPortal2.checkWithdrawal(_withdrawalHash, address(this)); + } + + /// @notice Tests that checkWithdrawal reverts if the proof timestamp is greater than the game + /// creation timestamp. + function testFuzz_checkWithdrawal_ifInvalidProofTimestamp_reverts(uint64 _createdAt) external { + // Prove the withdrawal transaction. + optimismPortal2.proveWithdrawalTransaction({ + _tx: _defaultTx, + _disputeGameIndex: _proposedGameIndex, + _outputRootProof: _outputRootProof, + _withdrawalProof: _withdrawalProof + }); + + // Mock the game creation timestamp to be greater than the proof timestamp. + _createdAt = uint64(bound(_createdAt, block.timestamp, type(uint64).max)); + vm.mockCall(address(game), abi.encodeCall(game.createdAt, ()), abi.encode(_createdAt)); + + // Warp beyond the proof maturity delay. + vm.warp(block.timestamp + optimismPortal2.proofMaturityDelaySeconds() + 1); + + // Mark the dispute game as CHALLENGER_WINS. + vm.mockCall(address(game), abi.encodeCall(game.status, ()), abi.encode(GameStatus.CHALLENGER_WINS)); + + // Mock isGameClaimValid to return true. + vm.mockCall( + address(anchorStateRegistry), abi.encodeCall(anchorStateRegistry.isGameClaimValid, (game)), abi.encode(true) + ); + + // Should revert. + vm.expectRevert(IOptimismPortal2.OptimismPortal_InvalidProofTimestamp.selector); + optimismPortal2.checkWithdrawal(_withdrawalHash, address(this)); + } + + /// @notice Tests that checkWithdrawal reverts if the proof timestamp is less than the proof + /// maturity delay. + function test_checkWithdrawal_ifProofNotOldEnough_reverts() external { + // Prove but don't warp ahead past the proof maturity delay. + optimismPortal2.proveWithdrawalTransaction({ + _tx: _defaultTx, + _disputeGameIndex: _proposedGameIndex, + _outputRootProof: _outputRootProof, + _withdrawalProof: _withdrawalProof + }); + + // Should revert. + vm.warp(block.timestamp + optimismPortal2.proofMaturityDelaySeconds() - 1); + vm.expectRevert(IOptimismPortal2.OptimismPortal_ProofNotOldEnough.selector); + optimismPortal2.checkWithdrawal(_withdrawalHash, address(this)); + } + + /// @notice Tests that checkWithdrawal reverts if the root claim is invalid. + function test_checkWithdrawal_ifInvalidRootClaim_reverts() external { + // Prove the withdrawal. + optimismPortal2.proveWithdrawalTransaction({ + _tx: _defaultTx, + _disputeGameIndex: _proposedGameIndex, + _outputRootProof: _outputRootProof, + _withdrawalProof: _withdrawalProof + }); + + // Warp past the proof maturity delay. + vm.warp(block.timestamp + optimismPortal2.proofMaturityDelaySeconds() + 1); + + // Mock the game to have CHALLENGER_WINS status + vm.mockCall(address(game), abi.encodeCall(game.status, ()), abi.encode(GameStatus.CHALLENGER_WINS)); + + // Should revert. + vm.expectRevert(IOptimismPortal2.OptimismPortal_InvalidRootClaim.selector); + optimismPortal2.checkWithdrawal(_withdrawalHash, address(this)); + } } contract OptimismPortal2_Upgradeable_Test is CommonTest { @@ -1685,7 +1747,7 @@ contract OptimismPortal2_Upgradeable_Test is CommonTest { // The value passed to the initialize must be larger than the last value // that initialize was called with. IProxy(payable(address(optimismPortal2))).upgradeToAndCall( - address(nextImpl), abi.encodeCall(NextImpl.initialize, (2)) + address(nextImpl), abi.encodeCall(NextImpl.initialize, (3)) ); assertEq(IProxy(payable(address(optimismPortal2))).implementation(), address(nextImpl)); diff --git a/packages/contracts-bedrock/test/L1/OptimismPortalInterop.t.sol b/packages/contracts-bedrock/test/L1/OptimismPortalInterop.t.sol index df9100ce8817f..3a8c9973e62dc 100644 --- a/packages/contracts-bedrock/test/L1/OptimismPortalInterop.t.sol +++ b/packages/contracts-bedrock/test/L1/OptimismPortalInterop.t.sol @@ -7,7 +7,6 @@ import { CommonTest } from "test/setup/CommonTest.sol"; // Libraries import { Constants } from "src/libraries/Constants.sol"; import { Predeploys } from "src/libraries/Predeploys.sol"; -import "src/libraries/PortalErrors.sol"; // Interfaces import { IL1BlockInterop, ConfigType } from "interfaces/L2/IL1BlockInterop.sol"; @@ -46,7 +45,7 @@ contract OptimismPortalInterop_Test is CommonTest { /// @dev Tests that setting the add dependency config as not the system config reverts. function testFuzz_setConfig_addDependencyButNotSystemConfig_reverts(bytes calldata _value) public { - vm.expectRevert(Unauthorized.selector); + vm.expectRevert(IOptimismPortalInterop.OptimismPortal_Unauthorized.selector); _optimismPortalInterop().setConfig(ConfigType.ADD_DEPENDENCY, _value); } @@ -69,7 +68,7 @@ contract OptimismPortalInterop_Test is CommonTest { /// @dev Tests that setting the remove dependency config as not the system config reverts. function testFuzz_setConfig_removeDependencyButNotSystemConfig_reverts(bytes calldata _value) public { - vm.expectRevert(Unauthorized.selector); + vm.expectRevert(IOptimismPortalInterop.OptimismPortal_Unauthorized.selector); _optimismPortalInterop().setConfig(ConfigType.REMOVE_DEPENDENCY, _value); } diff --git a/packages/contracts-bedrock/test/dispute/AnchorStateRegistry.t.sol b/packages/contracts-bedrock/test/dispute/AnchorStateRegistry.t.sol index b3c7bf06400c3..c171d97737eb8 100644 --- a/packages/contracts-bedrock/test/dispute/AnchorStateRegistry.t.sol +++ b/packages/contracts-bedrock/test/dispute/AnchorStateRegistry.t.sol @@ -8,6 +8,7 @@ import { FaultDisputeGame_Init, _changeClaimStatus } from "test/dispute/FaultDis import { GameType, GameStatus, Hash, Claim, VMStatuses, OutputRoot } from "src/dispute/lib/Types.sol"; // Interfaces +import { IDisputeGame } from "interfaces/dispute/IDisputeGame.sol"; import { IFaultDisputeGame } from "interfaces/dispute/IFaultDisputeGame.sol"; import { IAnchorStateRegistry } from "interfaces/dispute/IAnchorStateRegistry.sol"; @@ -15,8 +16,9 @@ contract AnchorStateRegistry_Init is FaultDisputeGame_Init { /// @dev A valid l2BlockNumber that comes after the current anchor root block. uint256 validL2BlockNumber; - event AnchorNotUpdated(IFaultDisputeGame indexed game); event AnchorUpdated(IFaultDisputeGame indexed game); + event RespectedGameTypeSet(GameType gameType); + event RetirementTimestampSet(uint256 timestamp); function setUp() public virtual override { // Duplicating the initialization/setup logic of FaultDisputeGame_Test. @@ -46,7 +48,6 @@ contract AnchorStateRegistry_Initialize_Test is AnchorStateRegistry_Init { // Verify contract addresses. assert(anchorStateRegistry.superchainConfig() == superchainConfig); assert(anchorStateRegistry.disputeGameFactory() == disputeGameFactory); - assert(anchorStateRegistry.portal() == optimismPortal2); } } @@ -57,11 +58,11 @@ contract AnchorStateRegistry_Initialize_TestFail is AnchorStateRegistry_Init { anchorStateRegistry.initialize( superchainConfig, disputeGameFactory, - optimismPortal2, OutputRoot({ root: Hash.wrap(0xDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEF), l2BlockNumber: 0 - }) + }), + GameType.wrap(0) ); } } @@ -73,6 +74,25 @@ contract AnchorStateRegistry_Version_Test is AnchorStateRegistry_Init { } } +contract AnchorStateRegistry_Paused_Test is AnchorStateRegistry_Init { + /// @notice Tests that paused() will return the correct value. + function test_paused_succeeds() public { + // Pause the superchain. + vm.prank(superchainConfig.guardian()); + superchainConfig.pause("testing"); + + // Paused should return true. + assertTrue(anchorStateRegistry.paused()); + + // Unpause the superchain. + vm.prank(superchainConfig.guardian()); + superchainConfig.unpause(); + + // Paused should return false. + assertFalse(anchorStateRegistry.paused()); + } +} + contract AnchorStateRegistry_GetAnchorRoot_Test is AnchorStateRegistry_Init { /// @notice Tests that getAnchorRoot will return the value of the starting anchor root when no /// anchor game exists yet. @@ -106,6 +126,29 @@ contract AnchorStateRegistry_GetAnchorRoot_Test is AnchorStateRegistry_Init { assertEq(l2BlockNumber, gameProxy.l2BlockNumber()); } + /// @notice Tests that getAnchorRoot will return the latest anchor root even if the superchain + /// is paused. + function test_getAnchorRoot_superchainPaused_succeeds() public { + // Mock the game to be resolved. + vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.resolvedAt, ()), abi.encode(block.timestamp)); + vm.warp(block.timestamp + optimismPortal2.disputeGameFinalityDelaySeconds() + 1); + + // Mock the game to be the defender wins. + vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.status, ()), abi.encode(GameStatus.DEFENDER_WINS)); + + // Set the anchor game to the game proxy. + anchorStateRegistry.setAnchorState(gameProxy); + + // Pause the superchain. + vm.prank(superchainConfig.guardian()); + superchainConfig.pause("testing"); + + // We should get the anchor root back. + (Hash root, uint256 l2BlockNumber) = anchorStateRegistry.getAnchorRoot(); + assertEq(root.raw(), gameProxy.rootClaim().raw()); + assertEq(l2BlockNumber, gameProxy.l2BlockNumber()); + } + /// @notice Tests that getAnchorRoot returns even if the anchor game is blacklisted. function test_getAnchorRoot_blacklistedGame_succeeds() public { // Mock the game to be resolved. @@ -118,12 +161,9 @@ contract AnchorStateRegistry_GetAnchorRoot_Test is AnchorStateRegistry_Init { // Set the anchor game to the game proxy. anchorStateRegistry.setAnchorState(gameProxy); - // Mock the disputeGameBlacklist call to return true. - vm.mockCall( - address(optimismPortal2), - abi.encodeCall(optimismPortal2.disputeGameBlacklist, (gameProxy)), - abi.encode(true) - ); + // Blacklist the game. + vm.prank(superchainConfig.guardian()); + anchorStateRegistry.blacklistDisputeGame(gameProxy); // Get the anchor root. (Hash root, uint256 l2BlockNumber) = anchorStateRegistry.getAnchorRoot(); @@ -172,12 +212,11 @@ contract AnchorStateRegistry_IsGameRegistered_Test is AnchorStateRegistry_Init { contract AnchorStateRegistry_IsGameBlacklisted_Test is AnchorStateRegistry_Init { /// @notice Tests that isGameBlacklisted will return true if the game is blacklisted. function test_isGameBlacklisted_isActuallyBlacklisted_succeeds() public { - // Mock the disputeGameBlacklist call to return true. - vm.mockCall( - address(optimismPortal2), - abi.encodeCall(optimismPortal2.disputeGameBlacklist, (gameProxy)), - abi.encode(true) - ); + // Blacklist the game. + vm.prank(superchainConfig.guardian()); + anchorStateRegistry.blacklistDisputeGame(gameProxy); + + // Should return true. assertTrue(anchorStateRegistry.isGameBlacklisted(gameProxy)); } @@ -185,8 +224,8 @@ contract AnchorStateRegistry_IsGameBlacklisted_Test is AnchorStateRegistry_Init function test_isGameBlacklisted_isNotBlacklisted_succeeds() public { // Mock the disputeGameBlacklist call to return false. vm.mockCall( - address(optimismPortal2), - abi.encodeCall(optimismPortal2.disputeGameBlacklist, (gameProxy)), + address(anchorStateRegistry), + abi.encodeCall(anchorStateRegistry.disputeGameBlacklist, (gameProxy)), abi.encode(false) ); assertFalse(anchorStateRegistry.isGameBlacklisted(gameProxy)); @@ -214,34 +253,35 @@ contract AnchorStateRegistry_IsGameRespected_Test is AnchorStateRegistry_Init { contract AnchorStateRegistry_IsGameRetired_Test is AnchorStateRegistry_Init { /// @notice Tests that isGameRetired will return true if the game is retired. - /// @param _retirementTimestamp The retirement timestamp to use for the test. - function testFuzz_isGameRetired_isRetired_succeeds(uint64 _retirementTimestamp) public { - // Make sure retirement timestamp is greater than or equal to the game's creation time. - _retirementTimestamp = uint64(bound(_retirementTimestamp, gameProxy.createdAt().raw(), type(uint64).max)); + /// @param _createdAtTimestamp The createdAt timestamp to use for the test. + function testFuzz_isGameRetired_isRetired_succeeds(uint64 _createdAtTimestamp) public { + // Set the retirement timestamp to now. + vm.prank(superchainConfig.guardian()); + anchorStateRegistry.updateRetirementTimestamp(); + + // Make sure createdAt timestamp is less than or equal to the retirementTimestamp. + _createdAtTimestamp = uint64(bound(_createdAtTimestamp, 0, anchorStateRegistry.retirementTimestamp())); // Mock the respectedGameTypeUpdatedAt call. - vm.mockCall( - address(optimismPortal2), - abi.encodeCall(optimismPortal2.respectedGameTypeUpdatedAt, ()), - abi.encode(_retirementTimestamp) - ); + vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.createdAt, ()), abi.encode(_createdAtTimestamp)); // Game should be retired. assertTrue(anchorStateRegistry.isGameRetired(gameProxy)); } /// @notice Tests that isGameRetired will return false if the game is not retired. - /// @param _retirementTimestamp The retirement timestamp to use for the test. - function testFuzz_isGameRetired_isNotRetired_succeeds(uint64 _retirementTimestamp) public { - // Make sure retirement timestamp is earlier than the game's creation time. - _retirementTimestamp = uint64(bound(_retirementTimestamp, 0, gameProxy.createdAt().raw() - 1)); + /// @param _createdAtTimestamp The createdAt timestamp to use for the test. + function testFuzz_isGameRetired_isNotRetired_succeeds(uint64 _createdAtTimestamp) public { + // Set the retirement timestamp to now. + vm.prank(superchainConfig.guardian()); + anchorStateRegistry.updateRetirementTimestamp(); - // Mock the respectedGameTypeUpdatedAt call to be earlier than the game's creation time. - vm.mockCall( - address(optimismPortal2), - abi.encodeCall(optimismPortal2.respectedGameTypeUpdatedAt, ()), - abi.encode(_retirementTimestamp) - ); + // Make sure createdAt timestamp is greater than the retirementTimestamp. + _createdAtTimestamp = + uint64(bound(_createdAtTimestamp, anchorStateRegistry.retirementTimestamp() + 1, type(uint64).max)); + + // Mock the call to createdAt. + vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.createdAt, ()), abi.encode(_createdAtTimestamp)); // Game should not be retired. assertFalse(anchorStateRegistry.isGameRetired(gameProxy)); @@ -287,29 +327,38 @@ contract AnchorStateRegistry_IsGameProper_Test is AnchorStateRegistry_Init { /// @notice Tests that isGameProper will return false if the game is blacklisted. function test_isGameProper_isBlacklisted_succeeds() public { - // Mock the disputeGameBlacklist call to return true. - vm.mockCall( - address(optimismPortal2), - abi.encodeCall(optimismPortal2.disputeGameBlacklist, (gameProxy)), - abi.encode(true) - ); + // Blacklist the game. + vm.prank(superchainConfig.guardian()); + anchorStateRegistry.blacklistDisputeGame(gameProxy); + + // Should return false. + assertFalse(anchorStateRegistry.isGameProper(gameProxy)); + } + + /// @notice Tests that isGameProper will return false if the superchain is paused. + function test_isGameProper_superchainPaused_succeeds() public { + // Pause the superchain. + vm.prank(superchainConfig.guardian()); + superchainConfig.pause("testing"); + // Game should not be proper. assertFalse(anchorStateRegistry.isGameProper(gameProxy)); } /// @notice Tests that isGameProper will return false if the game is retired. - /// @param _retirementTimestamp The retirement timestamp to use for the test. - function testFuzz_isGameProper_isRetired_succeeds(uint64 _retirementTimestamp) public { - // Make sure retirement timestamp is later than the game's creation time. - _retirementTimestamp = uint64(bound(_retirementTimestamp, gameProxy.createdAt().raw() + 1, type(uint64).max)); + /// @param _createdAtTimestamp The createdAt timestamp to use for the test. + function testFuzz_isGameProper_isRetired_succeeds(uint64 _createdAtTimestamp) public { + // Set the retirement timestamp to now. + vm.prank(superchainConfig.guardian()); + anchorStateRegistry.updateRetirementTimestamp(); - // Mock the respectedGameTypeUpdatedAt call to be later than the game's creation time. - vm.mockCall( - address(optimismPortal2), - abi.encodeCall(optimismPortal2.respectedGameTypeUpdatedAt, ()), - abi.encode(_retirementTimestamp) - ); + // Make sure createdAt timestamp is less than or equal to the retirementTimestamp. + _createdAtTimestamp = uint64(bound(_createdAtTimestamp, 0, anchorStateRegistry.retirementTimestamp())); + // Mock the call to createdAt. + vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.createdAt, ()), abi.encode(_createdAtTimestamp)); + + // Game should not be proper. assertFalse(anchorStateRegistry.isGameProper(gameProxy)); } } @@ -476,8 +525,8 @@ contract AnchorStateRegistry_IsGameClaimValid_Test is AnchorStateRegistry_Init { function testFuzz_isGameClaimValid_isBlacklisted_succeeds() public { // Mock the disputeGameBlacklist call to return true. vm.mockCall( - address(optimismPortal2), - abi.encodeCall(optimismPortal2.disputeGameBlacklist, (gameProxy)), + address(anchorStateRegistry), + abi.encodeCall(anchorStateRegistry.disputeGameBlacklist, (gameProxy)), abi.encode(true) ); @@ -486,17 +535,17 @@ contract AnchorStateRegistry_IsGameClaimValid_Test is AnchorStateRegistry_Init { } /// @notice Tests that isGameClaimValid will return false if the game is retired. - /// @param _resolvedAtTimestamp The resolvedAt timestamp to use for the test. - function testFuzz_isGameClaimValid_isRetired_succeeds(uint256 _resolvedAtTimestamp) public { - // Make sure retirement timestamp is later than the game's creation time. - _resolvedAtTimestamp = uint64(bound(_resolvedAtTimestamp, gameProxy.createdAt().raw() + 1, type(uint64).max)); + /// @param _createdAtTimestamp The createdAt timestamp to use for the test. + function testFuzz_isGameClaimValid_isRetired_succeeds(uint256 _createdAtTimestamp) public { + // Set the retirement timestamp to now. + vm.prank(superchainConfig.guardian()); + anchorStateRegistry.updateRetirementTimestamp(); - // Mock the respectedGameTypeUpdatedAt call to be later than the game's creation time. - vm.mockCall( - address(optimismPortal2), - abi.encodeCall(optimismPortal2.respectedGameTypeUpdatedAt, ()), - abi.encode(_resolvedAtTimestamp) - ); + // Make sure createdAt timestamp is less than or equal to the retirementTimestamp. + _createdAtTimestamp = uint64(bound(_createdAtTimestamp, 0, anchorStateRegistry.retirementTimestamp())); + + // Mock the call to createdAt. + vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.createdAt, ()), abi.encode(_createdAtTimestamp)); // Claim should not be valid. assertFalse(anchorStateRegistry.isGameClaimValid(gameProxy)); @@ -528,6 +577,16 @@ contract AnchorStateRegistry_IsGameClaimValid_Test is AnchorStateRegistry_Init { // Claim should not be valid. assertFalse(anchorStateRegistry.isGameClaimValid(gameProxy)); } + + /// @notice Tests that isGameClaimValid will return false if the superchain is paused. + function test_isGameClaimValid_superchainPaused_succeeds() public { + // Pause the superchain. + vm.prank(superchainConfig.guardian()); + superchainConfig.pause("testing"); + + // Game should not be valid. + assertFalse(anchorStateRegistry.isGameClaimValid(gameProxy)); + } } contract AnchorStateRegistry_SetAnchorState_Test is AnchorStateRegistry_Init { @@ -768,12 +827,9 @@ contract AnchorStateRegistry_SetAnchorState_TestFail is AnchorStateRegistry_Init vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.resolvedAt, ()), abi.encode(block.timestamp)); vm.warp(block.timestamp + optimismPortal2.disputeGameFinalityDelaySeconds() + 1); - // Mock the disputeGameBlacklist call to return true. - vm.mockCall( - address(optimismPortal2), - abi.encodeCall(optimismPortal2.disputeGameBlacklist, (gameProxy)), - abi.encode(true) - ); + // Blacklist the game. + vm.prank(superchainConfig.guardian()); + anchorStateRegistry.blacklistDisputeGame(gameProxy); // Update the anchor state. vm.prank(address(gameProxy)); @@ -786,8 +842,7 @@ contract AnchorStateRegistry_SetAnchorState_TestFail is AnchorStateRegistry_Init assertEq(updatedRoot.raw(), root.raw()); } - /// @notice Tests that setAnchorState will revert if the game is valid and the game is - /// retired. + /// @notice Tests that setAnchorState will revert if the game is retired. /// @param _l2BlockNumber The L2 block number to use for the game. function testFuzz_setAnchorState_retiredGame_fails(uint256 _l2BlockNumber) public { // Grab block number of the existing anchor root. @@ -802,11 +857,15 @@ contract AnchorStateRegistry_SetAnchorState_TestFail is AnchorStateRegistry_Init // Mock that the game was respected. vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.wasRespectedGameTypeWhenCreated, ()), abi.encode(true)); - // Mock the respectedGameTypeUpdatedAt call to be later than the game's creation time. + // Set the retirement timestamp. + vm.prank(superchainConfig.guardian()); + anchorStateRegistry.updateRetirementTimestamp(); + + // Mock the call to createdAt. vm.mockCall( - address(optimismPortal2), - abi.encodeCall(optimismPortal2.respectedGameTypeUpdatedAt, ()), - abi.encode(gameProxy.createdAt().raw() + 1) + address(gameProxy), + abi.encodeCall(gameProxy.createdAt, ()), + abi.encode(anchorStateRegistry.retirementTimestamp() - 1) ); // Update the anchor state. @@ -819,4 +878,153 @@ contract AnchorStateRegistry_SetAnchorState_TestFail is AnchorStateRegistry_Init assertEq(updatedL2BlockNumber, l2BlockNumber); assertEq(updatedRoot.raw(), root.raw()); } + + /// @notice Tests that setAnchorState will revert if the superchain is paused. + function test_setAnchorState_superchainPaused_fails() public { + // Pause the superchain. + vm.prank(superchainConfig.guardian()); + superchainConfig.pause("testing"); + + // Update the anchor state. + vm.prank(address(gameProxy)); + vm.expectRevert(IAnchorStateRegistry.AnchorStateRegistry_InvalidAnchorGame.selector); + anchorStateRegistry.setAnchorState(gameProxy); + } +} + +contract AnchorStateRegistry_setRespectedGameType_Test is AnchorStateRegistry_Init { + /// @notice Tests that setRespectedGameType succeeds when called by the guardian + /// @param _gameType The game type to set as respected + function testFuzz_setRespectedGameType_succeeds(GameType _gameType) public { + // Call as guardian + vm.prank(superchainConfig.guardian()); + vm.expectEmit(address(anchorStateRegistry)); + emit RespectedGameTypeSet(_gameType); + anchorStateRegistry.setRespectedGameType(_gameType); + + // Verify the game type was set + assertEq(anchorStateRegistry.respectedGameType().raw(), _gameType.raw()); + } +} + +contract AnchorStateRegistry_setRespectedGameType_TestFail is AnchorStateRegistry_Init { + /// @notice Tests that setRespectedGameType reverts when not called by the guardian + /// @param _gameType The game type to attempt to set + /// @param _caller The address attempting to call the function + function testFuzz_setRespectedGameType_notGuardian_reverts(GameType _gameType, address _caller) public { + // Ensure caller is not the guardian + vm.assume(_caller != superchainConfig.guardian()); + + // Attempt to call as non-guardian + vm.prank(_caller); + vm.expectRevert(IAnchorStateRegistry.AnchorStateRegistry_Unauthorized.selector); + anchorStateRegistry.setRespectedGameType(_gameType); + } +} + +contract AnchorStateRegistry_updateRetirementTimestamp_Test is AnchorStateRegistry_Init { + /// @notice Tests that updateRetirementTimestamp succeeds when called by the guardian + function test_updateRetirementTimestamp_succeeds() public { + // Call as guardian + vm.prank(superchainConfig.guardian()); + vm.expectEmit(address(anchorStateRegistry)); + emit RetirementTimestampSet(block.timestamp); + anchorStateRegistry.updateRetirementTimestamp(); + + // Verify the timestamp was set + assertEq(anchorStateRegistry.retirementTimestamp(), block.timestamp); + } + + /// @notice Tests that updateRetirementTimestamp can be called multiple times by the guardian + function test_updateRetirementTimestamp_multipleUpdates_succeeds() public { + // First update + vm.prank(superchainConfig.guardian()); + anchorStateRegistry.updateRetirementTimestamp(); + uint64 firstTimestamp = anchorStateRegistry.retirementTimestamp(); + + // Warp forward and update again + vm.warp(block.timestamp + 1000); + vm.prank(superchainConfig.guardian()); + vm.expectEmit(address(anchorStateRegistry)); + emit RetirementTimestampSet(block.timestamp); + anchorStateRegistry.updateRetirementTimestamp(); + + // Verify the timestamp was updated + assertEq(anchorStateRegistry.retirementTimestamp(), block.timestamp); + assertGt(anchorStateRegistry.retirementTimestamp(), firstTimestamp); + } +} + +contract AnchorStateRegistry_updateRetirementTimestamp_TestFail is AnchorStateRegistry_Init { + /// @notice Tests that updateRetirementTimestamp reverts when not called by the guardian + /// @param _caller The address attempting to call the function + function testFuzz_updateRetirementTimestamp_notGuardian_reverts(address _caller) public { + // Ensure caller is not the guardian + vm.assume(_caller != superchainConfig.guardian()); + + // Attempt to call as non-guardian + vm.prank(_caller); + vm.expectRevert(IAnchorStateRegistry.AnchorStateRegistry_Unauthorized.selector); + anchorStateRegistry.updateRetirementTimestamp(); + } +} + +contract AnchorStateRegistry_blacklistDisputeGame_Test is AnchorStateRegistry_Init { + /// @notice Tests that blacklistDisputeGame succeeds when called by the guardian + function test_blacklistDisputeGame_succeeds() public { + // Call as guardian + vm.prank(superchainConfig.guardian()); + vm.expectEmit(address(anchorStateRegistry)); + emit DisputeGameBlacklisted(gameProxy); + anchorStateRegistry.blacklistDisputeGame(gameProxy); + + // Verify the game was blacklisted + assertTrue(anchorStateRegistry.disputeGameBlacklist(gameProxy)); + } + + /// @notice Tests that multiple games can be blacklisted + function test_blacklistDisputeGame_multipleGames_succeeds() public { + // Create a second game proxy + IDisputeGame secondGame = IDisputeGame(address(0x123)); + + // Blacklist both games + vm.startPrank(superchainConfig.guardian()); + anchorStateRegistry.blacklistDisputeGame(gameProxy); + anchorStateRegistry.blacklistDisputeGame(secondGame); + vm.stopPrank(); + + // Verify both games are blacklisted + assertTrue(anchorStateRegistry.disputeGameBlacklist(gameProxy)); + assertTrue(anchorStateRegistry.disputeGameBlacklist(secondGame)); + } +} + +contract AnchorStateRegistry_blacklistDisputeGame_TestFail is AnchorStateRegistry_Init { + /// @notice Tests that blacklistDisputeGame reverts when not called by the guardian + /// @param _caller The address attempting to call the function + function testFuzz_blacklistDisputeGame_notGuardian_reverts(address _caller) public { + // Ensure caller is not the guardian + vm.assume(_caller != superchainConfig.guardian()); + + // Attempt to call as non-guardian + vm.prank(_caller); + vm.expectRevert(IAnchorStateRegistry.AnchorStateRegistry_Unauthorized.selector); + anchorStateRegistry.blacklistDisputeGame(gameProxy); + } + + /// @notice Tests that blacklisting a game twice succeeds but doesn't change state + function test_blacklistDisputeGame_twice_succeeds() public { + // Blacklist the game + vm.startPrank(superchainConfig.guardian()); + anchorStateRegistry.blacklistDisputeGame(gameProxy); + + // Blacklist again - should emit event but not change state + vm.expectEmit(address(anchorStateRegistry)); + emit DisputeGameBlacklisted(gameProxy); + anchorStateRegistry.blacklistDisputeGame(gameProxy); + vm.stopPrank(); + + // Verify the game is still blacklisted + assertTrue(anchorStateRegistry.disputeGameBlacklist(gameProxy)); + } } diff --git a/packages/contracts-bedrock/test/dispute/FaultDisputeGame.t.sol b/packages/contracts-bedrock/test/dispute/FaultDisputeGame.t.sol index a90f62ea68f8a..a655fcf12f3b8 100644 --- a/packages/contracts-bedrock/test/dispute/FaultDisputeGame.t.sol +++ b/packages/contracts-bedrock/test/dispute/FaultDisputeGame.t.sol @@ -96,6 +96,12 @@ contract FaultDisputeGame_Init is DisputeGameFactory_Init { // Register the game implementation with the factory. disputeGameFactory.setImplementation(GAME_TYPE, gameImpl); uint256 bondAmount = disputeGameFactory.initBonds(GAME_TYPE); + + // Warp ahead of the game retirement timestamp if needed. + if (block.timestamp <= anchorStateRegistry.retirementTimestamp()) { + vm.warp(anchorStateRegistry.retirementTimestamp() + 1); + } + // Create a new game. gameProxy = IFaultDisputeGame( payable(address(disputeGameFactory.create{ value: bondAmount }(GAME_TYPE, rootClaim, extraData))) diff --git a/packages/contracts-bedrock/test/dispute/SuperFaultDisputeGame.t.sol b/packages/contracts-bedrock/test/dispute/SuperFaultDisputeGame.t.sol index 099a85e56c010..919deaa2c960b 100644 --- a/packages/contracts-bedrock/test/dispute/SuperFaultDisputeGame.t.sol +++ b/packages/contracts-bedrock/test/dispute/SuperFaultDisputeGame.t.sol @@ -98,7 +98,12 @@ contract SuperFaultDisputeGame_Init is DisputeGameFactory_Init { uint256 bondAmount = disputeGameFactory.initBonds(GAME_TYPE); vm.prank(superchainConfig.guardian()); - optimismPortal2.setRespectedGameType(GAME_TYPE); + anchorStateRegistry.setRespectedGameType(GAME_TYPE); + + // Warp ahead of the game retirement timestamp if needed. + if (block.timestamp <= anchorStateRegistry.retirementTimestamp()) { + vm.warp(anchorStateRegistry.retirementTimestamp() + 1); + } // Create a new game. gameProxy = IFaultDisputeGame( diff --git a/packages/contracts-bedrock/test/invariants/OptimismPortal2.t.sol b/packages/contracts-bedrock/test/invariants/OptimismPortal2.t.sol index 15ce33c252b18..dbb8f231b28c7 100644 --- a/packages/contracts-bedrock/test/invariants/OptimismPortal2.t.sol +++ b/packages/contracts-bedrock/test/invariants/OptimismPortal2.t.sol @@ -14,7 +14,6 @@ import { ResourceMetering } from "src/L1/ResourceMetering.sol"; import { Constants } from "src/libraries/Constants.sol"; import { Types } from "src/libraries/Types.sol"; import "src/dispute/lib/Types.sol"; -import "src/libraries/PortalErrors.sol"; // Interfaces import { IOptimismPortal2 } from "interfaces/L1/IOptimismPortal2.sol"; @@ -119,7 +118,7 @@ contract OptimismPortal2_Invariant_Harness is CommonTest { }); // Warp forward in time to ensure that the game is created after the retirement timestamp. - vm.warp(optimismPortal2.respectedGameTypeUpdatedAt() + 1 seconds); + vm.warp(anchorStateRegistry.retirementTimestamp() + 1); // Create a dispute game with the output root we've proposed. _proposedBlockNumber = 0xFF; @@ -188,7 +187,7 @@ contract OptimismPortal2_CannotTimeTravel is OptimismPortal2_Invariant_Harness { /// A withdrawal that has been proven should not be able to be finalized /// until after the proof maturity period has elapsed. function invariant_cannotFinalizeBeforePeriodHasPassed() external { - vm.expectRevert("OptimismPortal: proven withdrawal has not matured yet"); + vm.expectRevert(IOptimismPortal2.OptimismPortal_ProofNotOldEnough.selector); optimismPortal2.finalizeWithdrawalTransaction(_defaultTx); } } @@ -217,7 +216,7 @@ contract OptimismPortal2_CannotFinalizeTwice is OptimismPortal2_Invariant_Harnes /// Ensures that there is no chain of calls that can be made that allows a withdrawal to be /// finalized twice. function invariant_cannotFinalizeTwice() external { - vm.expectRevert(AlreadyFinalized.selector); + vm.expectRevert(IOptimismPortal2.OptimismPortal_AlreadyFinalized.selector); optimismPortal2.finalizeWithdrawalTransaction(_defaultTx); } } diff --git a/packages/contracts-bedrock/test/kontrol/proofs/OptimismPortal2.k.sol b/packages/contracts-bedrock/test/kontrol/proofs/OptimismPortal2.k.sol index 7d7b49e3a0c4a..756b225751e90 100644 --- a/packages/contracts-bedrock/test/kontrol/proofs/OptimismPortal2.k.sol +++ b/packages/contracts-bedrock/test/kontrol/proofs/OptimismPortal2.k.sol @@ -6,7 +6,6 @@ import { KontrolUtils } from "./utils/KontrolUtils.sol"; import { Types } from "src/libraries/Types.sol"; import { IOptimismPortal2 as OptimismPortal } from "interfaces/L1/IOptimismPortal2.sol"; import { ISuperchainConfig as SuperchainConfig } from "interfaces/L1/ISuperchainConfig.sol"; -import "src/libraries/PortalErrors.sol"; contract OptimismPortal2Kontrol is DeploymentSummaryFaultProofs, KontrolUtils { OptimismPortal optimismPortal; @@ -26,7 +25,7 @@ contract OptimismPortal2Kontrol is DeploymentSummaryFaultProofs, KontrolUtils { vm.prank(optimismPortal.guardian()); superchainConfig.pause("identifier"); - vm.expectRevert(CallPaused.selector); + vm.expectRevert(OptimismPortal.OptimismPortal_CallPaused.selector); optimismPortal.finalizeWithdrawalTransaction(_tx); } @@ -47,7 +46,7 @@ contract OptimismPortal2Kontrol is DeploymentSummaryFaultProofs, KontrolUtils { vm.prank(optimismPortal.guardian()); superchainConfig.pause("identifier"); - vm.expectRevert(CallPaused.selector); + vm.expectRevert(OptimismPortal.OptimismPortal_CallPaused.selector); optimismPortal.proveWithdrawalTransaction(_tx, _l2OutputIndex, _outputRootProof, _withdrawalProof); } diff --git a/packages/contracts-bedrock/test/opcm/DeployImplementations.t.sol b/packages/contracts-bedrock/test/opcm/DeployImplementations.t.sol index d2fc138e91f76..1f54630223a71 100644 --- a/packages/contracts-bedrock/test/opcm/DeployImplementations.t.sol +++ b/packages/contracts-bedrock/test/opcm/DeployImplementations.t.sol @@ -288,7 +288,7 @@ contract DeployImplementations_Test is Test { deployImplementations.deployPreimageOracleSingleton(dii, dio); deployImplementations.deployMipsSingleton(dii, dio); deployImplementations.deployDisputeGameFactoryImpl(dio); - deployImplementations.deployAnchorStateRegistryImpl(dio); + deployImplementations.deployAnchorStateRegistryImpl(dii, dio); deployImplementations.deployOPContractsManager(dii, dio); // Store the original addresses. @@ -316,7 +316,7 @@ contract DeployImplementations_Test is Test { deployImplementations.deployPreimageOracleSingleton(dii, dio); deployImplementations.deployMipsSingleton(dii, dio); deployImplementations.deployDisputeGameFactoryImpl(dio); - deployImplementations.deployAnchorStateRegistryImpl(dio); + deployImplementations.deployAnchorStateRegistryImpl(dii, dio); deployImplementations.deployOPContractsManager(dii, dio); // Assert that the addresses did not change. diff --git a/packages/contracts-bedrock/test/opcm/SetDisputeGameImpl.t.sol b/packages/contracts-bedrock/test/opcm/SetDisputeGameImpl.t.sol index 2dca24b486bdc..1ece4e242bb9e 100644 --- a/packages/contracts-bedrock/test/opcm/SetDisputeGameImpl.t.sol +++ b/packages/contracts-bedrock/test/opcm/SetDisputeGameImpl.t.sol @@ -4,15 +4,14 @@ pragma solidity ^0.8.0; import { Test } from "forge-std/Test.sol"; import { IDisputeGame } from "interfaces/dispute/IDisputeGame.sol"; import { IDisputeGameFactory } from "interfaces/dispute/IDisputeGameFactory.sol"; -import { GameType } from "src/dispute/lib/Types.sol"; +import { GameType, OutputRoot, Hash } from "src/dispute/lib/Types.sol"; import { SetDisputeGameImpl, SetDisputeGameImplInput } from "scripts/deploy/SetDisputeGameImpl.s.sol"; import { DisputeGameFactory } from "src/dispute/DisputeGameFactory.sol"; import { Proxy } from "src/universal/Proxy.sol"; -import { OptimismPortal2 } from "src/L1/OptimismPortal2.sol"; -import { IOptimismPortal2 } from "interfaces/L1/IOptimismPortal2.sol"; -import { ISystemConfig } from "interfaces/L1/ISystemConfig.sol"; import { ISuperchainConfig } from "interfaces/L1/ISuperchainConfig.sol"; import { SuperchainConfig } from "src/L1/SuperchainConfig.sol"; +import { AnchorStateRegistry } from "src/dispute/AnchorStateRegistry.sol"; +import { IAnchorStateRegistry } from "interfaces/dispute/IAnchorStateRegistry.sol"; contract SetDisputeGameImplInput_Test is Test { SetDisputeGameImplInput input; @@ -70,7 +69,7 @@ contract SetDisputeGameImpl_Test is Test { SetDisputeGameImpl script; SetDisputeGameImplInput input; IDisputeGameFactory factory; - IOptimismPortal2 portal; + IAnchorStateRegistry anchorStateRegistry; address mockImpl; uint32 gameType; @@ -78,8 +77,8 @@ contract SetDisputeGameImpl_Test is Test { script = new SetDisputeGameImpl(); input = new SetDisputeGameImplInput(); DisputeGameFactory dgfImpl = new DisputeGameFactory(); - OptimismPortal2 portalImpl = new OptimismPortal2(0, 0); SuperchainConfig supConfigImpl = new SuperchainConfig(); + AnchorStateRegistry anchorStateRegistryImpl = new AnchorStateRegistry(0); Proxy supConfigProxy = new Proxy(address(1)); vm.prank(address(1)); @@ -92,21 +91,21 @@ contract SetDisputeGameImpl_Test is Test { factoryProxy.upgradeToAndCall(address(dgfImpl), abi.encodeCall(dgfImpl.initialize, (address(this)))); factory = IDisputeGameFactory(address(factoryProxy)); - Proxy portalProxy = new Proxy(address(1)); + Proxy anchorStateRegistryProxy = new Proxy(address(1)); vm.prank(address(1)); - portalProxy.upgradeToAndCall( - address(portalImpl), + anchorStateRegistryProxy.upgradeToAndCall( + address(anchorStateRegistryImpl), abi.encodeCall( - portalImpl.initialize, + anchorStateRegistryImpl.initialize, ( - factory, - ISystemConfig(makeAddr("sysConfig")), ISuperchainConfig(address(supConfigProxy)), + factory, + OutputRoot({ root: Hash.wrap(0), l2BlockNumber: 0 }), GameType.wrap(100) ) ) ); - portal = IOptimismPortal2(payable(address(portalProxy))); + anchorStateRegistry = IAnchorStateRegistry(address(anchorStateRegistryProxy)); mockImpl = makeAddr("impl"); gameType = 999; @@ -115,7 +114,7 @@ contract SetDisputeGameImpl_Test is Test { function test_run_succeeds() public { input.set(input.factory.selector, address(factory)); input.set(input.impl.selector, mockImpl); - input.set(input.portal.selector, address(portal)); + input.set(input.anchorStateRegistry.selector, address(anchorStateRegistry)); input.set(input.gameType.selector, gameType); script.run(input); @@ -124,7 +123,7 @@ contract SetDisputeGameImpl_Test is Test { function test_run_whenImplAlreadySet_reverts() public { input.set(input.factory.selector, address(factory)); input.set(input.impl.selector, mockImpl); - input.set(input.portal.selector, address(portal)); + input.set(input.anchorStateRegistry.selector, address(anchorStateRegistry)); input.set(input.gameType.selector, gameType); // First run should succeed diff --git a/packages/contracts-bedrock/test/safe/DeputyGuardianModule.t.sol b/packages/contracts-bedrock/test/safe/DeputyGuardianModule.t.sol index 593173acffedb..cdecf1110fa74 100644 --- a/packages/contracts-bedrock/test/safe/DeputyGuardianModule.t.sol +++ b/packages/contracts-bedrock/test/safe/DeputyGuardianModule.t.sol @@ -14,17 +14,14 @@ import "src/dispute/lib/Types.sol"; // Interfaces import { IDisputeGame } from "interfaces/dispute/IDisputeGame.sol"; -import { IFaultDisputeGame } from "interfaces/dispute/IFaultDisputeGame.sol"; import { IAnchorStateRegistry } from "interfaces/dispute/IAnchorStateRegistry.sol"; import { DeployUtils } from "scripts/libraries/DeployUtils.sol"; contract DeputyGuardianModule_TestInit is CommonTest, SafeTestTools { using SafeTestLib for SafeInstance; - error Unauthorized(); - error ExecutionFailed(string); - event ExecutionFromModuleSuccess(address indexed); + event RetirementTimestampUpdated(Timestamp indexed); IDeputyGuardianModule deputyGuardianModule; SafeInstance safeInstance; @@ -91,7 +88,7 @@ contract DeputyGuardianModule_Pause_Test is DeputyGuardianModule_TestInit { contract DeputyGuardianModule_Pause_TestFail is DeputyGuardianModule_TestInit { /// @dev Tests that `pause` reverts when called by a non deputy guardian. function test_pause_notDeputyGuardian_reverts() external { - vm.expectRevert(abi.encodeWithSelector(Unauthorized.selector)); + vm.expectRevert(abi.encodeWithSelector(IDeputyGuardianModule.DeputyGuardianModule_Unauthorized.selector)); deputyGuardianModule.pause(); } @@ -104,7 +101,12 @@ contract DeputyGuardianModule_Pause_TestFail is DeputyGuardianModule_TestInit { ); vm.prank(address(deputyGuardian)); - vm.expectRevert(abi.encodeWithSelector(ExecutionFailed.selector, "SuperchainConfig: pause() reverted")); + vm.expectRevert( + abi.encodeWithSelector( + IDeputyGuardianModule.DeputyGuardianModule_ExecutionFailed.selector, + "SuperchainConfig: pause() reverted" + ) + ); deputyGuardianModule.pause(); } } @@ -140,7 +142,7 @@ contract DeputyGuardianModule_Unpause_Test is DeputyGuardianModule_TestInit { contract DeputyGuardianModule_Unpause_TestFail is DeputyGuardianModule_Unpause_Test { /// @dev Tests that `unpause` reverts when called by a non deputy guardian. function test_unpause_notDeputyGuardian_reverts() external { - vm.expectRevert(abi.encodeWithSelector(Unauthorized.selector)); + vm.expectRevert(abi.encodeWithSelector(IDeputyGuardianModule.DeputyGuardianModule_Unauthorized.selector)); deputyGuardianModule.unpause(); assertTrue(superchainConfig.paused()); } @@ -153,42 +155,14 @@ contract DeputyGuardianModule_Unpause_TestFail is DeputyGuardianModule_Unpause_T "SuperchainConfig: unpause reverted" ); - vm.prank(address(deputyGuardian)); - vm.expectRevert(abi.encodeWithSelector(ExecutionFailed.selector, "SuperchainConfig: unpause reverted")); - deputyGuardianModule.unpause(); - } -} - -contract DeputyGuardianModule_SetAnchorState_TestFail is DeputyGuardianModule_TestInit { - function test_setAnchorState_notDeputyGuardian_reverts() external { - IAnchorStateRegistry asr = IAnchorStateRegistry(makeAddr("asr")); - vm.expectRevert(abi.encodeWithSelector(Unauthorized.selector)); - deputyGuardianModule.setAnchorState(asr, IFaultDisputeGame(address(0))); - } - - function test_setAnchorState_targetReverts_reverts() external { - IAnchorStateRegistry asr = IAnchorStateRegistry(makeAddr("asr")); - vm.mockCallRevert( - address(asr), abi.encodePacked(asr.setAnchorState.selector), "AnchorStateRegistry: setAnchorState reverted" - ); vm.prank(address(deputyGuardian)); vm.expectRevert( - abi.encodeWithSelector(ExecutionFailed.selector, "AnchorStateRegistry: setAnchorState reverted") - ); - deputyGuardianModule.setAnchorState(asr, IFaultDisputeGame(address(0))); - } -} - -contract DeputyGuardianModule_SetAnchorState_Test is DeputyGuardianModule_TestInit { - function test_setAnchorState_succeeds() external { - IAnchorStateRegistry asr = IAnchorStateRegistry(makeAddr("asr")); - vm.mockCall( - address(asr), abi.encodeCall(IAnchorStateRegistry.setAnchorState, (IFaultDisputeGame(address(0)))), "" + abi.encodeWithSelector( + IDeputyGuardianModule.DeputyGuardianModule_ExecutionFailed.selector, + "SuperchainConfig: unpause reverted" + ) ); - vm.expectEmit(address(safeInstance.safe)); - emit ExecutionFromModuleSuccess(address(deputyGuardianModule)); - vm.prank(address(deputyGuardian)); - deputyGuardianModule.setAnchorState(asr, IFaultDisputeGame(address(0))); + deputyGuardianModule.unpause(); } } @@ -205,8 +179,8 @@ contract DeputyGuardianModule_BlacklistDisputeGame_Test is DeputyGuardianModule_ emit DisputeGameBlacklisted(game); vm.prank(address(deputyGuardian)); - deputyGuardianModule.blacklistDisputeGame(optimismPortal2, game); - assertTrue(optimismPortal2.disputeGameBlacklist(game)); + deputyGuardianModule.blacklistDisputeGame(anchorStateRegistry, game); + assertTrue(anchorStateRegistry.disputeGameBlacklist(game)); } } @@ -214,25 +188,28 @@ contract DeputyGuardianModule_BlacklistDisputeGame_TestFail is DeputyGuardianMod /// @dev Tests that `blacklistDisputeGame` reverts when called by a non deputy guardian. function test_blacklistDisputeGame_notDeputyGuardian_reverts() external { IDisputeGame game = IDisputeGame(makeAddr("game")); - vm.expectRevert(abi.encodeWithSelector(Unauthorized.selector)); - deputyGuardianModule.blacklistDisputeGame(optimismPortal2, game); - assertFalse(optimismPortal2.disputeGameBlacklist(game)); + vm.expectRevert(abi.encodeWithSelector(IDeputyGuardianModule.DeputyGuardianModule_Unauthorized.selector)); + deputyGuardianModule.blacklistDisputeGame(anchorStateRegistry, game); + assertFalse(anchorStateRegistry.disputeGameBlacklist(game)); } /// @dev Tests that when the call from the Safe reverts, the error message is returned. function test_blacklistDisputeGame_targetReverts_reverts() external { vm.mockCallRevert( - address(optimismPortal2), - abi.encodePacked(optimismPortal2.blacklistDisputeGame.selector), - "OptimismPortal2: blacklistDisputeGame reverted" + address(anchorStateRegistry), + abi.encodePacked(anchorStateRegistry.blacklistDisputeGame.selector), + "AnchorStateRegistry: blacklistDisputeGame reverted" ); IDisputeGame game = IDisputeGame(makeAddr("game")); vm.prank(address(deputyGuardian)); vm.expectRevert( - abi.encodeWithSelector(ExecutionFailed.selector, "OptimismPortal2: blacklistDisputeGame reverted") + abi.encodeWithSelector( + IDeputyGuardianModule.DeputyGuardianModule_ExecutionFailed.selector, + "AnchorStateRegistry: blacklistDisputeGame reverted" + ) ); - deputyGuardianModule.blacklistDisputeGame(optimismPortal2, game); + deputyGuardianModule.blacklistDisputeGame(anchorStateRegistry, game); } } @@ -240,11 +217,6 @@ contract DeputyGuardianModule_setRespectedGameType_Test is DeputyGuardianModule_ /// @dev Tests that `setRespectedGameType` successfully updates the respected game type when called by the deputy /// guardian. function testFuzz_setRespectedGameType_succeeds(GameType _gameType) external { - // Game type(uint32).max is reserved for setting the respectedGameTypeUpdatedAt timestamp. - // TODO(kelvin): Remove this once we've removed the hack. - uint32 boundedGameType = uint32(bound(_gameType.raw(), 0, type(uint32).max - 1)); - _gameType = GameType.wrap(boundedGameType); - vm.expectEmit(address(safeInstance.safe)); emit ExecutionFromModuleSuccess(address(deputyGuardianModule)); @@ -252,9 +224,8 @@ contract DeputyGuardianModule_setRespectedGameType_Test is DeputyGuardianModule_ emit RespectedGameTypeSet(_gameType, Timestamp.wrap(uint64(block.timestamp))); vm.prank(address(deputyGuardian)); - deputyGuardianModule.setRespectedGameType(optimismPortal2, _gameType); - assertEq(GameType.unwrap(optimismPortal2.respectedGameType()), GameType.unwrap(_gameType)); - assertEq(optimismPortal2.respectedGameTypeUpdatedAt(), uint64(block.timestamp)); + deputyGuardianModule.setRespectedGameType(anchorStateRegistry, _gameType); + assertEq(GameType.unwrap(anchorStateRegistry.respectedGameType()), GameType.unwrap(_gameType)); } } @@ -262,31 +233,81 @@ contract DeputyGuardianModule_setRespectedGameType_TestFail is DeputyGuardianMod /// @dev Tests that `setRespectedGameType` when called by a non deputy guardian. function testFuzz_setRespectedGameType_notDeputyGuardian_reverts(GameType _gameType) external { // Change the game type if it's the same to avoid test rejections. - if (GameType.unwrap(optimismPortal2.respectedGameType()) == GameType.unwrap(_gameType)) { + if (GameType.unwrap(anchorStateRegistry.respectedGameType()) == GameType.unwrap(_gameType)) { unchecked { _gameType = GameType.wrap(GameType.unwrap(_gameType) + 1); } } - vm.expectRevert(abi.encodeWithSelector(Unauthorized.selector)); - deputyGuardianModule.setRespectedGameType(optimismPortal2, _gameType); - assertNotEq(GameType.unwrap(optimismPortal2.respectedGameType()), GameType.unwrap(_gameType)); + vm.expectRevert(abi.encodeWithSelector(IDeputyGuardianModule.DeputyGuardianModule_Unauthorized.selector)); + deputyGuardianModule.setRespectedGameType(anchorStateRegistry, _gameType); + assertNotEq(GameType.unwrap(anchorStateRegistry.respectedGameType()), GameType.unwrap(_gameType)); } /// @dev Tests that when the call from the Safe reverts, the error message is returned. function test_setRespectedGameType_targetReverts_reverts() external { vm.mockCallRevert( - address(optimismPortal2), - abi.encodePacked(optimismPortal2.setRespectedGameType.selector), - "OptimismPortal2: setRespectedGameType reverted" + address(anchorStateRegistry), + abi.encodePacked(anchorStateRegistry.setRespectedGameType.selector), + "AnchorStateRegistry: setRespectedGameType reverted" ); GameType gameType = GameType.wrap(1); vm.prank(address(deputyGuardian)); vm.expectRevert( - abi.encodeWithSelector(ExecutionFailed.selector, "OptimismPortal2: setRespectedGameType reverted") + abi.encodeWithSelector( + IDeputyGuardianModule.DeputyGuardianModule_ExecutionFailed.selector, + "AnchorStateRegistry: setRespectedGameType reverted" + ) + ); + deputyGuardianModule.setRespectedGameType(anchorStateRegistry, gameType); + } +} + +contract DeputyGuardianModule_updateRetirementTimestamp_Test is DeputyGuardianModule_TestInit { + /// @notice Tests that updateRetirementTimestamp() successfully updates the retirement timestamp + /// when called by the deputy guardian. + function test_updateRetirementTimestamp_succeeds() external { + vm.expectEmit(address(safeInstance.safe)); + emit ExecutionFromModuleSuccess(address(deputyGuardianModule)); + + vm.expectEmit(address(deputyGuardianModule)); + emit RetirementTimestampUpdated(Timestamp.wrap(uint64(block.timestamp))); + + vm.prank(address(deputyGuardian)); + deputyGuardianModule.updateRetirementTimestamp(anchorStateRegistry); + assertEq(anchorStateRegistry.retirementTimestamp(), block.timestamp); + } +} + +contract DeputyGuardianModule_updateRetirementTimestamp_TestFail is DeputyGuardianModule_TestInit { + /// @notice Tests that updateRetirementTimestamp() reverts when called by an address other than + /// the deputy guardian. + function testFuzz_updateRetirementTimestamp_notDeputyGuardian_reverts(address _caller) external { + vm.assume(_caller != address(deputyGuardian)); + vm.prank(_caller); + vm.expectRevert(abi.encodeWithSelector(IDeputyGuardianModule.DeputyGuardianModule_Unauthorized.selector)); + deputyGuardianModule.updateRetirementTimestamp(anchorStateRegistry); + } + + /// @notice Tests that when the call from the Safe reverts, the error message is returned. + function test_updateRetirementTimestamp_targetReverts_reverts() external { + // Mock a revert from the ASR. + vm.mockCallRevert( + address(anchorStateRegistry), + abi.encodePacked(anchorStateRegistry.updateRetirementTimestamp.selector), + "AnchorStateRegistry: updateRetirementTimestamp reverted" + ); + + // Call the function and expect a revert. + vm.prank(address(deputyGuardian)); + vm.expectRevert( + abi.encodeWithSelector( + IDeputyGuardianModule.DeputyGuardianModule_ExecutionFailed.selector, + "AnchorStateRegistry: updateRetirementTimestamp reverted" + ) ); - deputyGuardianModule.setRespectedGameType(optimismPortal2, gameType); + deputyGuardianModule.updateRetirementTimestamp(anchorStateRegistry); } } @@ -296,16 +317,14 @@ contract DeputyGuardianModule_NoPortalCollisions_Test is DeputyGuardianModule_Te function test_noPortalCollisions_succeeds() external { string[] memory excludes = new string[](5); excludes[0] = "src/dispute/lib/*"; - excludes[1] = "src/L1/OptimismPortal2.sol"; - excludes[2] = "src/L1/OptimismPortalInterop.sol"; - excludes[3] = "interfaces/L1/IOptimismPortal2.sol"; - excludes[4] = "interfaces/L1/IOptimismPortalInterop.sol"; + excludes[1] = "src/dispute/AnchorStateRegistry.sol"; + excludes[2] = "interfaces/dispute/IAnchorStateRegistry.sol"; Abi[] memory abis = ForgeArtifacts.getContractFunctionAbis("src/{L1,dispute,universal}", excludes); for (uint256 i; i < abis.length; i++) { for (uint256 j; j < abis[i].entries.length; j++) { bytes4 sel = abis[i].entries[j].sel; - assertNotEq(sel, optimismPortal2.blacklistDisputeGame.selector); - assertNotEq(sel, optimismPortal2.setRespectedGameType.selector); + assertNotEq(sel, anchorStateRegistry.blacklistDisputeGame.selector); + assertNotEq(sel, anchorStateRegistry.setRespectedGameType.selector); } } } diff --git a/packages/contracts-bedrock/test/safe/DeputyPauseModule.t.sol b/packages/contracts-bedrock/test/safe/DeputyPauseModule.t.sol index 25c54a7d3d3a5..ca87a54662a53 100644 --- a/packages/contracts-bedrock/test/safe/DeputyPauseModule.t.sol +++ b/packages/contracts-bedrock/test/safe/DeputyPauseModule.t.sol @@ -568,7 +568,8 @@ contract DeputyPauseModule_Pause_TestFail is DeputyPauseModule_TestInit { IDeputyPauseModule.DeputyPauseModule_ExecutionFailed.selector, string( abi.encodeWithSelector( - IDeputyGuardianModule.ExecutionFailed.selector, "SuperchainConfig: pause() reverted" + IDeputyGuardianModule.DeputyGuardianModule_ExecutionFailed.selector, + "SuperchainConfig: pause() reverted" ) ) ) diff --git a/packages/contracts-bedrock/test/universal/Specs.t.sol b/packages/contracts-bedrock/test/universal/Specs.t.sol index 61e1c6a4a0582..92b2bef16a8cf 100644 --- a/packages/contracts-bedrock/test/universal/Specs.t.sol +++ b/packages/contracts-bedrock/test/universal/Specs.t.sol @@ -207,6 +207,8 @@ contract Specification_Test is CommonTest { _addSpec({ _name: "L1StandardBridge", _sel: _getSel("version()") }); // OptimismPortalInterop + _addSpec({ _name: "OptimismPortalInterop", _sel: _getSel("anchorStateRegistry()") }); + _addSpec({ _name: "OptimismPortalInterop", _sel: _getSel("checkWithdrawal(bytes32,address)") }); _addSpec({ _name: "OptimismPortalInterop", _sel: _getSel("depositTransaction(address,uint256,uint64,bool,bytes)") @@ -224,7 +226,7 @@ contract Specification_Test is CommonTest { }); _addSpec({ _name: "OptimismPortalInterop", _sel: _getSel("finalizedWithdrawals(bytes32)") }); _addSpec({ _name: "OptimismPortalInterop", _sel: _getSel("guardian()") }); - _addSpec({ _name: "OptimismPortalInterop", _sel: _getSel("initialize(address,address,address,uint32)") }); + _addSpec({ _name: "OptimismPortalInterop", _sel: _getSel("initialize(address,address,address)") }); _addSpec({ _name: "OptimismPortalInterop", _sel: _getSel("l2Sender()") }); _addSpec({ _name: "OptimismPortalInterop", _sel: _getSel("minimumGasLimit(uint64)") }); _addSpec({ _name: "OptimismPortalInterop", _sel: _getSel("params()") }); @@ -239,25 +241,13 @@ contract Specification_Test is CommonTest { _addSpec({ _name: "OptimismPortalInterop", _sel: _getSel("systemConfig()") }); _addSpec({ _name: "OptimismPortalInterop", _sel: _getSel("version()") }); _addSpec({ _name: "OptimismPortalInterop", _sel: _getSel("disputeGameFactory()") }); - _addSpec({ _name: "OptimismPortalInterop", _sel: _getSel("disputeGameBlacklist(address)") }); _addSpec({ _name: "OptimismPortalInterop", _sel: _getSel("respectedGameType()") }); - // Comment out the auth to not disturb the testDeputyGuardianAuth test. This code is not meant to run in - // production, - // and will be merged into the OptimismPortal2 contract itself in the future. - _addSpec({ - _name: "OptimismPortalInterop", - _sel: _getSel("blacklistDisputeGame(address)") /*, _auth: Role.GUARDIAN*/ - }); - _addSpec({ - _name: "OptimismPortalInterop", - _sel: _getSel("setRespectedGameType(uint32)") /*, _auth: Role.GUARDIAN*/ - }); - _addSpec({ _name: "OptimismPortalInterop", _sel: _getSel("checkWithdrawal(bytes32,address)") }); _addSpec({ _name: "OptimismPortalInterop", _sel: _getSel("proofMaturityDelaySeconds()") }); _addSpec({ _name: "OptimismPortalInterop", _sel: _getSel("disputeGameFinalityDelaySeconds()") }); _addSpec({ _name: "OptimismPortalInterop", _sel: _getSel("respectedGameTypeUpdatedAt()") }); _addSpec({ _name: "OptimismPortalInterop", _sel: _getSel("proofSubmitters(bytes32,uint256)") }); _addSpec({ _name: "OptimismPortalInterop", _sel: _getSel("numProofSubmitters(bytes32)") }); + _addSpec({ _name: "OptimismPortalInterop", _sel: _getSel("upgrade(address)") }); _addSpec({ _name: "OptimismPortalInterop", _sel: IOptimismPortalInterop.setConfig.selector, @@ -265,6 +255,8 @@ contract Specification_Test is CommonTest { }); // OptimismPortal2 + _addSpec({ _name: "OptimismPortal2", _sel: _getSel("anchorStateRegistry()") }); + _addSpec({ _name: "OptimismPortal2", _sel: _getSel("checkWithdrawal(bytes32,address)") }); _addSpec({ _name: "OptimismPortal2", _sel: _getSel("depositTransaction(address,uint256,uint64,bool,bytes)") }); _addSpec({ _name: "OptimismPortal2", _sel: _getSel("donateETH()") }); _addSpec({ @@ -279,7 +271,7 @@ contract Specification_Test is CommonTest { }); _addSpec({ _name: "OptimismPortal2", _sel: _getSel("finalizedWithdrawals(bytes32)") }); _addSpec({ _name: "OptimismPortal2", _sel: _getSel("guardian()") }); - _addSpec({ _name: "OptimismPortal2", _sel: _getSel("initialize(address,address,address,uint32)") }); + _addSpec({ _name: "OptimismPortal2", _sel: _getSel("initialize(address,address,address)") }); _addSpec({ _name: "OptimismPortal2", _sel: _getSel("l2Sender()") }); _addSpec({ _name: "OptimismPortal2", _sel: _getSel("minimumGasLimit(uint64)") }); _addSpec({ _name: "OptimismPortal2", _sel: _getSel("params()") }); @@ -294,16 +286,13 @@ contract Specification_Test is CommonTest { _addSpec({ _name: "OptimismPortal2", _sel: _getSel("systemConfig()") }); _addSpec({ _name: "OptimismPortal2", _sel: _getSel("version()") }); _addSpec({ _name: "OptimismPortal2", _sel: _getSel("disputeGameFactory()") }); - _addSpec({ _name: "OptimismPortal2", _sel: _getSel("disputeGameBlacklist(address)") }); _addSpec({ _name: "OptimismPortal2", _sel: _getSel("respectedGameType()") }); - _addSpec({ _name: "OptimismPortal2", _sel: _getSel("blacklistDisputeGame(address)"), _auth: Role.GUARDIAN }); - _addSpec({ _name: "OptimismPortal2", _sel: _getSel("setRespectedGameType(uint32)"), _auth: Role.GUARDIAN }); - _addSpec({ _name: "OptimismPortal2", _sel: _getSel("checkWithdrawal(bytes32,address)") }); _addSpec({ _name: "OptimismPortal2", _sel: _getSel("proofMaturityDelaySeconds()") }); _addSpec({ _name: "OptimismPortal2", _sel: _getSel("disputeGameFinalityDelaySeconds()") }); _addSpec({ _name: "OptimismPortal2", _sel: _getSel("respectedGameTypeUpdatedAt()") }); _addSpec({ _name: "OptimismPortal2", _sel: _getSel("proofSubmitters(bytes32,uint256)") }); _addSpec({ _name: "OptimismPortal2", _sel: _getSel("numProofSubmitters(bytes32)") }); + _addSpec({ _name: "OptimismPortal2", _sel: _getSel("upgrade(address)") }); // ProtocolVersions _addSpec({ _name: "ProtocolVersions", _sel: _getSel("RECOMMENDED_SLOT()") }); @@ -556,7 +545,7 @@ contract Specification_Test is CommonTest { _addSpec({ _name: "AnchorStateRegistry", _sel: _getSel("anchors(uint32)") }); _addSpec({ _name: "AnchorStateRegistry", _sel: _getSel("getAnchorRoot()") }); _addSpec({ _name: "AnchorStateRegistry", _sel: _getSel("disputeGameFactory()") }); - _addSpec({ _name: "AnchorStateRegistry", _sel: _getSel("initialize(address,address,address,(bytes32,uint256))") }); + _addSpec({ _name: "AnchorStateRegistry", _sel: _getSel("initialize(address,address,(bytes32,uint256),uint32)") }); _addSpec({ _name: "AnchorStateRegistry", _sel: _getSel("isGameBlacklisted(address)") }); _addSpec({ _name: "AnchorStateRegistry", _sel: _getSel("isGameClaimValid(address)") }); _addSpec({ _name: "AnchorStateRegistry", _sel: _getSel("isGameFinalized(address)") }); @@ -565,11 +554,17 @@ contract Specification_Test is CommonTest { _addSpec({ _name: "AnchorStateRegistry", _sel: _getSel("isGameResolved(address)") }); _addSpec({ _name: "AnchorStateRegistry", _sel: _getSel("isGameRespected(address)") }); _addSpec({ _name: "AnchorStateRegistry", _sel: _getSel("isGameRetired(address)") }); - _addSpec({ _name: "AnchorStateRegistry", _sel: _getSel("portal()") }); + _addSpec({ _name: "AnchorStateRegistry", _sel: _getSel("paused()") }); _addSpec({ _name: "AnchorStateRegistry", _sel: _getSel("respectedGameType()") }); _addSpec({ _name: "AnchorStateRegistry", _sel: _getSel("setAnchorState(address)") }); _addSpec({ _name: "AnchorStateRegistry", _sel: _getSel("superchainConfig()") }); _addSpec({ _name: "AnchorStateRegistry", _sel: _getSel("version()") }); + _addSpec({ _name: "AnchorStateRegistry", _sel: _getSel("disputeGameFinalityDelaySeconds()") }); + _addSpec({ _name: "AnchorStateRegistry", _sel: _getSel("disputeGameBlacklist(address)") }); + _addSpec({ _name: "AnchorStateRegistry", _sel: _getSel("retirementTimestamp()") }); + _addSpec({ _name: "AnchorStateRegistry", _sel: _getSel("blacklistDisputeGame(address)"), _auth: Role.GUARDIAN }); + _addSpec({ _name: "AnchorStateRegistry", _sel: _getSel("setRespectedGameType(uint32)"), _auth: Role.GUARDIAN }); + _addSpec({ _name: "AnchorStateRegistry", _sel: _getSel("updateRetirementTimestamp()"), _auth: Role.GUARDIAN }); // PermissionedDisputeGame _addSpec({ _name: "PermissionedDisputeGame", _sel: _getSel("absolutePrestate()") }); @@ -952,7 +947,7 @@ contract Specification_Test is CommonTest { }); _addSpec({ _name: "DeputyGuardianModule", - _sel: _getSel("setAnchorState(address,address)"), + _sel: _getSel("updateRetirementTimestamp(address)"), _auth: Role.DEPUTYGUARDIAN }); _addSpec({ _name: "DeputyGuardianModule", _sel: _getSel("pause()"), _auth: Role.DEPUTYGUARDIAN }); @@ -1117,13 +1112,12 @@ 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 - // Additional role for `setAnchorState` which is in DGM but no longer role-restricted. - assertEq(specsByRole[Role.GUARDIAN].length, 4); - assertEq(specsByRole[Role.DEPUTYGUARDIAN].length, specsByRole[Role.GUARDIAN].length + 3); + assertEq(specsByRole[Role.GUARDIAN].length, 5); + assertEq(specsByRole[Role.DEPUTYGUARDIAN].length, specsByRole[Role.GUARDIAN].length + 2); mapping(bytes4 => Spec) storage dgmFuncSpecs = specs["DeputyGuardianModule"]; mapping(bytes4 => Spec) storage superchainConfigFuncSpecs = specs["SuperchainConfig"]; - mapping(bytes4 => Spec) storage portal2FuncSpecs = specs["OptimismPortal2"]; + mapping(bytes4 => Spec) storage asrSpecs = specs["AnchorStateRegistry"]; // Ensure that for each of the DeputyGuardianModule's methods there is a corresponding method on another // system contract authed to the Guardian role. @@ -1134,11 +1128,12 @@ contract Specification_Test is CommonTest { _assertRolesEq(superchainConfigFuncSpecs[_getSel("unpause()")].auth, Role.GUARDIAN); _assertRolesEq(dgmFuncSpecs[_getSel("blacklistDisputeGame(address,address)")].auth, Role.DEPUTYGUARDIAN); - _assertRolesEq(portal2FuncSpecs[_getSel("blacklistDisputeGame(address)")].auth, Role.GUARDIAN); + _assertRolesEq(asrSpecs[_getSel("blacklistDisputeGame(address)")].auth, Role.GUARDIAN); _assertRolesEq(dgmFuncSpecs[_getSel("setRespectedGameType(address,uint32)")].auth, Role.DEPUTYGUARDIAN); - _assertRolesEq(portal2FuncSpecs[_getSel("setRespectedGameType(uint32)")].auth, Role.GUARDIAN); + _assertRolesEq(asrSpecs[_getSel("setRespectedGameType(uint32)")].auth, Role.GUARDIAN); - _assertRolesEq(dgmFuncSpecs[_getSel("setAnchorState(address,address)")].auth, Role.DEPUTYGUARDIAN); + _assertRolesEq(dgmFuncSpecs[_getSel("updateRetirementTimestamp(address)")].auth, Role.DEPUTYGUARDIAN); + _assertRolesEq(asrSpecs[_getSel("updateRetirementTimestamp()")].auth, Role.GUARDIAN); } } diff --git a/packages/contracts-bedrock/test/vendor/Initializable.t.sol b/packages/contracts-bedrock/test/vendor/Initializable.t.sol index 9835e4e73a0af..8464682c88c45 100644 --- a/packages/contracts-bedrock/test/vendor/Initializable.t.sol +++ b/packages/contracts-bedrock/test/vendor/Initializable.t.sol @@ -18,7 +18,6 @@ import { ISystemConfig } from "interfaces/L1/ISystemConfig.sol"; import { IResourceMetering } from "interfaces/L1/IResourceMetering.sol"; import { ISuperchainConfig } from "interfaces/L1/ISuperchainConfig.sol"; import { IDisputeGameFactory } from "interfaces/dispute/IDisputeGameFactory.sol"; -import { IOptimismPortal2 } from "interfaces/L1/IOptimismPortal2.sol"; import { ProtocolVersion } from "interfaces/L1/IProtocolVersions.sol"; /// @title Initializer_Test @@ -124,13 +123,7 @@ contract Initializer_Test is CommonTest { name: "OptimismPortal2Impl", target: EIP1967Helper.getImplementation(address(optimismPortal2)), initCalldata: abi.encodeCall( - optimismPortal2.initialize, - ( - disputeGameFactory, - systemConfig, - superchainConfig, - GameType.wrap(uint32(deploy.cfg().respectedGameType())) - ) + optimismPortal2.initialize, (systemConfig, superchainConfig, anchorStateRegistry) ) }) ); @@ -140,13 +133,7 @@ contract Initializer_Test is CommonTest { name: "OptimismPortal2Proxy", target: address(optimismPortal2), initCalldata: abi.encodeCall( - optimismPortal2.initialize, - ( - disputeGameFactory, - systemConfig, - superchainConfig, - GameType.wrap(uint32(deploy.cfg().respectedGameType())) - ) + optimismPortal2.initialize, (systemConfig, superchainConfig, anchorStateRegistry) ) }) ); @@ -314,8 +301,8 @@ contract Initializer_Test is CommonTest { ( ISuperchainConfig(address(0)), IDisputeGameFactory(address(0)), - IOptimismPortal2(payable(0)), - OutputRoot({ root: Hash.wrap(bytes32(0)), l2BlockNumber: 0 }) + OutputRoot({ root: Hash.wrap(bytes32(0)), l2BlockNumber: 0 }), + GameType.wrap(uint32(deploy.cfg().respectedGameType())) ) ) }) @@ -330,8 +317,8 @@ contract Initializer_Test is CommonTest { ( ISuperchainConfig(address(0)), IDisputeGameFactory(address(0)), - IOptimismPortal2(payable(0)), - OutputRoot({ root: Hash.wrap(bytes32(0)), l2BlockNumber: 0 }) + OutputRoot({ root: Hash.wrap(bytes32(0)), l2BlockNumber: 0 }), + GameType.wrap(uint32(deploy.cfg().respectedGameType())) ) ) }) From 3cf1adb5b03544c522488bb5fdd5a979ef5a77f8 Mon Sep 17 00:00:00 2001 From: Disco <131301107+0xDiscotech@users.noreply.github.com> Date: Thu, 27 Feb 2025 10:24:35 -0300 Subject: [PATCH 02/50] feat: new eth lockbox (#285) * feat: create new eth lockbox contract with interface * chore: add it on the deployment scripts (wip) * feat: create the test base to check if the setup for it is working * feat: add all eth lockbox tests * fix: opcm deployment script issue * fix: lockbox tests * feat: add no withdrawal tx unlock eth check * chore: check proper initialization on test * fix: pre-pr fixes * chore: run pre-pr * fix: opcm tests related to eth lockbox * feat: add admin owner check on authorize portal and lockbox as well * refactor: add prefix to errors and declare them on contract * chore add no withdrawal tx comment * feat: authorize portal in lockbox on the opcm * chore: add one more check for eth lockbox * fix: intializable and specs tests * fix: pre-pr --- .../interfaces/L1/IETHLockbox.sol | 38 ++ .../interfaces/L1/IOPContractsManager.sol | 7 +- .../interfaces/L1/IOptimismPortal2.sol | 3 +- .../interfaces/L1/IOptimismPortalInterop.sol | 1 + .../interfaces/L1/IProxyAdminOwnable.sol | 6 + .../scripts/deploy/ChainAssertions.sol | 25 + .../scripts/deploy/Deploy.s.sol | 6 +- .../deploy/DeployImplementations.s.sol | 35 +- .../scripts/deploy/DeployOPChain.s.sol | 30 +- .../scripts/libraries/DeployUtils.sol | 15 + .../scripts/libraries/Types.sol | 1 + .../snapshots/abi/ETHLockbox.json | 314 ++++++++++++ .../snapshots/abi/OPContractsManager.json | 15 + .../abi/OPContractsManagerInterop.json | 15 + .../snapshots/abi/OPPrestateUpdater.json | 10 + .../snapshots/abi/OptimismPortal2.json | 13 + .../snapshots/abi/OptimismPortalInterop.json | 13 + .../snapshots/semver-lock.json | 24 +- .../snapshots/storageLayout/ETHLockbox.json | 16 + .../storageLayout/OPContractsManager.json | 4 +- .../OPContractsManagerInterop.json | 4 +- .../storageLayout/OPPrestateUpdater.json | 4 +- .../contracts-bedrock/src/L1/ETHLockbox.sol | 177 +++++++ .../src/L1/OPContractsManager.sol | 14 + .../src/L1/OPPrestateUpdater.sol | 5 +- .../src/L1/OptimismPortal2.sol | 15 +- .../test/L1/ETHLockbox.t.sol | 472 ++++++++++++++++++ .../test/L1/OPContractsManager.t.sol | 7 + .../test/L1/OPPrestateUpdater.t.sol | 5 + .../test/L1/OptimismPortal2.t.sol | 7 + .../test/opcm/DeployImplementations.t.sol | 15 + .../contracts-bedrock/test/setup/Setup.sol | 3 + .../test/universal/Specs.t.sol | 21 +- .../test/vendor/Initializable.t.sol | 5 +- .../test/vendor/InitializableOZv5.t.sol | 46 +- 35 files changed, 1356 insertions(+), 35 deletions(-) create mode 100644 packages/contracts-bedrock/interfaces/L1/IETHLockbox.sol create mode 100644 packages/contracts-bedrock/interfaces/L1/IProxyAdminOwnable.sol create mode 100644 packages/contracts-bedrock/snapshots/abi/ETHLockbox.json create mode 100644 packages/contracts-bedrock/snapshots/storageLayout/ETHLockbox.json create mode 100644 packages/contracts-bedrock/src/L1/ETHLockbox.sol create mode 100644 packages/contracts-bedrock/test/L1/ETHLockbox.t.sol diff --git a/packages/contracts-bedrock/interfaces/L1/IETHLockbox.sol b/packages/contracts-bedrock/interfaces/L1/IETHLockbox.sol new file mode 100644 index 0000000000000..a2ad27a9f4070 --- /dev/null +++ b/packages/contracts-bedrock/interfaces/L1/IETHLockbox.sol @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import { ISemver } from "interfaces/universal/ISemver.sol"; +import { ISuperchainConfig } from "interfaces/L1/ISuperchainConfig.sol"; +import { IProxyAdminOwnable } from "interfaces/L1/IProxyAdminOwnable.sol"; + +interface IETHLockbox is IProxyAdminOwnable, ISemver { + error InvalidInitialization(); + error NotInitializing(); + error ETHLockbox_Unauthorized(); + error ETHLockbox_Paused(); + error ETHLockbox_NoWithdrawalTransactions(); + error ETHLockbox_AlreadyAuthorized(); + error ETHLockbox_DifferentAdminOwner(); + + event Initialized(uint64 version); + event ETHLocked(address indexed portal, uint256 amount); + event ETHUnlocked(address indexed portal, uint256 amount); + event PortalAuthorized(address indexed portal); + event LockboxAuthorized(address indexed lockbox); + event LiquidityMigrated(address indexed lockbox); + event LiquidityReceived(address indexed lockbox); + + function initialize(address _superchainConfig) external; + function superchainConfig() external view returns (ISuperchainConfig superchainConfig_); + function paused() external view returns (bool); + function authorizedPortals(address) external view returns (bool); + function authorizedLockboxes(address) external view returns (bool); + function receiveLiquidity() external payable; + function lockETH() external payable; + function unlockETH(uint256 _value) external; + function authorizePortal(address _portal) external; + function authorizeLockbox(address _lockbox) external; + function migrateLiquidity(address _lockbox) external; + + function __constructor__() external; +} diff --git a/packages/contracts-bedrock/interfaces/L1/IOPContractsManager.sol b/packages/contracts-bedrock/interfaces/L1/IOPContractsManager.sol index d3e92ae006a67..317394788bf02 100644 --- a/packages/contracts-bedrock/interfaces/L1/IOPContractsManager.sol +++ b/packages/contracts-bedrock/interfaces/L1/IOPContractsManager.sol @@ -22,6 +22,7 @@ import { IL1CrossDomainMessenger } from "interfaces/L1/IL1CrossDomainMessenger.s import { IL1ERC721Bridge } from "interfaces/L1/IL1ERC721Bridge.sol"; import { IL1StandardBridge } from "interfaces/L1/IL1StandardBridge.sol"; import { IOptimismMintableERC20Factory } from "interfaces/universal/IOptimismMintableERC20Factory.sol"; +import { IETHLockbox } from "interfaces/L1/IETHLockbox.sol"; interface IOPContractsManager { // -------- Structs -------- @@ -65,6 +66,7 @@ interface IOPContractsManager { IOptimismMintableERC20Factory optimismMintableERC20FactoryProxy; IL1StandardBridge l1StandardBridgeProxy; IL1CrossDomainMessenger l1CrossDomainMessengerProxy; + IETHLockbox ethLockboxProxy; // Fault proof contracts below. IOptimismPortal2 optimismPortalProxy; IDisputeGameFactory disputeGameFactoryProxy; @@ -98,6 +100,7 @@ interface IOPContractsManager { address protocolVersionsImpl; address l1ERC721BridgeImpl; address optimismPortalImpl; + address ethLockboxImpl; address systemConfigImpl; address optimismMintableERC20FactoryImpl; address l1CrossDomainMessengerImpl; @@ -171,7 +174,9 @@ interface IOPContractsManager { /// @param gameType Type of the game being added /// @param newDisputeGame Address of the deployed dispute game /// @param oldDisputeGame Address of the old dispute game - event GameTypeAdded(uint256 indexed l2ChainId, GameType indexed gameType, IDisputeGame newDisputeGame, IDisputeGame oldDisputeGame); + event GameTypeAdded( + uint256 indexed l2ChainId, GameType indexed gameType, IDisputeGame newDisputeGame, IDisputeGame oldDisputeGame + ); // -------- Errors -------- diff --git a/packages/contracts-bedrock/interfaces/L1/IOptimismPortal2.sol b/packages/contracts-bedrock/interfaces/L1/IOptimismPortal2.sol index 76b0111f171e2..be07452904e74 100644 --- a/packages/contracts-bedrock/interfaces/L1/IOptimismPortal2.sol +++ b/packages/contracts-bedrock/interfaces/L1/IOptimismPortal2.sol @@ -8,8 +8,9 @@ import { IDisputeGameFactory } from "interfaces/dispute/IDisputeGameFactory.sol" import { ISystemConfig } from "interfaces/L1/ISystemConfig.sol"; import { ISuperchainConfig } from "interfaces/L1/ISuperchainConfig.sol"; import { IAnchorStateRegistry } from "interfaces/dispute/IAnchorStateRegistry.sol"; +import { IProxyAdminOwnable } from "interfaces/L1/IProxyAdminOwnable.sol"; -interface IOptimismPortal2 { +interface IOptimismPortal2 is IProxyAdminOwnable { error ContentLengthMismatch(); error EmptyItem(); error InvalidDataRemainder(); diff --git a/packages/contracts-bedrock/interfaces/L1/IOptimismPortalInterop.sol b/packages/contracts-bedrock/interfaces/L1/IOptimismPortalInterop.sol index 716b42f41985b..49404083e1d3f 100644 --- a/packages/contracts-bedrock/interfaces/L1/IOptimismPortalInterop.sol +++ b/packages/contracts-bedrock/interfaces/L1/IOptimismPortalInterop.sol @@ -65,6 +65,7 @@ interface IOptimismPortalInterop { external; function finalizedWithdrawals(bytes32) external view returns (bool); function guardian() external view returns (address); + function adminOwner() external view returns (address); function initialize( ISystemConfig _systemConfig, ISuperchainConfig _superchainConfig, diff --git a/packages/contracts-bedrock/interfaces/L1/IProxyAdminOwnable.sol b/packages/contracts-bedrock/interfaces/L1/IProxyAdminOwnable.sol new file mode 100644 index 0000000000000..e45fb51fb3fa6 --- /dev/null +++ b/packages/contracts-bedrock/interfaces/L1/IProxyAdminOwnable.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +interface IProxyAdminOwnable { + function adminOwner() external view returns (address); +} diff --git a/packages/contracts-bedrock/scripts/deploy/ChainAssertions.sol b/packages/contracts-bedrock/scripts/deploy/ChainAssertions.sol index f79e58bf45cf2..70aec28b70971 100644 --- a/packages/contracts-bedrock/scripts/deploy/ChainAssertions.sol +++ b/packages/contracts-bedrock/scripts/deploy/ChainAssertions.sol @@ -32,6 +32,7 @@ import { IOptimismMintableERC20Factory } from "interfaces/universal/IOptimismMin import { IPreimageOracle } from "interfaces/cannon/IPreimageOracle.sol"; import { IMIPS } from "interfaces/cannon/IMIPS.sol"; import { IProxyAdmin } from "interfaces/universal/IProxyAdmin.sol"; +import { IETHLockbox } from "interfaces/L1/IETHLockbox.sol"; library ChainAssertions { Vm internal constant vm = Vm(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D); @@ -402,6 +403,30 @@ library ChainAssertions { require(vm.load(address(portal), bytes32(uint256(61))) == bytes32(0), "CHECK-OP2-120"); } + /// @notice Asserts that the ETHLockbox is setup correctly + function checkETHLockbox(Types.ContractSet memory _contracts, bool _isProxy) internal view { + IETHLockbox ethLockbox = IETHLockbox(_contracts.ETHLockbox); + ISuperchainConfig superchainConfig = ISuperchainConfig(_contracts.SuperchainConfig); + + console.log( + "Running chain assertions on the ETHLockbox %s at %s", + _isProxy ? "proxy" : "implementation", + address(ethLockbox) + ); + + require(address(ethLockbox) != address(0), "CHECK-ELB-10"); + + // Check that the contract is initialized + DeployUtils.assertInitializedOZv5({ _contractAddress: address(ethLockbox), _isProxy: _isProxy }); + + if (_isProxy) { + require(ethLockbox.superchainConfig() == superchainConfig, "CHECK-ELB-20"); + require(ethLockbox.authorizedPortals(_contracts.OptimismPortal), "CHECK-ELB-30"); + } else { + require(address(ethLockbox.superchainConfig()) == address(0), "CHECK-ELB-40"); + } + } + /// @notice Asserts that the ProtocolVersions is setup correctly function checkProtocolVersions( Types.ContractSet memory _contracts, diff --git a/packages/contracts-bedrock/scripts/deploy/Deploy.s.sol b/packages/contracts-bedrock/scripts/deploy/Deploy.s.sol index 1d556b0f6db83..0f746c0609dac 100644 --- a/packages/contracts-bedrock/scripts/deploy/Deploy.s.sol +++ b/packages/contracts-bedrock/scripts/deploy/Deploy.s.sol @@ -118,6 +118,7 @@ contract Deploy is Deployer { AnchorStateRegistry: artifacts.getAddress("AnchorStateRegistryProxy"), OptimismMintableERC20Factory: artifacts.getAddress("OptimismMintableERC20FactoryProxy"), OptimismPortal: artifacts.getAddress("OptimismPortalProxy"), + ETHLockbox: artifacts.getAddress("ETHLockboxProxy"), SystemConfig: artifacts.getAddress("SystemConfigProxy"), L1ERC721Bridge: artifacts.getAddress("L1ERC721BridgeProxy"), ProtocolVersions: artifacts.getAddress("ProtocolVersionsProxy"), @@ -137,6 +138,7 @@ contract Deploy is Deployer { AnchorStateRegistry: artifacts.getAddress("AnchorStateRegistryImpl"), OptimismMintableERC20Factory: artifacts.getAddress("OptimismMintableERC20FactoryImpl"), OptimismPortal: artifacts.getAddress("OptimismPortal2Impl"), + ETHLockbox: artifacts.getAddress("ETHLockboxImpl"), SystemConfig: artifacts.getAddress("SystemConfigImpl"), L1ERC721Bridge: artifacts.getAddress("L1ERC721BridgeImpl"), ProtocolVersions: artifacts.getAddress("ProtocolVersionsImpl"), @@ -317,6 +319,7 @@ contract Deploy is Deployer { AnchorStateRegistry: address(0), OptimismMintableERC20Factory: address(dio.optimismMintableERC20FactoryImpl()), OptimismPortal: address(dio.optimismPortalImpl()), + ETHLockbox: address(dio.ethLockboxImpl()), SystemConfig: address(dio.systemConfigImpl()), L1ERC721Bridge: address(dio.l1ERC721BridgeImpl()), ProtocolVersions: address(dio.protocolVersionsImpl()), @@ -327,6 +330,7 @@ contract Deploy is Deployer { ChainAssertions.checkL1StandardBridge({ _contracts: impls, _isProxy: false }); ChainAssertions.checkL1ERC721Bridge({ _contracts: impls, _isProxy: false }); ChainAssertions.checkOptimismPortal2({ _contracts: impls, _cfg: cfg, _isProxy: false }); + ChainAssertions.checkETHLockbox({ _contracts: impls, _isProxy: false }); ChainAssertions.checkOptimismMintableERC20Factory({ _contracts: impls, _isProxy: false }); ChainAssertions.checkDisputeGameFactory({ _contracts: impls, _expectedOwner: address(0), _isProxy: false }); ChainAssertions.checkDelayedWETH({ _contracts: impls, _cfg: cfg, _isProxy: false, _expectedOwner: address(0) }); @@ -371,6 +375,7 @@ contract Deploy is Deployer { artifacts.save("OptimismMintableERC20FactoryProxy", address(deployOutput.optimismMintableERC20FactoryProxy)); artifacts.save("L1StandardBridgeProxy", address(deployOutput.l1StandardBridgeProxy)); artifacts.save("L1CrossDomainMessengerProxy", address(deployOutput.l1CrossDomainMessengerProxy)); + artifacts.save("ETHLockboxProxy", address(deployOutput.ethLockboxProxy)); // Fault Proof contracts artifacts.save("DisputeGameFactoryProxy", address(deployOutput.disputeGameFactoryProxy)); @@ -379,7 +384,6 @@ contract Deploy is Deployer { artifacts.save("PermissionedDisputeGame", address(deployOutput.permissionedDisputeGame)); artifacts.save("OptimismPortalProxy", address(deployOutput.optimismPortalProxy)); artifacts.save("OptimismPortal2Proxy", address(deployOutput.optimismPortalProxy)); - // Check if the permissionless game implementation is already set IDisputeGameFactory factory = IDisputeGameFactory(artifacts.mustGetAddress("DisputeGameFactoryProxy")); address permissionlessGameImpl = address(factory.gameImpls(GameTypes.CANNON)); diff --git a/packages/contracts-bedrock/scripts/deploy/DeployImplementations.s.sol b/packages/contracts-bedrock/scripts/deploy/DeployImplementations.s.sol index 8e38f08e9b6aa..0426173fdbd8a 100644 --- a/packages/contracts-bedrock/scripts/deploy/DeployImplementations.s.sol +++ b/packages/contracts-bedrock/scripts/deploy/DeployImplementations.s.sol @@ -20,6 +20,7 @@ import { IAnchorStateRegistry } from "interfaces/dispute/IAnchorStateRegistry.so import { IOPContractsManager } from "interfaces/L1/IOPContractsManager.sol"; import { IOPContractsManagerInterop } from "interfaces/L1/IOPContractsManagerInterop.sol"; import { IOptimismPortal2 } from "interfaces/L1/IOptimismPortal2.sol"; +import { IETHLockbox } from "interfaces/L1/IETHLockbox.sol"; import { ISystemConfig } from "interfaces/L1/ISystemConfig.sol"; import { IL1CrossDomainMessenger } from "interfaces/L1/IL1CrossDomainMessenger.sol"; import { IL1ERC721Bridge } from "interfaces/L1/IL1ERC721Bridge.sol"; @@ -150,6 +151,7 @@ contract DeployImplementationsOutput is BaseDeployIO { IOPContractsManager internal _opcm; IDelayedWETH internal _delayedWETHImpl; IOptimismPortal2 internal _optimismPortalImpl; + IETHLockbox internal _ethLockboxImpl; IPreimageOracle internal _preimageOracleSingleton; IMIPS internal _mipsSingleton; ISystemConfig internal _systemConfigImpl; @@ -170,6 +172,7 @@ contract DeployImplementationsOutput is BaseDeployIO { else if (_sel == this.superchainConfigImpl.selector) _superchainConfigImpl = ISuperchainConfig(_addr); else if (_sel == this.protocolVersionsImpl.selector) _protocolVersionsImpl = IProtocolVersions(_addr); else if (_sel == this.optimismPortalImpl.selector) _optimismPortalImpl = IOptimismPortal2(payable(_addr)); + else if (_sel == this.ethLockboxImpl.selector) _ethLockboxImpl = IETHLockbox(payable(_addr)); else if (_sel == this.delayedWETHImpl.selector) _delayedWETHImpl = IDelayedWETH(payable(_addr)); else if (_sel == this.preimageOracleSingleton.selector) _preimageOracleSingleton = IPreimageOracle(_addr); else if (_sel == this.mipsSingleton.selector) _mipsSingleton = IMIPS(_addr); @@ -204,7 +207,8 @@ contract DeployImplementationsOutput is BaseDeployIO { address(this.l1StandardBridgeImpl()), address(this.optimismMintableERC20FactoryImpl()), address(this.disputeGameFactoryImpl()), - address(this.anchorStateRegistryImpl()) + address(this.anchorStateRegistryImpl()), + address(this.ethLockboxImpl()) ); DeployUtils.assertValidContractAddresses(Solarray.extend(addrs1, addrs2)); @@ -232,6 +236,11 @@ contract DeployImplementationsOutput is BaseDeployIO { return _optimismPortalImpl; } + function ethLockboxImpl() public view returns (IETHLockbox) { + DeployUtils.assertValidContractAddress(address(_ethLockboxImpl)); + return _ethLockboxImpl; + } + function delayedWETHImpl() public view returns (IDelayedWETH) { DeployUtils.assertValidContractAddress(address(_delayedWETHImpl)); return _delayedWETHImpl; @@ -294,6 +303,7 @@ contract DeployImplementationsOutput is BaseDeployIO { assertValidOpcm(_dii); assertValidOptimismMintableERC20FactoryImpl(_dii); assertValidOptimismPortalImpl(_dii); + assertValidETHLockboxImpl(_dii); assertValidPreimageOracleSingleton(_dii); assertValidSystemConfigImpl(_dii); } @@ -320,6 +330,14 @@ contract DeployImplementationsOutput is BaseDeployIO { require(vm.load(address(portal), bytes32(uint256(61))) == bytes32(0), "PORTAL-50"); } + function assertValidETHLockboxImpl(DeployImplementationsInput) internal view { + IETHLockbox lockbox = ethLockboxImpl(); + + DeployUtils.assertInitializedOZv5({ _contractAddress: address(lockbox), _isProxy: false }); + + require(address(lockbox.superchainConfig()) == address(0), "ELB-10"); + } + function assertValidDelayedWETHImpl(DeployImplementationsInput _dii) internal view { IDelayedWETH delayedWETH = delayedWETHImpl(); @@ -452,6 +470,7 @@ contract DeployImplementations is Script { deployL1StandardBridgeImpl(_dio); deployOptimismMintableERC20FactoryImpl(_dio); deployOptimismPortalImpl(_dii, _dio); + deployETHLockboxImpl(_dio); deployDelayedWETHImpl(_dii, _dio); deployPreimageOracleSingleton(_dii, _dio); deployMipsSingleton(_dii, _dio); @@ -488,6 +507,7 @@ contract DeployImplementations is Script { protocolVersionsImpl: address(_dio.protocolVersionsImpl()), l1ERC721BridgeImpl: address(_dio.l1ERC721BridgeImpl()), optimismPortalImpl: address(_dio.optimismPortalImpl()), + ethLockboxImpl: address(_dio.ethLockboxImpl()), systemConfigImpl: address(_dio.systemConfigImpl()), optimismMintableERC20FactoryImpl: address(_dio.optimismMintableERC20FactoryImpl()), l1CrossDomainMessengerImpl: address(_dio.l1CrossDomainMessengerImpl()), @@ -646,6 +666,18 @@ contract DeployImplementations is Script { _dio.set(_dio.optimismMintableERC20FactoryImpl.selector, address(impl)); } + function deployETHLockboxImpl(DeployImplementationsOutput _dio) public virtual { + IETHLockbox impl = IETHLockbox( + DeployUtils.createDeterministic({ + _name: "ETHLockbox", + _args: DeployUtils.encodeConstructor(abi.encodeCall(IETHLockbox.__constructor__, ())), + _salt: _salt + }) + ); + vm.label(address(impl), "ETHLockboxImpl"); + _dio.set(_dio.ethLockboxImpl.selector, address(impl)); + } + // --- Fault Proofs Contracts --- // The fault proofs contracts are configured as follows: @@ -872,6 +904,7 @@ contract DeployImplementationsInterop is DeployImplementations { protocolVersionsImpl: address(_dio.protocolVersionsImpl()), l1ERC721BridgeImpl: address(_dio.l1ERC721BridgeImpl()), optimismPortalImpl: address(_dio.optimismPortalImpl()), + ethLockboxImpl: address(_dio.ethLockboxImpl()), systemConfigImpl: address(_dio.systemConfigImpl()), optimismMintableERC20FactoryImpl: address(_dio.optimismMintableERC20FactoryImpl()), l1CrossDomainMessengerImpl: address(_dio.l1CrossDomainMessengerImpl()), diff --git a/packages/contracts-bedrock/scripts/deploy/DeployOPChain.s.sol b/packages/contracts-bedrock/scripts/deploy/DeployOPChain.s.sol index ba384992d44db..14d094c2044ab 100644 --- a/packages/contracts-bedrock/scripts/deploy/DeployOPChain.s.sol +++ b/packages/contracts-bedrock/scripts/deploy/DeployOPChain.s.sol @@ -33,6 +33,7 @@ import { IL1CrossDomainMessenger } from "interfaces/L1/IL1CrossDomainMessenger.s import { IL1ERC721Bridge } from "interfaces/L1/IL1ERC721Bridge.sol"; import { IL1StandardBridge } from "interfaces/L1/IL1StandardBridge.sol"; import { IOptimismMintableERC20Factory } from "interfaces/universal/IOptimismMintableERC20Factory.sol"; +import { IETHLockbox } from "interfaces/L1/IETHLockbox.sol"; contract DeployOPChainInput is BaseDeployIO { address internal _opChainProxyAdminOwner; @@ -225,6 +226,7 @@ contract DeployOPChainOutput is BaseDeployIO { IL1StandardBridge internal _l1StandardBridgeProxy; IL1CrossDomainMessenger internal _l1CrossDomainMessengerProxy; IOptimismPortal2 internal _optimismPortalProxy; + IETHLockbox internal _ethLockboxProxy; IDisputeGameFactory internal _disputeGameFactoryProxy; IAnchorStateRegistry internal _anchorStateRegistryProxy; IFaultDisputeGame internal _faultDisputeGame; @@ -243,6 +245,7 @@ contract DeployOPChainOutput is BaseDeployIO { else if (_sel == this.l1StandardBridgeProxy.selector) _l1StandardBridgeProxy = IL1StandardBridge(payable(_addr)) ; else if (_sel == this.l1CrossDomainMessengerProxy.selector) _l1CrossDomainMessengerProxy = IL1CrossDomainMessenger(_addr) ; else if (_sel == this.optimismPortalProxy.selector) _optimismPortalProxy = IOptimismPortal2(payable(_addr)) ; + else if (_sel == this.ethLockboxProxy.selector) _ethLockboxProxy = IETHLockbox(payable(_addr)) ; else if (_sel == this.disputeGameFactoryProxy.selector) _disputeGameFactoryProxy = IDisputeGameFactory(_addr) ; else if (_sel == this.anchorStateRegistryProxy.selector) _anchorStateRegistryProxy = IAnchorStateRegistry(_addr) ; else if (_sel == this.faultDisputeGame.selector) _faultDisputeGame = IFaultDisputeGame(_addr) ; @@ -299,6 +302,12 @@ contract DeployOPChainOutput is BaseDeployIO { return _optimismPortalProxy; } + function ethLockboxProxy() public returns (IETHLockbox) { + DeployUtils.assertValidContractAddress(address(_ethLockboxProxy)); + DeployUtils.assertERC1967ImplementationSet(address(_ethLockboxProxy)); + return _ethLockboxProxy; + } + function disputeGameFactoryProxy() public returns (IDisputeGameFactory) { DeployUtils.assertValidContractAddress(address(_disputeGameFactoryProxy)); DeployUtils.assertERC1967ImplementationSet(address(_disputeGameFactoryProxy)); @@ -375,6 +384,7 @@ contract DeployOPChain is Script { vm.label(address(deployOutput.l1StandardBridgeProxy), "l1StandardBridgeProxy"); vm.label(address(deployOutput.l1CrossDomainMessengerProxy), "l1CrossDomainMessengerProxy"); vm.label(address(deployOutput.optimismPortalProxy), "optimismPortalProxy"); + vm.label(address(deployOutput.ethLockboxProxy), "ethLockboxProxy"); vm.label(address(deployOutput.disputeGameFactoryProxy), "disputeGameFactoryProxy"); vm.label(address(deployOutput.anchorStateRegistryProxy), "anchorStateRegistryProxy"); // vm.label(address(deployOutput.faultDisputeGame), "faultDisputeGame"); @@ -393,6 +403,7 @@ contract DeployOPChain is Script { _doo.set(_doo.l1StandardBridgeProxy.selector, address(deployOutput.l1StandardBridgeProxy)); _doo.set(_doo.l1CrossDomainMessengerProxy.selector, address(deployOutput.l1CrossDomainMessengerProxy)); _doo.set(_doo.optimismPortalProxy.selector, address(deployOutput.optimismPortalProxy)); + _doo.set(_doo.ethLockboxProxy.selector, address(deployOutput.ethLockboxProxy)); _doo.set(_doo.disputeGameFactoryProxy.selector, address(deployOutput.disputeGameFactoryProxy)); _doo.set(_doo.anchorStateRegistryProxy.selector, address(deployOutput.anchorStateRegistryProxy)); // _doo.set(_doo.faultDisputeGame.selector, address(deployOutput.faultDisputeGame)); @@ -425,7 +436,8 @@ contract DeployOPChain is Script { address(_doo.anchorStateRegistryProxy()), address(_doo.permissionedDisputeGame()), // address(_doo.faultDisputeGame()), - address(_doo.delayedWETHPermissionedGameProxy()) + address(_doo.delayedWETHPermissionedGameProxy()), + address(_doo.ethLockboxProxy()) ); // TODO: Eventually switch from Permissioned to Permissionless. Add this address back in. // address(_delayedWETHPermissionlessGameProxy) @@ -444,6 +456,7 @@ contract DeployOPChain is Script { assertValidL1StandardBridge(_doi, _doo); assertValidOptimismMintableERC20Factory(_doi, _doo); assertValidOptimismPortal(_doi, _doo); + assertValidETHLockbox(_doi, _doo); assertValidPermissionedDisputeGame(_doi, _doo); assertValidSystemConfig(_doi, _doo); assertValidAddressManager(_doi, _doo); @@ -608,6 +621,16 @@ contract DeployOPChain is Script { // This slot is the custom gas token _balance and this check ensures // that it stays unset for forwards compatibility with custom gas token. require(vm.load(address(portal), bytes32(uint256(61))) == bytes32(0), "PORTAL-80"); + + // Check once the portal is updated to use the new lockbox. + require(address(portal.ethLockbox()) == address(_doo.ethLockboxProxy()), "PORTAL-90"); + } + + function assertValidETHLockbox(DeployOPChainInput _doi, DeployOPChainOutput _doo) internal { + IETHLockbox lockbox = _doo.ethLockboxProxy(); + + require(address(lockbox.superchainConfig()) == address(_doi.opcm().superchainConfig()), "ETHLOCKBOX-10"); + require(lockbox.authorizedPortals(address(_doo.optimismPortalProxy())), "ETHLOCKBOX-20"); } function assertValidDisputeGameFactory(DeployOPChainInput _doi, DeployOPChainOutput _doo) internal { @@ -688,6 +711,11 @@ contract DeployOPChain is Script { == DeployUtils.assertERC1967ImplementationSet(address(_doo.anchorStateRegistryProxy())), "OPCPA-110" ); + require( + admin.getProxyImplementation(address(_doo.ethLockboxProxy())) + == DeployUtils.assertERC1967ImplementationSet(address(_doo.ethLockboxProxy())), + "OPCPA-120" + ); } // -------- Utilities -------- diff --git a/packages/contracts-bedrock/scripts/libraries/DeployUtils.sol b/packages/contracts-bedrock/scripts/libraries/DeployUtils.sol index 1cec8d04778b1..3a17494861faf 100644 --- a/packages/contracts-bedrock/scripts/libraries/DeployUtils.sol +++ b/packages/contracts-bedrock/scripts/libraries/DeployUtils.sol @@ -366,6 +366,21 @@ library DeployUtils { } } + 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" + ); + } + } + /// @notice Etches a contract, labels it, and allows cheatcodes for it. /// @param _etchTo Address of the contract to etch. /// @param _cname The contract name (also used to label the contract). diff --git a/packages/contracts-bedrock/scripts/libraries/Types.sol b/packages/contracts-bedrock/scripts/libraries/Types.sol index 01d0995aec61c..f8c6eb9026463 100644 --- a/packages/contracts-bedrock/scripts/libraries/Types.sol +++ b/packages/contracts-bedrock/scripts/libraries/Types.sol @@ -14,6 +14,7 @@ library Types { address AnchorStateRegistry; address OptimismMintableERC20Factory; address OptimismPortal; + address ETHLockbox; address SystemConfig; address L1ERC721Bridge; address ProtocolVersions; diff --git a/packages/contracts-bedrock/snapshots/abi/ETHLockbox.json b/packages/contracts-bedrock/snapshots/abi/ETHLockbox.json new file mode 100644 index 0000000000000..c8ce32b2dd7e0 --- /dev/null +++ b/packages/contracts-bedrock/snapshots/abi/ETHLockbox.json @@ -0,0 +1,314 @@ +[ + { + "inputs": [], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [], + "name": "adminOwner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_lockbox", + "type": "address" + } + ], + "name": "authorizeLockbox", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_portal", + "type": "address" + } + ], + "name": "authorizePortal", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "authorizedLockboxes", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "authorizedPortals", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_superchainConfig", + "type": "address" + } + ], + "name": "initialize", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "lockETH", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_lockbox", + "type": "address" + } + ], + "name": "migrateLiquidity", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "paused", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "receiveLiquidity", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [], + "name": "superchainConfig", + "outputs": [ + { + "internalType": "contract ISuperchainConfig", + "name": "superchainConfig_", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_value", + "type": "uint256" + } + ], + "name": "unlockETH", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "version", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "portal", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "ETHLocked", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "portal", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "ETHUnlocked", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint64", + "name": "version", + "type": "uint64" + } + ], + "name": "Initialized", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "lockbox", + "type": "address" + } + ], + "name": "LiquidityMigrated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "lockbox", + "type": "address" + } + ], + "name": "LiquidityReceived", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "lockbox", + "type": "address" + } + ], + "name": "LockboxAuthorized", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "portal", + "type": "address" + } + ], + "name": "PortalAuthorized", + "type": "event" + }, + { + "inputs": [], + "name": "ETHLockbox_AlreadyAuthorized", + "type": "error" + }, + { + "inputs": [], + "name": "ETHLockbox_DifferentAdminOwner", + "type": "error" + }, + { + "inputs": [], + "name": "ETHLockbox_NoWithdrawalTransactions", + "type": "error" + }, + { + "inputs": [], + "name": "ETHLockbox_Paused", + "type": "error" + }, + { + "inputs": [], + "name": "ETHLockbox_Unauthorized", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidInitialization", + "type": "error" + }, + { + "inputs": [], + "name": "NotInitializing", + "type": "error" + } +] \ No newline at end of file diff --git a/packages/contracts-bedrock/snapshots/abi/OPContractsManager.json b/packages/contracts-bedrock/snapshots/abi/OPContractsManager.json index 22c1d40b77cd2..f48fa119127e6 100644 --- a/packages/contracts-bedrock/snapshots/abi/OPContractsManager.json +++ b/packages/contracts-bedrock/snapshots/abi/OPContractsManager.json @@ -95,6 +95,11 @@ "name": "optimismPortalImpl", "type": "address" }, + { + "internalType": "address", + "name": "ethLockboxImpl", + "type": "address" + }, { "internalType": "address", "name": "systemConfigImpl", @@ -472,6 +477,11 @@ "name": "l1CrossDomainMessengerProxy", "type": "address" }, + { + "internalType": "contract IETHLockbox", + "name": "ethLockboxProxy", + "type": "address" + }, { "internalType": "contract IOptimismPortal2", "name": "optimismPortalProxy", @@ -542,6 +552,11 @@ "name": "optimismPortalImpl", "type": "address" }, + { + "internalType": "address", + "name": "ethLockboxImpl", + "type": "address" + }, { "internalType": "address", "name": "systemConfigImpl", diff --git a/packages/contracts-bedrock/snapshots/abi/OPContractsManagerInterop.json b/packages/contracts-bedrock/snapshots/abi/OPContractsManagerInterop.json index 22c1d40b77cd2..f48fa119127e6 100644 --- a/packages/contracts-bedrock/snapshots/abi/OPContractsManagerInterop.json +++ b/packages/contracts-bedrock/snapshots/abi/OPContractsManagerInterop.json @@ -95,6 +95,11 @@ "name": "optimismPortalImpl", "type": "address" }, + { + "internalType": "address", + "name": "ethLockboxImpl", + "type": "address" + }, { "internalType": "address", "name": "systemConfigImpl", @@ -472,6 +477,11 @@ "name": "l1CrossDomainMessengerProxy", "type": "address" }, + { + "internalType": "contract IETHLockbox", + "name": "ethLockboxProxy", + "type": "address" + }, { "internalType": "contract IOptimismPortal2", "name": "optimismPortalProxy", @@ -542,6 +552,11 @@ "name": "optimismPortalImpl", "type": "address" }, + { + "internalType": "address", + "name": "ethLockboxImpl", + "type": "address" + }, { "internalType": "address", "name": "systemConfigImpl", diff --git a/packages/contracts-bedrock/snapshots/abi/OPPrestateUpdater.json b/packages/contracts-bedrock/snapshots/abi/OPPrestateUpdater.json index d44b099737a6f..56d14b0ab4894 100644 --- a/packages/contracts-bedrock/snapshots/abi/OPPrestateUpdater.json +++ b/packages/contracts-bedrock/snapshots/abi/OPPrestateUpdater.json @@ -390,6 +390,11 @@ "name": "l1CrossDomainMessengerProxy", "type": "address" }, + { + "internalType": "contract IETHLockbox", + "name": "ethLockboxProxy", + "type": "address" + }, { "internalType": "contract IOptimismPortal2", "name": "optimismPortalProxy", @@ -460,6 +465,11 @@ "name": "optimismPortalImpl", "type": "address" }, + { + "internalType": "address", + "name": "ethLockboxImpl", + "type": "address" + }, { "internalType": "address", "name": "systemConfigImpl", diff --git a/packages/contracts-bedrock/snapshots/abi/OptimismPortal2.json b/packages/contracts-bedrock/snapshots/abi/OptimismPortal2.json index ebc1b9132de40..add6c23040841 100644 --- a/packages/contracts-bedrock/snapshots/abi/OptimismPortal2.json +++ b/packages/contracts-bedrock/snapshots/abi/OptimismPortal2.json @@ -14,6 +14,19 @@ "stateMutability": "payable", "type": "receive" }, + { + "inputs": [], + "name": "adminOwner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [], "name": "anchorStateRegistry", diff --git a/packages/contracts-bedrock/snapshots/abi/OptimismPortalInterop.json b/packages/contracts-bedrock/snapshots/abi/OptimismPortalInterop.json index 4a6c342105f2c..642b7d6144472 100644 --- a/packages/contracts-bedrock/snapshots/abi/OptimismPortalInterop.json +++ b/packages/contracts-bedrock/snapshots/abi/OptimismPortalInterop.json @@ -14,6 +14,19 @@ "stateMutability": "payable", "type": "receive" }, + { + "inputs": [], + "name": "adminOwner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [], "name": "anchorStateRegistry", diff --git a/packages/contracts-bedrock/snapshots/semver-lock.json b/packages/contracts-bedrock/snapshots/semver-lock.json index 752e8131e6ca9..16e0d15d33823 100644 --- a/packages/contracts-bedrock/snapshots/semver-lock.json +++ b/packages/contracts-bedrock/snapshots/semver-lock.json @@ -3,6 +3,10 @@ "initCodeHash": "0xacbae98cc7c0f7ecbf36dc44bbf7cb0a011e6e6b781e28b9dbf947e31482b30d", "sourceCodeHash": "0xe772f7db8033e4a738850cb28ac4849d3a454c93732135a8a10d4f7cb498088e" }, + "src/L1/ETHLockbox.sol": { + "initCodeHash": "0x2ec1bdb12bec5ac84a7f44fba03247d6d2e442e3c4508f8ec3a7eceb7b68d6cc", + "sourceCodeHash": "0x7da93ca2d0d5f2a61ca9de2f63134d6e94b7ccf9b3b2a29c352d2606ff8a1ca3" + }, "src/L1/L1CrossDomainMessenger.sol": { "initCodeHash": "0x03a3c0eb2418aba9f93bb89723ba2ee7cb9e1988ca00f380503c960149c85b7a", "sourceCodeHash": "0x5907cdb82ec5f6bef2184558a511d049ab3ee65388cf44d0c20d0f234ef8ca44" @@ -16,24 +20,24 @@ "sourceCodeHash": "0x7f614d0c5251365580c02dd898900a583ece2aa28579adea74a3808b575949b4" }, "src/L1/OPContractsManager.sol": { - "initCodeHash": "0x3da482f3107d23948d9535eac40f2d09861605570f0d7e1c2ca4766e1ae71943", - "sourceCodeHash": "0x8cbad8c307c0b56514ce52cba4c3519e2e70cbd19957cd50a65eac208a09cf7d" + "initCodeHash": "0x19cb3ece689cc764790e84b171a0ce921c7b642f633e10b261f551045cc938a8", + "sourceCodeHash": "0x39b86ad4aad4107154723b7c5074666ea19247ad32d25553887330a2b0398c5a" }, "src/L1/OPContractsManagerInterop.sol": { - "initCodeHash": "0xff7784d6851f3dc751e3c8ebe1d0046484c512c765802a16685c8f7d107e3c88", + "initCodeHash": "0x59e9b7ebc3fdd31a69334ec1bc7c44cd054dc089d30ed805ee985496b0077503", "sourceCodeHash": "0xd04bb5babc02fd93618397678506e9bcce5f1881b008785da0767bda74d8447c" }, "src/L1/OPPrestateUpdater.sol": { - "initCodeHash": "0x2d55d1a4826bcb5c65e5b66300fe470ea02398fcdd0a436755024855b372360d", - "sourceCodeHash": "0x985c33cc1933d68de38d6c13afc1ac42ef7e455cb06bb255c1fed22b06b97a62" + "initCodeHash": "0x9f67dafae867eb0f412f5516a02942e2b039c8a3ded5f26da06e4909a31723a7", + "sourceCodeHash": "0xcdeb0f532ff90288ee13694a51a15aaee99392b7dfe2d46fffacf2af82cdb38f" }, "src/L1/OptimismPortal2.sol": { - "initCodeHash": "0xcc7b2a3f10786983f995f557306944eb27c77f7a502bcebfac111ded62a08344", - "sourceCodeHash": "0x016cf228b63fedd9fafbeed5b804786235bcc07ceb499f4919dfad40cdeda322" + "initCodeHash": "0x1465cb07c5715ddcfdfff5d1abd63857dba0afe7f2ef424ce2cb83da01a51b9b", + "sourceCodeHash": "0x4e7dcab58532a9f0d51f477927d828caaff4f19cc90ec025df748db739464396" }, "src/L1/OptimismPortalInterop.sol": { - "initCodeHash": "0x561140ae220a1cd3f8fabd35e435a128fb9d9f2d83686b89494a90cee8d8707f", - "sourceCodeHash": "0x1b2ce1f2e3125bd449501b9d228b207d31f2bc39c7f23262c674e0d1f47c0d0b" + "initCodeHash": "0x5cfb2feddcb8e7b29091226a95a8f00947f682a0620ea087d0a1a3a429fdf053", + "sourceCodeHash": "0x090fe170883616f935b6b30d0f6738264f5c1e7d4dc2a89d330317a7a48d5f6c" }, "src/L1/ProtocolVersions.sol": { "initCodeHash": "0x5a76c8530cb24cf23d3baacc6eefaac226382af13f1e2a35535d2ec2b0573b29", @@ -239,4 +243,4 @@ "initCodeHash": "0x2bfce526f82622288333d53ca3f43a0a94306ba1bab99241daa845f8f4b18bd4", "sourceCodeHash": "0xf49d7b0187912a6bb67926a3222ae51121e9239495213c975b3b4b217ee57a1b" } -} \ No newline at end of file +} diff --git a/packages/contracts-bedrock/snapshots/storageLayout/ETHLockbox.json b/packages/contracts-bedrock/snapshots/storageLayout/ETHLockbox.json new file mode 100644 index 0000000000000..effc5277149fc --- /dev/null +++ b/packages/contracts-bedrock/snapshots/storageLayout/ETHLockbox.json @@ -0,0 +1,16 @@ +[ + { + "bytes": "32", + "label": "authorizedPortals", + "offset": 0, + "slot": "0", + "type": "mapping(address => bool)" + }, + { + "bytes": "32", + "label": "authorizedLockboxes", + "offset": 0, + "slot": "1", + "type": "mapping(address => bool)" + } +] \ No newline at end of file diff --git a/packages/contracts-bedrock/snapshots/storageLayout/OPContractsManager.json b/packages/contracts-bedrock/snapshots/storageLayout/OPContractsManager.json index 3c1445a982928..68f5f65cc895d 100644 --- a/packages/contracts-bedrock/snapshots/storageLayout/OPContractsManager.json +++ b/packages/contracts-bedrock/snapshots/storageLayout/OPContractsManager.json @@ -14,7 +14,7 @@ "type": "struct OPContractsManager.Blueprints" }, { - "bytes": "384", + "bytes": "416", "label": "implementation", "offset": 0, "slot": "10", @@ -24,7 +24,7 @@ "bytes": "1", "label": "isRC", "offset": 0, - "slot": "22", + "slot": "23", "type": "bool" } ] \ No newline at end of file diff --git a/packages/contracts-bedrock/snapshots/storageLayout/OPContractsManagerInterop.json b/packages/contracts-bedrock/snapshots/storageLayout/OPContractsManagerInterop.json index 3c1445a982928..68f5f65cc895d 100644 --- a/packages/contracts-bedrock/snapshots/storageLayout/OPContractsManagerInterop.json +++ b/packages/contracts-bedrock/snapshots/storageLayout/OPContractsManagerInterop.json @@ -14,7 +14,7 @@ "type": "struct OPContractsManager.Blueprints" }, { - "bytes": "384", + "bytes": "416", "label": "implementation", "offset": 0, "slot": "10", @@ -24,7 +24,7 @@ "bytes": "1", "label": "isRC", "offset": 0, - "slot": "22", + "slot": "23", "type": "bool" } ] \ No newline at end of file diff --git a/packages/contracts-bedrock/snapshots/storageLayout/OPPrestateUpdater.json b/packages/contracts-bedrock/snapshots/storageLayout/OPPrestateUpdater.json index 3c1445a982928..68f5f65cc895d 100644 --- a/packages/contracts-bedrock/snapshots/storageLayout/OPPrestateUpdater.json +++ b/packages/contracts-bedrock/snapshots/storageLayout/OPPrestateUpdater.json @@ -14,7 +14,7 @@ "type": "struct OPContractsManager.Blueprints" }, { - "bytes": "384", + "bytes": "416", "label": "implementation", "offset": 0, "slot": "10", @@ -24,7 +24,7 @@ "bytes": "1", "label": "isRC", "offset": 0, - "slot": "22", + "slot": "23", "type": "bool" } ] \ No newline at end of file diff --git a/packages/contracts-bedrock/src/L1/ETHLockbox.sol b/packages/contracts-bedrock/src/L1/ETHLockbox.sol new file mode 100644 index 0000000000000..bbb3c433faf02 --- /dev/null +++ b/packages/contracts-bedrock/src/L1/ETHLockbox.sol @@ -0,0 +1,177 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.25; + +// Contracts +import { Initializable } from "@openzeppelin/contracts-v5/proxy/utils/Initializable.sol"; + +// Libraries +import { Storage } from "src/libraries/Storage.sol"; +import { Constants } from "src/libraries/Constants.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 { IProxyAdmin } from "interfaces/universal/IProxyAdmin.sol"; +import { IProxyAdminOwnable } from "interfaces/L1/IProxyAdminOwnable.sol"; + +/// @custom:proxied true +/// @title ETHLockbox +/// @notice Manages ETH liquidity locking and unlocking for authorized OptimismPortals, enabling unified ETH liquidity +/// management across chains in the superchain cluster. +contract ETHLockbox is Initializable, ISemver { + /// @notice Thrown when the lockbox is paused. + error ETHLockbox_Paused(); + + /// @notice Thrown when the caller is not authorized. + error ETHLockbox_Unauthorized(); + + /// @notice Thrown when an already authorized portal or lockbox attempts to be authorized again. + error ETHLockbox_AlreadyAuthorized(); + + /// @notice Thrown when attempting to unlock ETH from the lockbox through a withdrawal transaction. + error ETHLockbox_NoWithdrawalTransactions(); + + /// @notice Thrown when the admin owner of the lockbox is different from the admin owner of the proxy admin. + error ETHLockbox_DifferentAdminOwner(); + + /// @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. + event ETHLocked(address indexed portal, uint256 amount); + + /// @notice Emitted when ETH is unlocked from the lockbox by an authorized portal. + /// @param portal The address of the portal that unlocked the ETH. + /// @param amount The amount of ETH unlocked. + event ETHUnlocked(address indexed portal, uint256 amount); + + /// @notice Emitted when a portal is authorized to lock and unlock ETH. + /// @param portal The address of the portal that was authorized. + event PortalAuthorized(address indexed portal); + + /// @notice Emitted when an ETH lockbox is authorized to migrate its liquidity to the current ETH lockbox. + /// @param lockbox The address of the ETH lockbox that was authorized. + event LockboxAuthorized(address indexed lockbox); + + /// @notice Emitted when ETH liquidity is migrated from the current ETH lockbox to another. + /// @param lockbox The address of the ETH lockbox that was migrated. + event LiquidityMigrated(address indexed lockbox); + + /// @notice Emitted when ETH liquidity is received during an authorized lockbox migration. + /// @param lockbox The address of the ETH lockbox that received the liquidity. + event LiquidityReceived(address indexed lockbox); + + /// @notice The address of the SuperchainConfig contract. + bytes32 internal constant _SUPERCHAIN_CONFIG_SLOT = bytes32(uint256(keccak256("ETHLockbox.superchainConfig")) - 1); + + /// @notice Mapping of authorized portals. + mapping(address => bool) public authorizedPortals; + + /// @notice Mapping of authorized lockboxes. + mapping(address => bool) public authorizedLockboxes; + + /// @notice Semantic version. + /// @custom:semver 1.0.0-beta.1 + function version() public view virtual returns (string memory) { + return "1.0.0-beta.1"; + } + + /// @notice Constructs the ETHLockbox contract. + constructor() { + _disableInitializers(); + } + + /// @notice Initializer. + /// @param _superchainConfig The address of the SuperchainConfig contract. + function initialize(address _superchainConfig) external initializer { + Storage.setAddress(_SUPERCHAIN_CONFIG_SLOT, _superchainConfig); + } + + /// @notice Getter for the SuperchainConfig contract. + function superchainConfig() public view returns (ISuperchainConfig superchainConfig_) { + superchainConfig_ = ISuperchainConfig(Storage.getAddress(_SUPERCHAIN_CONFIG_SLOT)); + } + + /// @notice Getter for the owner of the proxy admin. + /// The ProxyAdmin is the owner of the Proxy contract, which is the proxy used for the ETHLockbox. + function adminOwner() public view returns (address) { + // Get the proxy admin address reading for the reserved slot it has on the Proxy contract. + IProxyAdmin proxyAdmin = IProxyAdmin(Storage.getAddress(Constants.PROXY_OWNER_ADDRESS)); + // Return the owner of the proxy admin. + return proxyAdmin.owner(); + } + + /// @notice Getter for the current paused status. + function paused() public view returns (bool) { + return superchainConfig().paused(); + } + + /// @notice Receives the ETH liquidity migrated from an authorized lockbox. + function receiveLiquidity() external payable { + if (!authorizedLockboxes[msg.sender]) revert ETHLockbox_Unauthorized(); + emit LiquidityReceived(msg.sender); + } + + /// @notice Locks ETH in the lockbox. + /// Called by an authorized portal on a deposit to lock the ETH value. + function lockETH() external payable { + if (!authorizedPortals[msg.sender]) revert ETHLockbox_Unauthorized(); + emit ETHLocked(msg.sender, msg.value); + } + + /// @notice Unlocks ETH from the lockbox. + /// Called by an authorized portal when finalizing a withdrawal that requires ETH. + /// Cannot be called if the lockbox is paused. + /// @param _value The amount of ETH to unlock. + function unlockETH(uint256 _value) external { + if (paused()) revert ETHLockbox_Paused(); + if (!authorizedPortals[msg.sender]) revert ETHLockbox_Unauthorized(); + /// NOTE: Check l2Sender is not set to avoid this function to be called as a target on a withdrawal transaction + if (IOptimismPortal(payable(msg.sender)).l2Sender() != Constants.DEFAULT_L2_SENDER) { + revert ETHLockbox_NoWithdrawalTransactions(); + } + + // Using `donateETH` to avoid triggering a deposit + IOptimismPortal(payable(msg.sender)).donateETH{ value: _value }(); + emit ETHUnlocked(msg.sender, _value); + } + + /// @notice Authorizes a portal to lock and unlock ETH. + /// @param _portal The address of the portal to authorize. + function authorizePortal(address _portal) external { + if (msg.sender != adminOwner()) revert ETHLockbox_Unauthorized(); + if (!_sameAdminOwner(_portal)) revert ETHLockbox_DifferentAdminOwner(); + if (authorizedPortals[_portal]) revert ETHLockbox_AlreadyAuthorized(); + + authorizedPortals[_portal] = true; + emit PortalAuthorized(_portal); + } + + /// @notice Authorizes an ETH lockbox to migrate its liquidity to the current ETH lockbox. + /// @param _lockbox The address of the ETH lockbox to authorize. + function authorizeLockbox(address _lockbox) external { + if (msg.sender != adminOwner()) revert ETHLockbox_Unauthorized(); + if (!_sameAdminOwner(_lockbox)) revert ETHLockbox_DifferentAdminOwner(); + if (authorizedLockboxes[_lockbox]) revert ETHLockbox_AlreadyAuthorized(); + + authorizedLockboxes[_lockbox] = true; + emit LockboxAuthorized(_lockbox); + } + + /// @notice Migrates liquidity from the current ETH lockbox to another. + /// @param _lockbox The address of the ETH lockbox to migrate liquidity to. + function migrateLiquidity(address _lockbox) external { + if (msg.sender != adminOwner()) revert ETHLockbox_Unauthorized(); + if (!_sameAdminOwner(_lockbox)) revert ETHLockbox_DifferentAdminOwner(); + + ETHLockbox(_lockbox).receiveLiquidity{ value: address(this).balance }(); + emit LiquidityMigrated(_lockbox); + } + + /// @notice Checks if the ProxyAdmin owner of the current contract is the same as the ProxyAdmin owner of the given + /// proxy. + /// @param _proxy The address of the proxy to check. + function _sameAdminOwner(address _proxy) internal view returns (bool) { + return adminOwner() == IProxyAdminOwnable(_proxy).adminOwner(); + } +} diff --git a/packages/contracts-bedrock/src/L1/OPContractsManager.sol b/packages/contracts-bedrock/src/L1/OPContractsManager.sol index 272b611c4b918..b3fbc5a6aa2d4 100644 --- a/packages/contracts-bedrock/src/L1/OPContractsManager.sol +++ b/packages/contracts-bedrock/src/L1/OPContractsManager.sol @@ -30,6 +30,7 @@ import { IL1ERC721Bridge } from "interfaces/L1/IL1ERC721Bridge.sol"; import { IL1StandardBridge } from "interfaces/L1/IL1StandardBridge.sol"; import { IOptimismMintableERC20Factory } from "interfaces/universal/IOptimismMintableERC20Factory.sol"; import { IHasSuperchainConfig } from "interfaces/L1/IHasSuperchainConfig.sol"; +import { IETHLockbox } from "interfaces/L1/IETHLockbox.sol"; contract OPContractsManager is ISemver { // -------- Structs -------- @@ -73,6 +74,7 @@ contract OPContractsManager is ISemver { IOptimismMintableERC20Factory optimismMintableERC20FactoryProxy; IL1StandardBridge l1StandardBridgeProxy; IL1CrossDomainMessenger l1CrossDomainMessengerProxy; + IETHLockbox ethLockboxProxy; // Fault proof contracts below. IOptimismPortal2 optimismPortalProxy; IDisputeGameFactory disputeGameFactoryProxy; @@ -106,6 +108,7 @@ contract OPContractsManager is ISemver { address protocolVersionsImpl; address l1ERC721BridgeImpl; address optimismPortalImpl; + address ethLockboxImpl; address systemConfigImpl; address optimismMintableERC20FactoryImpl; address l1CrossDomainMessengerImpl; @@ -307,6 +310,7 @@ contract OPContractsManager is ISemver { IL1ERC721Bridge(deployProxy(l2ChainId, output.opChainProxyAdmin, saltMixer, "L1ERC721Bridge")); output.optimismPortalProxy = IOptimismPortal2(payable(deployProxy(l2ChainId, output.opChainProxyAdmin, saltMixer, "OptimismPortal"))); + output.ethLockboxProxy = IETHLockbox(deployProxy(l2ChainId, output.opChainProxyAdmin, saltMixer, "ETHLockbox")); output.systemConfigProxy = ISystemConfig(deployProxy(l2ChainId, output.opChainProxyAdmin, saltMixer, "SystemConfig")); output.optimismMintableERC20FactoryProxy = IOptimismMintableERC20Factory( @@ -393,6 +397,11 @@ contract OPContractsManager is ISemver { output.opChainProxyAdmin, address(output.optimismPortalProxy), implementation.optimismPortalImpl, data ); + data = encodeETHLockboxInitializer(); + upgradeToAndCall(output.opChainProxyAdmin, address(output.ethLockboxProxy), implementation.ethLockboxImpl, data); + // Besides initializing with the `SuperchainConfig`, authorize the `OptimismPortal` on the `ETHLockbox`. + output.ethLockboxProxy.authorizePortal(address(output.optimismPortalProxy)); + data = encodeSystemConfigInitializer(_input, output); upgradeToAndCall( output.opChainProxyAdmin, address(output.systemConfigProxy), implementation.systemConfigImpl, data @@ -856,6 +865,11 @@ contract OPContractsManager is ISemver { ); } + /// @notice Helper method for encoding the ETHLockbox initializer data. + function encodeETHLockboxInitializer() internal view virtual returns (bytes memory) { + return abi.encodeCall(IETHLockbox.initialize, (address(superchainConfig))); + } + /// @notice Helper method for encoding the SystemConfig initializer data. function encodeSystemConfigInitializer( DeployInput memory _input, diff --git a/packages/contracts-bedrock/src/L1/OPPrestateUpdater.sol b/packages/contracts-bedrock/src/L1/OPPrestateUpdater.sol index dd6f50989fec0..0e1b44dd8bc65 100644 --- a/packages/contracts-bedrock/src/L1/OPPrestateUpdater.sol +++ b/packages/contracts-bedrock/src/L1/OPPrestateUpdater.sol @@ -27,9 +27,9 @@ contract OPPrestateUpdater is OPContractsManager { error PrestateRequired(); // @return Version string - /// @custom:semver 1.7.1 + /// @custom:semver 1.7.2 function version() public pure override returns (string memory) { - return "1.7.1"; + return "1.7.2"; } // @notice Constructs the OPPrestateUpdater contract @@ -52,6 +52,7 @@ contract OPPrestateUpdater is OPContractsManager { address(0), //protocolVersionsImpl address(0), //l1ERC721BridgeImpl address(0), //optimismPortalImpl + address(0), //ethLockboxImpl address(0), //systemConfigImpl address(0), //optimismMintableERC20FactoryImpl address(0), //l1CrossDomainMessengerImpl diff --git a/packages/contracts-bedrock/src/L1/OptimismPortal2.sol b/packages/contracts-bedrock/src/L1/OptimismPortal2.sol index b567caf8eacc0..3d593c549cf7c 100644 --- a/packages/contracts-bedrock/src/L1/OptimismPortal2.sol +++ b/packages/contracts-bedrock/src/L1/OptimismPortal2.sol @@ -15,6 +15,7 @@ import { Hashing } from "src/libraries/Hashing.sol"; import { SecureMerkleTrie } from "src/libraries/trie/SecureMerkleTrie.sol"; import { AddressAliasHelper } from "src/vendor/AddressAliasHelper.sol"; import { GameStatus, GameType } from "src/dispute/lib/Types.sol"; +import { Storage } from "src/libraries/Storage.sol"; // Interfaces import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; @@ -25,6 +26,7 @@ import { ISuperchainConfig } from "interfaces/L1/ISuperchainConfig.sol"; import { IDisputeGameFactory } from "interfaces/dispute/IDisputeGameFactory.sol"; import { IDisputeGame } from "interfaces/dispute/IDisputeGame.sol"; import { IAnchorStateRegistry } from "interfaces/dispute/IAnchorStateRegistry.sol"; +import { IProxyAdmin } from "interfaces/universal/IProxyAdmin.sol"; /// @custom:proxied true /// @title OptimismPortal2 @@ -200,9 +202,9 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ISemver { } /// @notice Semantic version. - /// @custom:semver 4.0.0 + /// @custom:semver 4.0.1 function version() public pure virtual returns (string memory) { - return "4.0.0"; + return "4.0.1"; } /// @param _proofMaturityDelaySeconds The proof maturity delay in seconds. @@ -263,6 +265,15 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ISemver { return superchainConfig.guardian(); } + /// @notice Getter for the owner of the proxy admin. + /// The ProxyAdmin is the owner of the Proxy contract, which is the proxy used for the ETHLockbox. + function adminOwner() public view returns (address) { + // Get the proxy admin address reading for the reserved slot it has on the Proxy contract. + IProxyAdmin proxyAdmin = IProxyAdmin(Storage.getAddress(Constants.PROXY_OWNER_ADDRESS)); + // Return the owner of the proxy admin. + return proxyAdmin.owner(); + } + /// @custom:legacy /// @notice Getter for the dispute game finality delay. function disputeGameFinalityDelaySeconds() external view returns (uint256) { diff --git a/packages/contracts-bedrock/test/L1/ETHLockbox.t.sol b/packages/contracts-bedrock/test/L1/ETHLockbox.t.sol new file mode 100644 index 0000000000000..bb300fb07db12 --- /dev/null +++ b/packages/contracts-bedrock/test/L1/ETHLockbox.t.sol @@ -0,0 +1,472 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.15; + +// Testing utilities +import { Constants } from "src/libraries/Constants.sol"; + +// Interfaces +import { IOptimismPortal2 as IOptimismPortal } from "interfaces/L1/IOptimismPortal2.sol"; + +import { IETHLockbox } from "interfaces/L1/IETHLockbox.sol"; + +import { ISuperchainConfig } from "interfaces/L1/ISuperchainConfig.sol"; +import { IProxyAdminOwnable } from "interfaces/L1/IProxyAdminOwnable.sol"; + +// Test +import { CommonTest } from "test/setup/CommonTest.sol"; + +import { Predeploys } from "src/libraries/Predeploys.sol"; +import { ProxyAdmin } from "src/universal/ProxyAdmin.sol"; + +contract ETHLockboxTest is CommonTest { + error InvalidInitialization(); + + event ETHLocked(address indexed portal, uint256 amount); + event ETHUnlocked(address indexed portal, uint256 amount); + event PortalAuthorized(address indexed portal); + event LockboxAuthorized(address indexed lockbox); + event LiquidityMigrated(address indexed lockbox); + event LiquidityReceived(address indexed lockbox); + + ProxyAdmin public proxyAdmin = ProxyAdmin(Predeploys.PROXY_ADMIN); + address public adminOwner; + + function setUp() public virtual override { + super.setUp(); + adminOwner = proxyAdmin.owner(); + } + + /// @notice Tests the superchain config was correctly set during initialization. + function test_initialization_succeeds() public view { + assertEq(address(ethLockbox.superchainConfig()), address(superchainConfig)); + } + + /// @notice Tests it reverts when the contract is already initialized. + function test_initialize_alreadyInitialized_reverts() public { + vm.expectRevert(InvalidInitialization.selector); + ethLockbox.initialize(address(superchainConfig)); + } + + /// @notice Tests the proxy admin owner is correctly returned. + function test_proxyAdminOwner_succeeds() public view { + assertEq(ethLockbox.adminOwner(), adminOwner); + } + + /// @notice Tests the paused status is correctly returned. + function test_paused_succeeds() public { + // Assert the paused status is false + assertEq(ethLockbox.paused(), false); + + // Mock the superchain config to return true for the paused status + vm.mockCall(address(superchainConfig), abi.encodeCall(ISuperchainConfig.paused, ()), abi.encode(true)); + + // Assert the paused status is true + assertEq(ethLockbox.paused(), true); + } + + /// @notice Tests the liquidity is correctly received. + function testFuzz_receiveLiquidity_succeeds(address _lockbox, uint256 _value) public { + vm.assume(!ethLockbox.authorizedLockboxes(_lockbox)); + + // Deal the value to the lockbox + deal(address(_lockbox), _value); + + // Mock the admin owner of the lockbox to be the same as the current lockbox proxy admin owner + vm.mockCall( + address(_lockbox), abi.encodeCall(IProxyAdminOwnable.adminOwner, ()), abi.encode(proxyAdmin.owner()) + ); + + // Authorize the lockbox + vm.prank(adminOwner); + ethLockbox.authorizeLockbox(_lockbox); + + // Get the balance of the lockbox before the receive + uint256 _lockboxBalanceBefore = address(ethLockbox).balance; + + // Expect the `LiquidityReceived` event to be emitted + vm.expectEmit(address(ethLockbox)); + emit LiquidityReceived(_lockbox); + + // Call the `receiveLiquidity` function + vm.prank(address(_lockbox)); + ethLockbox.receiveLiquidity{ value: _value }(); + + // Assert the lockbox's balance increased by the amount received + assertEq(address(ethLockbox).balance, _lockboxBalanceBefore + _value); + } + + /// @notice Tests it reverts when the caller is not an authorized portal. + function testFuzz_lockETH_unauthorizedPortal_reverts(address _caller) public { + vm.assume(!ethLockbox.authorizedPortals(_caller)); + + // Expect the revert with `Unauthorized` selector + vm.expectRevert(IETHLockbox.ETHLockbox_Unauthorized.selector); + + // Call the `lockETH` function with an unauthorized caller + vm.prank(_caller); + ethLockbox.lockETH(); + } + + /// @notice Tests the ETH is correctly locked when the caller is an authorized portal. + function testFuzz_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(ethLockbox).balance; + + // Look for the emit of the `ETHLocked` event + vm.expectEmit(address(ethLockbox)); + emit ETHLocked(address(optimismPortal2), _amount); + + // Call the `lockETH` function with the portal + vm.prank(address(optimismPortal2)); + ethLockbox.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(ethLockbox).balance, _lockboxBalanceBefore + _amount); + } + + /// @notice Tests the ETH is correctly locked when the caller is an authorized portal with different portals. + function testFuzz_lockETH_multiplePortals_succeeds(address _portal, uint256 _amount) public { + vm.assume(_portal != address(ethLockbox)); + + // Mock the admin owner of the portal to be the same as the current lockbox proxy admin owner + vm.mockCall(address(_portal), abi.encodeCall(IProxyAdminOwnable.adminOwner, ()), abi.encode(proxyAdmin.owner())); + + // Set the portal as an authorized portal + vm.prank(adminOwner); + ethLockbox.authorizePortal(_portal); + + // Deal the ETH amount to the portal + vm.deal(_portal, _amount); + + // Get the balance of the portal and lockbox before the lock to compare later on the assertions + uint256 _portalBalanceBefore = address(_portal).balance; + uint256 _lockboxBalanceBefore = address(ethLockbox).balance; + + // Look for the emit of the `ETHLocked` event + vm.expectEmit(address(ethLockbox)); + emit ETHLocked(_portal, _amount); + + // Call the `lockETH` function with the portal + vm.prank(_portal); + ethLockbox.lockETH{ value: _amount }(); + + // Assert the portal's balance decreased and the lockbox's balance increased by the amount locked + assertEq(address(_portal).balance, _portalBalanceBefore - _amount); + assertEq(address(ethLockbox).balance, _lockboxBalanceBefore + _amount); + } + + /// @notice Tests `unlockETH` reverts when the contract is paused. + function testFuzz_unlockETH_paused_reverts(address _caller, uint256 _value) public { + // Mock the superchain config to return true for the paused status + vm.mockCall(address(superchainConfig), abi.encodeCall(ISuperchainConfig.paused, ()), abi.encode(true)); + + // Expect the revert with `Paused` selector + vm.expectRevert(IETHLockbox.ETHLockbox_Paused.selector); + + // Call the `unlockETH` function with the caller + vm.prank(_caller); + ethLockbox.unlockETH(_value); + } + + /// @notice Tests it reverts when the caller is not an authorized portal. + function testFuzz_unlockETH_unauthorizedPortal_reverts(address _caller, uint256 _value) public { + vm.assume(!ethLockbox.authorizedPortals(_caller)); + + // Expect the revert with `Unauthorized` selector + vm.expectRevert(IETHLockbox.ETHLockbox_Unauthorized.selector); + + // Call the `unlockETH` function with an unauthorized caller + vm.prank(_caller); + ethLockbox.unlockETH(_value); + } + + /// @notice Tests `unlockETH` reverts when the portal is not the L2 sender to prevent unlocking ETH from the lockbox + /// through a withdrawal transaction. + function testFuzz_unlockETH_withdrawalTransaction_reverts(uint256 _value, address _l2Sender) public { + vm.assume(_l2Sender != Constants.DEFAULT_L2_SENDER); + + // Mock the L2 sender + vm.mockCall(address(optimismPortal2), abi.encodeCall(IOptimismPortal.l2Sender, ()), abi.encode(_l2Sender)); + + // Expect the revert with `NoWithdrawalTransactions` selector + vm.expectRevert(IETHLockbox.ETHLockbox_NoWithdrawalTransactions.selector); + + // Call the `unlockETH` function with the portal + vm.prank(address(optimismPortal2)); + ethLockbox.unlockETH(_value); + } + + /// @notice Tests the ETH is correctly unlocked when the caller is an authorized portal. + function testFuzz_unlockETH_succeeds(uint256 _value) public { + // Deal the ETH amount to the lockbox + vm.deal(address(ethLockbox), _value); + + // Get the balance of the portal and lockbox before the unlock to compare later on the assertions + uint256 portalBalanceBefore = address(optimismPortal2).balance; + uint256 lockboxBalanceBefore = address(ethLockbox).balance; + + // Expect `donateETH` function to be called on Portal + vm.expectCall(address(optimismPortal2), abi.encodeCall(IOptimismPortal.donateETH, ())); + + // Look for the emit of the `ETHUnlocked` event + vm.expectEmit(address(ethLockbox)); + emit ETHUnlocked(address(optimismPortal2), _value); + + // Call the `unlockETH` function with the portal + vm.prank(address(optimismPortal2)); + ethLockbox.unlockETH(_value); + + // Assert the portal's balance increased and the lockbox's balance decreased by the amount unlocked + assertEq(address(optimismPortal2).balance, portalBalanceBefore + _value); + assertEq(address(ethLockbox).balance, lockboxBalanceBefore - _value); + } + + /// @notice Tests the ETH is correctly unlocked when the caller is an authorized portal. + function testFuzz_unlockETH_multiplePortals_succeeds(address _portal, uint256 _value) public { + vm.assume(_portal != address(ethLockbox)); + + // Mock the admin owner of the portal to be the same as the current lockbox proxy admin owner + vm.mockCall(address(_portal), abi.encodeCall(IProxyAdminOwnable.adminOwner, ()), abi.encode(proxyAdmin.owner())); + + // Set the portal as an authorized portal + vm.prank(adminOwner); + ethLockbox.authorizePortal(_portal); + + // Deal the ETH amount to the lockbox + vm.deal(address(ethLockbox), _value); + + // Get the balance of the portal and lockbox before the unlock to compare later on the assertions + uint256 portalBalanceBefore = address(optimismPortal2).balance; + uint256 lockboxBalanceBefore = address(ethLockbox).balance; + + // Expect `donateETH` function to be called on Portal + vm.expectCall(address(optimismPortal2), abi.encodeCall(IOptimismPortal.donateETH, ())); + + // Look for the emit of the `ETHUnlocked` event + vm.expectEmit(address(ethLockbox)); + emit ETHUnlocked(address(optimismPortal2), _value); + + // Call the `unlockETH` function with the portal + vm.prank(address(optimismPortal2)); + ethLockbox.unlockETH(_value); + + // Assert the portal's balance increased and the lockbox's balance decreased by the amount unlocked + assertEq(address(optimismPortal2).balance, portalBalanceBefore + _value); + assertEq(address(ethLockbox).balance, lockboxBalanceBefore - _value); + } + + /// @notice Tests the `authorizePortal` function reverts when the caller is not the proxy admin. + function testFuzz_authorizePortal_unauthorized_reverts(address _caller) public { + vm.assume(_caller != proxyAdmin.owner()); + + // Expect the revert with `Unauthorized` selector + vm.expectRevert(IETHLockbox.ETHLockbox_Unauthorized.selector); + + // Call the `authorizePortal` function with an unauthorized caller + vm.prank(_caller); + ethLockbox.authorizePortal(address(optimismPortal2)); + } + + /// @notice Tests the `authorizePortal` function reverts when the portal is already authorized. + function testFuzz_authorizePortal_alreadyAuthorized_reverts(address _portal) public { + // Authorize the portal + if (!ethLockbox.authorizedPortals(_portal)) { + // Mock the admin owner of the portal to be the same as the current lockbox proxy admin owner + vm.mockCall( + address(_portal), abi.encodeCall(IProxyAdminOwnable.adminOwner, ()), abi.encode(proxyAdmin.owner()) + ); + + // Authorize the portal + vm.prank(adminOwner); + ethLockbox.authorizePortal(_portal); + } + + // Expect the revert with `AlreadyAuthorized` selector + vm.expectRevert(IETHLockbox.ETHLockbox_AlreadyAuthorized.selector); + + // Call the `authorizePortal` function with the portal + vm.prank(adminOwner); + ethLockbox.authorizePortal(_portal); + } + + /// @notice Tests the `authorizePortal` function reverts when the admin owner of the portal is not the same as the + /// admin owner of the lockbox. + function testFuzz_authorizePortal_differentAdminOwner_reverts(address _portal) public { + vm.mockCall(address(_portal), abi.encodeCall(IProxyAdminOwnable.adminOwner, ()), abi.encode(address(0))); + + // Expect the revert with `DifferentOwner` selector + vm.expectRevert(IETHLockbox.ETHLockbox_DifferentAdminOwner.selector); + + // Call the `authorizePortal` function + vm.prank(adminOwner); + ethLockbox.authorizePortal(_portal); + } + + /// @notice Tests the `authorizeLockbox` function succeeds using the `optimismPortal2` address as the portal. + function test_authorizePortal_succeeds() public { + // Calculate the correct storage slot for the mapping value + bytes32 mappingSlot = bytes32(uint256(0)); // position on the layout + address key = address(optimismPortal2); + bytes32 slot = keccak256(abi.encode(key, mappingSlot)); + + // Reset the authorization status to false + vm.store(address(ethLockbox), slot, bytes32(0)); + + // Expect the `PortalAuthorized` event to be emitted + vm.expectEmit(address(ethLockbox)); + emit PortalAuthorized(address(optimismPortal2)); + + // Call the `authorizePortal` function with the portal + vm.prank(adminOwner); + ethLockbox.authorizePortal(address(optimismPortal2)); + + // Assert the portal is authorized + assertTrue(ethLockbox.authorizedPortals(address(optimismPortal2))); + } + + /// @notice Tests the `authorizeLockbox` function succeeds + function testFuzz_authorizePortal_succeeds(address _portal) public { + vm.assume(!ethLockbox.authorizedPortals(_portal)); + + // Mock the admin owner of the portal to be the same as the current lockbox proxy admin owner + vm.mockCall(address(_portal), abi.encodeCall(IProxyAdminOwnable.adminOwner, ()), abi.encode(proxyAdmin.owner())); + + // Expect the `PortalAuthorized` event to be emitted + vm.expectEmit(address(ethLockbox)); + emit PortalAuthorized(_portal); + + // Call the `authorizePortal` function with the portal + vm.prank(adminOwner); + ethLockbox.authorizePortal(_portal); + + // Assert the portal is authorized + assertTrue(ethLockbox.authorizedPortals(_portal)); + } + + /// @notice Tests the `authorizeLockbox` function reverts when the caller is not the proxy admin. + function testFuzz_authorizeLockbox_unauthorized_reverts(address _caller) public { + vm.assume(_caller != proxyAdmin.owner()); + + // Expect the revert with `Unauthorized` selector + vm.expectRevert(IETHLockbox.ETHLockbox_Unauthorized.selector); + + // Call the `authorizeLockbox` function with an unauthorized caller + vm.prank(_caller); + ethLockbox.authorizeLockbox(address(optimismPortal2)); + } + + /// @notice Tests the `authorizeLockbox` function reverts when the lockbox is already authorized. + function testFuzz_authorizeLockbox_alreadyAuthorized_reverts(address _lockbox) public { + // Authorize the lockbox + if (!ethLockbox.authorizedLockboxes(_lockbox)) { + vm.mockCall( + address(_lockbox), abi.encodeCall(IProxyAdminOwnable.adminOwner, ()), abi.encode(proxyAdmin.owner()) + ); + + vm.prank(adminOwner); + ethLockbox.authorizeLockbox(_lockbox); + } + + // Expect the revert with `AlreadyAuthorized` selector + vm.expectRevert(IETHLockbox.ETHLockbox_AlreadyAuthorized.selector); + + // Call the `authorizeLockbox` function with the lockbox + vm.prank(adminOwner); + ethLockbox.authorizeLockbox(_lockbox); + } + + /// @notice Tests the `authorizeLockbox` function reverts when the admin owner of the lockbox is not the same as the + /// admin owner of the proxy admin. + function testFuzz_authorizeLockbox_differentAdminOwner_reverts(address _lockbox) public { + vm.mockCall(address(_lockbox), abi.encodeCall(IProxyAdminOwnable.adminOwner, ()), abi.encode(address(0))); + + // Expect the revert with `ETHLockbox_DifferentAdminOwner` selector + vm.expectRevert(IETHLockbox.ETHLockbox_DifferentAdminOwner.selector); + + // Call the `authorizeLockbox` function with the lockbox + vm.prank(adminOwner); + ethLockbox.authorizeLockbox(_lockbox); + } + + /// @notice Tests the `authorizeLockbox` function succeeds + function testFuzz_authorizeLockbox_succeeds(address _lockbox) public { + vm.assume(!ethLockbox.authorizedLockboxes(_lockbox)); + + // Mock the admin owner of the lockbox to be the same as the current lockbox proxy admin owner + vm.mockCall( + address(_lockbox), abi.encodeCall(IProxyAdminOwnable.adminOwner, ()), abi.encode(proxyAdmin.owner()) + ); + + // Expect the `LockboxAuthorized` event to be emitted + vm.expectEmit(address(ethLockbox)); + emit LockboxAuthorized(_lockbox); + + // Authorize the lockbox + vm.prank(adminOwner); + ethLockbox.authorizeLockbox(_lockbox); + + // Assert the lockbox is authorized + assertTrue(ethLockbox.authorizedLockboxes(_lockbox)); + } + + /// @notice Tests the `migrateLiquidity` function reverts when the caller is not the proxy admin. + function testFuzz_migrateLiquidity_unauthorized_reverts(address _caller) public { + vm.assume(_caller != proxyAdmin.owner()); + + // Expect the revert with `Unauthorized` selector + vm.expectRevert(IETHLockbox.ETHLockbox_Unauthorized.selector); + + // Call the `migrateLiquidity` function with an unauthorized caller + vm.prank(_caller); + ethLockbox.migrateLiquidity(address(optimismPortal2)); + } + + /// @notice Tests the `migrateLiquidity` function reverts when the admin owner of the lockbox is not the same as the + /// admin owner of the proxy admin. + function testFuzz_migrateLiquidity_differentAdminOwner_reverts(address _lockbox) public { + vm.mockCall(address(_lockbox), abi.encodeCall(IProxyAdminOwnable.adminOwner, ()), abi.encode(address(0))); + + // Expect the revert with `ETHLockbox_DifferentAdminOwner` selector + vm.expectRevert(IETHLockbox.ETHLockbox_DifferentAdminOwner.selector); + + // Call the `migrateLiquidity` function with the lockbox + vm.prank(adminOwner); + ethLockbox.migrateLiquidity(_lockbox); + } + + /// @notice Tests the `migrateLiquidity` function succeeds + function testFuzz_migrateLiquidity_succeeds(uint256 _balance, address _lockbox) public { + // Mock on the lockbox that will receive the migration for it to succeed + vm.mockCall( + address(_lockbox), abi.encodeCall(IProxyAdminOwnable.adminOwner, ()), abi.encode(proxyAdmin.owner()) + ); + vm.mockCall( + address(_lockbox), abi.encodeCall(IETHLockbox.authorizedLockboxes, (address(ethLockbox))), abi.encode(true) + ); + vm.mockCall(address(_lockbox), abi.encodeCall(IETHLockbox.receiveLiquidity, ()), abi.encode(true)); + + // Deal the balance to the lockbox + deal(address(_lockbox), _balance); + + // Expect the `LiquidityMigrated` event to be emitted + vm.expectEmit(address(ethLockbox)); + emit LiquidityMigrated(_lockbox); + + // Get balances before the migration + uint256 ethLockboxBalanceBefore = address(ethLockbox).balance; + uint256 newLockboxBalanceBefore = address(_lockbox).balance; + + // Call the `migrateLiquidity` function with the lockbox + vm.prank(adminOwner); + ethLockbox.migrateLiquidity(_lockbox); + + // Assert the liquidity was migrated + assertEq(address(_lockbox).balance, newLockboxBalanceBefore + ethLockboxBalanceBefore); + assertEq(address(ethLockbox).balance, 0); + } +} diff --git a/packages/contracts-bedrock/test/L1/OPContractsManager.t.sol b/packages/contracts-bedrock/test/L1/OPContractsManager.t.sol index b597cd52497e8..890c0d8bfcb1b 100644 --- a/packages/contracts-bedrock/test/L1/OPContractsManager.t.sol +++ b/packages/contracts-bedrock/test/L1/OPContractsManager.t.sol @@ -39,6 +39,7 @@ import { IFaultDisputeGame } from "interfaces/dispute/IFaultDisputeGame.sol"; import { ISystemConfig } from "interfaces/L1/ISystemConfig.sol"; import { IOPContractsManager } from "interfaces/L1/IOPContractsManager.sol"; import { ISemver } from "interfaces/universal/ISemver.sol"; +import { IETHLockbox } from "interfaces/L1/IETHLockbox.sol"; // Contracts import { OPContractsManager } from "src/L1/OPContractsManager.sol"; @@ -298,6 +299,7 @@ contract OPContractsManager_Upgrade_Harness is CommonTest { expectEmitUpgraded(impls.l1ERC721BridgeImpl, address(l1ERC721Bridge)); expectEmitUpgraded(impls.disputeGameFactoryImpl, address(disputeGameFactory)); expectEmitUpgraded(impls.optimismPortalImpl, address(optimismPortal2)); + expectEmitUpgraded(impls.ethLockboxImpl, address(ethLockbox)); expectEmitUpgraded(impls.optimismMintableERC20FactoryImpl, address(l1OptimismMintableERC20Factory)); vm.expectEmit(address(newAnchorStateRegistryProxy)); emit AdminChanged(address(0), address(proxyAdmin)); @@ -339,6 +341,7 @@ contract OPContractsManager_Upgrade_Harness is CommonTest { assertEq(impls.l1ERC721BridgeImpl, EIP1967Helper.getImplementation(address(l1ERC721Bridge))); assertEq(impls.disputeGameFactoryImpl, EIP1967Helper.getImplementation(address(disputeGameFactory))); assertEq(impls.optimismPortalImpl, EIP1967Helper.getImplementation(address(optimismPortal2))); + assertEq(impls.ethLockboxImpl, EIP1967Helper.getImplementation(address(ethLockbox))); assertEq( impls.optimismMintableERC20FactoryImpl, EIP1967Helper.getImplementation(address(l1OptimismMintableERC20Factory)) @@ -589,6 +592,10 @@ contract OPContractsManager_AddGameType_Test is Test { _name: "OptimismPortal2", _args: DeployUtils.encodeConstructor(abi.encodeCall(IOptimismPortal2.__constructor__, (1))) }), + ethLockboxImpl: DeployUtils.create1({ + _name: "ETHLockbox", + _args: DeployUtils.encodeConstructor(abi.encodeCall(IETHLockbox.__constructor__, ())) + }), systemConfigImpl: DeployUtils.create1({ _name: "SystemConfig", _args: DeployUtils.encodeConstructor(abi.encodeCall(ISystemConfig.__constructor__, ())) diff --git a/packages/contracts-bedrock/test/L1/OPPrestateUpdater.t.sol b/packages/contracts-bedrock/test/L1/OPPrestateUpdater.t.sol index 56edac1f8fa2b..c2ea6f42b15e6 100644 --- a/packages/contracts-bedrock/test/L1/OPPrestateUpdater.t.sol +++ b/packages/contracts-bedrock/test/L1/OPPrestateUpdater.t.sol @@ -29,6 +29,7 @@ import { IOPContractsManager } from "interfaces/L1/IOPContractsManager.sol"; import { IDisputeGameFactory } from "interfaces/dispute/IDisputeGameFactory.sol"; import { ISystemConfig } from "interfaces/L1/ISystemConfig.sol"; import { IOPPrestateUpdater } from "interfaces/L1/IOPPrestateUpdater.sol"; +import { IETHLockbox } from "interfaces/L1/IETHLockbox.sol"; // Contracts import { OPContractsManager } from "src/L1/OPContractsManager.sol"; @@ -88,6 +89,10 @@ contract OPPrestateUpdater_Test is Test { _name: "OptimismPortal2", _args: DeployUtils.encodeConstructor(abi.encodeCall(IOptimismPortal2.__constructor__, (1))) }), + ethLockboxImpl: DeployUtils.create1({ + _name: "ETHLockbox", + _args: DeployUtils.encodeConstructor(abi.encodeCall(IETHLockbox.__constructor__, ())) + }), systemConfigImpl: DeployUtils.create1({ _name: "SystemConfig", _args: DeployUtils.encodeConstructor(abi.encodeCall(ISystemConfig.__constructor__, ())) diff --git a/packages/contracts-bedrock/test/L1/OptimismPortal2.t.sol b/packages/contracts-bedrock/test/L1/OptimismPortal2.t.sol index a2b55af457bc9..6fc7d0e3490b0 100644 --- a/packages/contracts-bedrock/test/L1/OptimismPortal2.t.sol +++ b/packages/contracts-bedrock/test/L1/OptimismPortal2.t.sol @@ -9,6 +9,7 @@ import { EIP1967Helper } from "test/mocks/EIP1967Helper.sol"; // Contracts import { SuperchainConfig } from "src/L1/SuperchainConfig.sol"; +import { ProxyAdmin } from "src/universal/ProxyAdmin.sol"; // Libraries import { Types } from "src/libraries/Types.sol"; @@ -17,6 +18,7 @@ import { Constants } from "src/libraries/Constants.sol"; import { AddressAliasHelper } from "src/vendor/AddressAliasHelper.sol"; import { EIP1967Helper } from "test/mocks/EIP1967Helper.sol"; import "src/dispute/lib/Types.sol"; +import { Predeploys } from "src/libraries/Predeploys.sol"; // Interfaces import { IResourceMetering } from "interfaces/L1/IResourceMetering.sol"; @@ -140,6 +142,11 @@ contract OptimismPortal2_Test is CommonTest { assertEq(optimismPortal2.paused(), true); } + /// @notice Tests the proxy admin owner is correctly returned. + function test_proxyAdminOwner_succeeds() public view { + assertEq(optimismPortal2.adminOwner(), ProxyAdmin(Predeploys.PROXY_ADMIN).owner()); + } + /// @dev Tests that `receive` successdully deposits ETH. function testFuzz_receive_succeeds(uint256 _value) external { uint256 balanceBefore = address(optimismPortal2).balance; diff --git a/packages/contracts-bedrock/test/opcm/DeployImplementations.t.sol b/packages/contracts-bedrock/test/opcm/DeployImplementations.t.sol index 1f54630223a71..1551f06937626 100644 --- a/packages/contracts-bedrock/test/opcm/DeployImplementations.t.sol +++ b/packages/contracts-bedrock/test/opcm/DeployImplementations.t.sol @@ -25,6 +25,7 @@ import { IL1StandardBridge } from "interfaces/L1/IL1StandardBridge.sol"; import { IOptimismMintableERC20Factory } from "interfaces/universal/IOptimismMintableERC20Factory.sol"; import { IProxyAdmin } from "interfaces/universal/IProxyAdmin.sol"; import { IProxy } from "interfaces/universal/IProxy.sol"; +import { IETHLockbox } from "interfaces/L1/IETHLockbox.sol"; import { DeployImplementationsInput, @@ -89,6 +90,7 @@ contract DeployImplementationsOutput_Test is Test { function test_set_succeeds() public { IOPContractsManager opcm = IOPContractsManager(address(makeAddr("opcm"))); IOptimismPortal2 optimismPortalImpl = IOptimismPortal2(payable(makeAddr("optimismPortalImpl"))); + IETHLockbox ethLockboxImpl = IETHLockbox(payable(makeAddr("ethLockboxImpl"))); IDelayedWETH delayedWETHImpl = IDelayedWETH(payable(makeAddr("delayedWETHImpl"))); IPreimageOracle preimageOracleSingleton = IPreimageOracle(makeAddr("preimageOracleSingleton")); IMIPS mipsSingleton = IMIPS(makeAddr("mipsSingleton")); @@ -104,6 +106,7 @@ contract DeployImplementationsOutput_Test is Test { vm.etch(address(opcm), hex"01"); vm.etch(address(optimismPortalImpl), hex"01"); + vm.etch(address(ethLockboxImpl), hex"01"); vm.etch(address(delayedWETHImpl), hex"01"); vm.etch(address(preimageOracleSingleton), hex"01"); vm.etch(address(mipsSingleton), hex"01"); @@ -116,6 +119,7 @@ contract DeployImplementationsOutput_Test is Test { vm.etch(address(anchorStateRegistryImpl), hex"01"); dio.set(dio.opcm.selector, address(opcm)); dio.set(dio.optimismPortalImpl.selector, address(optimismPortalImpl)); + dio.set(dio.ethLockboxImpl.selector, address(ethLockboxImpl)); dio.set(dio.delayedWETHImpl.selector, address(delayedWETHImpl)); dio.set(dio.preimageOracleSingleton.selector, address(preimageOracleSingleton)); dio.set(dio.mipsSingleton.selector, address(mipsSingleton)); @@ -139,6 +143,7 @@ contract DeployImplementationsOutput_Test is Test { assertEq(address(optimismMintableERC20FactoryImpl), address(dio.optimismMintableERC20FactoryImpl()), "900"); assertEq(address(disputeGameFactoryImpl), address(dio.disputeGameFactoryImpl()), "950"); assertEq(address(anchorStateRegistryImpl), address(dio.anchorStateRegistryImpl()), "960"); + assertEq(address(ethLockboxImpl), address(dio.ethLockboxImpl()), "1000"); } function test_getters_whenNotSet_reverts() public { @@ -147,6 +152,8 @@ contract DeployImplementationsOutput_Test is Test { vm.expectRevert(expectedErr); dio.optimismPortalImpl(); + vm.expectRevert(expectedErr); + dio.ethLockboxImpl(); vm.expectRevert(expectedErr); dio.delayedWETHImpl(); @@ -186,6 +193,10 @@ contract DeployImplementationsOutput_Test is Test { vm.expectRevert(expectedErr); dio.optimismPortalImpl(); + dio.set(dio.ethLockboxImpl.selector, emptyAddr); + vm.expectRevert(expectedErr); + dio.ethLockboxImpl(); + dio.set(dio.delayedWETHImpl.selector, emptyAddr); vm.expectRevert(expectedErr); dio.delayedWETHImpl(); @@ -284,6 +295,7 @@ contract DeployImplementations_Test is Test { deployImplementations.deployL1StandardBridgeImpl(dio); deployImplementations.deployOptimismMintableERC20FactoryImpl(dio); deployImplementations.deployOptimismPortalImpl(dii, dio); + deployImplementations.deployETHLockboxImpl(dio); deployImplementations.deployDelayedWETHImpl(dii, dio); deployImplementations.deployPreimageOracleSingleton(dii, dio); deployImplementations.deployMipsSingleton(dii, dio); @@ -298,6 +310,7 @@ contract DeployImplementations_Test is Test { address l1StandardBridgeImpl = address(dio.l1StandardBridgeImpl()); address optimismMintableERC20FactoryImpl = address(dio.optimismMintableERC20FactoryImpl()); address optimismPortalImpl = address(dio.optimismPortalImpl()); + address ethLockboxImpl = address(dio.ethLockboxImpl()); address delayedWETHImpl = address(dio.delayedWETHImpl()); address preimageOracleSingleton = address(dio.preimageOracleSingleton()); address mipsSingleton = address(dio.mipsSingleton()); @@ -312,6 +325,7 @@ contract DeployImplementations_Test is Test { deployImplementations.deployL1StandardBridgeImpl(dio); deployImplementations.deployOptimismMintableERC20FactoryImpl(dio); deployImplementations.deployOptimismPortalImpl(dii, dio); + deployImplementations.deployETHLockboxImpl(dio); deployImplementations.deployDelayedWETHImpl(dii, dio); deployImplementations.deployPreimageOracleSingleton(dii, dio); deployImplementations.deployMipsSingleton(dii, dio); @@ -332,6 +346,7 @@ contract DeployImplementations_Test is Test { assertEq(disputeGameFactoryImpl, address(dio.disputeGameFactoryImpl()), "1000"); assertEq(anchorStateRegistryImpl, address(dio.anchorStateRegistryImpl()), "1100"); assertEq(opcm, address(dio.opcm()), "1200"); + assertEq(ethLockboxImpl, address(dio.ethLockboxImpl()), "1300"); } function testFuzz_run_memory_succeeds(bytes32 _seed) public { diff --git a/packages/contracts-bedrock/test/setup/Setup.sol b/packages/contracts-bedrock/test/setup/Setup.sol index f984c72360b7d..03458f9564019 100644 --- a/packages/contracts-bedrock/test/setup/Setup.sol +++ b/packages/contracts-bedrock/test/setup/Setup.sol @@ -23,6 +23,7 @@ import { Chains } from "scripts/libraries/Chains.sol"; // Interfaces import { IOPContractsManager } from "interfaces/L1/IOPContractsManager.sol"; import { IOptimismPortal2 } from "interfaces/L1/IOptimismPortal2.sol"; +import { IETHLockbox } from "interfaces/L1/IETHLockbox.sol"; import { IL1CrossDomainMessenger } from "interfaces/L1/IL1CrossDomainMessenger.sol"; import { ISystemConfig } from "interfaces/L1/ISystemConfig.sol"; import { ISuperchainConfig } from "interfaces/L1/ISuperchainConfig.sol"; @@ -96,6 +97,7 @@ contract Setup { // L1 contracts - core IOptimismPortal2 optimismPortal2; + IETHLockbox ethLockbox; ISystemConfig systemConfig; IL1StandardBridge l1StandardBridge; IL1CrossDomainMessenger l1CrossDomainMessenger; @@ -227,6 +229,7 @@ contract Setup { console.log("Setup: completed L1 deployment, registering addresses now"); optimismPortal2 = IOptimismPortal2(artifacts.mustGetAddress("OptimismPortalProxy")); + ethLockbox = IETHLockbox(artifacts.mustGetAddress("ETHLockboxProxy")); systemConfig = ISystemConfig(artifacts.mustGetAddress("SystemConfigProxy")); l1StandardBridge = IL1StandardBridge(artifacts.mustGetAddress("L1StandardBridgeProxy")); l1CrossDomainMessenger = IL1CrossDomainMessenger(artifacts.mustGetAddress("L1CrossDomainMessengerProxy")); diff --git a/packages/contracts-bedrock/test/universal/Specs.t.sol b/packages/contracts-bedrock/test/universal/Specs.t.sol index 92b2bef16a8cf..1363237c710bb 100644 --- a/packages/contracts-bedrock/test/universal/Specs.t.sol +++ b/packages/contracts-bedrock/test/universal/Specs.t.sol @@ -226,7 +226,8 @@ contract Specification_Test is CommonTest { }); _addSpec({ _name: "OptimismPortalInterop", _sel: _getSel("finalizedWithdrawals(bytes32)") }); _addSpec({ _name: "OptimismPortalInterop", _sel: _getSel("guardian()") }); - _addSpec({ _name: "OptimismPortalInterop", _sel: _getSel("initialize(address,address,address)") }); + _addSpec({ _name: "OptimismPortalInterop", _sel: _getSel("adminOwner()") }); + _addSpec({ _name: "OptimismPortalInterop", _sel: _getSel("initialize(address,address,address,address)") }); _addSpec({ _name: "OptimismPortalInterop", _sel: _getSel("l2Sender()") }); _addSpec({ _name: "OptimismPortalInterop", _sel: _getSel("minimumGasLimit(uint64)") }); _addSpec({ _name: "OptimismPortalInterop", _sel: _getSel("params()") }); @@ -271,7 +272,8 @@ contract Specification_Test is CommonTest { }); _addSpec({ _name: "OptimismPortal2", _sel: _getSel("finalizedWithdrawals(bytes32)") }); _addSpec({ _name: "OptimismPortal2", _sel: _getSel("guardian()") }); - _addSpec({ _name: "OptimismPortal2", _sel: _getSel("initialize(address,address,address)") }); + _addSpec({ _name: "OptimismPortal2", _sel: _getSel("adminOwner()") }); + _addSpec({ _name: "OptimismPortal2", _sel: _getSel("initialize(address,address,address,address)") }); _addSpec({ _name: "OptimismPortal2", _sel: _getSel("l2Sender()") }); _addSpec({ _name: "OptimismPortal2", _sel: _getSel("minimumGasLimit(uint64)") }); _addSpec({ _name: "OptimismPortal2", _sel: _getSel("params()") }); @@ -316,6 +318,21 @@ contract Specification_Test is CommonTest { _addSpec({ _name: "ProtocolVersions", _sel: _getSel("transferOwnership(address)") }); _addSpec({ _name: "ProtocolVersions", _sel: _getSel("version()") }); + // ETHLockbox + _addSpec({ _name: "ETHLockbox", _sel: _getSel("version()") }); + _addSpec({ _name: "ETHLockbox", _sel: _getSel("initialize(address)") }); + _addSpec({ _name: "ETHLockbox", _sel: _getSel("superchainConfig()") }); + _addSpec({ _name: "ETHLockbox", _sel: _getSel("adminOwner()") }); + _addSpec({ _name: "ETHLockbox", _sel: _getSel("paused()") }); + _addSpec({ _name: "ETHLockbox", _sel: _getSel("authorizedPortals(address)") }); + _addSpec({ _name: "ETHLockbox", _sel: _getSel("authorizedLockboxes(address)") }); + _addSpec({ _name: "ETHLockbox", _sel: _getSel("receiveLiquidity()") }); + _addSpec({ _name: "ETHLockbox", _sel: _getSel("lockETH()") }); + _addSpec({ _name: "ETHLockbox", _sel: _getSel("unlockETH(uint256)") }); + _addSpec({ _name: "ETHLockbox", _sel: _getSel("authorizePortal(address)") }); + _addSpec({ _name: "ETHLockbox", _sel: _getSel("authorizeLockbox(address)") }); + _addSpec({ _name: "ETHLockbox", _sel: _getSel("migrateLiquidity(address)") }); + // ResourceMetering _addSpec({ _name: "ResourceMetering", _sel: _getSel("params()") }); diff --git a/packages/contracts-bedrock/test/vendor/Initializable.t.sol b/packages/contracts-bedrock/test/vendor/Initializable.t.sol index 8464682c88c45..0a4159f65eda7 100644 --- a/packages/contracts-bedrock/test/vendor/Initializable.t.sol +++ b/packages/contracts-bedrock/test/vendor/Initializable.t.sol @@ -137,6 +137,7 @@ contract Initializer_Test is CommonTest { ) }) ); + // SystemConfigImpl contracts.push( InitializeableContract({ @@ -331,7 +332,7 @@ 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[](11); + string[] memory excludes = new string[](12); // 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. @@ -355,6 +356,8 @@ contract Initializer_Test is CommonTest { excludes[9] = "src/L1/OPContractsManagerInterop.sol"; // L2 contract initialization is tested in Predeploys.t.sol excludes[10] = "src/L2/*"; + // ETHLockbox uses OZ v5 initializer + excludes[11] = "src/L1/ETHLockbox.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 51c2fce266786..e499ec39bcd46 100644 --- a/packages/contracts-bedrock/test/vendor/InitializableOZv5.t.sol +++ b/packages/contracts-bedrock/test/vendor/InitializableOZv5.t.sol @@ -1,15 +1,19 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.25; +pragma solidity 0.8.15; -import { Test } from "forge-std/Test.sol"; +import { CommonTest } from "test/setup/CommonTest.sol"; +import { EIP1967Helper } from "test/mocks/EIP1967Helper.sol"; import { IOptimismSuperchainERC20 } from "interfaces/L2/IOptimismSuperchainERC20.sol"; -import { Initializable } from "@openzeppelin/contracts-v5/proxy/utils/Initializable.sol"; import { DeployUtils } from "scripts/libraries/DeployUtils.sol"; +import { IETHLockbox } from "interfaces/L1/IETHLockbox.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 { + error InvalidInitialization(); + /// @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; @@ -17,6 +21,7 @@ contract InitializerOZv5_Test is Test { /// @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 +30,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 +51,24 @@ contract InitializerOZv5_Test is Test { initCalldata: abi.encodeCall(IOptimismSuperchainERC20.initialize, (address(0), "", "", 18)) }) ); + + // ETHLockboxImpl + contracts.push( + InitializeableContract({ + name: "ETHLockboxImpl", + target: EIP1967Helper.getImplementation(address(ethLockbox)), + initCalldata: abi.encodeCall(IETHLockbox.initialize, (address(0))) + }) + ); + + // ETHLockboxProxy + contracts.push( + InitializeableContract({ + name: "ETHLockboxProxy", + target: address(ethLockbox), + initCalldata: abi.encodeCall(IETHLockbox.initialize, (address(0))) + }) + ); } /// @notice Tests that: @@ -61,12 +89,16 @@ 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); + 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); } } } From c47da68cb681ddd3ffc96de2f82922a0aa52c423 Mon Sep 17 00:00:00 2001 From: AgusDuha <81362284+agusduha@users.noreply.github.com> Date: Fri, 28 Feb 2025 13:41:27 -0300 Subject: [PATCH 03/50] feat: integrate portal with lockbox (#291) * feat: integrate portal with lockbox * fix: migrate natspec Co-authored-by: Disco <131301107+0xDiscotech@users.noreply.github.com> * fix: delete natspec Co-authored-by: Disco <131301107+0xDiscotech@users.noreply.github.com> * fix: natspec nits * chore: natspec wording * chore: function order * chore: remove internals * feat: add eth lockbox as portal arg on opcm * feat: add checks on scripts and unit tests * feat: lock and unlock tests * feat: add migrate liquidity tests * fix: other failing portal tests * chore: run pre-pr * chore: improve lock and unlock checks on tests * chore: undo portal version change and run pre-pr * refactor: enhance expect call checks * fix: fork test string error * fix: just test failing tests * refactor: pao base * refactor: setup portal and lockbox integration on initialization * feat: update lockbox setter * chore: update pao naming on tests * chore: portal comment * chore: update version and run pre-pr * fix: spec * fix: lockbox storage layout * fix: pr comments and failing test * feat: portal upgrade test * chore: remove portal balance check on chain assertions * fix: error string on chain assertions * feat: add test for portal unsafe target * fix: approval --------- Co-authored-by: Disco <131301107+0xDiscotech@users.noreply.github.com> --- .../interfaces/L1/IETHLockbox.sol | 14 +- .../interfaces/L1/IOptimismPortal2.sol | 16 +- .../interfaces/L1/IOptimismPortalInterop.sol | 17 +- .../interfaces/L1/IPAOBase.sol | 6 + .../interfaces/L1/IProxyAdminOwnable.sol | 6 - .../scripts/deploy/ChainAssertions.sol | 7 +- .../deploy/DeployImplementations.s.sol | 5 +- .../snapshots/abi/ETHLockbox.json | 25 ++- .../snapshots/abi/OptimismPortal2.json | 82 +++++++++- .../snapshots/abi/OptimismPortalInterop.json | 77 ++++++++- .../snapshots/semver-lock.json | 18 +-- .../snapshots/storageLayout/ETHLockbox.json | 25 ++- .../storageLayout/OptimismPortal2.json | 7 + .../storageLayout/OptimismPortalInterop.json | 7 + .../contracts-bedrock/src/L1/ETHLockbox.sol | 76 ++++----- .../src/L1/OPContractsManager.sol | 45 +++++- .../src/L1/OptimismPortal2.sol | 91 +++++++---- .../src/L1/OptimismPortalInterop.sol | 3 - packages/contracts-bedrock/src/L1/PAOBase.sol | 26 ++++ .../test/L1/ETHLockbox.t.sol | 106 ++++++------- .../test/L1/L1StandardBridge.t.sol | 32 ++-- .../test/L1/OptimismPortal2.t.sol | 147 ++++++++++++++++-- .../test/invariants/OptimismPortal2.t.sol | 2 +- .../contracts-bedrock/test/setup/Events.sol | 4 + .../test/universal/Specs.t.sol | 37 +++-- .../test/vendor/Initializable.t.sol | 22 ++- .../test/vendor/InitializableOZv5.t.sol | 20 --- 27 files changed, 666 insertions(+), 257 deletions(-) create mode 100644 packages/contracts-bedrock/interfaces/L1/IPAOBase.sol delete mode 100644 packages/contracts-bedrock/interfaces/L1/IProxyAdminOwnable.sol create mode 100644 packages/contracts-bedrock/src/L1/PAOBase.sol diff --git a/packages/contracts-bedrock/interfaces/L1/IETHLockbox.sol b/packages/contracts-bedrock/interfaces/L1/IETHLockbox.sol index a2ad27a9f4070..796cc01fb3a46 100644 --- a/packages/contracts-bedrock/interfaces/L1/IETHLockbox.sol +++ b/packages/contracts-bedrock/interfaces/L1/IETHLockbox.sol @@ -3,18 +3,16 @@ pragma solidity ^0.8.0; import { ISemver } from "interfaces/universal/ISemver.sol"; import { ISuperchainConfig } from "interfaces/L1/ISuperchainConfig.sol"; -import { IProxyAdminOwnable } from "interfaces/L1/IProxyAdminOwnable.sol"; +import { IPAOBase } from "interfaces/L1/IPAOBase.sol"; -interface IETHLockbox is IProxyAdminOwnable, ISemver { - error InvalidInitialization(); - error NotInitializing(); +interface IETHLockbox is IPAOBase, ISemver { error ETHLockbox_Unauthorized(); error ETHLockbox_Paused(); error ETHLockbox_NoWithdrawalTransactions(); error ETHLockbox_AlreadyAuthorized(); - error ETHLockbox_DifferentAdminOwner(); + error ETHLockbox_DifferentPAO(); - event Initialized(uint64 version); + event Initialized(uint8 version); event ETHLocked(address indexed portal, uint256 amount); event ETHUnlocked(address indexed portal, uint256 amount); event PortalAuthorized(address indexed portal); @@ -22,8 +20,8 @@ interface IETHLockbox is IProxyAdminOwnable, ISemver { event LiquidityMigrated(address indexed lockbox); event LiquidityReceived(address indexed lockbox); - function initialize(address _superchainConfig) external; - function superchainConfig() external view returns (ISuperchainConfig superchainConfig_); + function initialize(address _superchainConfig, address[] calldata _portals) external; + function superchainConfig() external view returns (ISuperchainConfig); function paused() external view returns (bool); function authorizedPortals(address) external view returns (bool); function authorizedLockboxes(address) external view returns (bool); diff --git a/packages/contracts-bedrock/interfaces/L1/IOptimismPortal2.sol b/packages/contracts-bedrock/interfaces/L1/IOptimismPortal2.sol index be07452904e74..4b391a3b874c1 100644 --- a/packages/contracts-bedrock/interfaces/L1/IOptimismPortal2.sol +++ b/packages/contracts-bedrock/interfaces/L1/IOptimismPortal2.sol @@ -8,9 +8,11 @@ import { IDisputeGameFactory } from "interfaces/dispute/IDisputeGameFactory.sol" import { ISystemConfig } from "interfaces/L1/ISystemConfig.sol"; import { ISuperchainConfig } from "interfaces/L1/ISuperchainConfig.sol"; import { IAnchorStateRegistry } from "interfaces/dispute/IAnchorStateRegistry.sol"; -import { IProxyAdminOwnable } from "interfaces/L1/IProxyAdminOwnable.sol"; +import { IPAOBase } from "interfaces/L1/IPAOBase.sol"; +import { IETHLockbox } from "interfaces/L1/IETHLockbox.sol"; -interface IOptimismPortal2 is IProxyAdminOwnable { +interface IOptimismPortal2 is IPAOBase { + error OptimismPortal_Unauthorized(); error ContentLengthMismatch(); error EmptyItem(); error InvalidDataRemainder(); @@ -39,10 +41,13 @@ interface IOptimismPortal2 is IProxyAdminOwnable { 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 ethBalance); + event LockboxUpdated(address oldLockbox, address newLockbox); receive() external payable; function anchorStateRegistry() external view returns (IAnchorStateRegistry); + function ethLockbox() external view returns (IETHLockbox); function checkWithdrawal(bytes32 _withdrawalHash, address _proofSubmitter) external view; function depositTransaction( address _to, @@ -56,6 +61,7 @@ interface IOptimismPortal2 is IProxyAdminOwnable { function disputeGameFactory() external view returns (IDisputeGameFactory); function disputeGameFinalityDelaySeconds() external view returns (uint256); function donateETH() external payable; + function updateLockbox(address _newLockbox) external; function finalizeWithdrawalTransaction(Types.WithdrawalTransaction memory _tx) external; function finalizeWithdrawalTransactionExternalProof( Types.WithdrawalTransaction memory _tx, @@ -67,7 +73,8 @@ interface IOptimismPortal2 is IProxyAdminOwnable { function initialize( ISystemConfig _systemConfig, ISuperchainConfig _superchainConfig, - IAnchorStateRegistry _anchorStateRegistry + IAnchorStateRegistry _anchorStateRegistry, + IETHLockbox _ethLockbox ) external; function l2Sender() external view returns (address); @@ -95,8 +102,9 @@ interface IOptimismPortal2 is IProxyAdminOwnable { function respectedGameTypeUpdatedAt() external view returns (uint64); function superchainConfig() external view returns (ISuperchainConfig); function systemConfig() external view returns (ISystemConfig); - function upgrade(IAnchorStateRegistry _anchorStateRegistry) external; + function upgrade(IAnchorStateRegistry _anchorStateRegistry, IETHLockbox _ethLockbox) external; function version() external pure returns (string memory); + function migrateLiquidity() external; function __constructor__(uint256 _proofMaturityDelaySeconds) external; } diff --git a/packages/contracts-bedrock/interfaces/L1/IOptimismPortalInterop.sol b/packages/contracts-bedrock/interfaces/L1/IOptimismPortalInterop.sol index 49404083e1d3f..e606940ed7fea 100644 --- a/packages/contracts-bedrock/interfaces/L1/IOptimismPortalInterop.sol +++ b/packages/contracts-bedrock/interfaces/L1/IOptimismPortalInterop.sol @@ -9,12 +9,15 @@ import { ISystemConfig } from "interfaces/L1/ISystemConfig.sol"; import { ISuperchainConfig } from "interfaces/L1/ISuperchainConfig.sol"; import { ConfigType } from "interfaces/L2/IL1BlockInterop.sol"; import { IAnchorStateRegistry } from "interfaces/dispute/IAnchorStateRegistry.sol"; +import { IETHLockbox } from "interfaces/L1/IETHLockbox.sol"; +import { IPAOBase } from "interfaces/l1/IPAOBase.sol"; -interface IOptimismPortalInterop { +interface IOptimismPortalInterop is IPAOBase { error ContentLengthMismatch(); error EmptyItem(); error InvalidDataRemainder(); error InvalidHeader(); + error OptimismPortal_Unauthorized(); error OptimismPortal_AlreadyFinalized(); error OptimismPortal_BadTarget(); error OptimismPortal_CallPaused(); @@ -29,7 +32,6 @@ interface IOptimismPortalInterop { error OptimismPortal_InvalidRootClaim(); error OptimismPortal_NoReentrancy(); error OptimismPortal_ProofNotOldEnough(); - error OptimismPortal_Unauthorized(); error OptimismPortal_Unproven(); error OutOfGas(); error UnexpectedList(); @@ -40,10 +42,13 @@ 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 ethBalance); + event LockboxUpdated(address oldLockbox, address newLockbox); receive() external payable; function anchorStateRegistry() external view returns (IAnchorStateRegistry); + function ethLockbox() external view returns (IETHLockbox); function checkWithdrawal(bytes32 _withdrawalHash, address _proofSubmitter) external view; function depositTransaction( address _to, @@ -57,19 +62,21 @@ interface IOptimismPortalInterop { function disputeGameFactory() external view returns (IDisputeGameFactory); function disputeGameFinalityDelaySeconds() external view returns (uint256); function donateETH() external payable; + function updateLockbox(address _newLockbox) external; function finalizeWithdrawalTransaction(Types.WithdrawalTransaction memory _tx) external; function finalizeWithdrawalTransactionExternalProof( Types.WithdrawalTransaction memory _tx, address _proofSubmitter ) external; + function migrateLiquidity() external; function finalizedWithdrawals(bytes32) external view returns (bool); function guardian() external view returns (address); - function adminOwner() external view returns (address); function initialize( ISystemConfig _systemConfig, ISuperchainConfig _superchainConfig, - IAnchorStateRegistry _anchorStateRegistry + IAnchorStateRegistry _anchorStateRegistry, + IETHLockbox _ethLockbox ) external; function l2Sender() external view returns (address); @@ -98,7 +105,7 @@ interface IOptimismPortalInterop { function setConfig(ConfigType _type, bytes memory _value) external; function superchainConfig() external view returns (ISuperchainConfig); function systemConfig() external view returns (ISystemConfig); - function upgrade(IAnchorStateRegistry _anchorStateRegistry) external; + function upgrade(IAnchorStateRegistry _anchorStateRegistry, IETHLockbox _ethLockbox) external; function version() external pure returns (string memory); function __constructor__(uint256 _proofMaturityDelaySeconds) external; diff --git a/packages/contracts-bedrock/interfaces/L1/IPAOBase.sol b/packages/contracts-bedrock/interfaces/L1/IPAOBase.sol new file mode 100644 index 0000000000000..edfc72606784c --- /dev/null +++ b/packages/contracts-bedrock/interfaces/L1/IPAOBase.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +interface IPAOBase { + function PAO() external view returns (address); +} diff --git a/packages/contracts-bedrock/interfaces/L1/IProxyAdminOwnable.sol b/packages/contracts-bedrock/interfaces/L1/IProxyAdminOwnable.sol deleted file mode 100644 index e45fb51fb3fa6..0000000000000 --- a/packages/contracts-bedrock/interfaces/L1/IProxyAdminOwnable.sol +++ /dev/null @@ -1,6 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; - -interface IProxyAdminOwnable { - function adminOwner() external view returns (address); -} diff --git a/packages/contracts-bedrock/scripts/deploy/ChainAssertions.sol b/packages/contracts-bedrock/scripts/deploy/ChainAssertions.sol index 70aec28b70971..520d70a41defe 100644 --- a/packages/contracts-bedrock/scripts/deploy/ChainAssertions.sol +++ b/packages/contracts-bedrock/scripts/deploy/ChainAssertions.sol @@ -392,15 +392,17 @@ library ChainAssertions { require(address(portal.superchainConfig()) == address(_contracts.SuperchainConfig), "CHECK-OP2-50"); require(portal.paused() == ISuperchainConfig(_contracts.SuperchainConfig).paused(), "CHECK-OP2-60"); require(portal.l2Sender() == Constants.DEFAULT_L2_SENDER, "CHECK-OP2-70"); + require(address(portal.ethLockbox()) == _contracts.ETHLockbox, "CHECK-OP2-80"); } else { require(address(portal.anchorStateRegistry()) == address(0), "CHECK-OP2-80"); require(address(portal.systemConfig()) == address(0), "CHECK-OP2-90"); require(address(portal.superchainConfig()) == address(0), "CHECK-OP2-100"); require(portal.l2Sender() == address(0), "CHECK-OP2-110"); + require(address(portal.ethLockbox()) == address(0), "CHECK-OP2-120"); } // This slot is the custom gas token _balance and this check ensures // that it stays unset for forwards compatibility with custom gas token. - require(vm.load(address(portal), bytes32(uint256(61))) == bytes32(0), "CHECK-OP2-120"); + require(vm.load(address(portal), bytes32(uint256(61))) == bytes32(0), "CHECK-OP2-140"); } /// @notice Asserts that the ETHLockbox is setup correctly @@ -417,13 +419,14 @@ library ChainAssertions { require(address(ethLockbox) != address(0), "CHECK-ELB-10"); // Check that the contract is initialized - DeployUtils.assertInitializedOZv5({ _contractAddress: address(ethLockbox), _isProxy: _isProxy }); + DeployUtils.assertInitialized({ _contractAddress: address(ethLockbox), _isProxy: _isProxy, _slot: 0, _offset: 0 }); if (_isProxy) { require(ethLockbox.superchainConfig() == superchainConfig, "CHECK-ELB-20"); require(ethLockbox.authorizedPortals(_contracts.OptimismPortal), "CHECK-ELB-30"); } else { require(address(ethLockbox.superchainConfig()) == address(0), "CHECK-ELB-40"); + require(ethLockbox.authorizedPortals(_contracts.OptimismPortal) == false, "CHECK-ELB-50"); } } diff --git a/packages/contracts-bedrock/scripts/deploy/DeployImplementations.s.sol b/packages/contracts-bedrock/scripts/deploy/DeployImplementations.s.sol index 0426173fdbd8a..3a46e7fd4add6 100644 --- a/packages/contracts-bedrock/scripts/deploy/DeployImplementations.s.sol +++ b/packages/contracts-bedrock/scripts/deploy/DeployImplementations.s.sol @@ -328,14 +328,17 @@ contract DeployImplementationsOutput is BaseDeployIO { // This slot is the custom gas token _balance and this check ensures // that it stays unset for forwards compatibility with custom gas token. require(vm.load(address(portal), bytes32(uint256(61))) == bytes32(0), "PORTAL-50"); + + require(address(portal.ethLockbox()) == address(0), "PORTAL-60"); } function assertValidETHLockboxImpl(DeployImplementationsInput) internal view { IETHLockbox lockbox = ethLockboxImpl(); - DeployUtils.assertInitializedOZv5({ _contractAddress: address(lockbox), _isProxy: false }); + DeployUtils.assertInitialized({ _contractAddress: address(lockbox), _isProxy: false, _slot: 0, _offset: 0 }); require(address(lockbox.superchainConfig()) == address(0), "ELB-10"); + require(lockbox.authorizedPortals(address(optimismPortalImpl())) == false, "ELB-20"); } function assertValidDelayedWETHImpl(DeployImplementationsInput _dii) internal view { diff --git a/packages/contracts-bedrock/snapshots/abi/ETHLockbox.json b/packages/contracts-bedrock/snapshots/abi/ETHLockbox.json index c8ce32b2dd7e0..07aabc72bbbed 100644 --- a/packages/contracts-bedrock/snapshots/abi/ETHLockbox.json +++ b/packages/contracts-bedrock/snapshots/abi/ETHLockbox.json @@ -6,7 +6,7 @@ }, { "inputs": [], - "name": "adminOwner", + "name": "PAO", "outputs": [ { "internalType": "address", @@ -87,6 +87,11 @@ "internalType": "address", "name": "_superchainConfig", "type": "address" + }, + { + "internalType": "address[]", + "name": "_portals", + "type": "address[]" } ], "name": "initialize", @@ -140,7 +145,7 @@ "outputs": [ { "internalType": "contract ISuperchainConfig", - "name": "superchainConfig_", + "name": "", "type": "address" } ], @@ -216,9 +221,9 @@ "inputs": [ { "indexed": false, - "internalType": "uint64", + "internalType": "uint8", "name": "version", - "type": "uint64" + "type": "uint8" } ], "name": "Initialized", @@ -283,7 +288,7 @@ }, { "inputs": [], - "name": "ETHLockbox_DifferentAdminOwner", + "name": "ETHLockbox_DifferentPAO", "type": "error" }, { @@ -300,15 +305,5 @@ "inputs": [], "name": "ETHLockbox_Unauthorized", "type": "error" - }, - { - "inputs": [], - "name": "InvalidInitialization", - "type": "error" - }, - { - "inputs": [], - "name": "NotInitializing", - "type": "error" } ] \ 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 add6c23040841..b501dad8f1fc1 100644 --- a/packages/contracts-bedrock/snapshots/abi/OptimismPortal2.json +++ b/packages/contracts-bedrock/snapshots/abi/OptimismPortal2.json @@ -16,7 +16,7 @@ }, { "inputs": [], - "name": "adminOwner", + "name": "PAO", "outputs": [ { "internalType": "address", @@ -124,6 +124,19 @@ "stateMutability": "payable", "type": "function" }, + { + "inputs": [], + "name": "ethLockbox", + "outputs": [ + { + "internalType": "contract IETHLockbox", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [ { @@ -267,6 +280,11 @@ "internalType": "contract IAnchorStateRegistry", "name": "_anchorStateRegistry", "type": "address" + }, + { + "internalType": "contract IETHLockbox", + "name": "_ethLockbox", + "type": "address" } ], "name": "initialize", @@ -287,6 +305,13 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [], + "name": "migrateLiquidity", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [ { @@ -561,12 +586,30 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [ + { + "internalType": "address", + "name": "_newLockbox", + "type": "address" + } + ], + "name": "updateLockbox", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [ { "internalType": "contract IAnchorStateRegistry", "name": "_anchorStateRegistry", "type": "address" + }, + { + "internalType": "contract IETHLockbox", + "name": "_ethLockbox", + "type": "address" } ], "name": "upgrade", @@ -587,6 +630,19 @@ "stateMutability": "pure", "type": "function" }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "ethBalance", + "type": "uint256" + } + ], + "name": "ETHMigrated", + "type": "event" + }, { "anonymous": false, "inputs": [ @@ -600,6 +656,25 @@ "name": "Initialized", "type": "event" }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "oldLockbox", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "newLockbox", + "type": "address" + } + ], + "name": "LockboxUpdated", + "type": "event" + }, { "anonymous": false, "inputs": [ @@ -784,6 +859,11 @@ "name": "OptimismPortal_ProofNotOldEnough", "type": "error" }, + { + "inputs": [], + "name": "OptimismPortal_Unauthorized", + "type": "error" + }, { "inputs": [], "name": "OptimismPortal_Unproven", diff --git a/packages/contracts-bedrock/snapshots/abi/OptimismPortalInterop.json b/packages/contracts-bedrock/snapshots/abi/OptimismPortalInterop.json index 642b7d6144472..dbc4f2da16f4b 100644 --- a/packages/contracts-bedrock/snapshots/abi/OptimismPortalInterop.json +++ b/packages/contracts-bedrock/snapshots/abi/OptimismPortalInterop.json @@ -16,7 +16,7 @@ }, { "inputs": [], - "name": "adminOwner", + "name": "PAO", "outputs": [ { "internalType": "address", @@ -124,6 +124,19 @@ "stateMutability": "payable", "type": "function" }, + { + "inputs": [], + "name": "ethLockbox", + "outputs": [ + { + "internalType": "contract IETHLockbox", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [ { @@ -267,6 +280,11 @@ "internalType": "contract IAnchorStateRegistry", "name": "_anchorStateRegistry", "type": "address" + }, + { + "internalType": "contract IETHLockbox", + "name": "_ethLockbox", + "type": "address" } ], "name": "initialize", @@ -287,6 +305,13 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [], + "name": "migrateLiquidity", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [ { @@ -579,12 +604,30 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [ + { + "internalType": "address", + "name": "_newLockbox", + "type": "address" + } + ], + "name": "updateLockbox", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [ { "internalType": "contract IAnchorStateRegistry", "name": "_anchorStateRegistry", "type": "address" + }, + { + "internalType": "contract IETHLockbox", + "name": "_ethLockbox", + "type": "address" } ], "name": "upgrade", @@ -605,6 +648,19 @@ "stateMutability": "pure", "type": "function" }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "ethBalance", + "type": "uint256" + } + ], + "name": "ETHMigrated", + "type": "event" + }, { "anonymous": false, "inputs": [ @@ -618,6 +674,25 @@ "name": "Initialized", "type": "event" }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "oldLockbox", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "newLockbox", + "type": "address" + } + ], + "name": "LockboxUpdated", + "type": "event" + }, { "anonymous": false, "inputs": [ diff --git a/packages/contracts-bedrock/snapshots/semver-lock.json b/packages/contracts-bedrock/snapshots/semver-lock.json index 16e0d15d33823..5b02921d382bf 100644 --- a/packages/contracts-bedrock/snapshots/semver-lock.json +++ b/packages/contracts-bedrock/snapshots/semver-lock.json @@ -4,8 +4,8 @@ "sourceCodeHash": "0xe772f7db8033e4a738850cb28ac4849d3a454c93732135a8a10d4f7cb498088e" }, "src/L1/ETHLockbox.sol": { - "initCodeHash": "0x2ec1bdb12bec5ac84a7f44fba03247d6d2e442e3c4508f8ec3a7eceb7b68d6cc", - "sourceCodeHash": "0x7da93ca2d0d5f2a61ca9de2f63134d6e94b7ccf9b3b2a29c352d2606ff8a1ca3" + "initCodeHash": "0xb5da33c92a6f0bc00eaaaec5d0b99c9c15a9dd9d7a25c416cd6740a96e7fc4d5", + "sourceCodeHash": "0xc9ac52290abc2f3c7071e12116593f4bad504961ffa11825161072a5aa94c23c" }, "src/L1/L1CrossDomainMessenger.sol": { "initCodeHash": "0x03a3c0eb2418aba9f93bb89723ba2ee7cb9e1988ca00f380503c960149c85b7a", @@ -20,11 +20,11 @@ "sourceCodeHash": "0x7f614d0c5251365580c02dd898900a583ece2aa28579adea74a3808b575949b4" }, "src/L1/OPContractsManager.sol": { - "initCodeHash": "0x19cb3ece689cc764790e84b171a0ce921c7b642f633e10b261f551045cc938a8", - "sourceCodeHash": "0x39b86ad4aad4107154723b7c5074666ea19247ad32d25553887330a2b0398c5a" + "initCodeHash": "0x2918a39985d0581084acb9fe4a172762d9ea25aa51ac9965a5c8eea79501aac1", + "sourceCodeHash": "0x7018bd24305dc432ddcbf18e668155d02142f7531327a9a64981299a4c0c633e" }, "src/L1/OPContractsManagerInterop.sol": { - "initCodeHash": "0x59e9b7ebc3fdd31a69334ec1bc7c44cd054dc089d30ed805ee985496b0077503", + "initCodeHash": "0x9cbacf4a5b118d2988747fd34d22508a7c886e699713877db755c50f6337df39", "sourceCodeHash": "0xd04bb5babc02fd93618397678506e9bcce5f1881b008785da0767bda74d8447c" }, "src/L1/OPPrestateUpdater.sol": { @@ -32,12 +32,12 @@ "sourceCodeHash": "0xcdeb0f532ff90288ee13694a51a15aaee99392b7dfe2d46fffacf2af82cdb38f" }, "src/L1/OptimismPortal2.sol": { - "initCodeHash": "0x1465cb07c5715ddcfdfff5d1abd63857dba0afe7f2ef424ce2cb83da01a51b9b", - "sourceCodeHash": "0x4e7dcab58532a9f0d51f477927d828caaff4f19cc90ec025df748db739464396" + "initCodeHash": "0x6323e33f92d47755a08337cc990f4443d17e72b14d075b94b5c79c37b1aa21fd", + "sourceCodeHash": "0x561b651787b1ff293e837cc76d6c7801a348074e3862e961a36eaafaa6367dfe" }, "src/L1/OptimismPortalInterop.sol": { - "initCodeHash": "0x5cfb2feddcb8e7b29091226a95a8f00947f682a0620ea087d0a1a3a429fdf053", - "sourceCodeHash": "0x090fe170883616f935b6b30d0f6738264f5c1e7d4dc2a89d330317a7a48d5f6c" + "initCodeHash": "0x279259614f9c0bdfc74c62068d237672b20e189c2da07fd6958683fcd2fc8d1f", + "sourceCodeHash": "0xdf4e628d152475d96b464526cb9adf96c3b0bff45a4a3f84d207faf9c7e7658c" }, "src/L1/ProtocolVersions.sol": { "initCodeHash": "0x5a76c8530cb24cf23d3baacc6eefaac226382af13f1e2a35535d2ec2b0573b29", diff --git a/packages/contracts-bedrock/snapshots/storageLayout/ETHLockbox.json b/packages/contracts-bedrock/snapshots/storageLayout/ETHLockbox.json index effc5277149fc..4ba870d921ec9 100644 --- a/packages/contracts-bedrock/snapshots/storageLayout/ETHLockbox.json +++ b/packages/contracts-bedrock/snapshots/storageLayout/ETHLockbox.json @@ -1,16 +1,37 @@ [ + { + "bytes": "1", + "label": "_initialized", + "offset": 0, + "slot": "0", + "type": "uint8" + }, + { + "bytes": "1", + "label": "_initializing", + "offset": 1, + "slot": "0", + "type": "bool" + }, + { + "bytes": "20", + "label": "superchainConfig", + "offset": 2, + "slot": "0", + "type": "contract ISuperchainConfig" + }, { "bytes": "32", "label": "authorizedPortals", "offset": 0, - "slot": "0", + "slot": "1", "type": "mapping(address => bool)" }, { "bytes": "32", "label": "authorizedLockboxes", "offset": 0, - "slot": "1", + "slot": "2", "type": "mapping(address => bool)" } ] \ No newline at end of file diff --git a/packages/contracts-bedrock/snapshots/storageLayout/OptimismPortal2.json b/packages/contracts-bedrock/snapshots/storageLayout/OptimismPortal2.json index ccb5d970d342e..74edc86c21a72 100644 --- a/packages/contracts-bedrock/snapshots/storageLayout/OptimismPortal2.json +++ b/packages/contracts-bedrock/snapshots/storageLayout/OptimismPortal2.json @@ -131,5 +131,12 @@ "offset": 0, "slot": "62", "type": "contract IAnchorStateRegistry" + }, + { + "bytes": "20", + "label": "ethLockbox", + "offset": 0, + "slot": "63", + "type": "contract IETHLockbox" } ] \ No newline at end of file diff --git a/packages/contracts-bedrock/snapshots/storageLayout/OptimismPortalInterop.json b/packages/contracts-bedrock/snapshots/storageLayout/OptimismPortalInterop.json index ccb5d970d342e..74edc86c21a72 100644 --- a/packages/contracts-bedrock/snapshots/storageLayout/OptimismPortalInterop.json +++ b/packages/contracts-bedrock/snapshots/storageLayout/OptimismPortalInterop.json @@ -131,5 +131,12 @@ "offset": 0, "slot": "62", "type": "contract IAnchorStateRegistry" + }, + { + "bytes": "20", + "label": "ethLockbox", + "offset": 0, + "slot": "63", + "type": "contract IETHLockbox" } ] \ No newline at end of file diff --git a/packages/contracts-bedrock/src/L1/ETHLockbox.sol b/packages/contracts-bedrock/src/L1/ETHLockbox.sol index bbb3c433faf02..f88e42a654af8 100644 --- a/packages/contracts-bedrock/src/L1/ETHLockbox.sol +++ b/packages/contracts-bedrock/src/L1/ETHLockbox.sol @@ -2,24 +2,22 @@ pragma solidity 0.8.25; // Contracts -import { Initializable } from "@openzeppelin/contracts-v5/proxy/utils/Initializable.sol"; +import { PAOBase } from "src/L1/PAOBase.sol"; +import { Initializable } from "@openzeppelin/contracts/proxy/utils/Initializable.sol"; // Libraries -import { Storage } from "src/libraries/Storage.sol"; import { Constants } from "src/libraries/Constants.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 { IProxyAdmin } from "interfaces/universal/IProxyAdmin.sol"; -import { IProxyAdminOwnable } from "interfaces/L1/IProxyAdminOwnable.sol"; /// @custom:proxied true /// @title ETHLockbox /// @notice Manages ETH liquidity locking and unlocking for authorized OptimismPortals, enabling unified ETH liquidity /// management across chains in the superchain cluster. -contract ETHLockbox is Initializable, ISemver { +contract ETHLockbox is PAOBase, Initializable, ISemver { /// @notice Thrown when the lockbox is paused. error ETHLockbox_Paused(); @@ -33,7 +31,7 @@ contract ETHLockbox is Initializable, ISemver { error ETHLockbox_NoWithdrawalTransactions(); /// @notice Thrown when the admin owner of the lockbox is different from the admin owner of the proxy admin. - error ETHLockbox_DifferentAdminOwner(); + error ETHLockbox_DifferentPAO(); /// @notice Emitted when ETH is locked in the lockbox by an authorized portal. /// @param portal The address of the portal that locked the ETH. @@ -62,7 +60,7 @@ contract ETHLockbox is Initializable, ISemver { event LiquidityReceived(address indexed lockbox); /// @notice The address of the SuperchainConfig contract. - bytes32 internal constant _SUPERCHAIN_CONFIG_SLOT = bytes32(uint256(keccak256("ETHLockbox.superchainConfig")) - 1); + ISuperchainConfig public superchainConfig; /// @notice Mapping of authorized portals. mapping(address => bool) public authorizedPortals; @@ -71,9 +69,9 @@ contract ETHLockbox is Initializable, ISemver { mapping(address => bool) public authorizedLockboxes; /// @notice Semantic version. - /// @custom:semver 1.0.0-beta.1 + /// @custom:semver 0.0.1 function version() public view virtual returns (string memory) { - return "1.0.0-beta.1"; + return "0.0.1"; } /// @notice Constructs the ETHLockbox contract. @@ -83,27 +81,24 @@ contract ETHLockbox is Initializable, ISemver { /// @notice Initializer. /// @param _superchainConfig The address of the SuperchainConfig contract. - function initialize(address _superchainConfig) external initializer { - Storage.setAddress(_SUPERCHAIN_CONFIG_SLOT, _superchainConfig); - } - - /// @notice Getter for the SuperchainConfig contract. - function superchainConfig() public view returns (ISuperchainConfig superchainConfig_) { - superchainConfig_ = ISuperchainConfig(Storage.getAddress(_SUPERCHAIN_CONFIG_SLOT)); + /// @param _portals The addresses of the portals to authorize. + function initialize(address _superchainConfig, address[] calldata _portals) external initializer { + superchainConfig = ISuperchainConfig(_superchainConfig); + for (uint256 i; i < _portals.length; i++) { + _authorizePortal(_portals[i]); + } } - /// @notice Getter for the owner of the proxy admin. - /// The ProxyAdmin is the owner of the Proxy contract, which is the proxy used for the ETHLockbox. - function adminOwner() public view returns (address) { - // Get the proxy admin address reading for the reserved slot it has on the Proxy contract. - IProxyAdmin proxyAdmin = IProxyAdmin(Storage.getAddress(Constants.PROXY_OWNER_ADDRESS)); - // Return the owner of the proxy admin. - return proxyAdmin.owner(); + /// @notice Authorizes a portal to lock and unlock ETH. + /// @param _portal The address of the portal to authorize. + function authorizePortal(address _portal) external { + if (msg.sender != PAO()) revert ETHLockbox_Unauthorized(); + _authorizePortal(_portal); } /// @notice Getter for the current paused status. function paused() public view returns (bool) { - return superchainConfig().paused(); + return superchainConfig.paused(); } /// @notice Receives the ETH liquidity migrated from an authorized lockbox. @@ -136,22 +131,11 @@ contract ETHLockbox is Initializable, ISemver { emit ETHUnlocked(msg.sender, _value); } - /// @notice Authorizes a portal to lock and unlock ETH. - /// @param _portal The address of the portal to authorize. - function authorizePortal(address _portal) external { - if (msg.sender != adminOwner()) revert ETHLockbox_Unauthorized(); - if (!_sameAdminOwner(_portal)) revert ETHLockbox_DifferentAdminOwner(); - if (authorizedPortals[_portal]) revert ETHLockbox_AlreadyAuthorized(); - - authorizedPortals[_portal] = true; - emit PortalAuthorized(_portal); - } - /// @notice Authorizes an ETH lockbox to migrate its liquidity to the current ETH lockbox. /// @param _lockbox The address of the ETH lockbox to authorize. function authorizeLockbox(address _lockbox) external { - if (msg.sender != adminOwner()) revert ETHLockbox_Unauthorized(); - if (!_sameAdminOwner(_lockbox)) revert ETHLockbox_DifferentAdminOwner(); + if (msg.sender != PAO()) revert ETHLockbox_Unauthorized(); + if (!_samePAO(_lockbox)) revert ETHLockbox_DifferentPAO(); if (authorizedLockboxes[_lockbox]) revert ETHLockbox_AlreadyAuthorized(); authorizedLockboxes[_lockbox] = true; @@ -161,17 +145,21 @@ contract ETHLockbox is Initializable, ISemver { /// @notice Migrates liquidity from the current ETH lockbox to another. /// @param _lockbox The address of the ETH lockbox to migrate liquidity to. function migrateLiquidity(address _lockbox) external { - if (msg.sender != adminOwner()) revert ETHLockbox_Unauthorized(); - if (!_sameAdminOwner(_lockbox)) revert ETHLockbox_DifferentAdminOwner(); + if (msg.sender != PAO()) revert ETHLockbox_Unauthorized(); + if (!_samePAO(_lockbox)) revert ETHLockbox_DifferentPAO(); ETHLockbox(_lockbox).receiveLiquidity{ value: address(this).balance }(); + emit LiquidityMigrated(_lockbox); } - /// @notice Checks if the ProxyAdmin owner of the current contract is the same as the ProxyAdmin owner of the given - /// proxy. - /// @param _proxy The address of the proxy to check. - function _sameAdminOwner(address _proxy) internal view returns (bool) { - return adminOwner() == IProxyAdminOwnable(_proxy).adminOwner(); + /// @notice Authorizes a portal to lock and unlock ETH. + /// @param _portal The address of the portal to authorize. + function _authorizePortal(address _portal) internal { + if (!_samePAO(_portal)) revert ETHLockbox_DifferentPAO(); + if (authorizedPortals[_portal]) revert ETHLockbox_AlreadyAuthorized(); + + authorizedPortals[_portal] = true; + emit PortalAuthorized(_portal); } } diff --git a/packages/contracts-bedrock/src/L1/OPContractsManager.sol b/packages/contracts-bedrock/src/L1/OPContractsManager.sol index b3fbc5a6aa2d4..32854de2e9bff 100644 --- a/packages/contracts-bedrock/src/L1/OPContractsManager.sol +++ b/packages/contracts-bedrock/src/L1/OPContractsManager.sol @@ -397,10 +397,10 @@ contract OPContractsManager is ISemver { output.opChainProxyAdmin, address(output.optimismPortalProxy), implementation.optimismPortalImpl, data ); - data = encodeETHLockboxInitializer(); + address[] memory portals = new address[](1); + portals[0] = address(output.optimismPortalProxy); + data = encodeETHLockboxInitializer(portals); upgradeToAndCall(output.opChainProxyAdmin, address(output.ethLockboxProxy), implementation.ethLockboxImpl, data); - // Besides initializing with the `SuperchainConfig`, authorize the `OptimismPortal` on the `ETHLockbox`. - output.ethLockboxProxy.authorizePortal(address(output.optimismPortalProxy)); data = encodeSystemConfigInitializer(_input, output); upgradeToAndCall( @@ -597,9 +597,37 @@ contract OPContractsManager is ISemver { ) ) ); + } - // Upgrade the OptimismPortal to have a reference to the new AnchorStateRegistry. - IOptimismPortal2(payable(opChainAddrs.optimismPortal)).upgrade(newAnchorStateRegistryProxy); + // Deploy the ETHLockbox contract implementation and proxy. + // Initialize the ETHLockbox, and then set the OptimismPortal as an authorized portal. + IETHLockbox ethLockbox; + { + // Deploy the ETHLockbox contract proxy. + ethLockbox = IETHLockbox( + deployProxy({ + _l2ChainId: l2ChainId, + _proxyAdmin: _opChainConfigs[i].proxyAdmin, + _saltMixer: reusableSaltMixer(_opChainConfigs[i]), + _contractName: "ETHLockbox" + }) + ); + + // Initialize the ETHLockbox. + address[] memory portals = new address[](1); + portals[0] = opChainAddrs.optimismPortal; + upgradeToAndCall( + _opChainConfigs[i].proxyAdmin, + address(ethLockbox), + impls.ethLockboxImpl, + encodeETHLockboxInitializer(portals) + ); + + // Upgrade the OptimismPortal to have a reference to the new AnchorStateRegistry and ETHLockbox, + // and migrate the ETH balance to the ETHLockbox. + IOptimismPortal2(payable(opChainAddrs.optimismPortal)).upgrade( + newAnchorStateRegistryProxy, ethLockbox + ); } // Deploy and set a new permissioned game to update its prestate @@ -861,13 +889,14 @@ contract OPContractsManager is ISemver { returns (bytes memory) { return abi.encodeCall( - IOptimismPortal2.initialize, (_output.systemConfigProxy, superchainConfig, _output.anchorStateRegistryProxy) + IOptimismPortal2.initialize, + (_output.systemConfigProxy, superchainConfig, _output.anchorStateRegistryProxy, _output.ethLockboxProxy) ); } /// @notice Helper method for encoding the ETHLockbox initializer data. - function encodeETHLockboxInitializer() internal view virtual returns (bytes memory) { - return abi.encodeCall(IETHLockbox.initialize, (address(superchainConfig))); + function encodeETHLockboxInitializer(address[] memory _portals) internal view virtual returns (bytes memory) { + return abi.encodeCall(IETHLockbox.initialize, (address(superchainConfig), _portals)); } /// @notice Helper method for encoding the SystemConfig initializer data. diff --git a/packages/contracts-bedrock/src/L1/OptimismPortal2.sol b/packages/contracts-bedrock/src/L1/OptimismPortal2.sol index 3d593c549cf7c..a7ce1ff930516 100644 --- a/packages/contracts-bedrock/src/L1/OptimismPortal2.sol +++ b/packages/contracts-bedrock/src/L1/OptimismPortal2.sol @@ -2,11 +2,11 @@ pragma solidity 0.8.15; // Contracts +import { PAOBase } from "src/L1/PAOBase.sol"; import { Initializable } from "@openzeppelin/contracts/proxy/utils/Initializable.sol"; import { ResourceMetering } from "src/L1/ResourceMetering.sol"; // Libraries -import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import { EOA } from "src/libraries/EOA.sol"; import { SafeCall } from "src/libraries/SafeCall.sol"; import { Constants } from "src/libraries/Constants.sol"; @@ -15,10 +15,8 @@ import { Hashing } from "src/libraries/Hashing.sol"; import { SecureMerkleTrie } from "src/libraries/trie/SecureMerkleTrie.sol"; import { AddressAliasHelper } from "src/vendor/AddressAliasHelper.sol"; import { GameStatus, GameType } from "src/dispute/lib/Types.sol"; -import { Storage } from "src/libraries/Storage.sol"; // Interfaces -import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { ISemver } from "interfaces/universal/ISemver.sol"; import { ISystemConfig } from "interfaces/L1/ISystemConfig.sol"; import { IResourceMetering } from "interfaces/L1/IResourceMetering.sol"; @@ -26,17 +24,14 @@ import { ISuperchainConfig } from "interfaces/L1/ISuperchainConfig.sol"; import { IDisputeGameFactory } from "interfaces/dispute/IDisputeGameFactory.sol"; import { IDisputeGame } from "interfaces/dispute/IDisputeGame.sol"; import { IAnchorStateRegistry } from "interfaces/dispute/IAnchorStateRegistry.sol"; -import { IProxyAdmin } from "interfaces/universal/IProxyAdmin.sol"; +import { IETHLockbox } from "interfaces/L1/IETHLockbox.sol"; /// @custom:proxied true /// @title OptimismPortal2 /// @notice The OptimismPortal is a low-level contract responsible for passing messages between L1 /// 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 OptimismPortal2 is Initializable, ResourceMetering, ISemver { - /// @notice Allows for interactions with non standard ERC20 tokens. - using SafeERC20 for IERC20; - +contract OptimismPortal2 is PAOBase, Initializable, ResourceMetering, ISemver { /// @notice Represents a proven withdrawal. /// @custom:field disputeGameProxy Game that the withdrawal was proven against. /// @custom:field timestamp Timestamp at which the withdrawal was proven. @@ -124,6 +119,9 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ISemver { /// @notice Address of the AnchorStateRegistry contract. IAnchorStateRegistry public anchorStateRegistry; + /// @notice Address of the ETHLockbox contract. + IETHLockbox public ethLockbox; + /// @notice Emitted when a transaction is deposited from L1 to L2. The parameters of this event /// are read by the rollup node and used to derive deposit transactions on L2. /// @param from Address that triggered the deposit transaction. @@ -150,6 +148,15 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ISemver { /// @param success Whether the withdrawal transaction was successful. event WithdrawalFinalized(bytes32 indexed withdrawalHash, bool success); + /// @notice Emitted when the total ETH balance is migrated to the ETHLockbox. + /// @param ethBalance Amount of ETH migrated. + event ETHMigrated(uint256 ethBalance); + + /// @notice Emitted when the ETHLockbox contract is updated. + /// @param oldLockbox The address of the old ETHLockbox contract. + /// @param newLockbox The address of the new ETHLockbox contract. + event LockboxUpdated(address oldLockbox, address newLockbox); + /// @notice Thrown when a withdrawal has already been finalized. error OptimismPortal_AlreadyFinalized(); @@ -195,6 +202,9 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ISemver { /// @notice Thrown when a withdrawal has not been proven. error OptimismPortal_Unproven(); + /// @notice Thrown when the caller is not authorized to call the function. + error OptimismPortal_Unauthorized(); + /// @notice Reverts when paused. modifier whenNotPaused() { if (paused()) revert OptimismPortal_CallPaused(); @@ -202,9 +212,9 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ISemver { } /// @notice Semantic version. - /// @custom:semver 4.0.1 + /// @custom:semver 4.0.0 function version() public pure virtual returns (string memory) { - return "4.0.1"; + return "4.0.0"; } /// @param _proofMaturityDelaySeconds The proof maturity delay in seconds. @@ -217,10 +227,12 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ISemver { /// @param _systemConfig Address of the SystemConfig. /// @param _superchainConfig Address of the SuperchainConfig. /// @param _anchorStateRegistry Address of the AnchorStateRegistry. + /// @param _ethLockbox Contract of the ETHLockbox. function initialize( ISystemConfig _systemConfig, ISuperchainConfig _superchainConfig, - IAnchorStateRegistry _anchorStateRegistry + IAnchorStateRegistry _anchorStateRegistry, + IETHLockbox _ethLockbox ) external reinitializer(2) @@ -228,6 +240,7 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ISemver { systemConfig = _systemConfig; superchainConfig = _superchainConfig; anchorStateRegistry = _anchorStateRegistry; + ethLockbox = _ethLockbox; // Set the l2Sender slot, only if it is currently empty. This signals the first // initialization of the contract. @@ -240,8 +253,13 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ISemver { /// @notice Upgrades the OptimismPortal contract to have a reference to the AnchorStateRegistry. /// @param _anchorStateRegistry AnchorStateRegistry contract. - function upgrade(IAnchorStateRegistry _anchorStateRegistry) external reinitializer(2) { + /// @param _ethLockbox ETHLockbox contract. + function upgrade(IAnchorStateRegistry _anchorStateRegistry, IETHLockbox _ethLockbox) external reinitializer(2) { anchorStateRegistry = _anchorStateRegistry; + ethLockbox = _ethLockbox; + + // Migrate the whole ETH balance to the ETHLockbox. + _migrateLiquidity(); } /// @notice Getter for the current paused status. @@ -265,15 +283,6 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ISemver { return superchainConfig.guardian(); } - /// @notice Getter for the owner of the proxy admin. - /// The ProxyAdmin is the owner of the Proxy contract, which is the proxy used for the ETHLockbox. - function adminOwner() public view returns (address) { - // Get the proxy admin address reading for the reserved slot it has on the Proxy contract. - IProxyAdmin proxyAdmin = IProxyAdmin(Storage.getAddress(Constants.PROXY_OWNER_ADDRESS)); - // Return the owner of the proxy admin. - return proxyAdmin.owner(); - } - /// @custom:legacy /// @notice Getter for the dispute game finality delay. function disputeGameFinalityDelaySeconds() external view returns (uint256) { @@ -312,12 +321,21 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ISemver { } /// @notice Accepts ETH value without triggering a deposit to L2. - /// This function mainly exists for the sake of the migration between the legacy - /// Optimism system and Bedrock. function donateETH() external payable { // Intentionally empty. } + /// @notice Updates the ETHLockbox contract. + /// @param _newLockbox The address of the new ETHLockbox contract. + function updateLockbox(address _newLockbox) external { + if (msg.sender != PAO()) revert OptimismPortal_Unauthorized(); + + address oldLockbox = address(ethLockbox); + ethLockbox = IETHLockbox(_newLockbox); + + emit LockboxUpdated(oldLockbox, _newLockbox); + } + /// @notice Proves a withdrawal transaction. /// @param _tx Withdrawal transaction to finalize. /// @param _disputeGameIndex Index of the dispute game to prove the withdrawal against. @@ -447,6 +465,9 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ISemver { // Mark the withdrawal as finalized so it can't be replayed. finalizedWithdrawals[withdrawalHash] = true; + // Unlock the ETH from the ETHLockbox. + if (_tx.value > 0) ethLockbox.unlockETH(_tx.value); + // Set the l2Sender so contracts know who triggered this withdrawal on L2. l2Sender = _tx.sender; @@ -513,10 +534,18 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ISemver { } } + /// @notice Migrates the total ETH balance to the ETHLockbox. + function migrateLiquidity() public { + if (msg.sender != PAO()) revert OptimismPortal_Unauthorized(); + _migrateLiquidity(); + } + /// @notice Accepts deposits of ETH and data, and emits a TransactionDeposited event for use in /// deriving deposit transactions. Note that if a deposit is made by a contract, its /// address will be aliased when retrieved using `tx.origin` or `msg.sender`. Consider /// using the CrossDomainMessenger contracts for a simpler developer experience. + /// @dev The `msg.value` is locked on the ETHLockbox and minted as ETH when the deposit arrives on L2, + /// while `_value` specifies how much ETH to send to the target. /// @param _to Target address on L2. /// @param _value ETH value to send to the recipient. /// @param _gasLimit Amount of L2 gas to purchase by burning gas on L1. @@ -533,6 +562,9 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ISemver { payable metered(_gasLimit) { + // Lock the ETH in the ETHLockbox. + if (msg.value > 0) ethLockbox.lockETH{ value: msg.value }(); + // Just to be safe, make sure that people specify address(0) as the target when doing // contract creations. if (_isCreation && _to != address(0)) { @@ -578,9 +610,16 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ISemver { /// @notice Checks if a target address is unsafe. function _isUnsafeTarget(address _target) internal view virtual returns (bool) { - // Prevent users from creating a deposit transaction where this address is the message - // sender on L2. - return _target == address(this); + // Prevent users from targetting an unsafe target address on a withdrawal transaction. + return _target == address(this) || _target == address(ethLockbox); + } + + /// @notice Migrates the total ETH balance to the ETHLockbox. + function _migrateLiquidity() internal { + uint256 ethBalance = address(this).balance; + ethLockbox.lockETH{ value: ethBalance }(); + + emit ETHMigrated(ethBalance); } /// @notice Getter for the resource config. Used internally by the ResourceMetering contract. diff --git a/packages/contracts-bedrock/src/L1/OptimismPortalInterop.sol b/packages/contracts-bedrock/src/L1/OptimismPortalInterop.sol index 532f67ac0be5a..4627ab1ee9c9c 100644 --- a/packages/contracts-bedrock/src/L1/OptimismPortalInterop.sol +++ b/packages/contracts-bedrock/src/L1/OptimismPortalInterop.sol @@ -17,9 +17,6 @@ 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 Thrown when the caller is not the system config. - error OptimismPortal_Unauthorized(); - /// @param _proofMaturityDelaySeconds The proof maturity delay in seconds. constructor(uint256 _proofMaturityDelaySeconds) OptimismPortal2(_proofMaturityDelaySeconds) { } diff --git a/packages/contracts-bedrock/src/L1/PAOBase.sol b/packages/contracts-bedrock/src/L1/PAOBase.sol new file mode 100644 index 0000000000000..ecf5524d0c7ba --- /dev/null +++ b/packages/contracts-bedrock/src/L1/PAOBase.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.15; + +import { IProxyAdmin } from "interfaces/universal/IProxyAdmin.sol"; +import { Storage } from "src/libraries/Storage.sol"; +import { Constants } from "src/libraries/Constants.sol"; + +/// @notice Base contract for ProxyAdmin-owned contracts. It's main goal is to expose the ProxyAdmin owner address on +/// a function and also to check if the current contract and a given proxy have the same PAO. +abstract contract PAOBase { + /// @notice Getter for the owner of the ProxyAdmin. + /// The ProxyAdmin is the owner of the current proxy contract. + function PAO() public view returns (address) { + // Get the proxy admin address reading for the reserved slot it has on the Proxy contract. + IProxyAdmin proxyAdmin = IProxyAdmin(Storage.getAddress(Constants.PROXY_OWNER_ADDRESS)); + // Return the owner of the proxy admin. + return proxyAdmin.owner(); + } + + /// @notice Checks if the ProxyAdmin owner of the current contract is the same as the ProxyAdmin owner of the given + /// proxy. + /// @param _proxy The address of the proxy to check. + function _samePAO(address _proxy) internal view returns (bool) { + return PAO() == PAOBase(_proxy).PAO(); + } +} diff --git a/packages/contracts-bedrock/test/L1/ETHLockbox.t.sol b/packages/contracts-bedrock/test/L1/ETHLockbox.t.sol index bb300fb07db12..126f25e1ffd0e 100644 --- a/packages/contracts-bedrock/test/L1/ETHLockbox.t.sol +++ b/packages/contracts-bedrock/test/L1/ETHLockbox.t.sol @@ -10,7 +10,7 @@ import { IOptimismPortal2 as IOptimismPortal } from "interfaces/L1/IOptimismPort import { IETHLockbox } from "interfaces/L1/IETHLockbox.sol"; import { ISuperchainConfig } from "interfaces/L1/ISuperchainConfig.sol"; -import { IProxyAdminOwnable } from "interfaces/L1/IProxyAdminOwnable.sol"; +import { IPAOBase } from "interfaces/L1/IPAOBase.sol"; // Test import { CommonTest } from "test/setup/CommonTest.sol"; @@ -29,27 +29,29 @@ contract ETHLockboxTest is CommonTest { event LiquidityReceived(address indexed lockbox); ProxyAdmin public proxyAdmin = ProxyAdmin(Predeploys.PROXY_ADMIN); - address public adminOwner; + address public PAO; function setUp() public virtual override { super.setUp(); - adminOwner = proxyAdmin.owner(); + PAO = proxyAdmin.owner(); } /// @notice Tests the superchain config was correctly set during initialization. function test_initialization_succeeds() public view { assertEq(address(ethLockbox.superchainConfig()), address(superchainConfig)); + assertEq(ethLockbox.authorizedPortals(address(optimismPortal2)), true); } /// @notice Tests it reverts when the contract is already initialized. function test_initialize_alreadyInitialized_reverts() public { - vm.expectRevert(InvalidInitialization.selector); - ethLockbox.initialize(address(superchainConfig)); + vm.expectRevert("Initializable: contract is already initialized"); + address[] memory _portals = new address[](1); + ethLockbox.initialize(address(superchainConfig), _portals); } /// @notice Tests the proxy admin owner is correctly returned. - function test_proxyAdminOwner_succeeds() public view { - assertEq(ethLockbox.adminOwner(), adminOwner); + function test_proxyPAO_succeeds() public view { + assertEq(ethLockbox.PAO(), PAO); } /// @notice Tests the paused status is correctly returned. @@ -72,12 +74,10 @@ contract ETHLockboxTest is CommonTest { deal(address(_lockbox), _value); // Mock the admin owner of the lockbox to be the same as the current lockbox proxy admin owner - vm.mockCall( - address(_lockbox), abi.encodeCall(IProxyAdminOwnable.adminOwner, ()), abi.encode(proxyAdmin.owner()) - ); + vm.mockCall(address(_lockbox), abi.encodeCall(IPAOBase.PAO, ()), abi.encode(proxyAdmin.owner())); // Authorize the lockbox - vm.prank(adminOwner); + vm.prank(PAO); ethLockbox.authorizeLockbox(_lockbox); // Get the balance of the lockbox before the receive @@ -134,10 +134,10 @@ contract ETHLockboxTest is CommonTest { vm.assume(_portal != address(ethLockbox)); // Mock the admin owner of the portal to be the same as the current lockbox proxy admin owner - vm.mockCall(address(_portal), abi.encodeCall(IProxyAdminOwnable.adminOwner, ()), abi.encode(proxyAdmin.owner())); + vm.mockCall(address(_portal), abi.encodeCall(IPAOBase.PAO, ()), abi.encode(proxyAdmin.owner())); // Set the portal as an authorized portal - vm.prank(adminOwner); + vm.prank(PAO); ethLockbox.authorizePortal(_portal); // Deal the ETH amount to the portal @@ -231,10 +231,10 @@ contract ETHLockboxTest is CommonTest { vm.assume(_portal != address(ethLockbox)); // Mock the admin owner of the portal to be the same as the current lockbox proxy admin owner - vm.mockCall(address(_portal), abi.encodeCall(IProxyAdminOwnable.adminOwner, ()), abi.encode(proxyAdmin.owner())); + vm.mockCall(address(_portal), abi.encodeCall(IPAOBase.PAO, ()), abi.encode(proxyAdmin.owner())); // Set the portal as an authorized portal - vm.prank(adminOwner); + vm.prank(PAO); ethLockbox.authorizePortal(_portal); // Deal the ETH amount to the lockbox @@ -277,12 +277,10 @@ contract ETHLockboxTest is CommonTest { // Authorize the portal if (!ethLockbox.authorizedPortals(_portal)) { // Mock the admin owner of the portal to be the same as the current lockbox proxy admin owner - vm.mockCall( - address(_portal), abi.encodeCall(IProxyAdminOwnable.adminOwner, ()), abi.encode(proxyAdmin.owner()) - ); + vm.mockCall(address(_portal), abi.encodeCall(IPAOBase.PAO, ()), abi.encode(proxyAdmin.owner())); // Authorize the portal - vm.prank(adminOwner); + vm.prank(PAO); ethLockbox.authorizePortal(_portal); } @@ -290,27 +288,27 @@ contract ETHLockboxTest is CommonTest { vm.expectRevert(IETHLockbox.ETHLockbox_AlreadyAuthorized.selector); // Call the `authorizePortal` function with the portal - vm.prank(adminOwner); + vm.prank(PAO); ethLockbox.authorizePortal(_portal); } - /// @notice Tests the `authorizePortal` function reverts when the admin owner of the portal is not the same as the - /// admin owner of the lockbox. - function testFuzz_authorizePortal_differentAdminOwner_reverts(address _portal) public { - vm.mockCall(address(_portal), abi.encodeCall(IProxyAdminOwnable.adminOwner, ()), abi.encode(address(0))); + /// @notice Tests the `authorizePortal` function reverts when the PAO of the portal is not the same as the PAO of + /// the lockbox. + function testFuzz_authorizePortal_differentPAO_reverts(address _portal) public { + vm.mockCall(address(_portal), abi.encodeCall(IPAOBase.PAO, ()), abi.encode(address(0))); // Expect the revert with `DifferentOwner` selector - vm.expectRevert(IETHLockbox.ETHLockbox_DifferentAdminOwner.selector); + vm.expectRevert(IETHLockbox.ETHLockbox_DifferentPAO.selector); // Call the `authorizePortal` function - vm.prank(adminOwner); + vm.prank(PAO); ethLockbox.authorizePortal(_portal); } /// @notice Tests the `authorizeLockbox` function succeeds using the `optimismPortal2` address as the portal. function test_authorizePortal_succeeds() public { // Calculate the correct storage slot for the mapping value - bytes32 mappingSlot = bytes32(uint256(0)); // position on the layout + bytes32 mappingSlot = bytes32(uint256(1)); // position on the layout address key = address(optimismPortal2); bytes32 slot = keccak256(abi.encode(key, mappingSlot)); @@ -322,7 +320,7 @@ contract ETHLockboxTest is CommonTest { emit PortalAuthorized(address(optimismPortal2)); // Call the `authorizePortal` function with the portal - vm.prank(adminOwner); + vm.prank(PAO); ethLockbox.authorizePortal(address(optimismPortal2)); // Assert the portal is authorized @@ -334,14 +332,14 @@ contract ETHLockboxTest is CommonTest { vm.assume(!ethLockbox.authorizedPortals(_portal)); // Mock the admin owner of the portal to be the same as the current lockbox proxy admin owner - vm.mockCall(address(_portal), abi.encodeCall(IProxyAdminOwnable.adminOwner, ()), abi.encode(proxyAdmin.owner())); + vm.mockCall(address(_portal), abi.encodeCall(IPAOBase.PAO, ()), abi.encode(proxyAdmin.owner())); // Expect the `PortalAuthorized` event to be emitted vm.expectEmit(address(ethLockbox)); emit PortalAuthorized(_portal); // Call the `authorizePortal` function with the portal - vm.prank(adminOwner); + vm.prank(PAO); ethLockbox.authorizePortal(_portal); // Assert the portal is authorized @@ -364,11 +362,9 @@ contract ETHLockboxTest is CommonTest { function testFuzz_authorizeLockbox_alreadyAuthorized_reverts(address _lockbox) public { // Authorize the lockbox if (!ethLockbox.authorizedLockboxes(_lockbox)) { - vm.mockCall( - address(_lockbox), abi.encodeCall(IProxyAdminOwnable.adminOwner, ()), abi.encode(proxyAdmin.owner()) - ); + vm.mockCall(address(_lockbox), abi.encodeCall(IPAOBase.PAO, ()), abi.encode(proxyAdmin.owner())); - vm.prank(adminOwner); + vm.prank(PAO); ethLockbox.authorizeLockbox(_lockbox); } @@ -376,20 +372,20 @@ contract ETHLockboxTest is CommonTest { vm.expectRevert(IETHLockbox.ETHLockbox_AlreadyAuthorized.selector); // Call the `authorizeLockbox` function with the lockbox - vm.prank(adminOwner); + vm.prank(PAO); ethLockbox.authorizeLockbox(_lockbox); } - /// @notice Tests the `authorizeLockbox` function reverts when the admin owner of the lockbox is not the same as the - /// admin owner of the proxy admin. - function testFuzz_authorizeLockbox_differentAdminOwner_reverts(address _lockbox) public { - vm.mockCall(address(_lockbox), abi.encodeCall(IProxyAdminOwnable.adminOwner, ()), abi.encode(address(0))); + /// @notice Tests the `authorizeLockbox` function reverts when the PAO of the lockbox is not the same as the PAO of + /// the proxy admin. + function testFuzz_authorizeLockbox_differentPAO_reverts(address _lockbox) public { + vm.mockCall(address(_lockbox), abi.encodeCall(IPAOBase.PAO, ()), abi.encode(address(0))); - // Expect the revert with `ETHLockbox_DifferentAdminOwner` selector - vm.expectRevert(IETHLockbox.ETHLockbox_DifferentAdminOwner.selector); + // Expect the revert with `ETHLockbox_DifferentPAO` selector + vm.expectRevert(IETHLockbox.ETHLockbox_DifferentPAO.selector); // Call the `authorizeLockbox` function with the lockbox - vm.prank(adminOwner); + vm.prank(PAO); ethLockbox.authorizeLockbox(_lockbox); } @@ -398,16 +394,14 @@ contract ETHLockboxTest is CommonTest { vm.assume(!ethLockbox.authorizedLockboxes(_lockbox)); // Mock the admin owner of the lockbox to be the same as the current lockbox proxy admin owner - vm.mockCall( - address(_lockbox), abi.encodeCall(IProxyAdminOwnable.adminOwner, ()), abi.encode(proxyAdmin.owner()) - ); + vm.mockCall(address(_lockbox), abi.encodeCall(IPAOBase.PAO, ()), abi.encode(proxyAdmin.owner())); // Expect the `LockboxAuthorized` event to be emitted vm.expectEmit(address(ethLockbox)); emit LockboxAuthorized(_lockbox); // Authorize the lockbox - vm.prank(adminOwner); + vm.prank(PAO); ethLockbox.authorizeLockbox(_lockbox); // Assert the lockbox is authorized @@ -426,25 +420,23 @@ contract ETHLockboxTest is CommonTest { ethLockbox.migrateLiquidity(address(optimismPortal2)); } - /// @notice Tests the `migrateLiquidity` function reverts when the admin owner of the lockbox is not the same as the - /// admin owner of the proxy admin. - function testFuzz_migrateLiquidity_differentAdminOwner_reverts(address _lockbox) public { - vm.mockCall(address(_lockbox), abi.encodeCall(IProxyAdminOwnable.adminOwner, ()), abi.encode(address(0))); + /// @notice Tests the `migrateLiquidity` function reverts when the PAO of the lockbox is not the same as the PAO of + /// the proxy admin. + function testFuzz_migrateLiquidity_differentPAO_reverts(address _lockbox) public { + vm.mockCall(address(_lockbox), abi.encodeCall(IPAOBase.PAO, ()), abi.encode(address(0))); - // Expect the revert with `ETHLockbox_DifferentAdminOwner` selector - vm.expectRevert(IETHLockbox.ETHLockbox_DifferentAdminOwner.selector); + // Expect the revert with `ETHLockbox_DifferentPAO` selector + vm.expectRevert(IETHLockbox.ETHLockbox_DifferentPAO.selector); // Call the `migrateLiquidity` function with the lockbox - vm.prank(adminOwner); + vm.prank(PAO); ethLockbox.migrateLiquidity(_lockbox); } /// @notice Tests the `migrateLiquidity` function succeeds function testFuzz_migrateLiquidity_succeeds(uint256 _balance, address _lockbox) public { // Mock on the lockbox that will receive the migration for it to succeed - vm.mockCall( - address(_lockbox), abi.encodeCall(IProxyAdminOwnable.adminOwner, ()), abi.encode(proxyAdmin.owner()) - ); + vm.mockCall(address(_lockbox), abi.encodeCall(IPAOBase.PAO, ()), abi.encode(proxyAdmin.owner())); vm.mockCall( address(_lockbox), abi.encodeCall(IETHLockbox.authorizedLockboxes, (address(ethLockbox))), abi.encode(true) ); @@ -462,7 +454,7 @@ contract ETHLockboxTest is CommonTest { uint256 newLockboxBalanceBefore = address(_lockbox).balance; // Call the `migrateLiquidity` function with the lockbox - vm.prank(adminOwner); + vm.prank(PAO); ethLockbox.migrateLiquidity(_lockbox); // Assert the liquidity was migrated diff --git a/packages/contracts-bedrock/test/L1/L1StandardBridge.t.sol b/packages/contracts-bedrock/test/L1/L1StandardBridge.t.sol index 093c87c5e19fc..f5a418319ca11 100644 --- a/packages/contracts-bedrock/test/L1/L1StandardBridge.t.sol +++ b/packages/contracts-bedrock/test/L1/L1StandardBridge.t.sol @@ -163,7 +163,8 @@ contract L1StandardBridge_Initialize_TestFail is CommonTest { } contract L1StandardBridge_Receive_Test is CommonTest { /// @dev Tests receive bridges ETH successfully. function test_receive_succeeds() external { - uint256 balanceBefore = address(optimismPortal2).balance; + uint256 portalBalanceBefore = address(optimismPortal2).balance; + uint256 ethLockboxBalanceBefore = address(ethLockbox).balance; // The legacy event must be emitted for backwards compatibility vm.expectEmit(address(l1StandardBridge)); @@ -187,7 +188,8 @@ contract L1StandardBridge_Receive_Test is CommonTest { vm.prank(alice, alice); (bool success,) = address(l1StandardBridge).call{ value: 100 }(hex""); assertEq(success, true); - assertEq(address(optimismPortal2).balance, balanceBefore + 100); + assertEq(address(optimismPortal2).balance, portalBalanceBefore); + assertEq(address(ethLockbox).balance, ethLockboxBalanceBefore + 100); } } @@ -196,7 +198,7 @@ contract PreBridgeETH is CommonTest { /// on whether the bridge call is legacy or not. function _preBridgeETH(bool isLegacy, uint256 value) internal { if (!isForkTest()) { - assertEq(address(optimismPortal2).balance, 0); + assertEq(address(optimismPortal2).balance, 0, "OptimismPortal2 balance should be 0"); } uint256 nonce = l1CrossDomainMessenger.messageNonce(); uint256 version = 0; // Internal constant in the OptimismPortal: DEPOSIT_VERSION @@ -277,9 +279,11 @@ contract L1StandardBridge_DepositETH_Test is PreBridgeETH { vm.etch(alice, abi.encodePacked(hex"EF0100", address(0))); _preBridgeETH({ isLegacy: true, value: 500 }); - uint256 balanceBefore = address(optimismPortal2).balance; + uint256 portalBalanceBefore = address(optimismPortal2).balance; + uint256 ethLockboxBalanceBefore = address(ethLockbox).balance; l1StandardBridge.depositETH{ value: 500 }(50000, hex"dead"); - assertEq(address(optimismPortal2).balance, balanceBefore + 500); + assertEq(address(optimismPortal2).balance, portalBalanceBefore); + assertEq(address(ethLockbox).balance, ethLockboxBalanceBefore + 500); } } @@ -301,9 +305,11 @@ contract L1StandardBridge_BridgeETH_Test is PreBridgeETH { /// ETH ends up in the optimismPortal. function test_bridgeETH_succeeds() external { _preBridgeETH({ isLegacy: false, value: 500 }); - uint256 balanceBefore = address(optimismPortal2).balance; + uint256 portalBalanceBefore = address(optimismPortal2).balance; + uint256 ethLockboxBalanceBefore = address(ethLockbox).balance; l1StandardBridge.bridgeETH{ value: 500 }(50000, hex"dead"); - assertEq(address(optimismPortal2).balance, balanceBefore + 500); + assertEq(address(optimismPortal2).balance, portalBalanceBefore); + assertEq(address(ethLockbox).balance, ethLockboxBalanceBefore + 500); } } @@ -381,9 +387,11 @@ contract L1StandardBridge_DepositETHTo_Test is PreBridgeETHTo { /// ETH ends up in the optimismPortal. function test_depositETHTo_succeeds() external { _preBridgeETHTo({ isLegacy: true, value: 600 }); - uint256 balanceBefore = address(optimismPortal2).balance; + uint256 portalBalanceBefore = address(optimismPortal2).balance; + uint256 ethLockboxBalanceBefore = address(ethLockbox).balance; l1StandardBridge.depositETHTo{ value: 600 }(bob, 60000, hex"dead"); - assertEq(address(optimismPortal2).balance, balanceBefore + 600); + assertEq(address(optimismPortal2).balance, portalBalanceBefore); + assertEq(address(ethLockbox).balance, ethLockboxBalanceBefore + 600); } } @@ -395,9 +403,11 @@ contract L1StandardBridge_BridgeETHTo_Test is PreBridgeETHTo { /// ETH ends up in the optimismPortal. function test_bridgeETHTo_succeeds() external { _preBridgeETHTo({ isLegacy: false, value: 600 }); - uint256 balanceBefore = address(optimismPortal2).balance; + uint256 portalBalanceBefore = address(optimismPortal2).balance; + uint256 ethLockboxBalanceBefore = address(ethLockbox).balance; l1StandardBridge.bridgeETHTo{ value: 600 }(bob, 60000, hex"dead"); - assertEq(address(optimismPortal2).balance, balanceBefore + 600); + assertEq(address(optimismPortal2).balance, portalBalanceBefore); + assertEq(address(ethLockbox).balance, ethLockboxBalanceBefore + 600); } } diff --git a/packages/contracts-bedrock/test/L1/OptimismPortal2.t.sol b/packages/contracts-bedrock/test/L1/OptimismPortal2.t.sol index 6fc7d0e3490b0..34efbdccbd09d 100644 --- a/packages/contracts-bedrock/test/L1/OptimismPortal2.t.sol +++ b/packages/contracts-bedrock/test/L1/OptimismPortal2.t.sol @@ -9,7 +9,7 @@ import { EIP1967Helper } from "test/mocks/EIP1967Helper.sol"; // Contracts import { SuperchainConfig } from "src/L1/SuperchainConfig.sol"; -import { ProxyAdmin } from "src/universal/ProxyAdmin.sol"; +import { Predeploys } from "src/libraries/Predeploys.sol"; // Libraries import { Types } from "src/libraries/Types.sol"; @@ -18,7 +18,6 @@ import { Constants } from "src/libraries/Constants.sol"; import { AddressAliasHelper } from "src/vendor/AddressAliasHelper.sol"; import { EIP1967Helper } from "test/mocks/EIP1967Helper.sol"; import "src/dispute/lib/Types.sol"; -import { Predeploys } from "src/libraries/Predeploys.sol"; // Interfaces import { IResourceMetering } from "interfaces/L1/IResourceMetering.sol"; @@ -26,6 +25,8 @@ import { IOptimismPortal2 } from "interfaces/L1/IOptimismPortal2.sol"; import { IDisputeGame } from "interfaces/dispute/IDisputeGame.sol"; import { IFaultDisputeGame } from "interfaces/dispute/IFaultDisputeGame.sol"; import { IProxy } from "interfaces/universal/IProxy.sol"; +import { IAnchorStateRegistry } from "interfaces/dispute/IAnchorStateRegistry.sol"; +import { IETHLockbox } from "interfaces/L1/IETHLockbox.sol"; contract OptimismPortal2_Test is CommonTest { address depositor; @@ -55,8 +56,11 @@ contract OptimismPortal2_Test is CommonTest { assertEq(opImpl.l2Sender(), address(0)); // TODO(opcm upgrades): remove skip once upgrade path is implemented - returnIfForkTest("OptimismPortal2_Test: anchorStateRegistry is nonzero on OP mainnet"); + returnIfForkTest("OptimismPortal2_Test: anchorStateRegistry getter doesn't yet exist on OP mainnet"); assertEq(address(opImpl.anchorStateRegistry()), address(0)); + + returnIfForkTest("OptimismPortal2_Test: ethLockbox getter doesn't yet exist on OP mainnet"); + assertEq(address(opImpl.ethLockbox()), address(0)); } /// @dev Tests that the initializer sets the correct values. @@ -69,6 +73,7 @@ contract OptimismPortal2_Test is CommonTest { assertEq(optimismPortal2.l2Sender(), Constants.DEFAULT_L2_SENDER); assertEq(optimismPortal2.paused(), false); assertEq(address(optimismPortal2.systemConfig()), address(systemConfig)); + assertEq(address(optimismPortal2.ethLockbox()), address(ethLockbox)); returnIfForkTest( "OptimismPortal2_Initialize_Test: Do not check guardian and respectedGameType on forked networks" @@ -81,6 +86,28 @@ contract OptimismPortal2_Test is CommonTest { assertEq(optimismPortal2.respectedGameType().raw(), deploy.cfg().respectedGameType()); } + function testFuzz_upgrade_succeeds(address _newAnchorStateRegistry, uint256 _balance) external { + // Set the initialize state of the portal to false. + vm.store(address(optimismPortal2), bytes32(uint256(0)), bytes32(uint256(0))); + + // Set the balance of the portal and get the lockbox balance before the upgrade. + deal(address(optimismPortal2), _balance); + uint256 lockboxBalanceBefore = address(ethLockbox).balance; + + // Expect the ETH to be migrated to the lockbox. + vm.expectCall(address(ethLockbox), _balance, abi.encodeCall(ethLockbox.lockETH, ())); + + // Call the upgrade function. + vm.prank(Predeploys.PROXY_ADMIN); + optimismPortal2.upgrade(IAnchorStateRegistry(_newAnchorStateRegistry), IETHLockbox(ethLockbox)); + + // Assert the portal is properly upgraded. + assertEq(address(optimismPortal2.ethLockbox()), address(ethLockbox)); + assertEq(address(optimismPortal2.anchorStateRegistry()), _newAnchorStateRegistry); + assertEq(address(optimismPortal2).balance, 0); + assertEq(address(ethLockbox).balance, lockboxBalanceBefore + _balance); + } + /// @dev Tests that `pause` successfully pauses /// when called by the GUARDIAN. function test_pause_succeeds() external { @@ -142,14 +169,10 @@ contract OptimismPortal2_Test is CommonTest { assertEq(optimismPortal2.paused(), true); } - /// @notice Tests the proxy admin owner is correctly returned. - function test_proxyAdminOwner_succeeds() public view { - assertEq(optimismPortal2.adminOwner(), ProxyAdmin(Predeploys.PROXY_ADMIN).owner()); - } - /// @dev Tests that `receive` successdully deposits ETH. function testFuzz_receive_succeeds(uint256 _value) external { uint256 balanceBefore = address(optimismPortal2).balance; + uint256 lockboxBalanceBefore = address(ethLockbox).balance; _value = bound(_value, 0, type(uint256).max - balanceBefore); vm.expectEmit(address(optimismPortal2)); @@ -163,13 +186,17 @@ contract OptimismPortal2_Test is CommonTest { _data: hex"" }); + // Expect call to the ETHLockbox to lock the funds only if the value is greater than 0. + vm.expectCall(address(ethLockbox), _value, abi.encodeCall(ethLockbox.lockETH, ()), _value > 0 ? 1 : 0); + // 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, balanceBefore + _value); + assertEq(address(optimismPortal2).balance, balanceBefore); + assertEq(address(ethLockbox).balance, lockboxBalanceBefore + _value); } /// @dev Tests that `depositTransaction` reverts when the destination address is non-zero @@ -257,6 +284,7 @@ contract OptimismPortal2_Test is CommonTest { if (_isCreation) _to = address(0); uint256 balanceBefore = address(optimismPortal2).balance; + uint256 lockboxBalanceBefore = address(ethLockbox).balance; _mint = bound(_mint, 0, type(uint256).max - balanceBefore); // EOA emulation @@ -271,6 +299,9 @@ contract OptimismPortal2_Test is CommonTest { _data: _data }); + // Expect call to the ETHLockbox to lock the funds only if the value is greater than 0. + vm.expectCall(address(ethLockbox), _mint, abi.encodeCall(ethLockbox.lockETH, ()), _mint > 0 ? 1 : 0); + vm.deal(depositor, _mint); vm.prank(depositor, depositor); optimismPortal2.depositTransaction{ value: _mint }({ @@ -280,7 +311,9 @@ contract OptimismPortal2_Test is CommonTest { _isCreation: _isCreation, _data: _data }); - assertEq(address(optimismPortal2).balance, balanceBefore + _mint); + + assertEq(address(optimismPortal2).balance, balanceBefore); + assertEq(address(ethLockbox).balance, lockboxBalanceBefore + _mint); } /// @dev Tests that `depositTransaction` succeeds for an EOA using 7702 delegation. @@ -355,6 +388,7 @@ contract OptimismPortal2_Test is CommonTest { if (_isCreation) _to = address(0); uint256 balanceBefore = address(optimismPortal2).balance; + uint256 lockboxBalanceBefore = address(ethLockbox).balance; _mint = bound(_mint, 0, type(uint256).max - balanceBefore); vm.expectEmit(address(optimismPortal2)); @@ -368,6 +402,9 @@ contract OptimismPortal2_Test is CommonTest { _data: _data }); + // Expect call to the ETHLockbox to lock the funds only if the value is greater than 0. + vm.expectCall(address(ethLockbox), _mint, abi.encodeCall(ethLockbox.lockETH, ()), _mint > 0 ? 1 : 0); + vm.deal(address(this), _mint); vm.prank(address(this)); optimismPortal2.depositTransaction{ value: _mint }({ @@ -377,7 +414,8 @@ contract OptimismPortal2_Test is CommonTest { _isCreation: _isCreation, _data: _data }); - assertEq(address(optimismPortal2).balance, balanceBefore + _mint); + assertEq(address(optimismPortal2).balance, balanceBefore); + assertEq(address(ethLockbox).balance, lockboxBalanceBefore + _mint); } /// @dev Tests that the donateETH function donates ETH and does no state read/write @@ -386,6 +424,7 @@ contract OptimismPortal2_Test is CommonTest { vm.deal(alice, _amount); uint256 preBalance = address(optimismPortal2).balance; + uint256 lockboxBalanceBefore = address(ethLockbox).balance; _amount = bound(_amount, 0, type(uint256).max - preBalance); vm.startStateDiffRecording(); @@ -394,6 +433,8 @@ contract OptimismPortal2_Test is CommonTest { // not necessary since it's checked below assertEq(address(optimismPortal2).balance, preBalance + _amount); + // check that the ETHLockbox balance is unchanged + assertEq(address(ethLockbox).balance, lockboxBalanceBefore); // 0 for extcodesize of proxy before being called by this test, // 1 for the call to the proxy by the pranked address @@ -421,6 +462,29 @@ contract OptimismPortal2_Test is CommonTest { // storage accesses of delegate call of proxy to impl is empty (No storage read or write!) assertEq(accountAccesses[2].storageAccesses.length, 0); } + + /// @dev Tests that `updateLockbox` reverts if the caller is not the PAO. + function testFuzz_updateLockbox_notPAO_reverts(address _caller) external { + vm.assume(_caller != optimismPortal2.PAO()); + vm.expectRevert(IOptimismPortal2.OptimismPortal_Unauthorized.selector); + + vm.prank(_caller); + optimismPortal2.updateLockbox(address(1)); + } + + /// @dev Tests that `updateLockbox` updates the ETHLockbox contract. + function testFuzz_updateLockbox_succeeds(address _newLockbox) external { + address oldLockbox = address(optimismPortal2.ethLockbox()); + vm.assume(_newLockbox != oldLockbox); + + vm.expectEmit(address(optimismPortal2)); + emit LockboxUpdated(oldLockbox, _newLockbox); + + vm.prank(optimismPortal2.PAO()); + optimismPortal2.updateLockbox(_newLockbox); + + assertEq(address(optimismPortal2.ethLockbox()), _newLockbox); + } } contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { @@ -501,7 +565,7 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { vm.warp(block.timestamp + game.maxClockDuration().raw() + 1 seconds); // Fund the portal so that we can withdraw ETH. - vm.deal(address(optimismPortal2), 0xFFFFFFFF); + vm.deal(address(ethLockbox), 0xFFFFFFFF); } /// @dev Asserts that the reentrant call will revert. @@ -514,6 +578,17 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { assertFalse(optimismPortal2.finalizedWithdrawals(Hashing.hashWithdrawal(_defaultTx))); } + /// @dev Tests that `finalizeWithdrawalTransaction` reverts when the target is the portal contract or the lockbox. + function test_finalizeWithdrawalTransaction_badTarget_reverts() external { + _defaultTx.target = address(optimismPortal2); + vm.expectRevert(IOptimismPortal2.OptimismPortal_BadTarget.selector); + optimismPortal2.finalizeWithdrawalTransaction(_defaultTx); + + _defaultTx.target = address(ethLockbox); + vm.expectRevert(IOptimismPortal2.OptimismPortal_BadTarget.selector); + optimismPortal2.finalizeWithdrawalTransaction(_defaultTx); + } + /// @dev Tests that `proveWithdrawalTransaction` reverts when paused. function test_proveWithdrawalTransaction_paused_reverts() external { vm.prank(optimismPortal2.guardian()); @@ -538,6 +613,15 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { _outputRootProof: _outputRootProof, _withdrawalProof: _withdrawalProof }); + + _defaultTx.target = address(ethLockbox); + vm.expectRevert(IOptimismPortal2.OptimismPortal_BadTarget.selector); + optimismPortal2.proveWithdrawalTransaction({ + _tx: _defaultTx, + _disputeGameIndex: _proposedGameIndex, + _outputRootProof: _outputRootProof, + _withdrawalProof: _withdrawalProof + }); } /// @dev Tests that `proveWithdrawalTransaction` reverts when the current timestamp is less @@ -875,7 +959,7 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { vm.warp(block.timestamp + game_noData.maxClockDuration().raw() + 1 seconds); // Fund the portal so that we can withdraw ETH. vm.store(address(optimismPortal2), bytes32(uint256(61)), bytes32(uint256(0xFFFFFFFF))); - vm.deal(address(optimismPortal2), 0xFFFFFFFF); + vm.deal(address(ethLockbox), 0xFFFFFFFF); uint256 bobBalanceBefore = bob.balance; @@ -1259,7 +1343,7 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { // Total ETH supply is currently about 120M ETH. uint256 value = bound(_value, 0, 200_000_000 ether); - vm.deal(address(optimismPortal2), value); + vm.deal(address(ethLockbox), value); uint256 gasLimit = bound(_gasLimit, 0, 50_000_000); uint256 nonce = l2ToL1MessagePasser.messageNonce(); @@ -1339,7 +1423,7 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { // Total ETH supply is currently about 120M ETH. uint256 value = bound(_value, 0, 200_000_000 ether); - vm.deal(address(optimismPortal2), value); + vm.deal(address(ethLockbox), value); uint256 gasLimit = bound(_gasLimit, 0, 50_000_000); uint256 nonce = l2ToL1MessagePasser.messageNonce(); @@ -1765,6 +1849,39 @@ contract OptimismPortal2_Upgradeable_Test is CommonTest { } } +contract OptimismPortal2_LiquidityMigration_Test is CommonTest { + function setUp() public override { + super.setUp(); + } + + /// @notice Tests the liquidity migration from the portal to the lockbox reverts if not called by the admin owner. + function testFuzz_migrateLiquidity_notPAO_reverts(address _caller) external { + vm.assume(_caller != optimismPortal2.PAO()); + vm.expectRevert(IOptimismPortal2.OptimismPortal_Unauthorized.selector); + vm.prank(_caller); + optimismPortal2.migrateLiquidity(); + } + + /// @notice Tests that the liquidity migration from the portal to the lockbox succeeds. + function test_migrateLiquidity_succeeds(uint256 _portalBalance) external { + vm.deal(address(optimismPortal2), _portalBalance); + + uint256 lockboxBalanceBefore = address(ethLockbox).balance; + address PAO = optimismPortal2.PAO(); + + vm.expectCall(address(ethLockbox), _portalBalance, abi.encodeCall(ethLockbox.lockETH, ())); + + vm.expectEmit(address(optimismPortal2)); + emit ETHMigrated(_portalBalance); + + vm.prank(PAO); + optimismPortal2.migrateLiquidity(); + + assertEq(address(optimismPortal2).balance, 0); + assertEq(address(ethLockbox).balance, lockboxBalanceBefore + _portalBalance); + } +} + /// @title OptimismPortal2_ResourceFuzz_Test /// @dev Test various values of the resource metering config to ensure that deposits cannot be /// broken by changing the config. diff --git a/packages/contracts-bedrock/test/invariants/OptimismPortal2.t.sol b/packages/contracts-bedrock/test/invariants/OptimismPortal2.t.sol index dbb8f231b28c7..8ad36653893fe 100644 --- a/packages/contracts-bedrock/test/invariants/OptimismPortal2.t.sol +++ b/packages/contracts-bedrock/test/invariants/OptimismPortal2.t.sol @@ -139,7 +139,7 @@ contract OptimismPortal2_Invariant_Harness is CommonTest { game.resolve(); // Fund the portal so that we can withdraw ETH. - vm.deal(address(optimismPortal2), 0xFFFFFFFF); + vm.deal(address(ethLockbox), 0xFFFFFFFF); } } diff --git a/packages/contracts-bedrock/test/setup/Events.sol b/packages/contracts-bedrock/test/setup/Events.sol index 7056f0cbdd6b0..59a348fb27d19 100644 --- a/packages/contracts-bedrock/test/setup/Events.sol +++ b/packages/contracts-bedrock/test/setup/Events.sol @@ -106,4 +106,8 @@ contract Events { event Unpaused(); event BalanceChanged(address account, uint256 balance); + + event ETHMigrated(uint256 ethBalance); + + event LockboxUpdated(address oldLockbox, address newLockbox); } diff --git a/packages/contracts-bedrock/test/universal/Specs.t.sol b/packages/contracts-bedrock/test/universal/Specs.t.sol index 1363237c710bb..d11979daafde3 100644 --- a/packages/contracts-bedrock/test/universal/Specs.t.sol +++ b/packages/contracts-bedrock/test/universal/Specs.t.sol @@ -214,27 +214,27 @@ contract Specification_Test is CommonTest { _sel: _getSel("depositTransaction(address,uint256,uint64,bool,bytes)") }); _addSpec({ _name: "OptimismPortalInterop", _sel: _getSel("donateETH()") }); + _addSpec({ _name: "OptimismPortalInterop", _sel: _getSel("updateLockbox(address)") }); _addSpec({ _name: "OptimismPortalInterop", - _sel: IOptimismPortal2.finalizeWithdrawalTransaction.selector, + _sel: IOptimismPortalInterop.finalizeWithdrawalTransaction.selector, _pausable: true }); _addSpec({ _name: "OptimismPortalInterop", - _sel: IOptimismPortal2.finalizeWithdrawalTransactionExternalProof.selector, + _sel: IOptimismPortalInterop.finalizeWithdrawalTransactionExternalProof.selector, _pausable: true }); _addSpec({ _name: "OptimismPortalInterop", _sel: _getSel("finalizedWithdrawals(bytes32)") }); _addSpec({ _name: "OptimismPortalInterop", _sel: _getSel("guardian()") }); - _addSpec({ _name: "OptimismPortalInterop", _sel: _getSel("adminOwner()") }); - _addSpec({ _name: "OptimismPortalInterop", _sel: _getSel("initialize(address,address,address,address)") }); + _addSpec({ _name: "OptimismPortalInterop", _sel: _getSel("initialize(address,address,address,address,address)") }); _addSpec({ _name: "OptimismPortalInterop", _sel: _getSel("l2Sender()") }); _addSpec({ _name: "OptimismPortalInterop", _sel: _getSel("minimumGasLimit(uint64)") }); _addSpec({ _name: "OptimismPortalInterop", _sel: _getSel("params()") }); _addSpec({ _name: "OptimismPortalInterop", _sel: _getSel("paused()") }); _addSpec({ _name: "OptimismPortalInterop", - _sel: IOptimismPortal2.proveWithdrawalTransaction.selector, + _sel: IOptimismPortalInterop.proveWithdrawalTransaction.selector, _pausable: true }); _addSpec({ _name: "OptimismPortalInterop", _sel: _getSel("provenWithdrawals(bytes32,address)") }); @@ -248,18 +248,18 @@ contract Specification_Test is CommonTest { _addSpec({ _name: "OptimismPortalInterop", _sel: _getSel("respectedGameTypeUpdatedAt()") }); _addSpec({ _name: "OptimismPortalInterop", _sel: _getSel("proofSubmitters(bytes32,uint256)") }); _addSpec({ _name: "OptimismPortalInterop", _sel: _getSel("numProofSubmitters(bytes32)") }); - _addSpec({ _name: "OptimismPortalInterop", _sel: _getSel("upgrade(address)") }); - _addSpec({ - _name: "OptimismPortalInterop", - _sel: IOptimismPortalInterop.setConfig.selector, - _auth: Role.SYSTEMCONFIGOWNER - }); + _addSpec({ _name: "OptimismPortalInterop", _sel: _getSel("upgrade(address,address)") }); + _addSpec({ _name: "OptimismPortalInterop", _sel: _getSel("ethLockbox()") }); + _addSpec({ _name: "OptimismPortalInterop", _sel: _getSel("migrateLiquidity()") }); + _addSpec({ _name: "OptimismPortalInterop", _sel: _getSel("PAO()") }); + _addSpec({ _name: "OptimismPortalInterop", _sel: _getSel("setConfig(uint8,bytes)") }); // OptimismPortal2 _addSpec({ _name: "OptimismPortal2", _sel: _getSel("anchorStateRegistry()") }); _addSpec({ _name: "OptimismPortal2", _sel: _getSel("checkWithdrawal(bytes32,address)") }); _addSpec({ _name: "OptimismPortal2", _sel: _getSel("depositTransaction(address,uint256,uint64,bool,bytes)") }); _addSpec({ _name: "OptimismPortal2", _sel: _getSel("donateETH()") }); + _addSpec({ _name: "OptimismPortal2", _sel: _getSel("updateLockbox(address)") }); _addSpec({ _name: "OptimismPortal2", _sel: IOptimismPortal2.finalizeWithdrawalTransaction.selector, @@ -272,8 +272,7 @@ contract Specification_Test is CommonTest { }); _addSpec({ _name: "OptimismPortal2", _sel: _getSel("finalizedWithdrawals(bytes32)") }); _addSpec({ _name: "OptimismPortal2", _sel: _getSel("guardian()") }); - _addSpec({ _name: "OptimismPortal2", _sel: _getSel("adminOwner()") }); - _addSpec({ _name: "OptimismPortal2", _sel: _getSel("initialize(address,address,address,address)") }); + _addSpec({ _name: "OptimismPortal2", _sel: _getSel("initialize(address,address,address,address,address)") }); _addSpec({ _name: "OptimismPortal2", _sel: _getSel("l2Sender()") }); _addSpec({ _name: "OptimismPortal2", _sel: _getSel("minimumGasLimit(uint64)") }); _addSpec({ _name: "OptimismPortal2", _sel: _getSel("params()") }); @@ -294,7 +293,13 @@ contract Specification_Test is CommonTest { _addSpec({ _name: "OptimismPortal2", _sel: _getSel("respectedGameTypeUpdatedAt()") }); _addSpec({ _name: "OptimismPortal2", _sel: _getSel("proofSubmitters(bytes32,uint256)") }); _addSpec({ _name: "OptimismPortal2", _sel: _getSel("numProofSubmitters(bytes32)") }); - _addSpec({ _name: "OptimismPortal2", _sel: _getSel("upgrade(address)") }); + _addSpec({ _name: "OptimismPortal2", _sel: _getSel("upgrade(address,address)") }); + _addSpec({ _name: "OptimismPortal2", _sel: _getSel("ethLockbox()") }); + _addSpec({ _name: "OptimismPortal2", _sel: _getSel("migrateLiquidity()") }); + _addSpec({ _name: "OptimismPortal2", _sel: _getSel("PAO()") }); + + // PAOBase + _addSpec({ _name: "PAOBase", _sel: _getSel("PAO()") }); // ProtocolVersions _addSpec({ _name: "ProtocolVersions", _sel: _getSel("RECOMMENDED_SLOT()") }); @@ -320,9 +325,8 @@ contract Specification_Test is CommonTest { // ETHLockbox _addSpec({ _name: "ETHLockbox", _sel: _getSel("version()") }); - _addSpec({ _name: "ETHLockbox", _sel: _getSel("initialize(address)") }); + _addSpec({ _name: "ETHLockbox", _sel: _getSel("initialize(address,address[])") }); _addSpec({ _name: "ETHLockbox", _sel: _getSel("superchainConfig()") }); - _addSpec({ _name: "ETHLockbox", _sel: _getSel("adminOwner()") }); _addSpec({ _name: "ETHLockbox", _sel: _getSel("paused()") }); _addSpec({ _name: "ETHLockbox", _sel: _getSel("authorizedPortals(address)") }); _addSpec({ _name: "ETHLockbox", _sel: _getSel("authorizedLockboxes(address)") }); @@ -332,6 +336,7 @@ contract Specification_Test is CommonTest { _addSpec({ _name: "ETHLockbox", _sel: _getSel("authorizePortal(address)") }); _addSpec({ _name: "ETHLockbox", _sel: _getSel("authorizeLockbox(address)") }); _addSpec({ _name: "ETHLockbox", _sel: _getSel("migrateLiquidity(address)") }); + _addSpec({ _name: "ETHLockbox", _sel: _getSel("PAO()") }); // ResourceMetering _addSpec({ _name: "ResourceMetering", _sel: _getSel("params()") }); diff --git a/packages/contracts-bedrock/test/vendor/Initializable.t.sol b/packages/contracts-bedrock/test/vendor/Initializable.t.sol index 0a4159f65eda7..ae3e50c7e6024 100644 --- a/packages/contracts-bedrock/test/vendor/Initializable.t.sol +++ b/packages/contracts-bedrock/test/vendor/Initializable.t.sol @@ -123,7 +123,7 @@ contract Initializer_Test is CommonTest { name: "OptimismPortal2Impl", target: EIP1967Helper.getImplementation(address(optimismPortal2)), initCalldata: abi.encodeCall( - optimismPortal2.initialize, (systemConfig, superchainConfig, anchorStateRegistry) + optimismPortal2.initialize, (systemConfig, superchainConfig, anchorStateRegistry, ethLockbox) ) }) ); @@ -133,7 +133,7 @@ contract Initializer_Test is CommonTest { name: "OptimismPortal2Proxy", target: address(optimismPortal2), initCalldata: abi.encodeCall( - optimismPortal2.initialize, (systemConfig, superchainConfig, anchorStateRegistry) + optimismPortal2.initialize, (systemConfig, superchainConfig, anchorStateRegistry, ethLockbox) ) }) ); @@ -324,6 +324,24 @@ contract Initializer_Test is CommonTest { ) }) ); + + // ETHLockboxImpl + contracts.push( + InitializeableContract({ + name: "ETHLockboxImpl", + target: EIP1967Helper.getImplementation(address(ethLockbox)), + initCalldata: abi.encodeCall(ethLockbox.initialize, (address(0), new address[](0))) + }) + ); + + // ETHLockboxProxy + contracts.push( + InitializeableContract({ + name: "ETHLockboxProxy", + target: address(ethLockbox), + initCalldata: abi.encodeCall(ethLockbox.initialize, (address(0), new address[](0))) + }) + ); } /// @notice Tests that: diff --git a/packages/contracts-bedrock/test/vendor/InitializableOZv5.t.sol b/packages/contracts-bedrock/test/vendor/InitializableOZv5.t.sol index e499ec39bcd46..eb0c1f8fa79dd 100644 --- a/packages/contracts-bedrock/test/vendor/InitializableOZv5.t.sol +++ b/packages/contracts-bedrock/test/vendor/InitializableOZv5.t.sol @@ -2,10 +2,8 @@ pragma solidity 0.8.15; import { CommonTest } from "test/setup/CommonTest.sol"; -import { EIP1967Helper } from "test/mocks/EIP1967Helper.sol"; import { IOptimismSuperchainERC20 } from "interfaces/L2/IOptimismSuperchainERC20.sol"; import { DeployUtils } from "scripts/libraries/DeployUtils.sol"; -import { IETHLockbox } from "interfaces/L1/IETHLockbox.sol"; /// @title InitializerOZv5_Test /// @dev Ensures that the `initialize()` function on contracts cannot be called more than @@ -51,24 +49,6 @@ contract InitializerOZv5_Test is CommonTest { initCalldata: abi.encodeCall(IOptimismSuperchainERC20.initialize, (address(0), "", "", 18)) }) ); - - // ETHLockboxImpl - contracts.push( - InitializeableContract({ - name: "ETHLockboxImpl", - target: EIP1967Helper.getImplementation(address(ethLockbox)), - initCalldata: abi.encodeCall(IETHLockbox.initialize, (address(0))) - }) - ); - - // ETHLockboxProxy - contracts.push( - InitializeableContract({ - name: "ETHLockboxProxy", - target: address(ethLockbox), - initCalldata: abi.encodeCall(IETHLockbox.initialize, (address(0))) - }) - ); } /// @notice Tests that: From f7b2b7fd6da642590b2cd0914c6af250696d3d5c Mon Sep 17 00:00:00 2001 From: agusduha Date: Fri, 28 Feb 2025 14:19:13 -0300 Subject: [PATCH 04/50] fix: semver lock --- .../contracts-bedrock/snapshots/semver-lock.json | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/contracts-bedrock/snapshots/semver-lock.json b/packages/contracts-bedrock/snapshots/semver-lock.json index 5b02921d382bf..2c9b09d6363f1 100644 --- a/packages/contracts-bedrock/snapshots/semver-lock.json +++ b/packages/contracts-bedrock/snapshots/semver-lock.json @@ -20,11 +20,11 @@ "sourceCodeHash": "0x7f614d0c5251365580c02dd898900a583ece2aa28579adea74a3808b575949b4" }, "src/L1/OPContractsManager.sol": { - "initCodeHash": "0x2918a39985d0581084acb9fe4a172762d9ea25aa51ac9965a5c8eea79501aac1", - "sourceCodeHash": "0x7018bd24305dc432ddcbf18e668155d02142f7531327a9a64981299a4c0c633e" + "initCodeHash": "0x6eaf1dcd6b87f4372445e8a8936efa4e9879cfd05c1de8a306acb69266ee03d9", + "sourceCodeHash": "0x624703c5d5b8a1d36ff44b61d1a1512e3a1776f179f2cc386f28d1b789c9fe9b" }, "src/L1/OPContractsManagerInterop.sol": { - "initCodeHash": "0x9cbacf4a5b118d2988747fd34d22508a7c886e699713877db755c50f6337df39", + "initCodeHash": "0x7865201a60d422cda5b33a2ff22b4ab707a15ba54016182d2a6cca367d618d74", "sourceCodeHash": "0xd04bb5babc02fd93618397678506e9bcce5f1881b008785da0767bda74d8447c" }, "src/L1/OPPrestateUpdater.sol": { @@ -32,12 +32,12 @@ "sourceCodeHash": "0xcdeb0f532ff90288ee13694a51a15aaee99392b7dfe2d46fffacf2af82cdb38f" }, "src/L1/OptimismPortal2.sol": { - "initCodeHash": "0x6323e33f92d47755a08337cc990f4443d17e72b14d075b94b5c79c37b1aa21fd", - "sourceCodeHash": "0x561b651787b1ff293e837cc76d6c7801a348074e3862e961a36eaafaa6367dfe" + "initCodeHash": "0x084ac1d109f9bdaba389fdad4dc3887af745b825356e1c70bc5ba43a23a8c7b7", + "sourceCodeHash": "0x91acba845145aca7a342cdafb55129546acefce122562739ebc0fdb655edad1a" }, "src/L1/OptimismPortalInterop.sol": { - "initCodeHash": "0x279259614f9c0bdfc74c62068d237672b20e189c2da07fd6958683fcd2fc8d1f", - "sourceCodeHash": "0xdf4e628d152475d96b464526cb9adf96c3b0bff45a4a3f84d207faf9c7e7658c" + "initCodeHash": "0x5f0e909be4f924aaf4742a84ed1dd134d84e5b91101e8cbd6c7ffbf893b4da84", + "sourceCodeHash": "0xd3db51a961120a97f8300a2587edda6311c862b48ba4178c4380f62368b07c09" }, "src/L1/ProtocolVersions.sol": { "initCodeHash": "0x5a76c8530cb24cf23d3baacc6eefaac226382af13f1e2a35535d2ec2b0573b29", @@ -243,4 +243,4 @@ "initCodeHash": "0x2bfce526f82622288333d53ca3f43a0a94306ba1bab99241daa845f8f4b18bd4", "sourceCodeHash": "0xf49d7b0187912a6bb67926a3222ae51121e9239495213c975b3b4b217ee57a1b" } -} +} \ No newline at end of file From a3815118c630279fa047cf7774f395e121fbbfc7 Mon Sep 17 00:00:00 2001 From: 0xDiscotech <131301107+0xDiscotech@users.noreply.github.com> Date: Fri, 28 Feb 2025 14:27:00 -0300 Subject: [PATCH 05/50] fix: failing tests after merge --- packages/contracts-bedrock/test/L1/L1StandardBridge.t.sol | 6 ++++-- packages/contracts-bedrock/test/L1/OptimismPortal2.t.sol | 8 +++++--- packages/contracts-bedrock/test/universal/Specs.t.sol | 4 ++-- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/packages/contracts-bedrock/test/L1/L1StandardBridge.t.sol b/packages/contracts-bedrock/test/L1/L1StandardBridge.t.sol index f5a418319ca11..8776c0f7a8397 100644 --- a/packages/contracts-bedrock/test/L1/L1StandardBridge.t.sol +++ b/packages/contracts-bedrock/test/L1/L1StandardBridge.t.sol @@ -268,9 +268,11 @@ contract L1StandardBridge_DepositETH_Test is PreBridgeETH { /// ETH ends up in the optimismPortal. function test_depositETH_fromEOA_succeeds() external { _preBridgeETH({ isLegacy: true, value: 500 }); - uint256 balanceBefore = address(optimismPortal2).balance; + uint256 portalBalanceBefore = address(optimismPortal2).balance; + uint256 ethLockboxBalanceBefore = address(ethLockbox).balance; l1StandardBridge.depositETH{ value: 500 }(50000, hex"dead"); - assertEq(address(optimismPortal2).balance, balanceBefore + 500); + assertEq(address(optimismPortal2).balance, portalBalanceBefore); + assertEq(address(ethLockbox).balance, ethLockboxBalanceBefore + 500); } /// @dev Tests that depositing ETH succeeds for an EOA using 7702 delegation. diff --git a/packages/contracts-bedrock/test/L1/OptimismPortal2.t.sol b/packages/contracts-bedrock/test/L1/OptimismPortal2.t.sol index 34efbdccbd09d..f4e2b0b5c5840 100644 --- a/packages/contracts-bedrock/test/L1/OptimismPortal2.t.sol +++ b/packages/contracts-bedrock/test/L1/OptimismPortal2.t.sol @@ -337,8 +337,9 @@ contract OptimismPortal2_Test is CommonTest { ); if (_isCreation) _to = address(0); - uint256 balanceBefore = address(optimismPortal2).balance; - _mint = bound(_mint, 0, type(uint256).max - balanceBefore); + uint256 portalBalanceBefore = address(optimismPortal2).balance; + uint256 lockboxBalanceBefore = address(ethLockbox).balance; + _mint = bound(_mint, 0, type(uint256).max - portalBalanceBefore); // EOA emulation vm.expectEmit(address(optimismPortal2)); @@ -364,7 +365,8 @@ contract OptimismPortal2_Test is CommonTest { _isCreation: _isCreation, _data: _data }); - assertEq(address(optimismPortal2).balance, balanceBefore + _mint); + assertEq(address(optimismPortal2).balance, portalBalanceBefore); + assertEq(address(ethLockbox).balance, lockboxBalanceBefore + _mint); } /// @dev Tests that `depositTransaction` succeeds for a contract. diff --git a/packages/contracts-bedrock/test/universal/Specs.t.sol b/packages/contracts-bedrock/test/universal/Specs.t.sol index d11979daafde3..83a9a7e810e51 100644 --- a/packages/contracts-bedrock/test/universal/Specs.t.sol +++ b/packages/contracts-bedrock/test/universal/Specs.t.sol @@ -227,7 +227,7 @@ contract Specification_Test is CommonTest { }); _addSpec({ _name: "OptimismPortalInterop", _sel: _getSel("finalizedWithdrawals(bytes32)") }); _addSpec({ _name: "OptimismPortalInterop", _sel: _getSel("guardian()") }); - _addSpec({ _name: "OptimismPortalInterop", _sel: _getSel("initialize(address,address,address,address,address)") }); + _addSpec({ _name: "OptimismPortalInterop", _sel: _getSel("initialize(address,address,address,address)") }); _addSpec({ _name: "OptimismPortalInterop", _sel: _getSel("l2Sender()") }); _addSpec({ _name: "OptimismPortalInterop", _sel: _getSel("minimumGasLimit(uint64)") }); _addSpec({ _name: "OptimismPortalInterop", _sel: _getSel("params()") }); @@ -272,7 +272,7 @@ contract Specification_Test is CommonTest { }); _addSpec({ _name: "OptimismPortal2", _sel: _getSel("finalizedWithdrawals(bytes32)") }); _addSpec({ _name: "OptimismPortal2", _sel: _getSel("guardian()") }); - _addSpec({ _name: "OptimismPortal2", _sel: _getSel("initialize(address,address,address,address,address)") }); + _addSpec({ _name: "OptimismPortal2", _sel: _getSel("initialize(address,address,address,address)") }); _addSpec({ _name: "OptimismPortal2", _sel: _getSel("l2Sender()") }); _addSpec({ _name: "OptimismPortal2", _sel: _getSel("minimumGasLimit(uint64)") }); _addSpec({ _name: "OptimismPortal2", _sel: _getSel("params()") }); From fda3e0c5dbb8a9e2256a742b93fe71cbcee3ad08 Mon Sep 17 00:00:00 2001 From: agusduha Date: Fri, 28 Feb 2025 14:59:56 -0300 Subject: [PATCH 06/50] fix: interface import --- .../contracts-bedrock/interfaces/L1/IOptimismPortalInterop.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/contracts-bedrock/interfaces/L1/IOptimismPortalInterop.sol b/packages/contracts-bedrock/interfaces/L1/IOptimismPortalInterop.sol index e606940ed7fea..642afe68055fb 100644 --- a/packages/contracts-bedrock/interfaces/L1/IOptimismPortalInterop.sol +++ b/packages/contracts-bedrock/interfaces/L1/IOptimismPortalInterop.sol @@ -10,7 +10,7 @@ import { ISuperchainConfig } from "interfaces/L1/ISuperchainConfig.sol"; import { ConfigType } from "interfaces/L2/IL1BlockInterop.sol"; import { IAnchorStateRegistry } from "interfaces/dispute/IAnchorStateRegistry.sol"; import { IETHLockbox } from "interfaces/L1/IETHLockbox.sol"; -import { IPAOBase } from "interfaces/l1/IPAOBase.sol"; +import { IPAOBase } from "interfaces/L1/IPAOBase.sol"; interface IOptimismPortalInterop is IPAOBase { error ContentLengthMismatch(); From 878d2a9f17d8e527bd7b4547b50da48cf3b45d96 Mon Sep 17 00:00:00 2001 From: 0xDiscotech <131301107+0xDiscotech@users.noreply.github.com> Date: Fri, 28 Feb 2025 15:25:21 -0300 Subject: [PATCH 07/50] chore: add lockbox pao matches final system owner check --- .../contracts-bedrock/scripts/deploy/ChainAssertions.sol | 9 +++++---- packages/contracts-bedrock/scripts/deploy/Deploy.s.sol | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/contracts-bedrock/scripts/deploy/ChainAssertions.sol b/packages/contracts-bedrock/scripts/deploy/ChainAssertions.sol index 520d70a41defe..341dbfe569e63 100644 --- a/packages/contracts-bedrock/scripts/deploy/ChainAssertions.sol +++ b/packages/contracts-bedrock/scripts/deploy/ChainAssertions.sol @@ -402,11 +402,11 @@ library ChainAssertions { } // This slot is the custom gas token _balance and this check ensures // that it stays unset for forwards compatibility with custom gas token. - require(vm.load(address(portal), bytes32(uint256(61))) == bytes32(0), "CHECK-OP2-140"); + require(vm.load(address(portal), bytes32(uint256(61))) == bytes32(0), "CHECK-OP2-130"); } /// @notice Asserts that the ETHLockbox is setup correctly - function checkETHLockbox(Types.ContractSet memory _contracts, bool _isProxy) internal view { + function checkETHLockbox(Types.ContractSet memory _contracts, DeployConfig _cfg, bool _isProxy) internal view { IETHLockbox ethLockbox = IETHLockbox(_contracts.ETHLockbox); ISuperchainConfig superchainConfig = ISuperchainConfig(_contracts.SuperchainConfig); @@ -424,9 +424,10 @@ library ChainAssertions { if (_isProxy) { require(ethLockbox.superchainConfig() == superchainConfig, "CHECK-ELB-20"); require(ethLockbox.authorizedPortals(_contracts.OptimismPortal), "CHECK-ELB-30"); + require(ethLockbox.PAO() == _cfg.finalSystemOwner(), "CHECK-ELB-40"); } else { - require(address(ethLockbox.superchainConfig()) == address(0), "CHECK-ELB-40"); - require(ethLockbox.authorizedPortals(_contracts.OptimismPortal) == false, "CHECK-ELB-50"); + require(address(ethLockbox.superchainConfig()) == address(0), "CHECK-ELB-50"); + require(ethLockbox.authorizedPortals(_contracts.OptimismPortal) == false, "CHECK-ELB-60"); } } diff --git a/packages/contracts-bedrock/scripts/deploy/Deploy.s.sol b/packages/contracts-bedrock/scripts/deploy/Deploy.s.sol index 0f746c0609dac..90169ef44c8fb 100644 --- a/packages/contracts-bedrock/scripts/deploy/Deploy.s.sol +++ b/packages/contracts-bedrock/scripts/deploy/Deploy.s.sol @@ -330,7 +330,7 @@ contract Deploy is Deployer { ChainAssertions.checkL1StandardBridge({ _contracts: impls, _isProxy: false }); ChainAssertions.checkL1ERC721Bridge({ _contracts: impls, _isProxy: false }); ChainAssertions.checkOptimismPortal2({ _contracts: impls, _cfg: cfg, _isProxy: false }); - ChainAssertions.checkETHLockbox({ _contracts: impls, _isProxy: false }); + ChainAssertions.checkETHLockbox({ _contracts: impls, _cfg: cfg, _isProxy: false }); ChainAssertions.checkOptimismMintableERC20Factory({ _contracts: impls, _isProxy: false }); ChainAssertions.checkDisputeGameFactory({ _contracts: impls, _expectedOwner: address(0), _isProxy: false }); ChainAssertions.checkDelayedWETH({ _contracts: impls, _cfg: cfg, _isProxy: false, _expectedOwner: address(0) }); From 053545f8a9bbc9b50423f294d91c2ce4121a8779 Mon Sep 17 00:00:00 2001 From: Disco <131301107+0xDiscotech@users.noreply.github.com> Date: Mon, 3 Mar 2025 12:02:41 -0300 Subject: [PATCH 08/50] fix: pr comments (#293) * chore: remove unused function * chore: add pao validation checks over portal and lockbox * refactor: use interface as arg type instead of address on functions * fix: comment max length * refactor: update lockbox param type as interface * refactor: use interface as type on migrate liquidity and authorize lockbox functions * chore: run pre pr * fix: high fuzz runs failing test using mostly assume not forge address * refactor: use ioptimis portal instead of ioptimism portal2 everywhere * chore: undo changes on unrelated files to the pr --- .../interfaces/L1/IETHLockbox.sol | 9 +- .../interfaces/L1/IOptimismPortal2.sol | 2 +- .../interfaces/L1/IOptimismPortalInterop.sol | 2 +- .../scripts/deploy/ChainAssertions.sol | 4 +- .../deploy/DeployImplementations.s.sol | 14 +-- .../scripts/deploy/DeployOPChain.s.sol | 12 +- .../scripts/libraries/DeployUtils.sol | 15 --- .../snapshots/abi/ETHLockbox.json | 10 +- .../snapshots/abi/OptimismPortal2.json | 2 +- .../snapshots/abi/OptimismPortalInterop.json | 2 +- .../snapshots/semver-lock.json | 6 +- .../contracts-bedrock/src/L1/ETHLockbox.sol | 33 +++-- .../src/L1/OPContractsManager.sol | 29 +++-- .../src/L1/OptimismPortal2.sol | 8 +- .../test/L1/ETHLockbox.t.sol | 115 +++++++++++------- .../test/L1/OptimismPortal2.t.sol | 86 ++++++------- .../test/opcm/DeployImplementations.t.sol | 4 +- .../contracts-bedrock/test/setup/Setup.sol | 6 +- .../test/vendor/Initializable.t.sol | 9 +- 19 files changed, 198 insertions(+), 170 deletions(-) diff --git a/packages/contracts-bedrock/interfaces/L1/IETHLockbox.sol b/packages/contracts-bedrock/interfaces/L1/IETHLockbox.sol index 796cc01fb3a46..849b47247a7d7 100644 --- a/packages/contracts-bedrock/interfaces/L1/IETHLockbox.sol +++ b/packages/contracts-bedrock/interfaces/L1/IETHLockbox.sol @@ -4,6 +4,7 @@ pragma solidity ^0.8.0; import { ISemver } from "interfaces/universal/ISemver.sol"; import { ISuperchainConfig } from "interfaces/L1/ISuperchainConfig.sol"; import { IPAOBase } from "interfaces/L1/IPAOBase.sol"; +import { IOptimismPortal2 } from "interfaces/L1/IOptimismPortal2.sol"; interface IETHLockbox is IPAOBase, ISemver { error ETHLockbox_Unauthorized(); @@ -20,7 +21,7 @@ interface IETHLockbox is IPAOBase, ISemver { event LiquidityMigrated(address indexed lockbox); event LiquidityReceived(address indexed lockbox); - function initialize(address _superchainConfig, address[] calldata _portals) external; + function initialize(ISuperchainConfig _superchainConfig, IOptimismPortal2[] calldata _portals) external; function superchainConfig() external view returns (ISuperchainConfig); function paused() external view returns (bool); function authorizedPortals(address) external view returns (bool); @@ -28,9 +29,9 @@ interface IETHLockbox is IPAOBase, ISemver { function receiveLiquidity() external payable; function lockETH() external payable; function unlockETH(uint256 _value) external; - function authorizePortal(address _portal) external; - function authorizeLockbox(address _lockbox) external; - function migrateLiquidity(address _lockbox) external; + function authorizePortal(IOptimismPortal2 _portal) external; + function authorizeLockbox(IETHLockbox _lockbox) external; + function migrateLiquidity(IETHLockbox _lockbox) external; function __constructor__() external; } diff --git a/packages/contracts-bedrock/interfaces/L1/IOptimismPortal2.sol b/packages/contracts-bedrock/interfaces/L1/IOptimismPortal2.sol index 4b391a3b874c1..4db6c003da562 100644 --- a/packages/contracts-bedrock/interfaces/L1/IOptimismPortal2.sol +++ b/packages/contracts-bedrock/interfaces/L1/IOptimismPortal2.sol @@ -61,7 +61,7 @@ interface IOptimismPortal2 is IPAOBase { function disputeGameFactory() external view returns (IDisputeGameFactory); function disputeGameFinalityDelaySeconds() external view returns (uint256); function donateETH() external payable; - function updateLockbox(address _newLockbox) external; + function updateLockbox(IETHLockbox _newLockbox) external; function finalizeWithdrawalTransaction(Types.WithdrawalTransaction memory _tx) external; function finalizeWithdrawalTransactionExternalProof( Types.WithdrawalTransaction memory _tx, diff --git a/packages/contracts-bedrock/interfaces/L1/IOptimismPortalInterop.sol b/packages/contracts-bedrock/interfaces/L1/IOptimismPortalInterop.sol index 642afe68055fb..6876b3a715e25 100644 --- a/packages/contracts-bedrock/interfaces/L1/IOptimismPortalInterop.sol +++ b/packages/contracts-bedrock/interfaces/L1/IOptimismPortalInterop.sol @@ -62,7 +62,7 @@ interface IOptimismPortalInterop is IPAOBase { function disputeGameFactory() external view returns (IDisputeGameFactory); function disputeGameFinalityDelaySeconds() external view returns (uint256); function donateETH() external payable; - function updateLockbox(address _newLockbox) external; + function updateLockbox(IETHLockbox _newLockbox) external; function finalizeWithdrawalTransaction(Types.WithdrawalTransaction memory _tx) external; function finalizeWithdrawalTransactionExternalProof( Types.WithdrawalTransaction memory _tx, diff --git a/packages/contracts-bedrock/scripts/deploy/ChainAssertions.sol b/packages/contracts-bedrock/scripts/deploy/ChainAssertions.sol index 341dbfe569e63..19265324fc112 100644 --- a/packages/contracts-bedrock/scripts/deploy/ChainAssertions.sol +++ b/packages/contracts-bedrock/scripts/deploy/ChainAssertions.sol @@ -22,7 +22,7 @@ import { IResourceMetering } from "interfaces/L1/IResourceMetering.sol"; import { ISystemConfig } from "interfaces/L1/ISystemConfig.sol"; import { ISuperchainConfig } from "interfaces/L1/ISuperchainConfig.sol"; import { IL1CrossDomainMessenger } from "interfaces/L1/IL1CrossDomainMessenger.sol"; -import { IOptimismPortal2 } from "interfaces/L1/IOptimismPortal2.sol"; +import { IOptimismPortal2 as IOptimismPortal } from "interfaces/L1/IOptimismPortal2.sol"; import { IL1ERC721Bridge } from "interfaces/L1/IL1ERC721Bridge.sol"; import { IL1StandardBridge } from "interfaces/L1/IL1StandardBridge.sol"; import { ProtocolVersion, IProtocolVersions } from "interfaces/L1/IProtocolVersions.sol"; @@ -368,7 +368,7 @@ library ChainAssertions { internal view { - IOptimismPortal2 portal = IOptimismPortal2(payable(_contracts.OptimismPortal)); + IOptimismPortal portal = IOptimismPortal(payable(_contracts.OptimismPortal)); console.log( "Running chain assertions on the OptimismPortal2 %s at %s", _isProxy ? "proxy" : "implementation", diff --git a/packages/contracts-bedrock/scripts/deploy/DeployImplementations.s.sol b/packages/contracts-bedrock/scripts/deploy/DeployImplementations.s.sol index 3a46e7fd4add6..bb8ba9aa5e070 100644 --- a/packages/contracts-bedrock/scripts/deploy/DeployImplementations.s.sol +++ b/packages/contracts-bedrock/scripts/deploy/DeployImplementations.s.sol @@ -19,7 +19,7 @@ import { IDisputeGameFactory } from "interfaces/dispute/IDisputeGameFactory.sol" import { IAnchorStateRegistry } from "interfaces/dispute/IAnchorStateRegistry.sol"; import { IOPContractsManager } from "interfaces/L1/IOPContractsManager.sol"; import { IOPContractsManagerInterop } from "interfaces/L1/IOPContractsManagerInterop.sol"; -import { IOptimismPortal2 } from "interfaces/L1/IOptimismPortal2.sol"; +import { IOptimismPortal2 as IOptimismPortal } from "interfaces/L1/IOptimismPortal2.sol"; import { IETHLockbox } from "interfaces/L1/IETHLockbox.sol"; import { ISystemConfig } from "interfaces/L1/ISystemConfig.sol"; import { IL1CrossDomainMessenger } from "interfaces/L1/IL1CrossDomainMessenger.sol"; @@ -150,7 +150,7 @@ contract DeployImplementationsInput is BaseDeployIO { contract DeployImplementationsOutput is BaseDeployIO { IOPContractsManager internal _opcm; IDelayedWETH internal _delayedWETHImpl; - IOptimismPortal2 internal _optimismPortalImpl; + IOptimismPortal internal _optimismPortalImpl; IETHLockbox internal _ethLockboxImpl; IPreimageOracle internal _preimageOracleSingleton; IMIPS internal _mipsSingleton; @@ -171,7 +171,7 @@ contract DeployImplementationsOutput is BaseDeployIO { if (_sel == this.opcm.selector) _opcm = IOPContractsManager(_addr); else if (_sel == this.superchainConfigImpl.selector) _superchainConfigImpl = ISuperchainConfig(_addr); else if (_sel == this.protocolVersionsImpl.selector) _protocolVersionsImpl = IProtocolVersions(_addr); - else if (_sel == this.optimismPortalImpl.selector) _optimismPortalImpl = IOptimismPortal2(payable(_addr)); + else if (_sel == this.optimismPortalImpl.selector) _optimismPortalImpl = IOptimismPortal(payable(_addr)); else if (_sel == this.ethLockboxImpl.selector) _ethLockboxImpl = IETHLockbox(payable(_addr)); else if (_sel == this.delayedWETHImpl.selector) _delayedWETHImpl = IDelayedWETH(payable(_addr)); else if (_sel == this.preimageOracleSingleton.selector) _preimageOracleSingleton = IPreimageOracle(_addr); @@ -231,7 +231,7 @@ contract DeployImplementationsOutput is BaseDeployIO { return _protocolVersionsImpl; } - function optimismPortalImpl() public view returns (IOptimismPortal2) { + function optimismPortalImpl() public view returns (IOptimismPortal) { DeployUtils.assertValidContractAddress(address(_optimismPortalImpl)); return _optimismPortalImpl; } @@ -316,7 +316,7 @@ contract DeployImplementationsOutput is BaseDeployIO { } function assertValidOptimismPortalImpl(DeployImplementationsInput) internal view { - IOptimismPortal2 portal = optimismPortalImpl(); + IOptimismPortal portal = optimismPortalImpl(); DeployUtils.assertInitialized({ _contractAddress: address(portal), _isProxy: false, _slot: 0, _offset: 0 }); @@ -726,11 +726,11 @@ contract DeployImplementations is Script { virtual { uint256 proofMaturityDelaySeconds = _dii.proofMaturityDelaySeconds(); - IOptimismPortal2 impl = IOptimismPortal2( + IOptimismPortal impl = IOptimismPortal( DeployUtils.createDeterministic({ _name: "OptimismPortal2", _args: DeployUtils.encodeConstructor( - abi.encodeCall(IOptimismPortal2.__constructor__, (proofMaturityDelaySeconds)) + abi.encodeCall(IOptimismPortal.__constructor__, (proofMaturityDelaySeconds)) ), _salt: _salt }) diff --git a/packages/contracts-bedrock/scripts/deploy/DeployOPChain.s.sol b/packages/contracts-bedrock/scripts/deploy/DeployOPChain.s.sol index 14d094c2044ab..87d112bd134db 100644 --- a/packages/contracts-bedrock/scripts/deploy/DeployOPChain.s.sol +++ b/packages/contracts-bedrock/scripts/deploy/DeployOPChain.s.sol @@ -27,7 +27,7 @@ import { IFaultDisputeGame } from "interfaces/dispute/IFaultDisputeGame.sol"; import { IPermissionedDisputeGame } from "interfaces/dispute/IPermissionedDisputeGame.sol"; import { Claim, Duration, GameType, GameTypes, Hash } from "src/dispute/lib/Types.sol"; -import { IOptimismPortal2 } from "interfaces/L1/IOptimismPortal2.sol"; +import { IOptimismPortal2 as IOptimismPortal } from "interfaces/L1/IOptimismPortal2.sol"; import { ISystemConfig } from "interfaces/L1/ISystemConfig.sol"; import { IL1CrossDomainMessenger } from "interfaces/L1/IL1CrossDomainMessenger.sol"; import { IL1ERC721Bridge } from "interfaces/L1/IL1ERC721Bridge.sol"; @@ -225,7 +225,7 @@ contract DeployOPChainOutput is BaseDeployIO { IOptimismMintableERC20Factory internal _optimismMintableERC20FactoryProxy; IL1StandardBridge internal _l1StandardBridgeProxy; IL1CrossDomainMessenger internal _l1CrossDomainMessengerProxy; - IOptimismPortal2 internal _optimismPortalProxy; + IOptimismPortal internal _optimismPortalProxy; IETHLockbox internal _ethLockboxProxy; IDisputeGameFactory internal _disputeGameFactoryProxy; IAnchorStateRegistry internal _anchorStateRegistryProxy; @@ -244,7 +244,7 @@ contract DeployOPChainOutput is BaseDeployIO { else if (_sel == this.optimismMintableERC20FactoryProxy.selector) _optimismMintableERC20FactoryProxy = IOptimismMintableERC20Factory(_addr) ; else if (_sel == this.l1StandardBridgeProxy.selector) _l1StandardBridgeProxy = IL1StandardBridge(payable(_addr)) ; else if (_sel == this.l1CrossDomainMessengerProxy.selector) _l1CrossDomainMessengerProxy = IL1CrossDomainMessenger(_addr) ; - else if (_sel == this.optimismPortalProxy.selector) _optimismPortalProxy = IOptimismPortal2(payable(_addr)) ; + else if (_sel == this.optimismPortalProxy.selector) _optimismPortalProxy = IOptimismPortal(payable(_addr)) ; else if (_sel == this.ethLockboxProxy.selector) _ethLockboxProxy = IETHLockbox(payable(_addr)) ; else if (_sel == this.disputeGameFactoryProxy.selector) _disputeGameFactoryProxy = IDisputeGameFactory(_addr) ; else if (_sel == this.anchorStateRegistryProxy.selector) _anchorStateRegistryProxy = IAnchorStateRegistry(_addr) ; @@ -296,7 +296,7 @@ contract DeployOPChainOutput is BaseDeployIO { return _l1CrossDomainMessengerProxy; } - function optimismPortalProxy() public returns (IOptimismPortal2) { + function optimismPortalProxy() public returns (IOptimismPortal) { DeployUtils.assertValidContractAddress(address(_optimismPortalProxy)); DeployUtils.assertERC1967ImplementationSet(address(_optimismPortalProxy)); return _optimismPortalProxy; @@ -607,7 +607,7 @@ contract DeployOPChain is Script { } function assertValidOptimismPortal(DeployOPChainInput _doi, DeployOPChainOutput _doo) internal { - IOptimismPortal2 portal = _doo.optimismPortalProxy(); + IOptimismPortal portal = _doo.optimismPortalProxy(); ISuperchainConfig superchainConfig = ISuperchainConfig(address(_doi.opcm().superchainConfig())); require(address(portal.anchorStateRegistry()) == address(_doo.anchorStateRegistryProxy()), "PORTAL-10"); @@ -624,6 +624,7 @@ contract DeployOPChain is Script { // Check once the portal is updated to use the new lockbox. require(address(portal.ethLockbox()) == address(_doo.ethLockboxProxy()), "PORTAL-90"); + require(portal.PAO() == _doi.opChainProxyAdminOwner(), "PORTAL-100"); } function assertValidETHLockbox(DeployOPChainInput _doi, DeployOPChainOutput _doo) internal { @@ -631,6 +632,7 @@ contract DeployOPChain is Script { require(address(lockbox.superchainConfig()) == address(_doi.opcm().superchainConfig()), "ETHLOCKBOX-10"); require(lockbox.authorizedPortals(address(_doo.optimismPortalProxy())), "ETHLOCKBOX-20"); + require(lockbox.PAO() == _doi.opChainProxyAdminOwner(), "ETHLOCKBOX-30"); } function assertValidDisputeGameFactory(DeployOPChainInput _doi, DeployOPChainOutput _doo) internal { diff --git a/packages/contracts-bedrock/scripts/libraries/DeployUtils.sol b/packages/contracts-bedrock/scripts/libraries/DeployUtils.sol index 3a17494861faf..1cec8d04778b1 100644 --- a/packages/contracts-bedrock/scripts/libraries/DeployUtils.sol +++ b/packages/contracts-bedrock/scripts/libraries/DeployUtils.sol @@ -366,21 +366,6 @@ library DeployUtils { } } - 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" - ); - } - } - /// @notice Etches a contract, labels it, and allows cheatcodes for it. /// @param _etchTo Address of the contract to etch. /// @param _cname The contract name (also used to label the contract). diff --git a/packages/contracts-bedrock/snapshots/abi/ETHLockbox.json b/packages/contracts-bedrock/snapshots/abi/ETHLockbox.json index 07aabc72bbbed..9bad5290bc5e4 100644 --- a/packages/contracts-bedrock/snapshots/abi/ETHLockbox.json +++ b/packages/contracts-bedrock/snapshots/abi/ETHLockbox.json @@ -20,7 +20,7 @@ { "inputs": [ { - "internalType": "address", + "internalType": "contract IETHLockbox", "name": "_lockbox", "type": "address" } @@ -33,7 +33,7 @@ { "inputs": [ { - "internalType": "address", + "internalType": "contract IOptimismPortal2", "name": "_portal", "type": "address" } @@ -84,12 +84,12 @@ { "inputs": [ { - "internalType": "address", + "internalType": "contract ISuperchainConfig", "name": "_superchainConfig", "type": "address" }, { - "internalType": "address[]", + "internalType": "contract IOptimismPortal2[]", "name": "_portals", "type": "address[]" } @@ -109,7 +109,7 @@ { "inputs": [ { - "internalType": "address", + "internalType": "contract IETHLockbox", "name": "_lockbox", "type": "address" } diff --git a/packages/contracts-bedrock/snapshots/abi/OptimismPortal2.json b/packages/contracts-bedrock/snapshots/abi/OptimismPortal2.json index b501dad8f1fc1..37d1162a356b7 100644 --- a/packages/contracts-bedrock/snapshots/abi/OptimismPortal2.json +++ b/packages/contracts-bedrock/snapshots/abi/OptimismPortal2.json @@ -589,7 +589,7 @@ { "inputs": [ { - "internalType": "address", + "internalType": "contract IETHLockbox", "name": "_newLockbox", "type": "address" } diff --git a/packages/contracts-bedrock/snapshots/abi/OptimismPortalInterop.json b/packages/contracts-bedrock/snapshots/abi/OptimismPortalInterop.json index dbc4f2da16f4b..ea524ef87b6dd 100644 --- a/packages/contracts-bedrock/snapshots/abi/OptimismPortalInterop.json +++ b/packages/contracts-bedrock/snapshots/abi/OptimismPortalInterop.json @@ -607,7 +607,7 @@ { "inputs": [ { - "internalType": "address", + "internalType": "contract IETHLockbox", "name": "_newLockbox", "type": "address" } diff --git a/packages/contracts-bedrock/snapshots/semver-lock.json b/packages/contracts-bedrock/snapshots/semver-lock.json index 2c9b09d6363f1..9619ec4b60396 100644 --- a/packages/contracts-bedrock/snapshots/semver-lock.json +++ b/packages/contracts-bedrock/snapshots/semver-lock.json @@ -5,7 +5,7 @@ }, "src/L1/ETHLockbox.sol": { "initCodeHash": "0xb5da33c92a6f0bc00eaaaec5d0b99c9c15a9dd9d7a25c416cd6740a96e7fc4d5", - "sourceCodeHash": "0xc9ac52290abc2f3c7071e12116593f4bad504961ffa11825161072a5aa94c23c" + "sourceCodeHash": "0x12af194d8fe23ff1c15c378151c7cb9e5a3854531b69c1153475432ef9f73d5e" }, "src/L1/L1CrossDomainMessenger.sol": { "initCodeHash": "0x03a3c0eb2418aba9f93bb89723ba2ee7cb9e1988ca00f380503c960149c85b7a", @@ -21,7 +21,7 @@ }, "src/L1/OPContractsManager.sol": { "initCodeHash": "0x6eaf1dcd6b87f4372445e8a8936efa4e9879cfd05c1de8a306acb69266ee03d9", - "sourceCodeHash": "0x624703c5d5b8a1d36ff44b61d1a1512e3a1776f179f2cc386f28d1b789c9fe9b" + "sourceCodeHash": "0x9205651bfa81ac60fc331892c792ae6f668601556270ac6a5cda9c5e2258f524" }, "src/L1/OPContractsManagerInterop.sol": { "initCodeHash": "0x7865201a60d422cda5b33a2ff22b4ab707a15ba54016182d2a6cca367d618d74", @@ -33,7 +33,7 @@ }, "src/L1/OptimismPortal2.sol": { "initCodeHash": "0x084ac1d109f9bdaba389fdad4dc3887af745b825356e1c70bc5ba43a23a8c7b7", - "sourceCodeHash": "0x91acba845145aca7a342cdafb55129546acefce122562739ebc0fdb655edad1a" + "sourceCodeHash": "0xf23dea8126e89157c43561cee63f8701118c3017d7662ed066d3fa157f32b441" }, "src/L1/OptimismPortalInterop.sol": { "initCodeHash": "0x5f0e909be4f924aaf4742a84ed1dd134d84e5b91101e8cbd6c7ffbf893b4da84", diff --git a/packages/contracts-bedrock/src/L1/ETHLockbox.sol b/packages/contracts-bedrock/src/L1/ETHLockbox.sol index f88e42a654af8..82cd74246a939 100644 --- a/packages/contracts-bedrock/src/L1/ETHLockbox.sol +++ b/packages/contracts-bedrock/src/L1/ETHLockbox.sol @@ -12,6 +12,7 @@ import { Constants } from "src/libraries/Constants.sol"; import { ISemver } from "interfaces/universal/ISemver.sol"; import { IOptimismPortal2 as IOptimismPortal } from "interfaces/L1/IOptimismPortal2.sol"; import { ISuperchainConfig } from "interfaces/L1/ISuperchainConfig.sol"; +import { IETHLockbox } from "interfaces/L1/IETHLockbox.sol"; /// @custom:proxied true /// @title ETHLockbox @@ -82,18 +83,24 @@ contract ETHLockbox is PAOBase, Initializable, ISemver { /// @notice Initializer. /// @param _superchainConfig The address of the SuperchainConfig contract. /// @param _portals The addresses of the portals to authorize. - function initialize(address _superchainConfig, address[] calldata _portals) external initializer { + function initialize( + ISuperchainConfig _superchainConfig, + IOptimismPortal[] calldata _portals + ) + external + initializer + { superchainConfig = ISuperchainConfig(_superchainConfig); for (uint256 i; i < _portals.length; i++) { - _authorizePortal(_portals[i]); + _authorizePortal(address(_portals[i])); } } /// @notice Authorizes a portal to lock and unlock ETH. /// @param _portal The address of the portal to authorize. - function authorizePortal(address _portal) external { + function authorizePortal(IOptimismPortal _portal) external { if (msg.sender != PAO()) revert ETHLockbox_Unauthorized(); - _authorizePortal(_portal); + _authorizePortal(address(_portal)); } /// @notice Getter for the current paused status. @@ -133,24 +140,24 @@ contract ETHLockbox is PAOBase, Initializable, ISemver { /// @notice Authorizes an ETH lockbox to migrate its liquidity to the current ETH lockbox. /// @param _lockbox The address of the ETH lockbox to authorize. - function authorizeLockbox(address _lockbox) external { + function authorizeLockbox(IETHLockbox _lockbox) external { if (msg.sender != PAO()) revert ETHLockbox_Unauthorized(); - if (!_samePAO(_lockbox)) revert ETHLockbox_DifferentPAO(); - if (authorizedLockboxes[_lockbox]) revert ETHLockbox_AlreadyAuthorized(); + if (!_samePAO(address(_lockbox))) revert ETHLockbox_DifferentPAO(); + if (authorizedLockboxes[address(_lockbox)]) revert ETHLockbox_AlreadyAuthorized(); - authorizedLockboxes[_lockbox] = true; - emit LockboxAuthorized(_lockbox); + authorizedLockboxes[address(_lockbox)] = true; + emit LockboxAuthorized(address(_lockbox)); } /// @notice Migrates liquidity from the current ETH lockbox to another. /// @param _lockbox The address of the ETH lockbox to migrate liquidity to. - function migrateLiquidity(address _lockbox) external { + function migrateLiquidity(IETHLockbox _lockbox) external { if (msg.sender != PAO()) revert ETHLockbox_Unauthorized(); - if (!_samePAO(_lockbox)) revert ETHLockbox_DifferentPAO(); + if (!_samePAO(address(_lockbox))) revert ETHLockbox_DifferentPAO(); - ETHLockbox(_lockbox).receiveLiquidity{ value: address(this).balance }(); + IETHLockbox(_lockbox).receiveLiquidity{ value: address(this).balance }(); - emit LiquidityMigrated(_lockbox); + emit LiquidityMigrated(address(_lockbox)); } /// @notice Authorizes a portal to lock and unlock ETH. diff --git a/packages/contracts-bedrock/src/L1/OPContractsManager.sol b/packages/contracts-bedrock/src/L1/OPContractsManager.sol index 32854de2e9bff..ad6819956a92a 100644 --- a/packages/contracts-bedrock/src/L1/OPContractsManager.sol +++ b/packages/contracts-bedrock/src/L1/OPContractsManager.sol @@ -23,7 +23,7 @@ import { IFaultDisputeGame } from "interfaces/dispute/IFaultDisputeGame.sol"; import { IPermissionedDisputeGame } from "interfaces/dispute/IPermissionedDisputeGame.sol"; import { ISuperchainConfig } from "interfaces/L1/ISuperchainConfig.sol"; import { IProtocolVersions } from "interfaces/L1/IProtocolVersions.sol"; -import { IOptimismPortal2 } from "interfaces/L1/IOptimismPortal2.sol"; +import { IOptimismPortal2 as IOptimismPortal } from "interfaces/L1/IOptimismPortal2.sol"; import { ISystemConfig } from "interfaces/L1/ISystemConfig.sol"; import { IL1CrossDomainMessenger } from "interfaces/L1/IL1CrossDomainMessenger.sol"; import { IL1ERC721Bridge } from "interfaces/L1/IL1ERC721Bridge.sol"; @@ -76,7 +76,7 @@ contract OPContractsManager is ISemver { IL1CrossDomainMessenger l1CrossDomainMessengerProxy; IETHLockbox ethLockboxProxy; // Fault proof contracts below. - IOptimismPortal2 optimismPortalProxy; + IOptimismPortal optimismPortalProxy; IDisputeGameFactory disputeGameFactoryProxy; IAnchorStateRegistry anchorStateRegistryProxy; IFaultDisputeGame faultDisputeGame; @@ -309,7 +309,7 @@ contract OPContractsManager is ISemver { output.l1ERC721BridgeProxy = IL1ERC721Bridge(deployProxy(l2ChainId, output.opChainProxyAdmin, saltMixer, "L1ERC721Bridge")); output.optimismPortalProxy = - IOptimismPortal2(payable(deployProxy(l2ChainId, output.opChainProxyAdmin, saltMixer, "OptimismPortal"))); + IOptimismPortal(payable(deployProxy(l2ChainId, output.opChainProxyAdmin, saltMixer, "OptimismPortal"))); output.ethLockboxProxy = IETHLockbox(deployProxy(l2ChainId, output.opChainProxyAdmin, saltMixer, "ETHLockbox")); output.systemConfigProxy = ISystemConfig(deployProxy(l2ChainId, output.opChainProxyAdmin, saltMixer, "SystemConfig")); @@ -397,8 +397,8 @@ contract OPContractsManager is ISemver { output.opChainProxyAdmin, address(output.optimismPortalProxy), implementation.optimismPortalImpl, data ); - address[] memory portals = new address[](1); - portals[0] = address(output.optimismPortalProxy); + IOptimismPortal[] memory portals = new IOptimismPortal[](1); + portals[0] = output.optimismPortalProxy; data = encodeETHLockboxInitializer(portals); upgradeToAndCall(output.opChainProxyAdmin, address(output.ethLockboxProxy), implementation.ethLockboxImpl, data); @@ -520,7 +520,7 @@ contract OPContractsManager is ISemver { // Grab the respected game type BEFORE upgrading the OptimismPortal or the function // won't work because the new implementation tries to get it from the // AnchorStateRegistry which doesn't have it yet. - GameType respectedGameType = IOptimismPortal2(payable(opChainAddrs.optimismPortal)).respectedGameType(); + GameType respectedGameType = IOptimismPortal(payable(opChainAddrs.optimismPortal)).respectedGameType(); // -------- Upgrade Contracts Stored in SystemConfig -------- upgradeTo( @@ -614,8 +614,8 @@ contract OPContractsManager is ISemver { ); // Initialize the ETHLockbox. - address[] memory portals = new address[](1); - portals[0] = opChainAddrs.optimismPortal; + IOptimismPortal[] memory portals = new IOptimismPortal[](1); + portals[0] = IOptimismPortal(payable(opChainAddrs.optimismPortal)); upgradeToAndCall( _opChainConfigs[i].proxyAdmin, address(ethLockbox), @@ -625,7 +625,7 @@ contract OPContractsManager is ISemver { // Upgrade the OptimismPortal to have a reference to the new AnchorStateRegistry and ETHLockbox, // and migrate the ETH balance to the ETHLockbox. - IOptimismPortal2(payable(opChainAddrs.optimismPortal)).upgrade( + IOptimismPortal(payable(opChainAddrs.optimismPortal)).upgrade( newAnchorStateRegistryProxy, ethLockbox ); } @@ -889,14 +889,19 @@ contract OPContractsManager is ISemver { returns (bytes memory) { return abi.encodeCall( - IOptimismPortal2.initialize, + IOptimismPortal.initialize, (_output.systemConfigProxy, superchainConfig, _output.anchorStateRegistryProxy, _output.ethLockboxProxy) ); } /// @notice Helper method for encoding the ETHLockbox initializer data. - function encodeETHLockboxInitializer(address[] memory _portals) internal view virtual returns (bytes memory) { - return abi.encodeCall(IETHLockbox.initialize, (address(superchainConfig), _portals)); + function encodeETHLockboxInitializer(IOptimismPortal[] memory _portals) + internal + view + virtual + returns (bytes memory) + { + return abi.encodeCall(IETHLockbox.initialize, (superchainConfig, _portals)); } /// @notice Helper method for encoding the SystemConfig initializer data. diff --git a/packages/contracts-bedrock/src/L1/OptimismPortal2.sol b/packages/contracts-bedrock/src/L1/OptimismPortal2.sol index a7ce1ff930516..df3d0630eb55b 100644 --- a/packages/contracts-bedrock/src/L1/OptimismPortal2.sol +++ b/packages/contracts-bedrock/src/L1/OptimismPortal2.sol @@ -327,13 +327,13 @@ contract OptimismPortal2 is PAOBase, Initializable, ResourceMetering, ISemver { /// @notice Updates the ETHLockbox contract. /// @param _newLockbox The address of the new ETHLockbox contract. - function updateLockbox(address _newLockbox) external { + function updateLockbox(IETHLockbox _newLockbox) external { if (msg.sender != PAO()) revert OptimismPortal_Unauthorized(); address oldLockbox = address(ethLockbox); ethLockbox = IETHLockbox(_newLockbox); - emit LockboxUpdated(oldLockbox, _newLockbox); + emit LockboxUpdated(oldLockbox, address(_newLockbox)); } /// @notice Proves a withdrawal transaction. @@ -544,8 +544,8 @@ contract OptimismPortal2 is PAOBase, Initializable, ResourceMetering, ISemver { /// deriving deposit transactions. Note that if a deposit is made by a contract, its /// address will be aliased when retrieved using `tx.origin` or `msg.sender`. Consider /// using the CrossDomainMessenger contracts for a simpler developer experience. - /// @dev The `msg.value` is locked on the ETHLockbox and minted as ETH when the deposit arrives on L2, - /// while `_value` specifies how much ETH to send to the target. + /// @dev The `msg.value` is locked on the ETHLockbox and minted as ETH when the deposit + /// arrives on L2, while `_value` specifies how much ETH to send to the target. /// @param _to Target address on L2. /// @param _value ETH value to send to the recipient. /// @param _gasLimit Amount of L2 gas to purchase by burning gas on L1. diff --git a/packages/contracts-bedrock/test/L1/ETHLockbox.t.sol b/packages/contracts-bedrock/test/L1/ETHLockbox.t.sol index 126f25e1ffd0e..eae5a819644f0 100644 --- a/packages/contracts-bedrock/test/L1/ETHLockbox.t.sol +++ b/packages/contracts-bedrock/test/L1/ETHLockbox.t.sol @@ -11,6 +11,7 @@ import { IETHLockbox } from "interfaces/L1/IETHLockbox.sol"; import { ISuperchainConfig } from "interfaces/L1/ISuperchainConfig.sol"; import { IPAOBase } from "interfaces/L1/IPAOBase.sol"; +import { IOptimismPortal2 } from "interfaces/L1/IOptimismPortal2.sol"; // Test import { CommonTest } from "test/setup/CommonTest.sol"; @@ -45,8 +46,8 @@ contract ETHLockboxTest is CommonTest { /// @notice Tests it reverts when the contract is already initialized. function test_initialize_alreadyInitialized_reverts() public { vm.expectRevert("Initializable: contract is already initialized"); - address[] memory _portals = new address[](1); - ethLockbox.initialize(address(superchainConfig), _portals); + IOptimismPortal2[] memory _portals = new IOptimismPortal2[](1); + ethLockbox.initialize(superchainConfig, _portals); } /// @notice Tests the proxy admin owner is correctly returned. @@ -68,7 +69,8 @@ contract ETHLockboxTest is CommonTest { /// @notice Tests the liquidity is correctly received. function testFuzz_receiveLiquidity_succeeds(address _lockbox, uint256 _value) public { - vm.assume(!ethLockbox.authorizedLockboxes(_lockbox)); + assumeNotForgeAddress(_lockbox); + vm.assume(address(_lockbox) != address(ethLockbox)); // Deal the value to the lockbox deal(address(_lockbox), _value); @@ -76,12 +78,14 @@ contract ETHLockboxTest is CommonTest { // Mock the admin owner of the lockbox to be the same as the current lockbox proxy admin owner vm.mockCall(address(_lockbox), abi.encodeCall(IPAOBase.PAO, ()), abi.encode(proxyAdmin.owner())); - // Authorize the lockbox - vm.prank(PAO); - ethLockbox.authorizeLockbox(_lockbox); + // Authorize the lockbox if needed + if (!ethLockbox.authorizedLockboxes(_lockbox)) { + vm.prank(PAO); + ethLockbox.authorizeLockbox(IETHLockbox(_lockbox)); + } // Get the balance of the lockbox before the receive - uint256 _lockboxBalanceBefore = address(ethLockbox).balance; + uint256 ethLockboxBalanceBefore = address(ethLockbox).balance; // Expect the `LiquidityReceived` event to be emitted vm.expectEmit(address(ethLockbox)); @@ -92,7 +96,7 @@ contract ETHLockboxTest is CommonTest { ethLockbox.receiveLiquidity{ value: _value }(); // Assert the lockbox's balance increased by the amount received - assertEq(address(ethLockbox).balance, _lockboxBalanceBefore + _value); + assertEq(address(ethLockbox).balance, ethLockboxBalanceBefore + _value); } /// @notice Tests it reverts when the caller is not an authorized portal. @@ -113,8 +117,8 @@ contract ETHLockboxTest is CommonTest { 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(ethLockbox).balance; + uint256 portalBalanceBefore = address(optimismPortal2).balance; + uint256 lockboxBalanceBefore = address(ethLockbox).balance; // Look for the emit of the `ETHLocked` event vm.expectEmit(address(ethLockbox)); @@ -125,39 +129,40 @@ contract ETHLockboxTest is CommonTest { ethLockbox.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(ethLockbox).balance, _lockboxBalanceBefore + _amount); + assertEq(address(optimismPortal2).balance, portalBalanceBefore - _amount); + assertEq(address(ethLockbox).balance, lockboxBalanceBefore + _amount); } /// @notice Tests the ETH is correctly locked when the caller is an authorized portal with different portals. - function testFuzz_lockETH_multiplePortals_succeeds(address _portal, uint256 _amount) public { - vm.assume(_portal != address(ethLockbox)); + function testFuzz_lockETH_multiplePortals_succeeds(IOptimismPortal2 _portal, uint256 _amount) public { + assumeNotForgeAddress(address(_portal)); + vm.assume(address(_portal) != address(ethLockbox)); // Mock the admin owner of the portal to be the same as the current lockbox proxy admin owner vm.mockCall(address(_portal), abi.encodeCall(IPAOBase.PAO, ()), abi.encode(proxyAdmin.owner())); - // Set the portal as an authorized portal - vm.prank(PAO); - ethLockbox.authorizePortal(_portal); + // Set the portal as an authorized portal if needed + if (!ethLockbox.authorizedPortals(address(_portal))) { + vm.prank(PAO); + ethLockbox.authorizePortal(_portal); + } // Deal the ETH amount to the portal - vm.deal(_portal, _amount); + vm.deal(address(_portal), _amount); - // Get the balance of the portal and lockbox before the lock to compare later on the assertions - uint256 _portalBalanceBefore = address(_portal).balance; - uint256 _lockboxBalanceBefore = address(ethLockbox).balance; + // Get the balance of the lockbox before the lock to compare later on the assertions + uint256 lockboxBalanceBefore = address(ethLockbox).balance; // Look for the emit of the `ETHLocked` event vm.expectEmit(address(ethLockbox)); - emit ETHLocked(_portal, _amount); + emit ETHLocked(address(_portal), _amount); // Call the `lockETH` function with the portal - vm.prank(_portal); + vm.prank(address(_portal)); ethLockbox.lockETH{ value: _amount }(); // Assert the portal's balance decreased and the lockbox's balance increased by the amount locked - assertEq(address(_portal).balance, _portalBalanceBefore - _amount); - assertEq(address(ethLockbox).balance, _lockboxBalanceBefore + _amount); + assertEq(address(ethLockbox).balance, lockboxBalanceBefore + _amount); } /// @notice Tests `unlockETH` reverts when the contract is paused. @@ -227,8 +232,10 @@ contract ETHLockboxTest is CommonTest { } /// @notice Tests the ETH is correctly unlocked when the caller is an authorized portal. - function testFuzz_unlockETH_multiplePortals_succeeds(address _portal, uint256 _value) public { - vm.assume(_portal != address(ethLockbox)); + function testFuzz_unlockETH_multiplePortals_succeeds(IOptimismPortal2 _portal, uint256 _value) public { + assumeNotForgeAddress(address(_portal)); + + vm.assume(!ethLockbox.authorizedPortals(address(_portal))); // Mock the admin owner of the portal to be the same as the current lockbox proxy admin owner vm.mockCall(address(_portal), abi.encodeCall(IPAOBase.PAO, ()), abi.encode(proxyAdmin.owner())); @@ -269,13 +276,14 @@ contract ETHLockboxTest is CommonTest { // Call the `authorizePortal` function with an unauthorized caller vm.prank(_caller); - ethLockbox.authorizePortal(address(optimismPortal2)); + ethLockbox.authorizePortal(optimismPortal2); } /// @notice Tests the `authorizePortal` function reverts when the portal is already authorized. - function testFuzz_authorizePortal_alreadyAuthorized_reverts(address _portal) public { + function testFuzz_authorizePortal_alreadyAuthorized_reverts(IOptimismPortal2 _portal) public { + assumeNotForgeAddress(address(_portal)); // Authorize the portal - if (!ethLockbox.authorizedPortals(_portal)) { + if (!ethLockbox.authorizedPortals(address(_portal))) { // Mock the admin owner of the portal to be the same as the current lockbox proxy admin owner vm.mockCall(address(_portal), abi.encodeCall(IPAOBase.PAO, ()), abi.encode(proxyAdmin.owner())); @@ -294,7 +302,8 @@ contract ETHLockboxTest is CommonTest { /// @notice Tests the `authorizePortal` function reverts when the PAO of the portal is not the same as the PAO of /// the lockbox. - function testFuzz_authorizePortal_differentPAO_reverts(address _portal) public { + function testFuzz_authorizePortal_differentPAO_reverts(IOptimismPortal2 _portal) public { + assumeNotForgeAddress(address(_portal)); vm.mockCall(address(_portal), abi.encodeCall(IPAOBase.PAO, ()), abi.encode(address(0))); // Expect the revert with `DifferentOwner` selector @@ -321,29 +330,30 @@ contract ETHLockboxTest is CommonTest { // Call the `authorizePortal` function with the portal vm.prank(PAO); - ethLockbox.authorizePortal(address(optimismPortal2)); + ethLockbox.authorizePortal(optimismPortal2); // Assert the portal is authorized assertTrue(ethLockbox.authorizedPortals(address(optimismPortal2))); } /// @notice Tests the `authorizeLockbox` function succeeds - function testFuzz_authorizePortal_succeeds(address _portal) public { - vm.assume(!ethLockbox.authorizedPortals(_portal)); + function testFuzz_authorizePortal_succeeds(IOptimismPortal2 _portal) public { + assumeNotForgeAddress(address(_portal)); + vm.assume(!ethLockbox.authorizedPortals(address(_portal))); // Mock the admin owner of the portal to be the same as the current lockbox proxy admin owner vm.mockCall(address(_portal), abi.encodeCall(IPAOBase.PAO, ()), abi.encode(proxyAdmin.owner())); // Expect the `PortalAuthorized` event to be emitted vm.expectEmit(address(ethLockbox)); - emit PortalAuthorized(_portal); + emit PortalAuthorized(address(_portal)); // Call the `authorizePortal` function with the portal vm.prank(PAO); ethLockbox.authorizePortal(_portal); // Assert the portal is authorized - assertTrue(ethLockbox.authorizedPortals(_portal)); + assertTrue(ethLockbox.authorizedPortals(address(_portal))); } /// @notice Tests the `authorizeLockbox` function reverts when the caller is not the proxy admin. @@ -355,30 +365,35 @@ contract ETHLockboxTest is CommonTest { // Call the `authorizeLockbox` function with an unauthorized caller vm.prank(_caller); - ethLockbox.authorizeLockbox(address(optimismPortal2)); + ethLockbox.authorizeLockbox(ethLockbox); } /// @notice Tests the `authorizeLockbox` function reverts when the lockbox is already authorized. function testFuzz_authorizeLockbox_alreadyAuthorized_reverts(address _lockbox) public { + assumeNotForgeAddress(_lockbox); + // Authorize the lockbox if (!ethLockbox.authorizedLockboxes(_lockbox)) { vm.mockCall(address(_lockbox), abi.encodeCall(IPAOBase.PAO, ()), abi.encode(proxyAdmin.owner())); vm.prank(PAO); - ethLockbox.authorizeLockbox(_lockbox); + ethLockbox.authorizeLockbox(IETHLockbox(_lockbox)); } + // Call the `authorizeLockbox` function with the lockbox + vm.startPrank(ethLockbox.PAO()); + // Expect the revert with `AlreadyAuthorized` selector vm.expectRevert(IETHLockbox.ETHLockbox_AlreadyAuthorized.selector); - // Call the `authorizeLockbox` function with the lockbox - vm.prank(PAO); - ethLockbox.authorizeLockbox(_lockbox); + ethLockbox.authorizeLockbox(IETHLockbox(_lockbox)); } /// @notice Tests the `authorizeLockbox` function reverts when the PAO of the lockbox is not the same as the PAO of /// the proxy admin. function testFuzz_authorizeLockbox_differentPAO_reverts(address _lockbox) public { + assumeNotForgeAddress(_lockbox); + vm.mockCall(address(_lockbox), abi.encodeCall(IPAOBase.PAO, ()), abi.encode(address(0))); // Expect the revert with `ETHLockbox_DifferentPAO` selector @@ -386,11 +401,12 @@ contract ETHLockboxTest is CommonTest { // Call the `authorizeLockbox` function with the lockbox vm.prank(PAO); - ethLockbox.authorizeLockbox(_lockbox); + ethLockbox.authorizeLockbox(IETHLockbox(_lockbox)); } /// @notice Tests the `authorizeLockbox` function succeeds function testFuzz_authorizeLockbox_succeeds(address _lockbox) public { + assumeNotForgeAddress(_lockbox); vm.assume(!ethLockbox.authorizedLockboxes(_lockbox)); // Mock the admin owner of the lockbox to be the same as the current lockbox proxy admin owner @@ -402,7 +418,7 @@ contract ETHLockboxTest is CommonTest { // Authorize the lockbox vm.prank(PAO); - ethLockbox.authorizeLockbox(_lockbox); + ethLockbox.authorizeLockbox(IETHLockbox(_lockbox)); // Assert the lockbox is authorized assertTrue(ethLockbox.authorizedLockboxes(_lockbox)); @@ -417,12 +433,14 @@ contract ETHLockboxTest is CommonTest { // Call the `migrateLiquidity` function with an unauthorized caller vm.prank(_caller); - ethLockbox.migrateLiquidity(address(optimismPortal2)); + ethLockbox.migrateLiquidity(ethLockbox); } /// @notice Tests the `migrateLiquidity` function reverts when the PAO of the lockbox is not the same as the PAO of /// the proxy admin. function testFuzz_migrateLiquidity_differentPAO_reverts(address _lockbox) public { + assumeNotForgeAddress(_lockbox); + vm.mockCall(address(_lockbox), abi.encodeCall(IPAOBase.PAO, ()), abi.encode(address(0))); // Expect the revert with `ETHLockbox_DifferentPAO` selector @@ -430,11 +448,14 @@ contract ETHLockboxTest is CommonTest { // Call the `migrateLiquidity` function with the lockbox vm.prank(PAO); - ethLockbox.migrateLiquidity(_lockbox); + ethLockbox.migrateLiquidity(IETHLockbox(_lockbox)); } /// @notice Tests the `migrateLiquidity` function succeeds function testFuzz_migrateLiquidity_succeeds(uint256 _balance, address _lockbox) public { + assumeNotForgeAddress(_lockbox); + vm.assume(address(_lockbox) != address(ethLockbox)); + // Mock on the lockbox that will receive the migration for it to succeed vm.mockCall(address(_lockbox), abi.encodeCall(IPAOBase.PAO, ()), abi.encode(proxyAdmin.owner())); vm.mockCall( @@ -455,10 +476,10 @@ contract ETHLockboxTest is CommonTest { // Call the `migrateLiquidity` function with the lockbox vm.prank(PAO); - ethLockbox.migrateLiquidity(_lockbox); + ethLockbox.migrateLiquidity(IETHLockbox(_lockbox)); // Assert the liquidity was migrated - assertEq(address(_lockbox).balance, newLockboxBalanceBefore + ethLockboxBalanceBefore); assertEq(address(ethLockbox).balance, 0); + assertEq(address(_lockbox).balance, newLockboxBalanceBefore + ethLockboxBalanceBefore); } } diff --git a/packages/contracts-bedrock/test/L1/OptimismPortal2.t.sol b/packages/contracts-bedrock/test/L1/OptimismPortal2.t.sol index f4e2b0b5c5840..226b6f653841d 100644 --- a/packages/contracts-bedrock/test/L1/OptimismPortal2.t.sol +++ b/packages/contracts-bedrock/test/L1/OptimismPortal2.t.sol @@ -21,7 +21,7 @@ import "src/dispute/lib/Types.sol"; // Interfaces import { IResourceMetering } from "interfaces/L1/IResourceMetering.sol"; -import { IOptimismPortal2 } from "interfaces/L1/IOptimismPortal2.sol"; +import { IOptimismPortal2 as IOptimismPortal } from "interfaces/L1/IOptimismPortal2.sol"; import { IDisputeGame } from "interfaces/dispute/IDisputeGame.sol"; import { IFaultDisputeGame } from "interfaces/dispute/IFaultDisputeGame.sol"; import { IProxy } from "interfaces/universal/IProxy.sol"; @@ -46,7 +46,7 @@ contract OptimismPortal2_Test is CommonTest { /// @notice Marked virtual to be overridden in /// test/kontrol/deployment/DeploymentSummary.t.sol function test_constructor_succeeds() external virtual { - IOptimismPortal2 opImpl = IOptimismPortal2(payable(EIP1967Helper.getImplementation(address(optimismPortal2)))); + IOptimismPortal opImpl = IOptimismPortal(payable(EIP1967Helper.getImplementation(address(optimismPortal2)))); assertEq(address(opImpl.anchorStateRegistry()), address(0)); assertEq(address(opImpl.systemConfig()), address(0)); assertEq(address(opImpl.superchainConfig()), address(0)); @@ -205,7 +205,7 @@ contract OptimismPortal2_Test is CommonTest { // TODO(opcm upgrades): remove skip once upgrade path is implemented skipIfForkTest("OptimismPortal2_Test: error is different on OP Mainnet"); // contract creation must have a target of address(0) - vm.expectRevert(IOptimismPortal2.OptimismPortal_BadTarget.selector); + vm.expectRevert(IOptimismPortal.OptimismPortal_BadTarget.selector); optimismPortal2.depositTransaction(address(1), 1, 0, true, hex""); } @@ -216,7 +216,7 @@ contract OptimismPortal2_Test is CommonTest { skipIfForkTest("OptimismPortal2_Test: error is different on OP Mainnet"); uint256 size = 120_001; uint64 gasLimit = optimismPortal2.minimumGasLimit(uint64(size)); - vm.expectRevert(IOptimismPortal2.OptimismPortal_CalldataTooLarge.selector); + vm.expectRevert(IOptimismPortal.OptimismPortal_CalldataTooLarge.selector); optimismPortal2.depositTransaction({ _to: address(0), _value: 0, @@ -230,7 +230,7 @@ contract OptimismPortal2_Test is CommonTest { function test_depositTransaction_smallGasLimit_reverts() external { // TODO(opcm upgrades): remove skip once upgrade path is implemented skipIfForkTest("OptimismPortal2_Test: error is different on OP Mainnet"); - vm.expectRevert(IOptimismPortal2.OptimismPortal_GasLimitTooLow.selector); + vm.expectRevert(IOptimismPortal.OptimismPortal_GasLimitTooLow.selector); optimismPortal2.depositTransaction({ _to: address(1), _value: 0, _gasLimit: 0, _isCreation: false, _data: hex"" }); } @@ -242,7 +242,7 @@ contract OptimismPortal2_Test is CommonTest { gasLimit = uint64(bound(gasLimit, 0, gasLimit - 1)); // TODO(opcm upgrades): remove skip once upgrade path is implemented skipIfForkTest("OptimismPortal2_Test: error is different on OP Mainnet"); - vm.expectRevert(IOptimismPortal2.OptimismPortal_GasLimitTooLow.selector); + vm.expectRevert(IOptimismPortal.OptimismPortal_GasLimitTooLow.selector); } optimismPortal2.depositTransaction({ @@ -328,6 +328,8 @@ contract OptimismPortal2_Test is CommonTest { ) external { + assumeNotForgeAddress(_7702Target); + _gasLimit = uint64( bound( _gasLimit, @@ -468,10 +470,10 @@ contract OptimismPortal2_Test is CommonTest { /// @dev Tests that `updateLockbox` reverts if the caller is not the PAO. function testFuzz_updateLockbox_notPAO_reverts(address _caller) external { vm.assume(_caller != optimismPortal2.PAO()); - vm.expectRevert(IOptimismPortal2.OptimismPortal_Unauthorized.selector); + vm.expectRevert(IOptimismPortal.OptimismPortal_Unauthorized.selector); vm.prank(_caller); - optimismPortal2.updateLockbox(address(1)); + optimismPortal2.updateLockbox(IETHLockbox(address(1))); } /// @dev Tests that `updateLockbox` updates the ETHLockbox contract. @@ -483,7 +485,7 @@ contract OptimismPortal2_Test is CommonTest { emit LockboxUpdated(oldLockbox, _newLockbox); vm.prank(optimismPortal2.PAO()); - optimismPortal2.updateLockbox(_newLockbox); + optimismPortal2.updateLockbox(IETHLockbox(_newLockbox)); assertEq(address(optimismPortal2.ethLockbox()), _newLockbox); } @@ -572,7 +574,7 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { /// @dev Asserts that the reentrant call will revert. function callPortalAndExpectRevert() external payable { - vm.expectRevert(IOptimismPortal2.OptimismPortal_NoReentrancy.selector); + vm.expectRevert(IOptimismPortal.OptimismPortal_NoReentrancy.selector); // Arguments here don't matter, as the require check is the first thing that happens. // We assume that this has already been proven. optimismPortal2.finalizeWithdrawalTransaction(_defaultTx); @@ -583,11 +585,11 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { /// @dev Tests that `finalizeWithdrawalTransaction` reverts when the target is the portal contract or the lockbox. function test_finalizeWithdrawalTransaction_badTarget_reverts() external { _defaultTx.target = address(optimismPortal2); - vm.expectRevert(IOptimismPortal2.OptimismPortal_BadTarget.selector); + vm.expectRevert(IOptimismPortal.OptimismPortal_BadTarget.selector); optimismPortal2.finalizeWithdrawalTransaction(_defaultTx); _defaultTx.target = address(ethLockbox); - vm.expectRevert(IOptimismPortal2.OptimismPortal_BadTarget.selector); + vm.expectRevert(IOptimismPortal.OptimismPortal_BadTarget.selector); optimismPortal2.finalizeWithdrawalTransaction(_defaultTx); } @@ -596,7 +598,7 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { vm.prank(optimismPortal2.guardian()); superchainConfig.pause("identifier"); - vm.expectRevert(IOptimismPortal2.OptimismPortal_CallPaused.selector); + vm.expectRevert(IOptimismPortal.OptimismPortal_CallPaused.selector); optimismPortal2.proveWithdrawalTransaction({ _tx: _defaultTx, _disputeGameIndex: _proposedGameIndex, @@ -608,7 +610,7 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { /// @dev Tests that `proveWithdrawalTransaction` reverts when the target is the portal contract. function test_proveWithdrawalTransaction_onSelfCall_reverts() external { _defaultTx.target = address(optimismPortal2); - vm.expectRevert(IOptimismPortal2.OptimismPortal_BadTarget.selector); + vm.expectRevert(IOptimismPortal.OptimismPortal_BadTarget.selector); optimismPortal2.proveWithdrawalTransaction({ _tx: _defaultTx, _disputeGameIndex: _proposedGameIndex, @@ -617,7 +619,7 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { }); _defaultTx.target = address(ethLockbox); - vm.expectRevert(IOptimismPortal2.OptimismPortal_BadTarget.selector); + vm.expectRevert(IOptimismPortal.OptimismPortal_BadTarget.selector); optimismPortal2.proveWithdrawalTransaction({ _tx: _defaultTx, _disputeGameIndex: _proposedGameIndex, @@ -636,7 +638,7 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { vm.warp(_timestamp); // Should revert. - vm.expectRevert(IOptimismPortal2.OptimismPortal_InvalidProofTimestamp.selector); + vm.expectRevert(IOptimismPortal.OptimismPortal_InvalidProofTimestamp.selector); optimismPortal2.proveWithdrawalTransaction({ _tx: _defaultTx, _disputeGameIndex: _proposedGameIndex, @@ -649,7 +651,7 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { function test_proveWithdrawalTransaction_onInvalidOutputRootProof_reverts() external { // Modify the version to invalidate the withdrawal proof. _outputRootProof.version = bytes32(uint256(1)); - vm.expectRevert(IOptimismPortal2.OptimismPortal_InvalidOutputRootProof.selector); + vm.expectRevert(IOptimismPortal.OptimismPortal_InvalidOutputRootProof.selector); optimismPortal2.proveWithdrawalTransaction({ _tx: _defaultTx, _disputeGameIndex: _proposedGameIndex, @@ -693,7 +695,7 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { 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(IOptimismPortal2.OptimismPortal_InvalidDisputeGame.selector); + vm.expectRevert(IOptimismPortal.OptimismPortal_InvalidDisputeGame.selector); optimismPortal2.proveWithdrawalTransaction({ _tx: _defaultTx, _disputeGameIndex: _proposedGameIndex, @@ -705,7 +707,7 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { /// @dev Tests that `proveWithdrawalTransaction` reverts if the game was not the respected game type when created. function test_proveWithdrawalTransaction_wasNotRespectedGameTypeWhenCreated_reverts() external { vm.mockCall(address(game), abi.encodeCall(game.wasRespectedGameTypeWhenCreated, ()), abi.encode(false)); - vm.expectRevert(IOptimismPortal2.OptimismPortal_InvalidDisputeGame.selector); + vm.expectRevert(IOptimismPortal.OptimismPortal_InvalidDisputeGame.selector); optimismPortal2.proveWithdrawalTransaction({ _tx: _defaultTx, _disputeGameIndex: _proposedGameIndex, @@ -748,7 +750,7 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { { _createdAt = uint64(bound(_createdAt, 0, optimismPortal2.respectedGameTypeUpdatedAt())); vm.mockCall(address(game), abi.encodeCall(game.createdAt, ()), abi.encode(uint64(_createdAt))); - vm.expectRevert(IOptimismPortal2.OptimismPortal_ImproperDisputeGame.selector); + vm.expectRevert(IOptimismPortal.OptimismPortal_ImproperDisputeGame.selector); optimismPortal2.proveWithdrawalTransaction({ _tx: _defaultTx, _disputeGameIndex: _proposedGameIndex, @@ -894,7 +896,7 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { emit WithdrawalFinalized(_withdrawalHash, true); optimismPortal2.finalizeWithdrawalTransactionExternalProof(_defaultTx, address(0xb0b)); - vm.expectRevert(IOptimismPortal2.OptimismPortal_AlreadyFinalized.selector); + vm.expectRevert(IOptimismPortal.OptimismPortal_AlreadyFinalized.selector); optimismPortal2.finalizeWithdrawalTransactionExternalProof(_defaultTx, address(this)); assert(address(bob).balance == bobBalanceBefore + 100); @@ -916,7 +918,7 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { vm.warp(block.timestamp + optimismPortal2.proofMaturityDelaySeconds() + 1 seconds); vm.startPrank(alice, Constants.ESTIMATION_ADDRESS); - vm.expectRevert(IOptimismPortal2.OptimismPortal_GasEstimation.selector); + vm.expectRevert(IOptimismPortal.OptimismPortal_GasEstimation.selector); optimismPortal2.finalizeWithdrawalTransaction(_defaultTx); } @@ -1065,7 +1067,7 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { // Ensure both proofs are registered successfully. assertEq(optimismPortal2.numProofSubmitters(_withdrawalHash), 2); - vm.expectRevert(IOptimismPortal2.OptimismPortal_InvalidRootClaim.selector); + vm.expectRevert(IOptimismPortal.OptimismPortal_InvalidRootClaim.selector); vm.prank(address(0xb0b)); optimismPortal2.finalizeWithdrawalTransaction(_defaultTx); @@ -1081,7 +1083,7 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { vm.prank(optimismPortal2.guardian()); superchainConfig.pause("identifier"); - vm.expectRevert(IOptimismPortal2.OptimismPortal_CallPaused.selector); + vm.expectRevert(IOptimismPortal.OptimismPortal_CallPaused.selector); optimismPortal2.finalizeWithdrawalTransaction(_defaultTx); } @@ -1089,7 +1091,7 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { function test_finalizeWithdrawalTransaction_ifWithdrawalNotProven_reverts() external { uint256 bobBalanceBefore = address(bob).balance; - vm.expectRevert(IOptimismPortal2.OptimismPortal_Unproven.selector); + vm.expectRevert(IOptimismPortal.OptimismPortal_Unproven.selector); optimismPortal2.finalizeWithdrawalTransaction(_defaultTx); assert(address(bob).balance == bobBalanceBefore); @@ -1111,7 +1113,7 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { _withdrawalProof: _withdrawalProof }); - vm.expectRevert(IOptimismPortal2.OptimismPortal_ProofNotOldEnough.selector); + vm.expectRevert(IOptimismPortal.OptimismPortal_ProofNotOldEnough.selector); optimismPortal2.finalizeWithdrawalTransaction(_defaultTx); assert(address(bob).balance == bobBalanceBefore); @@ -1141,7 +1143,7 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { vm.mockCall(address(game), abi.encodeCall(game.createdAt, ()), abi.encode(block.timestamp + 1)); // Attempt to finalize the withdrawal - vm.expectRevert(IOptimismPortal2.OptimismPortal_InvalidProofTimestamp.selector); + vm.expectRevert(IOptimismPortal.OptimismPortal_InvalidProofTimestamp.selector); optimismPortal2.finalizeWithdrawalTransaction(_defaultTx); // Ensure that bob's balance has remained the same @@ -1169,7 +1171,7 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { vm.warp(block.timestamp + optimismPortal2.proofMaturityDelaySeconds() + 1); // Attempt to finalize the withdrawal - vm.expectRevert(IOptimismPortal2.OptimismPortal_InvalidRootClaim.selector); + vm.expectRevert(IOptimismPortal.OptimismPortal_InvalidRootClaim.selector); optimismPortal2.finalizeWithdrawalTransaction(_defaultTx); // Ensure that bob's balance has remained the same @@ -1227,7 +1229,7 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { emit WithdrawalFinalized(_withdrawalHash, true); optimismPortal2.finalizeWithdrawalTransaction(_defaultTx); - vm.expectRevert(IOptimismPortal2.OptimismPortal_AlreadyFinalized.selector); + vm.expectRevert(IOptimismPortal.OptimismPortal_AlreadyFinalized.selector); optimismPortal2.finalizeWithdrawalTransaction(_defaultTx); } @@ -1506,7 +1508,7 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { vm.warp(block.timestamp + optimismPortal2.proofMaturityDelaySeconds() + 1); - vm.expectRevert(IOptimismPortal2.OptimismPortal_InvalidRootClaim.selector); + vm.expectRevert(IOptimismPortal.OptimismPortal_InvalidRootClaim.selector); optimismPortal2.finalizeWithdrawalTransaction(_defaultTx); } @@ -1532,7 +1534,7 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { game.resolve(); // Attempt to finalize the withdrawal directly after the game resolves. This should fail. - vm.expectRevert(IOptimismPortal2.OptimismPortal_InvalidRootClaim.selector); + vm.expectRevert(IOptimismPortal.OptimismPortal_InvalidRootClaim.selector); optimismPortal2.finalizeWithdrawalTransaction(_defaultTx); // Finalize the withdrawal transaction. This should succeed. @@ -1570,7 +1572,7 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { anchorStateRegistry.updateRetirementTimestamp(); // Should revert. - vm.expectRevert(IOptimismPortal2.OptimismPortal_InvalidRootClaim.selector); + vm.expectRevert(IOptimismPortal.OptimismPortal_InvalidRootClaim.selector); optimismPortal2.finalizeWithdrawalTransaction(_defaultTx); } @@ -1600,7 +1602,7 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { vm.mockCall(address(game), abi.encodeCall(game.wasRespectedGameTypeWhenCreated, ()), abi.encode(false)); - vm.expectRevert(IOptimismPortal2.OptimismPortal_InvalidRootClaim.selector); + vm.expectRevert(IOptimismPortal.OptimismPortal_InvalidRootClaim.selector); optimismPortal2.finalizeWithdrawalTransaction(_defaultTx); } @@ -1653,13 +1655,13 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { // Attempt to finalize the withdrawal transaction 1 second before the proof has matured. This should fail. vm.warp(block.timestamp + optimismPortal2.proofMaturityDelaySeconds()); - vm.expectRevert(IOptimismPortal2.OptimismPortal_ProofNotOldEnough.selector); + vm.expectRevert(IOptimismPortal.OptimismPortal_ProofNotOldEnough.selector); optimismPortal2.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(IOptimismPortal2.OptimismPortal_InvalidRootClaim.selector); + vm.expectRevert(IOptimismPortal.OptimismPortal_InvalidRootClaim.selector); optimismPortal2.finalizeWithdrawalTransaction(_defaultTx); // Finalize the dispute game and attempt to finalize the withdrawal again. This should also fail, since the @@ -1667,7 +1669,7 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { game.resolveClaim(0, 0); game.resolve(); vm.warp(block.timestamp + optimismPortal2.disputeGameFinalityDelaySeconds()); - vm.expectRevert(IOptimismPortal2.OptimismPortal_InvalidRootClaim.selector); + vm.expectRevert(IOptimismPortal.OptimismPortal_InvalidRootClaim.selector); optimismPortal2.finalizeWithdrawalTransaction(_defaultTx); // Warp 1 second in the future, past the air gap dispute game delay, and attempt to finalize the withdrawal. @@ -1730,7 +1732,7 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { optimismPortal2.finalizeWithdrawalTransaction(_defaultTx); // Should revert. - vm.expectRevert(IOptimismPortal2.OptimismPortal_AlreadyFinalized.selector); + vm.expectRevert(IOptimismPortal.OptimismPortal_AlreadyFinalized.selector); optimismPortal2.checkWithdrawal(_withdrawalHash, address(this)); } @@ -1738,7 +1740,7 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { function test_checkWithdrawal_ifUnproven_reverts() external { // Don't prove the withdrawal transaction. // Should revert. - vm.expectRevert(IOptimismPortal2.OptimismPortal_Unproven.selector); + vm.expectRevert(IOptimismPortal.OptimismPortal_Unproven.selector); optimismPortal2.checkWithdrawal(_withdrawalHash, address(this)); } @@ -1769,7 +1771,7 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { ); // Should revert. - vm.expectRevert(IOptimismPortal2.OptimismPortal_InvalidProofTimestamp.selector); + vm.expectRevert(IOptimismPortal.OptimismPortal_InvalidProofTimestamp.selector); optimismPortal2.checkWithdrawal(_withdrawalHash, address(this)); } @@ -1786,7 +1788,7 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { // Should revert. vm.warp(block.timestamp + optimismPortal2.proofMaturityDelaySeconds() - 1); - vm.expectRevert(IOptimismPortal2.OptimismPortal_ProofNotOldEnough.selector); + vm.expectRevert(IOptimismPortal.OptimismPortal_ProofNotOldEnough.selector); optimismPortal2.checkWithdrawal(_withdrawalHash, address(this)); } @@ -1807,7 +1809,7 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { vm.mockCall(address(game), abi.encodeCall(game.status, ()), abi.encode(GameStatus.CHALLENGER_WINS)); // Should revert. - vm.expectRevert(IOptimismPortal2.OptimismPortal_InvalidRootClaim.selector); + vm.expectRevert(IOptimismPortal.OptimismPortal_InvalidRootClaim.selector); optimismPortal2.checkWithdrawal(_withdrawalHash, address(this)); } } @@ -1859,7 +1861,7 @@ contract OptimismPortal2_LiquidityMigration_Test is CommonTest { /// @notice Tests the liquidity migration from the portal to the lockbox reverts if not called by the admin owner. function testFuzz_migrateLiquidity_notPAO_reverts(address _caller) external { vm.assume(_caller != optimismPortal2.PAO()); - vm.expectRevert(IOptimismPortal2.OptimismPortal_Unauthorized.selector); + vm.expectRevert(IOptimismPortal.OptimismPortal_Unauthorized.selector); vm.prank(_caller); optimismPortal2.migrateLiquidity(); } diff --git a/packages/contracts-bedrock/test/opcm/DeployImplementations.t.sol b/packages/contracts-bedrock/test/opcm/DeployImplementations.t.sol index 1551f06937626..b056f48eccbeb 100644 --- a/packages/contracts-bedrock/test/opcm/DeployImplementations.t.sol +++ b/packages/contracts-bedrock/test/opcm/DeployImplementations.t.sol @@ -17,7 +17,7 @@ import { IAnchorStateRegistry } from "interfaces/dispute/IAnchorStateRegistry.so import { ISuperchainConfig } from "interfaces/L1/ISuperchainConfig.sol"; import { IProtocolVersions } from "interfaces/L1/IProtocolVersions.sol"; import { IOPContractsManager } from "interfaces/L1/IOPContractsManager.sol"; -import { IOptimismPortal2 } from "interfaces/L1/IOptimismPortal2.sol"; +import { IOptimismPortal2 as IOptimismPortal } from "interfaces/L1/IOptimismPortal2.sol"; import { ISystemConfig } from "interfaces/L1/ISystemConfig.sol"; import { IL1CrossDomainMessenger } from "interfaces/L1/IL1CrossDomainMessenger.sol"; import { IL1ERC721Bridge } from "interfaces/L1/IL1ERC721Bridge.sol"; @@ -89,7 +89,7 @@ contract DeployImplementationsOutput_Test is Test { function test_set_succeeds() public { IOPContractsManager opcm = IOPContractsManager(address(makeAddr("opcm"))); - IOptimismPortal2 optimismPortalImpl = IOptimismPortal2(payable(makeAddr("optimismPortalImpl"))); + IOptimismPortal optimismPortalImpl = IOptimismPortal(payable(makeAddr("optimismPortalImpl"))); IETHLockbox ethLockboxImpl = IETHLockbox(payable(makeAddr("ethLockboxImpl"))); IDelayedWETH delayedWETHImpl = IDelayedWETH(payable(makeAddr("delayedWETHImpl"))); IPreimageOracle preimageOracleSingleton = IPreimageOracle(makeAddr("preimageOracleSingleton")); diff --git a/packages/contracts-bedrock/test/setup/Setup.sol b/packages/contracts-bedrock/test/setup/Setup.sol index 03458f9564019..aea2760508e90 100644 --- a/packages/contracts-bedrock/test/setup/Setup.sol +++ b/packages/contracts-bedrock/test/setup/Setup.sol @@ -22,7 +22,7 @@ import { Chains } from "scripts/libraries/Chains.sol"; // Interfaces import { IOPContractsManager } from "interfaces/L1/IOPContractsManager.sol"; -import { IOptimismPortal2 } from "interfaces/L1/IOptimismPortal2.sol"; +import { IOptimismPortal2 as IOptimismPortal } from "interfaces/L1/IOptimismPortal2.sol"; import { IETHLockbox } from "interfaces/L1/IETHLockbox.sol"; import { IL1CrossDomainMessenger } from "interfaces/L1/IL1CrossDomainMessenger.sol"; import { ISystemConfig } from "interfaces/L1/ISystemConfig.sol"; @@ -96,7 +96,7 @@ contract Setup { IDelayedWETH delayedWETHPermissionedGameProxy; // L1 contracts - core - IOptimismPortal2 optimismPortal2; + IOptimismPortal optimismPortal2; IETHLockbox ethLockbox; ISystemConfig systemConfig; IL1StandardBridge l1StandardBridge; @@ -228,7 +228,7 @@ contract Setup { console.log("Setup: completed L1 deployment, registering addresses now"); - optimismPortal2 = IOptimismPortal2(artifacts.mustGetAddress("OptimismPortalProxy")); + optimismPortal2 = IOptimismPortal(artifacts.mustGetAddress("OptimismPortalProxy")); ethLockbox = IETHLockbox(artifacts.mustGetAddress("ETHLockboxProxy")); systemConfig = ISystemConfig(artifacts.mustGetAddress("SystemConfigProxy")); l1StandardBridge = IL1StandardBridge(artifacts.mustGetAddress("L1StandardBridgeProxy")); diff --git a/packages/contracts-bedrock/test/vendor/Initializable.t.sol b/packages/contracts-bedrock/test/vendor/Initializable.t.sol index ae3e50c7e6024..bfabc38f94ae0 100644 --- a/packages/contracts-bedrock/test/vendor/Initializable.t.sol +++ b/packages/contracts-bedrock/test/vendor/Initializable.t.sol @@ -19,6 +19,7 @@ import { IResourceMetering } from "interfaces/L1/IResourceMetering.sol"; import { ISuperchainConfig } from "interfaces/L1/ISuperchainConfig.sol"; import { IDisputeGameFactory } from "interfaces/dispute/IDisputeGameFactory.sol"; import { ProtocolVersion } from "interfaces/L1/IProtocolVersions.sol"; +import { IOptimismPortal2 } from "interfaces/L1/IOptimismPortal2.sol"; /// @title Initializer_Test /// @dev Ensures that the `initialize()` function on contracts cannot be called more than @@ -330,7 +331,9 @@ contract Initializer_Test is CommonTest { InitializeableContract({ name: "ETHLockboxImpl", target: EIP1967Helper.getImplementation(address(ethLockbox)), - initCalldata: abi.encodeCall(ethLockbox.initialize, (address(0), new address[](0))) + initCalldata: abi.encodeCall( + ethLockbox.initialize, (ISuperchainConfig(address(0)), new IOptimismPortal2[](0)) + ) }) ); @@ -339,7 +342,9 @@ contract Initializer_Test is CommonTest { InitializeableContract({ name: "ETHLockboxProxy", target: address(ethLockbox), - initCalldata: abi.encodeCall(ethLockbox.initialize, (address(0), new address[](0))) + initCalldata: abi.encodeCall( + ethLockbox.initialize, (ISuperchainConfig(address(0)), new IOptimismPortal2[](0)) + ) }) ); } From 540c870141ad5cee832f69c4ccb0fd2e16e616e3 Mon Sep 17 00:00:00 2001 From: Kelvin Fichter Date: Tue, 25 Feb 2025 15:52:29 -0600 Subject: [PATCH 09/50] feat: add Super Root specific method to OptimismPortal Updates the OptimismPortal to include a method that allows users to prove against Super Roots for interop. --- op-chain-ops/interopgen/configs.go | 19 +- op-chain-ops/interopgen/deploy.go | 37 +-- op-chain-ops/interopgen/recipe.go | 19 +- op-chain-ops/solc/types.go | 2 + .../deployer/integration_test/apply_test.go | 1 + op-deployer/pkg/deployer/opcm/opchain.go | 1 + op-deployer/pkg/deployer/pipeline/opchain.go | 14 +- op-deployer/pkg/deployer/standard/standard.go | 1 + .../pkg/deployer/state/chain_intent.go | 1 + .../deployer/upgrade/v2_0_0/upgrade_test.go | 2 + op-e2e/config/init.go | 39 +-- .../interfaces/L1/IOPContractsManager.sol | 21 +- .../interfaces/L1/IOPPrestateUpdater.sol | 19 +- .../interfaces/L1/IOptimismPortal2.sol | 23 +- .../interfaces/L1/IOptimismPortalInterop.sol | 23 +- .../interfaces/L1/ISystemConfig.sol | 8 +- .../interfaces/L1/ISystemConfigInterop.sol | 7 +- .../universal/IReinitializableBase.sol | 11 + packages/contracts-bedrock/justfile | 1 + .../scripts/checks/reinitializer/main.go | 25 +- .../scripts/checks/reinitializer/main_test.go | 112 +++++++ .../scripts/deploy/Deploy.s.sol | 4 +- .../scripts/deploy/DeployOPChain.s.sol | 9 + .../scripts/go-ffi/differential-testing.go | 61 +++- .../contracts-bedrock/scripts/go-ffi/utils.go | 85 +++++ .../scripts/libraries/DeployUtils.sol | 15 +- .../snapshots/abi/OPContractsManager.json | 45 ++- .../abi/OPContractsManagerInterop.json | 45 ++- .../snapshots/abi/OPPrestateUpdater.json | 50 ++- .../snapshots/abi/OptimismPortal2.json | 192 +++++++++++ .../snapshots/abi/OptimismPortalInterop.json | 192 +++++++++++ .../snapshots/abi/SystemConfig.json | 49 +++ .../snapshots/abi/SystemConfigInterop.json | 54 ++++ .../snapshots/semver-lock.json | 24 +- .../storageLayout/OptimismPortal2.json | 7 + .../storageLayout/OptimismPortalInterop.json | 7 + .../snapshots/storageLayout/SystemConfig.json | 7 + .../storageLayout/SystemConfigInterop.json | 7 + .../src/L1/OPContractsManager.sol | 82 +++-- .../src/L1/OPContractsManagerInterop.sol | 3 +- .../src/L1/OptimismPortal2.sol | 153 +++++++-- .../contracts-bedrock/src/L1/SystemConfig.sol | 26 +- .../src/L1/SystemConfigInterop.sol | 10 +- .../src/libraries/Encoding.sol | 32 ++ .../src/libraries/Hashing.sol | 7 + .../contracts-bedrock/src/libraries/Types.sol | 18 ++ .../src/universal/ReinitializableBase.sol | 25 ++ .../test/L1/OPContractsManager.t.sol | 5 +- .../test/L1/OPPrestateUpdater.t.sol | 18 +- .../test/L1/OptimismPortal2.t.sol | 304 +++++++++++++++++- .../test/L1/SystemConfig.t.sol | 76 ++++- .../test/invariants/SystemConfig.t.sol | 3 +- .../test/libraries/Encoding.t.sol | 198 +++++++++++- .../test/libraries/Hashing.t.sol | 54 ++++ .../test/opcm/UpgradeOPChain.t.sol | 12 +- .../test/setup/FFIInterface.sol | 22 ++ .../test/setup/ForkLive.s.sol | 3 +- .../test/universal/ReinitializableBase.t.sol | 36 +++ .../test/universal/Specs.t.sol | 41 ++- .../test/vendor/Initializable.t.sol | 10 +- 60 files changed, 2174 insertions(+), 203 deletions(-) create mode 100644 packages/contracts-bedrock/interfaces/universal/IReinitializableBase.sol create mode 100644 packages/contracts-bedrock/src/universal/ReinitializableBase.sol create mode 100644 packages/contracts-bedrock/test/universal/ReinitializableBase.t.sol diff --git a/op-chain-ops/interopgen/configs.go b/op-chain-ops/interopgen/configs.go index 948d9daa30533..0943e2b490a4f 100644 --- a/op-chain-ops/interopgen/configs.go +++ b/op-chain-ops/interopgen/configs.go @@ -76,15 +76,16 @@ type L2Config struct { Challenger common.Address SystemConfigOwner common.Address genesis.L2InitializationConfig - Prefund map[common.Address]*big.Int - SaltMixer string - GasLimit uint64 - DisputeGameType uint32 - DisputeAbsolutePrestate common.Hash - DisputeMaxGameDepth uint64 - DisputeSplitDepth uint64 - DisputeClockExtension uint64 - DisputeMaxClockDuration uint64 + Prefund map[common.Address]*big.Int + SaltMixer string + GasLimit uint64 + DisputeGameUsesSuperRoots bool + DisputeGameType uint32 + DisputeAbsolutePrestate common.Hash + DisputeMaxGameDepth uint64 + DisputeSplitDepth uint64 + DisputeClockExtension uint64 + DisputeMaxClockDuration uint64 } func (c *L2Config) Check(log log.Logger) error { diff --git a/op-chain-ops/interopgen/deploy.go b/op-chain-ops/interopgen/deploy.go index 4ef1d32b658df..84a06245132ae 100644 --- a/op-chain-ops/interopgen/deploy.go +++ b/op-chain-ops/interopgen/deploy.go @@ -198,24 +198,25 @@ func DeployL2ToL1(l1Host *script.Host, superCfg *SuperchainConfig, superDeployme l1Host.SetTxOrigin(cfg.Deployer) output, err := opcm.DeployOPChain(l1Host, opcm.DeployOPChainInput{ - OpChainProxyAdminOwner: cfg.ProxyAdminOwner, - SystemConfigOwner: cfg.SystemConfigOwner, - Batcher: cfg.BatchSenderAddress, - UnsafeBlockSigner: cfg.P2PSequencerAddress, - Proposer: cfg.Proposer, - Challenger: cfg.Challenger, - BasefeeScalar: cfg.GasPriceOracleBaseFeeScalar, - BlobBaseFeeScalar: cfg.GasPriceOracleBlobBaseFeeScalar, - L2ChainId: new(big.Int).SetUint64(cfg.L2ChainID), - Opcm: superDeployment.Opcm, - SaltMixer: cfg.SaltMixer, - GasLimit: cfg.GasLimit, - DisputeGameType: cfg.DisputeGameType, - DisputeAbsolutePrestate: cfg.DisputeAbsolutePrestate, - DisputeMaxGameDepth: cfg.DisputeMaxGameDepth, - DisputeSplitDepth: cfg.DisputeSplitDepth, - DisputeClockExtension: cfg.DisputeClockExtension, - DisputeMaxClockDuration: cfg.DisputeMaxClockDuration, + OpChainProxyAdminOwner: cfg.ProxyAdminOwner, + SystemConfigOwner: cfg.SystemConfigOwner, + Batcher: cfg.BatchSenderAddress, + UnsafeBlockSigner: cfg.P2PSequencerAddress, + Proposer: cfg.Proposer, + Challenger: cfg.Challenger, + BasefeeScalar: cfg.GasPriceOracleBaseFeeScalar, + BlobBaseFeeScalar: cfg.GasPriceOracleBlobBaseFeeScalar, + L2ChainId: new(big.Int).SetUint64(cfg.L2ChainID), + Opcm: superDeployment.Opcm, + SaltMixer: cfg.SaltMixer, + GasLimit: cfg.GasLimit, + DisputeGameUsesSuperRoots: cfg.DisputeGameUsesSuperRoots, + DisputeGameType: cfg.DisputeGameType, + DisputeAbsolutePrestate: cfg.DisputeAbsolutePrestate, + DisputeMaxGameDepth: cfg.DisputeMaxGameDepth, + DisputeSplitDepth: cfg.DisputeSplitDepth, + DisputeClockExtension: cfg.DisputeClockExtension, + DisputeMaxClockDuration: cfg.DisputeMaxClockDuration, }) if err != nil { return nil, fmt.Errorf("failed to deploy L2 OP chain: %w", err) diff --git a/op-chain-ops/interopgen/recipe.go b/op-chain-ops/interopgen/recipe.go index 66444982c8c18..5605ec2a62ea5 100644 --- a/op-chain-ops/interopgen/recipe.go +++ b/op-chain-ops/interopgen/recipe.go @@ -256,15 +256,16 @@ func InteropL2DevConfig(l1ChainID, l2ChainID uint64, addrs devkeys.Addresses, me UseAltDA: false, }, }, - Prefund: make(map[common.Address]*big.Int), - SaltMixer: "", - GasLimit: 60_000_000, - DisputeGameType: 1, // PERMISSIONED_CANNON Game Type - DisputeAbsolutePrestate: common.HexToHash("0x038512e02c4c3f7bdaec27d00edf55b7155e0905301e1a88083e4e0a6764d54c"), - DisputeMaxGameDepth: 73, - DisputeSplitDepth: 30, - DisputeClockExtension: 10800, // 3 hours (input in seconds) - DisputeMaxClockDuration: 302400, // 3.5 days (input in seconds) + Prefund: make(map[common.Address]*big.Int), + SaltMixer: "", + GasLimit: 60_000_000, + DisputeGameUsesSuperRoots: true, + DisputeGameType: 1, // PERMISSIONED_CANNON Game Type + DisputeAbsolutePrestate: common.HexToHash("0x038512e02c4c3f7bdaec27d00edf55b7155e0905301e1a88083e4e0a6764d54c"), + DisputeMaxGameDepth: 73, + DisputeSplitDepth: 30, + DisputeClockExtension: 10800, // 3 hours (input in seconds) + DisputeMaxClockDuration: 302400, // 3.5 days (input in seconds) } // TODO(#11887): consider making the number of prefunded keys configurable. diff --git a/op-chain-ops/solc/types.go b/op-chain-ops/solc/types.go index d109231efff72..24f6a43a89f8e 100644 --- a/op-chain-ops/solc/types.go +++ b/op-chain-ops/solc/types.go @@ -262,6 +262,8 @@ type Expression struct { ReferencedDeclaration int `json:"referencedDeclaration,omitempty"` ArgumentTypes []AstTypeDescriptions `json:"argumentTypes,omitempty"` Value interface{} `json:"value,omitempty"` + Kind string `json:"kind,omitempty"` + Expression *Expression `json:"expression,omitempty"` } type ForgeArtifact struct { diff --git a/op-deployer/pkg/deployer/integration_test/apply_test.go b/op-deployer/pkg/deployer/integration_test/apply_test.go index f070de2fc3ce8..8d5ae26328911 100644 --- a/op-deployer/pkg/deployer/integration_test/apply_test.go +++ b/op-deployer/pkg/deployer/integration_test/apply_test.go @@ -458,6 +458,7 @@ func TestAdditionalDisputeGames(t *testing.T) { intent.Chains[0].AdditionalDisputeGames = []state.AdditionalDisputeGame{ { ChainProofParams: state.ChainProofParams{ + DisputeGameUsesSuperRoots: false, DisputeGameType: 255, DisputeAbsolutePrestate: standard.DisputeAbsolutePrestate, DisputeMaxGameDepth: 50, diff --git a/op-deployer/pkg/deployer/opcm/opchain.go b/op-deployer/pkg/deployer/opcm/opchain.go index 632ca3a053e75..924c2752161c0 100644 --- a/op-deployer/pkg/deployer/opcm/opchain.go +++ b/op-deployer/pkg/deployer/opcm/opchain.go @@ -33,6 +33,7 @@ type DeployOPChainInput struct { SaltMixer string GasLimit uint64 + DisputeGameUsesSuperRoots bool DisputeGameType uint32 DisputeAbsolutePrestate common.Hash DisputeMaxGameDepth uint64 diff --git a/op-deployer/pkg/deployer/pipeline/opchain.go b/op-deployer/pkg/deployer/pipeline/opchain.go index dadbb34820df5..bc2c0e898c375 100644 --- a/op-deployer/pkg/deployer/pipeline/opchain.go +++ b/op-deployer/pkg/deployer/pipeline/opchain.go @@ -73,12 +73,13 @@ func DeployOPChain(env *Env, intent *state.Intent, st *state.State, chainID comm func makeDCI(intent *state.Intent, thisIntent *state.ChainIntent, chainID common.Hash, st *state.State) (opcm.DeployOPChainInput, error) { proofParams, err := jsonutil.MergeJSON( state.ChainProofParams{ - DisputeGameType: standard.DisputeGameType, - DisputeAbsolutePrestate: standard.DisputeAbsolutePrestate, - DisputeMaxGameDepth: standard.DisputeMaxGameDepth, - DisputeSplitDepth: standard.DisputeSplitDepth, - DisputeClockExtension: standard.DisputeClockExtension, - DisputeMaxClockDuration: standard.DisputeMaxClockDuration, + DisputeGameUsesSuperRoots: standard.DisputeGameUsesSuperRoots, + DisputeGameType: standard.DisputeGameType, + DisputeAbsolutePrestate: standard.DisputeAbsolutePrestate, + DisputeMaxGameDepth: standard.DisputeMaxGameDepth, + DisputeSplitDepth: standard.DisputeSplitDepth, + DisputeClockExtension: standard.DisputeClockExtension, + DisputeMaxClockDuration: standard.DisputeMaxClockDuration, }, intent.GlobalDeployOverrides, thisIntent.DeployOverrides, @@ -100,6 +101,7 @@ func makeDCI(intent *state.Intent, thisIntent *state.ChainIntent, chainID common Opcm: st.ImplementationsDeployment.OpcmAddress, SaltMixer: st.Create2Salt.String(), // passing through salt generated at state initialization GasLimit: standard.GasLimit, + DisputeGameUsesSuperRoots: proofParams.DisputeGameUsesSuperRoots, DisputeGameType: proofParams.DisputeGameType, DisputeAbsolutePrestate: proofParams.DisputeAbsolutePrestate, DisputeMaxGameDepth: proofParams.DisputeMaxGameDepth, diff --git a/op-deployer/pkg/deployer/standard/standard.go b/op-deployer/pkg/deployer/standard/standard.go index ee88170662016..b0511f8bba097 100644 --- a/op-deployer/pkg/deployer/standard/standard.go +++ b/op-deployer/pkg/deployer/standard/standard.go @@ -28,6 +28,7 @@ const ( ProofMaturityDelaySeconds uint64 = 604800 DisputeGameFinalityDelaySeconds uint64 = 302400 MIPSVersion uint64 = 1 + DisputeGameUsesSuperRoots bool = false DisputeGameType uint32 = 1 // PERMISSIONED game type DisputeMaxGameDepth uint64 = 73 DisputeSplitDepth uint64 = 30 diff --git a/op-deployer/pkg/deployer/state/chain_intent.go b/op-deployer/pkg/deployer/state/chain_intent.go index aac1d27affe16..672305e1d9f86 100644 --- a/op-deployer/pkg/deployer/state/chain_intent.go +++ b/op-deployer/pkg/deployer/state/chain_intent.go @@ -17,6 +17,7 @@ const ( ) type ChainProofParams struct { + DisputeGameUsesSuperRoots bool `json:"disputeGameUsesSuperRoots" toml:"disputeGameUsesSuperRoots"` DisputeGameType uint32 `json:"respectedGameType" toml:"respectedGameType"` DisputeAbsolutePrestate common.Hash `json:"faultGameAbsolutePrestate" toml:"faultGameAbsolutePrestate"` DisputeMaxGameDepth uint64 `json:"faultGameMaxDepth" toml:"faultGameMaxDepth"` diff --git a/op-deployer/pkg/deployer/upgrade/v2_0_0/upgrade_test.go b/op-deployer/pkg/deployer/upgrade/v2_0_0/upgrade_test.go index 95585401b1182..b025855751b11 100644 --- a/op-deployer/pkg/deployer/upgrade/v2_0_0/upgrade_test.go +++ b/op-deployer/pkg/deployer/upgrade/v2_0_0/upgrade_test.go @@ -21,6 +21,7 @@ import ( ) func TestUpgradeOPChainInput_OpChainConfigs(t *testing.T) { + t.Skip("skipping for upgrade 15") input := &UpgradeOPChainInput{ Prank: common.Address{0xaa}, Opcm: common.Address{0xbb}, @@ -54,6 +55,7 @@ func TestUpgradeOPChainInput_OpChainConfigs(t *testing.T) { } func TestUpgrader_Upgrade(t *testing.T) { + t.Skip("skipping for upgrade 15") lgr := testlog.Logger(t, slog.LevelDebug) forkedL1, stopL1, err := devnet.NewForkedSepolia(lgr) diff --git a/op-e2e/config/init.go b/op-e2e/config/init.go index d2cd5728cfa96..1951960748db9 100644 --- a/op-e2e/config/init.go +++ b/op-e2e/config/init.go @@ -434,12 +434,13 @@ func defaultIntent(root string, loc *artifacts.Locator, deployer common.Address, { ChainProofParams: state.ChainProofParams{ // Fast game - DisputeGameType: 254, - DisputeAbsolutePrestate: defaultPrestate, - DisputeMaxGameDepth: 14 + 3 + 1, - DisputeSplitDepth: 14, - DisputeClockExtension: 0, - DisputeMaxClockDuration: 0, + DisputeGameUsesSuperRoots: false, + DisputeGameType: 254, + DisputeAbsolutePrestate: defaultPrestate, + DisputeMaxGameDepth: 14 + 3 + 1, + DisputeSplitDepth: 14, + DisputeClockExtension: 0, + DisputeMaxClockDuration: 0, }, VMType: state.VMTypeAlphabet, UseCustomOracle: true, @@ -450,23 +451,25 @@ func defaultIntent(root string, loc *artifacts.Locator, deployer common.Address, { ChainProofParams: state.ChainProofParams{ // Alphabet game - DisputeGameType: 255, - DisputeAbsolutePrestate: defaultPrestate, - DisputeMaxGameDepth: 14 + 3 + 1, - DisputeSplitDepth: 14, - DisputeClockExtension: 0, - DisputeMaxClockDuration: 1200, + DisputeGameUsesSuperRoots: false, + DisputeGameType: 255, + DisputeAbsolutePrestate: defaultPrestate, + DisputeMaxGameDepth: 14 + 3 + 1, + DisputeSplitDepth: 14, + DisputeClockExtension: 0, + DisputeMaxClockDuration: 1200, }, VMType: state.VMTypeAlphabet, }, { ChainProofParams: state.ChainProofParams{ - DisputeGameType: 0, - DisputeAbsolutePrestate: cannonPrestate(root, allocType), - DisputeMaxGameDepth: 50, - DisputeSplitDepth: 14, - DisputeClockExtension: 0, - DisputeMaxClockDuration: 1200, + DisputeGameUsesSuperRoots: false, + DisputeGameType: 0, + DisputeAbsolutePrestate: cannonPrestate(root, allocType), + DisputeMaxGameDepth: 50, + DisputeSplitDepth: 14, + DisputeClockExtension: 0, + DisputeMaxClockDuration: 1200, }, VMType: cannonVMType(allocType), }, diff --git a/packages/contracts-bedrock/interfaces/L1/IOPContractsManager.sol b/packages/contracts-bedrock/interfaces/L1/IOPContractsManager.sol index d3e92ae006a67..60ee03e8e5d3a 100644 --- a/packages/contracts-bedrock/interfaces/L1/IOPContractsManager.sol +++ b/packages/contracts-bedrock/interfaces/L1/IOPContractsManager.sol @@ -48,6 +48,7 @@ interface IOPContractsManager { string saltMixer; uint64 gasLimit; // Configurable dispute game parameters. + bool disputeGameUsesSuperRoots; GameType disputeGameType; Claim disputeAbsolutePrestate; uint256 disputeMaxGameDepth; @@ -113,6 +114,7 @@ interface IOPContractsManager { ISystemConfig systemConfigProxy; IProxyAdmin proxyAdmin; Claim absolutePrestate; + bool disputeGameUsesSuperRoots; } struct AddGameInput { @@ -197,8 +199,23 @@ interface IOPContractsManager { /// @notice Thrown when an invalid `l2ChainId` is provided to `deploy`. error InvalidChainId(); - /// @notice Thrown when a role's address is not valid. - error InvalidRoleAddress(string role); + /// @notice Thrown when a role's address is not valid (opChainProxyAdminOwner). + error InvalidRoleAddressPAO(); + + /// @notice Thrown when a role's address is not valid (systemConfigOwner). + error InvalidRoleAddressSCO(); + + /// @notice Thrown when a role's address is not valid (batcher). + error InvalidRoleAddressBatcher(); + + /// @notice Thrown when a role's address is not valid (unsafeBlockSigner). + error InvalidRoleAddressUBS(); + + /// @notice Thrown when a role's address is not valid (proposer). + error InvalidRoleAddressProposer(); + + /// @notice Thrown when a role's address is not valid (challenger). + error InvalidRoleAddressChallenger(); /// @notice Thrown when the latest release is not set upon initialization. error LatestReleaseNotSet(); diff --git a/packages/contracts-bedrock/interfaces/L1/IOPPrestateUpdater.sol b/packages/contracts-bedrock/interfaces/L1/IOPPrestateUpdater.sol index 7456fed5231d5..33533cf96a975 100644 --- a/packages/contracts-bedrock/interfaces/L1/IOPPrestateUpdater.sol +++ b/packages/contracts-bedrock/interfaces/L1/IOPPrestateUpdater.sol @@ -75,8 +75,23 @@ interface IOPPrestateUpdater { /// @notice Thrown when an invalid `l2ChainId` is provided to `deploy`. error InvalidChainId(); - /// @notice Thrown when a role's address is not valid. - error InvalidRoleAddress(string role); + /// @notice Thrown when a role's address is not valid (opChainProxyAdminOwner). + error InvalidRoleAddressPAO(); + + /// @notice Thrown when a role's address is not valid (systemConfigOwner). + error InvalidRoleAddressSCO(); + + /// @notice Thrown when a role's address is not valid (batcher). + error InvalidRoleAddressBatcher(); + + /// @notice Thrown when a role's address is not valid (unsafeBlockSigner). + error InvalidRoleAddressUBS(); + + /// @notice Thrown when a role's address is not valid (proposer). + error InvalidRoleAddressProposer(); + + /// @notice Thrown when a role's address is not valid (challenger). + error InvalidRoleAddressChallenger(); /// @notice Thrown when the latest release is not set upon initialization. error LatestReleaseNotSet(); diff --git a/packages/contracts-bedrock/interfaces/L1/IOptimismPortal2.sol b/packages/contracts-bedrock/interfaces/L1/IOptimismPortal2.sol index 76b0111f171e2..a4e05d87cb81f 100644 --- a/packages/contracts-bedrock/interfaces/L1/IOptimismPortal2.sol +++ b/packages/contracts-bedrock/interfaces/L1/IOptimismPortal2.sol @@ -14,6 +14,7 @@ interface IOptimismPortal2 { error EmptyItem(); error InvalidDataRemainder(); error InvalidHeader(); + error ReinitializableBase_ZeroInitVersion(); error OptimismPortal_AlreadyFinalized(); error OptimismPortal_BadTarget(); error OptimismPortal_CallPaused(); @@ -29,6 +30,12 @@ interface IOptimismPortal2 { error OptimismPortal_NoReentrancy(); error OptimismPortal_ProofNotOldEnough(); error OptimismPortal_Unproven(); + error OptimismPortal_InvalidOutputRootIndex(); + error OptimismPortal_InvalidSuperRootProof(); + error OptimismPortal_InvalidOutputRootChainId(); + error OptimismPortal_WrongProofMethod(); + error Encoding_EmptySuperRoot(); + error Encoding_InvalidSuperRootVersion(); error OutOfGas(); error UnexpectedList(); error UnexpectedString(); @@ -66,9 +73,11 @@ interface IOptimismPortal2 { function initialize( ISystemConfig _systemConfig, ISuperchainConfig _superchainConfig, - IAnchorStateRegistry _anchorStateRegistry + IAnchorStateRegistry _anchorStateRegistry, + bool _superRootsActive ) external; + function initVersion() external view returns (uint8); function l2Sender() external view returns (address); function minimumGasLimit(uint64 _byteCount) external pure returns (uint64); function numProofSubmitters(bytes32 _withdrawalHash) external view returns (uint256); @@ -83,6 +92,15 @@ interface IOptimismPortal2 { bytes[] memory _withdrawalProof ) external; + function proveWithdrawalTransaction( + Types.WithdrawalTransaction memory _tx, + IDisputeGame _disputeGameProxy, + uint256 _outputRootIndex, + Types.SuperRootProof memory _superRootProof, + Types.OutputRootProof memory _outputRootProof, + bytes[] memory _withdrawalProof + ) + external; function provenWithdrawals( bytes32, address @@ -93,8 +111,9 @@ interface IOptimismPortal2 { function respectedGameType() external view returns (GameType); function respectedGameTypeUpdatedAt() external view returns (uint64); function superchainConfig() external view returns (ISuperchainConfig); + function superRootsActive() external view returns (bool); function systemConfig() external view returns (ISystemConfig); - function upgrade(IAnchorStateRegistry _anchorStateRegistry) external; + function upgrade(IAnchorStateRegistry _anchorStateRegistry, bool _superRootsActive) external; function version() external pure returns (string memory); function __constructor__(uint256 _proofMaturityDelaySeconds) external; diff --git a/packages/contracts-bedrock/interfaces/L1/IOptimismPortalInterop.sol b/packages/contracts-bedrock/interfaces/L1/IOptimismPortalInterop.sol index 716b42f41985b..ff4df3b07357c 100644 --- a/packages/contracts-bedrock/interfaces/L1/IOptimismPortalInterop.sol +++ b/packages/contracts-bedrock/interfaces/L1/IOptimismPortalInterop.sol @@ -15,6 +15,7 @@ interface IOptimismPortalInterop { error EmptyItem(); error InvalidDataRemainder(); error InvalidHeader(); + error ReinitializableBase_ZeroInitVersion(); error OptimismPortal_AlreadyFinalized(); error OptimismPortal_BadTarget(); error OptimismPortal_CallPaused(); @@ -31,6 +32,12 @@ interface IOptimismPortalInterop { error OptimismPortal_ProofNotOldEnough(); error OptimismPortal_Unauthorized(); error OptimismPortal_Unproven(); + error OptimismPortal_InvalidOutputRootIndex(); + error OptimismPortal_InvalidSuperRootProof(); + error OptimismPortal_InvalidOutputRootChainId(); + error OptimismPortal_WrongProofMethod(); + error Encoding_EmptySuperRoot(); + error Encoding_InvalidSuperRootVersion(); error OutOfGas(); error UnexpectedList(); error UnexpectedString(); @@ -68,9 +75,11 @@ interface IOptimismPortalInterop { function initialize( ISystemConfig _systemConfig, ISuperchainConfig _superchainConfig, - IAnchorStateRegistry _anchorStateRegistry + IAnchorStateRegistry _anchorStateRegistry, + bool _superRootsActive ) external; + function initVersion() external view returns (uint8); function l2Sender() external view returns (address); function minimumGasLimit(uint64 _byteCount) external pure returns (uint64); function numProofSubmitters(bytes32 _withdrawalHash) external view returns (uint256); @@ -85,6 +94,15 @@ interface IOptimismPortalInterop { bytes[] memory _withdrawalProof ) external; + function proveWithdrawalTransaction( + Types.WithdrawalTransaction memory _tx, + IDisputeGame _disputeGameProxy, + uint256 _outputRootIndex, + Types.SuperRootProof memory _superRootProof, + Types.OutputRootProof memory _outputRootProof, + bytes[] memory _withdrawalProof + ) + external; function provenWithdrawals( bytes32, address @@ -96,8 +114,9 @@ interface IOptimismPortalInterop { function respectedGameTypeUpdatedAt() external view returns (uint64); function setConfig(ConfigType _type, bytes memory _value) external; function superchainConfig() external view returns (ISuperchainConfig); + function superRootsActive() external view returns (bool); function systemConfig() external view returns (ISystemConfig); - function upgrade(IAnchorStateRegistry _anchorStateRegistry) external; + function upgrade(IAnchorStateRegistry _anchorStateRegistry, bool _superRootsActive) external; function version() external pure returns (string memory); function __constructor__(uint256 _proofMaturityDelaySeconds) external; diff --git a/packages/contracts-bedrock/interfaces/L1/ISystemConfig.sol b/packages/contracts-bedrock/interfaces/L1/ISystemConfig.sol index e687a17e048bb..a080df0606702 100644 --- a/packages/contracts-bedrock/interfaces/L1/ISystemConfig.sol +++ b/packages/contracts-bedrock/interfaces/L1/ISystemConfig.sol @@ -21,6 +21,8 @@ interface ISystemConfig { address optimismMintableERC20Factory; } + error ReinitializableBase_ZeroInitVersion(); + event ConfigUpdate(uint256 indexed version, UpdateType indexed updateType, bytes data); event Initialized(uint8 version); event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); @@ -53,12 +55,15 @@ interface ISystemConfig { address _unsafeBlockSigner, IResourceMetering.ResourceConfig memory _config, address _batchInbox, - Addresses memory _addresses + Addresses memory _addresses, + uint256 _l2ChainId ) external; + function initVersion() external view returns (uint8); function l1CrossDomainMessenger() external view returns (address addr_); function l1ERC721Bridge() external view returns (address addr_); function l1StandardBridge() external view returns (address addr_); + function l2ChainId() external view returns (uint256); function maximumGasLimit() external pure returns (uint64); function minimumGasLimit() external view returns (uint64); function optimismMintableERC20Factory() external view returns (address addr_); @@ -77,6 +82,7 @@ interface ISystemConfig { function startBlock() external view returns (uint256 startBlock_); function transferOwnership(address newOwner) external; // nosemgrep function unsafeBlockSigner() external view returns (address addr_); + function upgrade(uint256 _l2ChainId) external; function version() external pure returns (string memory); function __constructor__() external; diff --git a/packages/contracts-bedrock/interfaces/L1/ISystemConfigInterop.sol b/packages/contracts-bedrock/interfaces/L1/ISystemConfigInterop.sol index c9b63db2b1df0..fcc1a062afbd3 100644 --- a/packages/contracts-bedrock/interfaces/L1/ISystemConfigInterop.sol +++ b/packages/contracts-bedrock/interfaces/L1/ISystemConfigInterop.sol @@ -5,6 +5,8 @@ import { ISystemConfig } from "interfaces/L1/ISystemConfig.sol"; import { IResourceMetering } from "interfaces/L1/IResourceMetering.sol"; interface ISystemConfigInterop { + error ReinitializableBase_ZeroInitVersion(); + event ConfigUpdate(uint256 indexed version, ISystemConfig.UpdateType indexed updateType, bytes data); event Initialized(uint8 version); event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); @@ -30,6 +32,7 @@ interface ISystemConfigInterop { function l1CrossDomainMessenger() external view returns (address addr_); function l1ERC721Bridge() external view returns (address addr_); function l1StandardBridge() external view returns (address addr_); + function l2ChainId() external view returns (uint256); function maximumGasLimit() external pure returns (uint64); function minimumGasLimit() external view returns (uint64); function optimismMintableERC20Factory() external view returns (address addr_); @@ -62,9 +65,11 @@ interface ISystemConfigInterop { IResourceMetering.ResourceConfig memory _config, address _batchInbox, ISystemConfig.Addresses memory _addresses, - address _dependencyManager + address _dependencyManager, + uint256 _l2ChainId ) external; + function initVersion() external view returns (uint8); function version() external pure returns (string memory); function __constructor__() external; diff --git a/packages/contracts-bedrock/interfaces/universal/IReinitializableBase.sol b/packages/contracts-bedrock/interfaces/universal/IReinitializableBase.sol new file mode 100644 index 0000000000000..d6d096a743bbe --- /dev/null +++ b/packages/contracts-bedrock/interfaces/universal/IReinitializableBase.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +interface IReinitializableBase { + error ReinitializableBase_ZeroInitVersion(); + + function initVersion() external view returns (uint8); + + // ReinitializerBase is abstract, so it has no constructor in its interface. + function __constructor__() external; +} diff --git a/packages/contracts-bedrock/justfile b/packages/contracts-bedrock/justfile index 9069958da79f5..9529bfb97f2c1 100644 --- a/packages/contracts-bedrock/justfile +++ b/packages/contracts-bedrock/justfile @@ -292,6 +292,7 @@ check: semver-diff-check-no-build \ validate-deploy-configs \ validate-spacers-no-build \ + reinitializer-check-no-build \ interfaces-check-no-build \ lint-forge-tests-check-no-build diff --git a/packages/contracts-bedrock/scripts/checks/reinitializer/main.go b/packages/contracts-bedrock/scripts/checks/reinitializer/main.go index c41e403b98562..e44c955d038bd 100644 --- a/packages/contracts-bedrock/scripts/checks/reinitializer/main.go +++ b/packages/contracts-bedrock/scripts/checks/reinitializer/main.go @@ -2,6 +2,7 @@ package main import ( "fmt" + "math" "os" "strconv" "strings" @@ -103,15 +104,23 @@ func getReinitializerValue(node *solc.AstNode) (uint64, error) { for _, modifier := range node.Modifiers { if modifier.ModifierName.Name == "reinitializer" { - valStr, ok := modifier.Arguments[0].Value.(string) - if !ok { - return 0, fmt.Errorf("reinitializer value is not a string") + if modifier.Arguments[0].Kind == "functionCall" { + if modifier.Arguments[0].Expression.Name == "initVersion" { + return math.MaxUint64, nil // uint64 max representing initVersion call + } else { + return 0, fmt.Errorf("reinitializer value is not a call to initVersion") + } + } else { + valStr, ok := modifier.Arguments[0].Value.(string) + if !ok { + return 0, fmt.Errorf("reinitializer value is not a string") + } + val, err := strconv.Atoi(valStr) + if err != nil { + return 0, fmt.Errorf("reinitializer value is not an integer") + } + return uint64(val), nil } - val, err := strconv.Atoi(valStr) - if err != nil { - return 0, fmt.Errorf("reinitializer value is not an integer") - } - return uint64(val), nil } } diff --git a/packages/contracts-bedrock/scripts/checks/reinitializer/main_test.go b/packages/contracts-bedrock/scripts/checks/reinitializer/main_test.go index 93ca35173b63c..0f9fc382e2def 100644 --- a/packages/contracts-bedrock/scripts/checks/reinitializer/main_test.go +++ b/packages/contracts-bedrock/scripts/checks/reinitializer/main_test.go @@ -1,6 +1,7 @@ package main import ( + "math" "testing" "github.com/ethereum-optimism/optimism/op-chain-ops/solc" @@ -155,6 +156,38 @@ func TestGetReinitializerValue(t *testing.T) { want: 0, wantErr: true, }, + { + name: "Valid reinitializer with initVersion call", + node: &solc.AstNode{ + Modifiers: []solc.AstNode{ + { + ModifierName: &solc.Expression{Name: "reinitializer"}, + Arguments: []solc.Expression{{ + Kind: "functionCall", + Expression: &solc.Expression{Name: "initVersion"}, + }}, + }, + }, + }, + want: math.MaxUint64, + wantErr: false, + }, + { + name: "Invalid function call - not initVersion", + node: &solc.AstNode{ + Modifiers: []solc.AstNode{ + { + ModifierName: &solc.Expression{Name: "reinitializer"}, + Arguments: []solc.Expression{{ + Kind: "functionCall", + Expression: &solc.Expression{Name: "someOtherFunction"}, + }}, + }, + }, + }, + want: 0, + wantErr: true, + }, } for _, tt := range tests { @@ -484,6 +517,85 @@ func TestCheckArtifact(t *testing.T) { }, wantErr: false, // Should return nil without error }, + { + name: "Matching reinitializer values with initVersion", + artifact: &solc.ForgeArtifact{ + Ast: solc.Ast{ + Nodes: []solc.AstNode{ + { + NodeType: "ContractDefinition", + Nodes: []solc.AstNode{ + { + NodeType: "FunctionDefinition", + Name: "initialize", + Modifiers: []solc.AstNode{ + { + ModifierName: &solc.Expression{Name: "reinitializer"}, + Arguments: []solc.Expression{{ + Kind: "functionCall", + Expression: &solc.Expression{Name: "initVersion"}, + }}, + }, + }, + }, + { + NodeType: "FunctionDefinition", + Name: "upgrade", + Modifiers: []solc.AstNode{ + { + ModifierName: &solc.Expression{Name: "reinitializer"}, + Arguments: []solc.Expression{{ + Kind: "functionCall", + Expression: &solc.Expression{Name: "initVersion"}, + }}, + }, + }, + }, + }, + }, + }, + }, + }, + wantErr: false, + }, + { + name: "Mismatched reinitializer values - one with initVersion, one with constant", + artifact: &solc.ForgeArtifact{ + Ast: solc.Ast{ + Nodes: []solc.AstNode{ + { + NodeType: "ContractDefinition", + Nodes: []solc.AstNode{ + { + NodeType: "FunctionDefinition", + Name: "initialize", + Modifiers: []solc.AstNode{ + { + ModifierName: &solc.Expression{Name: "reinitializer"}, + Arguments: []solc.Expression{{ + Kind: "functionCall", + Expression: &solc.Expression{Name: "initVersion"}, + }}, + }, + }, + }, + { + NodeType: "FunctionDefinition", + Name: "upgrade", + Modifiers: []solc.AstNode{ + { + ModifierName: &solc.Expression{Name: "reinitializer"}, + Arguments: []solc.Expression{{Value: "2"}}, + }, + }, + }, + }, + }, + }, + }, + }, + wantErr: true, + }, } for _, tt := range tests { diff --git a/packages/contracts-bedrock/scripts/deploy/Deploy.s.sol b/packages/contracts-bedrock/scripts/deploy/Deploy.s.sol index 1d556b0f6db83..a11ec0b26b56a 100644 --- a/packages/contracts-bedrock/scripts/deploy/Deploy.s.sol +++ b/packages/contracts-bedrock/scripts/deploy/Deploy.s.sol @@ -523,7 +523,8 @@ contract Deploy is Deployer { disputeGameFactory: artifacts.mustGetAddress("DisputeGameFactoryProxy"), optimismPortal: artifacts.mustGetAddress("OptimismPortalProxy"), optimismMintableERC20Factory: artifacts.mustGetAddress("OptimismMintableERC20FactoryProxy") - }) + }), + cfg.l2ChainID() ) ) }); @@ -964,6 +965,7 @@ contract Deploy is Deployer { ), saltMixer: saltMixer, gasLimit: uint64(cfg.l2GenesisBlockGasLimit()), + disputeGameUsesSuperRoots: false, disputeGameType: GameType.wrap(4), disputeAbsolutePrestate: Claim.wrap(bytes32(cfg.faultGameAbsolutePrestate())), disputeMaxGameDepth: cfg.faultGameMaxDepth(), diff --git a/packages/contracts-bedrock/scripts/deploy/DeployOPChain.s.sol b/packages/contracts-bedrock/scripts/deploy/DeployOPChain.s.sol index ba384992d44db..8641b7c6bf07a 100644 --- a/packages/contracts-bedrock/scripts/deploy/DeployOPChain.s.sol +++ b/packages/contracts-bedrock/scripts/deploy/DeployOPChain.s.sol @@ -51,6 +51,7 @@ contract DeployOPChainInput is BaseDeployIO { uint64 internal _gasLimit; // Configurable dispute game inputs + bool internal _disputeGameUsesSuperRoots; GameType internal _disputeGameType; Claim internal _disputeAbsolutePrestate; uint256 internal _disputeMaxGameDepth; @@ -91,6 +92,9 @@ contract DeployOPChainInput is BaseDeployIO { _disputeClockExtension = Duration.wrap(SafeCast.toUint64(_value)); } else if (_sel == this.disputeMaxClockDuration.selector) { _disputeMaxClockDuration = Duration.wrap(SafeCast.toUint64(_value)); + } else if (_sel == this.disputeGameUsesSuperRoots.selector) { + require(_value == 0 || _value == 1, "DeployOPChainInput: invalid disputeGameUsesSuperRoots"); + _disputeGameUsesSuperRoots = _value == 1; } else { revert("DeployOPChainInput: unknown selector"); } @@ -187,6 +191,10 @@ contract DeployOPChainInput is BaseDeployIO { return _gasLimit; } + function disputeGameUsesSuperRoots() public view returns (bool) { + return _disputeGameUsesSuperRoots; + } + function disputeGameType() public view returns (GameType) { return _disputeGameType; } @@ -356,6 +364,7 @@ contract DeployOPChain is Script { startingAnchorRoot: _doi.startingAnchorRoot(), saltMixer: _doi.saltMixer(), gasLimit: _doi.gasLimit(), + disputeGameUsesSuperRoots: _doi.disputeGameUsesSuperRoots(), disputeGameType: _doi.disputeGameType(), disputeAbsolutePrestate: _doi.disputeAbsolutePrestate(), disputeMaxGameDepth: _doi.disputeMaxGameDepth(), diff --git a/packages/contracts-bedrock/scripts/go-ffi/differential-testing.go b/packages/contracts-bedrock/scripts/go-ffi/differential-testing.go index e35d2a82c3036..7c328d23f2686 100644 --- a/packages/contracts-bedrock/scripts/go-ffi/differential-testing.go +++ b/packages/contracts-bedrock/scripts/go-ffi/differential-testing.go @@ -38,14 +38,15 @@ var ( {Type: fixedBytes}, } - uint32Type, _ = abi.NewType("uint32", "", nil) - // Plain address type addressType, _ = abi.NewType("address", "", nil) // Plain uint8 type uint8Type, _ = abi.NewType("uint8", "", nil) + // Plain uint32 type + uint32Type, _ = abi.NewType("uint32", "", nil) + // Plain uint256 type uint256Type, _ = abi.NewType("uint256", "", nil) @@ -102,6 +103,19 @@ var ( {Name: "symbol", Type: fixedBytes}, } + // Super root proof tuple (uint8, uint64, OutputRootWithChainId[]) + superRootProof, _ = abi.NewType("tuple", "SuperRootProof", []abi.ArgumentMarshaling{ + {Name: "version", Type: "bytes1"}, + {Name: "timestamp", Type: "uint64"}, + {Name: "outputRoots", Type: "tuple[]", Components: []abi.ArgumentMarshaling{ + {Name: "chainId", Type: "uint256"}, + {Name: "root", Type: "bytes32"}, + }}, + }) + superRootProofArgs = abi.Arguments{ + {Type: superRootProof}, + } + // Dependency tuple (uint256) dependencyArgs = abi.Arguments{{Name: "chainId", Type: uint256Type}} ) @@ -520,6 +534,49 @@ func DiffTestUtils() { packed, err := bytesArgs.Pack(&encoded) checkErr(err, "Error encoding output") + fmt.Print(hexutil.Encode(packed)) + case "encodeSuperRootProof": + // Parse input argument as abi encoded super root proof + if len(args) < 2 { + panic("Error: encodeSuperRoot requires at least 1 argument") + } + + // Parse the input as hex data + superRootProofData := common.FromHex(args[1]) + proof, err := parseSuperRootProof(superRootProofData) + checkErr(err, "Error parsing super root proof") + + // Encode super root proof + encoded, err := encodeSuperRootProof(proof) + checkErr(err, "Error encoding super root") + + // Pack encoded super root + packed, err := bytesArgs.Pack(&encoded) + checkErr(err, "Error encoding output") + + fmt.Print(hexutil.Encode(packed)) + case "hashSuperRootProof": + // Parse input argument as abi encoded super root proof + if len(args) < 2 { + panic("Error: hashSuperRootProof requires at least 1 argument") + } + + // Parse the input as hex data + superRootProofData := common.FromHex(args[1]) + proof, err := parseSuperRootProof(superRootProofData) + checkErr(err, "Error parsing super root proof") + + // Encode super root proof + encoded, err := encodeSuperRootProof(proof) + checkErr(err, "Error encoding super root proof") + + // Hash super root proof + hash := crypto.Keccak256Hash(encoded) + + // Pack hash + packed, err := fixedBytesArgs.Pack(&hash) + checkErr(err, "Error encoding output") + fmt.Print(hexutil.Encode(packed)) default: panic(fmt.Errorf("Unknown command: %s", args[0])) diff --git a/packages/contracts-bedrock/scripts/go-ffi/utils.go b/packages/contracts-bedrock/scripts/go-ffi/utils.go index 0c535c03feef1..157dd4d85c547 100644 --- a/packages/contracts-bedrock/scripts/go-ffi/utils.go +++ b/packages/contracts-bedrock/scripts/go-ffi/utils.go @@ -1,6 +1,7 @@ package main import ( + "encoding/binary" "errors" "fmt" "math/big" @@ -13,6 +14,18 @@ import ( "github.com/ethereum/go-ethereum/core/types" ) +type OutputRootWithChainId struct { + ChainId *big.Int + Root common.Hash +} + +// Define a proper type for SuperRootProof +type SuperRootProof struct { + Version uint8 + Timestamp uint64 + OutputRoots []OutputRootWithChainId +} + var UnknownNonceVersion = errors.New("Unknown nonce version") // checkOk checks if ok is false, and panics if so. @@ -50,6 +63,78 @@ func encodeCrossDomainMessage(nonce *big.Int, sender common.Address, target comm return encoded, err } +// parseSuperRootProof parses an abi encoded super root proof into a SuperRootProof struct. +func parseSuperRootProof(abiEncodedProof []byte) (*SuperRootProof, error) { + // Parse the input as hex data + unpacked, err := superRootProofArgs.Unpack(abiEncodedProof) + if err != nil { + return nil, err + } + + // The Unpack method returns a slice of interface{}, so we need to get the first element + if len(unpacked) != 1 { + return nil, errors.New("unexpected number of values after unpacking super root proof") + } + + // Use an anonymous struct matching the tuple’s layout. + tmp := unpacked[0].(struct { + Version [1]uint8 `json:"version"` + Timestamp uint64 `json:"timestamp"` + OutputRoots []struct { + ChainId *big.Int `json:"chainId"` + Root [32]byte `json:"root"` + } `json:"outputRoots"` + }) + + // Convert into our desired SuperRootProof type. + proof := SuperRootProof{ + Version: tmp.Version[0], + Timestamp: tmp.Timestamp, + } + for _, o := range tmp.OutputRoots { + proof.OutputRoots = append(proof.OutputRoots, OutputRootWithChainId{ + ChainId: o.ChainId, + Root: common.BytesToHash(o.Root[:]), + }) + } + + return &proof, nil +} + +// encodeSuperRootProof encodes a super root proof into a byte array. +func encodeSuperRootProof(superRootProof *SuperRootProof) ([]byte, error) { + // Version must match the expected version (0x01) + if superRootProof.Version != 0x01 { + return nil, errors.New("invalid super root version") + } + + // Output roots must not be empty + if len(superRootProof.OutputRoots) == 0 { + return nil, errors.New("empty super root") + } + + // Start with version byte and timestamp + encoded := []byte{superRootProof.Version} + + // Add timestamp as bytes8 (uint64) + timestampBytes := make([]byte, 8) + binary.BigEndian.PutUint64(timestampBytes, superRootProof.Timestamp) + encoded = append(encoded, timestampBytes...) + + // Add each output root (chainId + root) + for _, outputRoot := range superRootProof.OutputRoots { + // Append chainId bytes (padded to 32 bytes) + chainIdBytes := make([]byte, 32) + outputRoot.ChainId.FillBytes(chainIdBytes) + encoded = append(encoded, chainIdBytes...) + + // Append root hash (already 32 bytes) + encoded = append(encoded, outputRoot.Root.Bytes()...) + } + + return encoded, nil +} + // hashWithdrawal hashes a withdrawal transaction. func hashWithdrawal(nonce *big.Int, sender common.Address, target common.Address, value *big.Int, gasLimit *big.Int, data []byte) (common.Hash, error) { wd := crossdomain.Withdrawal{ diff --git a/packages/contracts-bedrock/scripts/libraries/DeployUtils.sol b/packages/contracts-bedrock/scripts/libraries/DeployUtils.sol index 1cec8d04778b1..b4c5bd7b141ed 100644 --- a/packages/contracts-bedrock/scripts/libraries/DeployUtils.sol +++ b/packages/contracts-bedrock/scripts/libraries/DeployUtils.sol @@ -17,6 +17,7 @@ import { IProxy } from "interfaces/universal/IProxy.sol"; import { IAddressManager } from "interfaces/legacy/IAddressManager.sol"; import { IL1ChugSplashProxy, IStaticL1ChugSplashProxy } from "interfaces/legacy/IL1ChugSplashProxy.sol"; import { IResolvedDelegateProxy } from "interfaces/legacy/IResolvedDelegateProxy.sol"; +import { IReinitializableBase } from "interfaces/universal/IReinitializableBase.sol"; library DeployUtils { Vm internal constant vm = Vm(address(uint160(uint256(keccak256("hevm cheat code"))))); @@ -360,7 +361,19 @@ library DeployUtils { bytes32 slotVal = vm.load(_contractAddress, bytes32(_slot)); uint8 val = uint8((uint256(slotVal) >> (_offset * 8)) & 0xFF); if (_isProxy) { - require(val == 1, "DeployUtils: storage value is not 1 at the given slot and offset"); + // Using a try/catch here to check if the contract has an initVersion() defined. + // EIP-150 safe because we require that we have at least 200k gas before the call which + // is more than enough to avoid running out of gas when 63/64 of the gas is provided to + // the initVersion() call (which simply reads an immutable variable). Since this is + // only ever triggered as part of a script, we can safely assume we'll have the gas. + require(gasleft() > 200_000, "DeployUtils: insufficient gas for initVersion() call"); + + // eip150-safe + try IReinitializableBase(_contractAddress).initVersion() returns (uint8 initVersion_) { + require(val == initVersion_, "DeployUtils: storage value is incorrect at the given slot and offset"); + } catch { + require(val == 1, "DeployUtils: storage value is not set at the given slot and offset"); + } } else { require(val == type(uint8).max, "DeployUtils: storage value is not 0xff at the given slot and offset"); } diff --git a/packages/contracts-bedrock/snapshots/abi/OPContractsManager.json b/packages/contracts-bedrock/snapshots/abi/OPContractsManager.json index 22c1d40b77cd2..4586d370d7f8c 100644 --- a/packages/contracts-bedrock/snapshots/abi/OPContractsManager.json +++ b/packages/contracts-bedrock/snapshots/abi/OPContractsManager.json @@ -397,6 +397,11 @@ "name": "gasLimit", "type": "uint64" }, + { + "internalType": "bool", + "name": "disputeGameUsesSuperRoots", + "type": "bool" + }, { "internalType": "GameType", "name": "disputeGameType", @@ -687,6 +692,11 @@ "internalType": "Claim", "name": "absolutePrestate", "type": "bytes32" + }, + { + "internalType": "bool", + "name": "disputeGameUsesSuperRoots", + "type": "bool" } ], "internalType": "struct OPContractsManager.OpChainConfig[]", @@ -864,14 +874,33 @@ "type": "error" }, { - "inputs": [ - { - "internalType": "string", - "name": "role", - "type": "string" - } - ], - "name": "InvalidRoleAddress", + "inputs": [], + "name": "InvalidRoleAddressBatcher", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidRoleAddressChallenger", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidRoleAddressPAO", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidRoleAddressProposer", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidRoleAddressSCO", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidRoleAddressUBS", "type": "error" }, { diff --git a/packages/contracts-bedrock/snapshots/abi/OPContractsManagerInterop.json b/packages/contracts-bedrock/snapshots/abi/OPContractsManagerInterop.json index 22c1d40b77cd2..4586d370d7f8c 100644 --- a/packages/contracts-bedrock/snapshots/abi/OPContractsManagerInterop.json +++ b/packages/contracts-bedrock/snapshots/abi/OPContractsManagerInterop.json @@ -397,6 +397,11 @@ "name": "gasLimit", "type": "uint64" }, + { + "internalType": "bool", + "name": "disputeGameUsesSuperRoots", + "type": "bool" + }, { "internalType": "GameType", "name": "disputeGameType", @@ -687,6 +692,11 @@ "internalType": "Claim", "name": "absolutePrestate", "type": "bytes32" + }, + { + "internalType": "bool", + "name": "disputeGameUsesSuperRoots", + "type": "bool" } ], "internalType": "struct OPContractsManager.OpChainConfig[]", @@ -864,14 +874,33 @@ "type": "error" }, { - "inputs": [ - { - "internalType": "string", - "name": "role", - "type": "string" - } - ], - "name": "InvalidRoleAddress", + "inputs": [], + "name": "InvalidRoleAddressBatcher", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidRoleAddressChallenger", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidRoleAddressPAO", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidRoleAddressProposer", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidRoleAddressSCO", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidRoleAddressUBS", "type": "error" }, { diff --git a/packages/contracts-bedrock/snapshots/abi/OPPrestateUpdater.json b/packages/contracts-bedrock/snapshots/abi/OPPrestateUpdater.json index d44b099737a6f..5ca40a1e48135 100644 --- a/packages/contracts-bedrock/snapshots/abi/OPPrestateUpdater.json +++ b/packages/contracts-bedrock/snapshots/abi/OPPrestateUpdater.json @@ -315,6 +315,11 @@ "name": "gasLimit", "type": "uint64" }, + { + "internalType": "bool", + "name": "disputeGameUsesSuperRoots", + "type": "bool" + }, { "internalType": "GameType", "name": "disputeGameType", @@ -605,6 +610,11 @@ "internalType": "Claim", "name": "absolutePrestate", "type": "bytes32" + }, + { + "internalType": "bool", + "name": "disputeGameUsesSuperRoots", + "type": "bool" } ], "internalType": "struct OPContractsManager.OpChainConfig[]", @@ -635,6 +645,11 @@ "internalType": "Claim", "name": "absolutePrestate", "type": "bytes32" + }, + { + "internalType": "bool", + "name": "disputeGameUsesSuperRoots", + "type": "bool" } ], "internalType": "struct OPContractsManager.OpChainConfig[]", @@ -812,14 +827,33 @@ "type": "error" }, { - "inputs": [ - { - "internalType": "string", - "name": "role", - "type": "string" - } - ], - "name": "InvalidRoleAddress", + "inputs": [], + "name": "InvalidRoleAddressBatcher", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidRoleAddressChallenger", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidRoleAddressPAO", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidRoleAddressProposer", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidRoleAddressSCO", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidRoleAddressUBS", "type": "error" }, { diff --git a/packages/contracts-bedrock/snapshots/abi/OptimismPortal2.json b/packages/contracts-bedrock/snapshots/abi/OptimismPortal2.json index ebc1b9132de40..e3d34b6617568 100644 --- a/packages/contracts-bedrock/snapshots/abi/OptimismPortal2.json +++ b/packages/contracts-bedrock/snapshots/abi/OptimismPortal2.json @@ -238,6 +238,19 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [], + "name": "initVersion", + "outputs": [ + { + "internalType": "uint8", + "name": "", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [ { @@ -254,6 +267,11 @@ "internalType": "contract IAnchorStateRegistry", "name": "_anchorStateRegistry", "type": "address" + }, + { + "internalType": "bool", + "name": "_superRootsActive", + "type": "bool" } ], "name": "initialize", @@ -467,6 +485,127 @@ "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [ + { + "components": [ + { + "internalType": "uint256", + "name": "nonce", + "type": "uint256" + }, + { + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "internalType": "address", + "name": "target", + "type": "address" + }, + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "gasLimit", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "internalType": "struct Types.WithdrawalTransaction", + "name": "_tx", + "type": "tuple" + }, + { + "internalType": "contract IDisputeGame", + "name": "_disputeGameProxy", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_outputRootIndex", + "type": "uint256" + }, + { + "components": [ + { + "internalType": "bytes1", + "name": "version", + "type": "bytes1" + }, + { + "internalType": "uint64", + "name": "timestamp", + "type": "uint64" + }, + { + "components": [ + { + "internalType": "uint256", + "name": "chainId", + "type": "uint256" + }, + { + "internalType": "bytes32", + "name": "root", + "type": "bytes32" + } + ], + "internalType": "struct Types.OutputRootWithChainId[]", + "name": "outputRoots", + "type": "tuple[]" + } + ], + "internalType": "struct Types.SuperRootProof", + "name": "_superRootProof", + "type": "tuple" + }, + { + "components": [ + { + "internalType": "bytes32", + "name": "version", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "stateRoot", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "messagePasserStorageRoot", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "latestBlockhash", + "type": "bytes32" + } + ], + "internalType": "struct Types.OutputRootProof", + "name": "_outputRootProof", + "type": "tuple" + }, + { + "internalType": "bytes[]", + "name": "_withdrawalProof", + "type": "bytes[]" + } + ], + "name": "proveWithdrawalTransaction", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [ { @@ -522,6 +661,19 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [], + "name": "superRootsActive", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [], "name": "superchainConfig", @@ -554,6 +706,11 @@ "internalType": "contract IAnchorStateRegistry", "name": "_anchorStateRegistry", "type": "address" + }, + { + "internalType": "bool", + "name": "_superRootsActive", + "type": "bool" } ], "name": "upgrade", @@ -691,6 +848,16 @@ "name": "EmptyItem", "type": "error" }, + { + "inputs": [], + "name": "Encoding_EmptySuperRoot", + "type": "error" + }, + { + "inputs": [], + "name": "Encoding_InvalidSuperRootVersion", + "type": "error" + }, { "inputs": [], "name": "InvalidDataRemainder", @@ -746,6 +913,16 @@ "name": "OptimismPortal_InvalidMerkleProof", "type": "error" }, + { + "inputs": [], + "name": "OptimismPortal_InvalidOutputRootChainId", + "type": "error" + }, + { + "inputs": [], + "name": "OptimismPortal_InvalidOutputRootIndex", + "type": "error" + }, { "inputs": [], "name": "OptimismPortal_InvalidOutputRootProof", @@ -761,6 +938,11 @@ "name": "OptimismPortal_InvalidRootClaim", "type": "error" }, + { + "inputs": [], + "name": "OptimismPortal_InvalidSuperRootProof", + "type": "error" + }, { "inputs": [], "name": "OptimismPortal_NoReentrancy", @@ -776,11 +958,21 @@ "name": "OptimismPortal_Unproven", "type": "error" }, + { + "inputs": [], + "name": "OptimismPortal_WrongProofMethod", + "type": "error" + }, { "inputs": [], "name": "OutOfGas", "type": "error" }, + { + "inputs": [], + "name": "ReinitializableBase_ZeroInitVersion", + "type": "error" + }, { "inputs": [], "name": "UnexpectedList", diff --git a/packages/contracts-bedrock/snapshots/abi/OptimismPortalInterop.json b/packages/contracts-bedrock/snapshots/abi/OptimismPortalInterop.json index 4a6c342105f2c..f5c13f585fb90 100644 --- a/packages/contracts-bedrock/snapshots/abi/OptimismPortalInterop.json +++ b/packages/contracts-bedrock/snapshots/abi/OptimismPortalInterop.json @@ -238,6 +238,19 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [], + "name": "initVersion", + "outputs": [ + { + "internalType": "uint8", + "name": "", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [ { @@ -254,6 +267,11 @@ "internalType": "contract IAnchorStateRegistry", "name": "_anchorStateRegistry", "type": "address" + }, + { + "internalType": "bool", + "name": "_superRootsActive", + "type": "bool" } ], "name": "initialize", @@ -467,6 +485,127 @@ "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [ + { + "components": [ + { + "internalType": "uint256", + "name": "nonce", + "type": "uint256" + }, + { + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "internalType": "address", + "name": "target", + "type": "address" + }, + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "gasLimit", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "internalType": "struct Types.WithdrawalTransaction", + "name": "_tx", + "type": "tuple" + }, + { + "internalType": "contract IDisputeGame", + "name": "_disputeGameProxy", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_outputRootIndex", + "type": "uint256" + }, + { + "components": [ + { + "internalType": "bytes1", + "name": "version", + "type": "bytes1" + }, + { + "internalType": "uint64", + "name": "timestamp", + "type": "uint64" + }, + { + "components": [ + { + "internalType": "uint256", + "name": "chainId", + "type": "uint256" + }, + { + "internalType": "bytes32", + "name": "root", + "type": "bytes32" + } + ], + "internalType": "struct Types.OutputRootWithChainId[]", + "name": "outputRoots", + "type": "tuple[]" + } + ], + "internalType": "struct Types.SuperRootProof", + "name": "_superRootProof", + "type": "tuple" + }, + { + "components": [ + { + "internalType": "bytes32", + "name": "version", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "stateRoot", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "messagePasserStorageRoot", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "latestBlockhash", + "type": "bytes32" + } + ], + "internalType": "struct Types.OutputRootProof", + "name": "_outputRootProof", + "type": "tuple" + }, + { + "internalType": "bytes[]", + "name": "_withdrawalProof", + "type": "bytes[]" + } + ], + "name": "proveWithdrawalTransaction", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [ { @@ -540,6 +679,19 @@ "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [], + "name": "superRootsActive", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [], "name": "superchainConfig", @@ -572,6 +724,11 @@ "internalType": "contract IAnchorStateRegistry", "name": "_anchorStateRegistry", "type": "address" + }, + { + "internalType": "bool", + "name": "_superRootsActive", + "type": "bool" } ], "name": "upgrade", @@ -709,6 +866,16 @@ "name": "EmptyItem", "type": "error" }, + { + "inputs": [], + "name": "Encoding_EmptySuperRoot", + "type": "error" + }, + { + "inputs": [], + "name": "Encoding_InvalidSuperRootVersion", + "type": "error" + }, { "inputs": [], "name": "InvalidDataRemainder", @@ -764,6 +931,16 @@ "name": "OptimismPortal_InvalidMerkleProof", "type": "error" }, + { + "inputs": [], + "name": "OptimismPortal_InvalidOutputRootChainId", + "type": "error" + }, + { + "inputs": [], + "name": "OptimismPortal_InvalidOutputRootIndex", + "type": "error" + }, { "inputs": [], "name": "OptimismPortal_InvalidOutputRootProof", @@ -779,6 +956,11 @@ "name": "OptimismPortal_InvalidRootClaim", "type": "error" }, + { + "inputs": [], + "name": "OptimismPortal_InvalidSuperRootProof", + "type": "error" + }, { "inputs": [], "name": "OptimismPortal_NoReentrancy", @@ -799,11 +981,21 @@ "name": "OptimismPortal_Unproven", "type": "error" }, + { + "inputs": [], + "name": "OptimismPortal_WrongProofMethod", + "type": "error" + }, { "inputs": [], "name": "OutOfGas", "type": "error" }, + { + "inputs": [], + "name": "ReinitializableBase_ZeroInitVersion", + "type": "error" + }, { "inputs": [], "name": "UnexpectedList", diff --git a/packages/contracts-bedrock/snapshots/abi/SystemConfig.json b/packages/contracts-bedrock/snapshots/abi/SystemConfig.json index 7105f16bf4485..bef520fb4cbe5 100644 --- a/packages/contracts-bedrock/snapshots/abi/SystemConfig.json +++ b/packages/contracts-bedrock/snapshots/abi/SystemConfig.json @@ -283,6 +283,19 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [], + "name": "initVersion", + "outputs": [ + { + "internalType": "uint8", + "name": "", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [ { @@ -393,6 +406,11 @@ "internalType": "struct SystemConfig.Addresses", "name": "_addresses", "type": "tuple" + }, + { + "internalType": "uint256", + "name": "_l2ChainId", + "type": "uint256" } ], "name": "initialize", @@ -439,6 +457,19 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [], + "name": "l2ChainId", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [], "name": "maximumGasLimit", @@ -714,6 +745,19 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_l2ChainId", + "type": "uint256" + } + ], + "name": "upgrade", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [], "name": "version", @@ -783,5 +827,10 @@ ], "name": "OwnershipTransferred", "type": "event" + }, + { + "inputs": [], + "name": "ReinitializableBase_ZeroInitVersion", + "type": "error" } ] \ No newline at end of file diff --git a/packages/contracts-bedrock/snapshots/abi/SystemConfigInterop.json b/packages/contracts-bedrock/snapshots/abi/SystemConfigInterop.json index f87d9e9e54678..96a79915b5a5b 100644 --- a/packages/contracts-bedrock/snapshots/abi/SystemConfigInterop.json +++ b/packages/contracts-bedrock/snapshots/abi/SystemConfigInterop.json @@ -304,6 +304,19 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [], + "name": "initVersion", + "outputs": [ + { + "internalType": "uint8", + "name": "", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [ { @@ -414,6 +427,11 @@ "internalType": "struct SystemConfig.Addresses", "name": "_addresses", "type": "tuple" + }, + { + "internalType": "uint256", + "name": "_l2ChainId", + "type": "uint256" } ], "name": "initialize", @@ -536,6 +554,11 @@ "internalType": "address", "name": "_dependencyManager", "type": "address" + }, + { + "internalType": "uint256", + "name": "_l2ChainId", + "type": "uint256" } ], "name": "initialize", @@ -582,6 +605,19 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [], + "name": "l2ChainId", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [], "name": "maximumGasLimit", @@ -870,6 +906,19 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_l2ChainId", + "type": "uint256" + } + ], + "name": "upgrade", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [], "name": "version", @@ -939,5 +988,10 @@ ], "name": "OwnershipTransferred", "type": "event" + }, + { + "inputs": [], + "name": "ReinitializableBase_ZeroInitVersion", + "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 752e8131e6ca9..c3456bc8ef954 100644 --- a/packages/contracts-bedrock/snapshots/semver-lock.json +++ b/packages/contracts-bedrock/snapshots/semver-lock.json @@ -16,23 +16,23 @@ "sourceCodeHash": "0x7f614d0c5251365580c02dd898900a583ece2aa28579adea74a3808b575949b4" }, "src/L1/OPContractsManager.sol": { - "initCodeHash": "0x3da482f3107d23948d9535eac40f2d09861605570f0d7e1c2ca4766e1ae71943", - "sourceCodeHash": "0x8cbad8c307c0b56514ce52cba4c3519e2e70cbd19957cd50a65eac208a09cf7d" + "initCodeHash": "0xfc5622ed6411ec01e7bcfb3c9250b130b8ca25e74ee40e70e4017fefa34cddf7", + "sourceCodeHash": "0x1f53a1e174479aa244ad78fc171d4cbeb8529505a57af59f89435837e2803bcc" }, "src/L1/OPContractsManagerInterop.sol": { - "initCodeHash": "0xff7784d6851f3dc751e3c8ebe1d0046484c512c765802a16685c8f7d107e3c88", - "sourceCodeHash": "0xd04bb5babc02fd93618397678506e9bcce5f1881b008785da0767bda74d8447c" + "initCodeHash": "0x6855bfe2f22b4ce53c62fca0d32029cc39f0a2ab2ded43808bb6c493acadc781", + "sourceCodeHash": "0x79b3ae68d83f021d9118f8a1943c1d0773e7e01a19f580f6b90ad4c00370cff5" }, "src/L1/OPPrestateUpdater.sol": { - "initCodeHash": "0x2d55d1a4826bcb5c65e5b66300fe470ea02398fcdd0a436755024855b372360d", + "initCodeHash": "0x840b42954a8921e8216a93ace6d2a047e28ef8df8f426181b1b229d2d5818fc1", "sourceCodeHash": "0x985c33cc1933d68de38d6c13afc1ac42ef7e455cb06bb255c1fed22b06b97a62" }, "src/L1/OptimismPortal2.sol": { - "initCodeHash": "0xcc7b2a3f10786983f995f557306944eb27c77f7a502bcebfac111ded62a08344", - "sourceCodeHash": "0x016cf228b63fedd9fafbeed5b804786235bcc07ceb499f4919dfad40cdeda322" + "initCodeHash": "0xac11984d6c3e58fd02e33509540ee329c4f5caedb94e422f8ab3a7a95ce7a546", + "sourceCodeHash": "0x79652f4f6aebc11dd4ad201dcf9a436d94db76e2edef07b12486c3de9cd804d2" }, "src/L1/OptimismPortalInterop.sol": { - "initCodeHash": "0x561140ae220a1cd3f8fabd35e435a128fb9d9f2d83686b89494a90cee8d8707f", + "initCodeHash": "0x8d76300f7a79686e522d4cac0582f4f7ec77b798f881747d0403762fa56b3a71", "sourceCodeHash": "0x1b2ce1f2e3125bd449501b9d228b207d31f2bc39c7f23262c674e0d1f47c0d0b" }, "src/L1/ProtocolVersions.sol": { @@ -44,12 +44,12 @@ "sourceCodeHash": "0xfd56e63e76b1f203cceeb9bbb14396ae803cbbbf7e80ca0ee11fb586321812af" }, "src/L1/SystemConfig.sol": { - "initCodeHash": "0x98c1049952199f55ae63e34ec61a839d43bde52b0892c482ae4246d0c088e826", - "sourceCodeHash": "0x9016b1979c2f1def83a849389543708d857cf0430756815737dadda8e63047c5" + "initCodeHash": "0x93e732c31e59dc78d6414ad12fcc0cbe4537d3a69a2ca34ff8713b0b51679b19", + "sourceCodeHash": "0x538518cf61bda80bc3f0b94e4fe236b65e53fff397fd43958e695705c57e02d9" }, "src/L1/SystemConfigInterop.sol": { - "initCodeHash": "0x36fd6c64d81c83fd97e039ab77fb6dfd3a76f12faba923bf04491bb124b590e8", - "sourceCodeHash": "0x609a10f2f85a2b1cc60a5accd795f65c84edc09b0e98124011bd9e7caeb905d9" + "initCodeHash": "0xe3651f84f0fecf6c4b60801d8168da55bfe3889912548fcd6b935c9f6dff91b3", + "sourceCodeHash": "0xe3031507a78ed71352c0c1523272881a48c8d6bd98d5322ce3a7cb8061eeade3" }, "src/L2/BaseFeeVault.sol": { "initCodeHash": "0xc403d4c555d8e69a2699e01d192ae7327136701fa02da10a6d75a584b3c364c9", diff --git a/packages/contracts-bedrock/snapshots/storageLayout/OptimismPortal2.json b/packages/contracts-bedrock/snapshots/storageLayout/OptimismPortal2.json index ccb5d970d342e..4a9abd3d2dc2c 100644 --- a/packages/contracts-bedrock/snapshots/storageLayout/OptimismPortal2.json +++ b/packages/contracts-bedrock/snapshots/storageLayout/OptimismPortal2.json @@ -131,5 +131,12 @@ "offset": 0, "slot": "62", "type": "contract IAnchorStateRegistry" + }, + { + "bytes": "1", + "label": "superRootsActive", + "offset": 20, + "slot": "62", + "type": "bool" } ] \ No newline at end of file diff --git a/packages/contracts-bedrock/snapshots/storageLayout/OptimismPortalInterop.json b/packages/contracts-bedrock/snapshots/storageLayout/OptimismPortalInterop.json index ccb5d970d342e..4a9abd3d2dc2c 100644 --- a/packages/contracts-bedrock/snapshots/storageLayout/OptimismPortalInterop.json +++ b/packages/contracts-bedrock/snapshots/storageLayout/OptimismPortalInterop.json @@ -131,5 +131,12 @@ "offset": 0, "slot": "62", "type": "contract IAnchorStateRegistry" + }, + { + "bytes": "1", + "label": "superRootsActive", + "offset": 20, + "slot": "62", + "type": "bool" } ] \ No newline at end of file diff --git a/packages/contracts-bedrock/snapshots/storageLayout/SystemConfig.json b/packages/contracts-bedrock/snapshots/storageLayout/SystemConfig.json index a6184a1f10dd5..3bbe34007506a 100644 --- a/packages/contracts-bedrock/snapshots/storageLayout/SystemConfig.json +++ b/packages/contracts-bedrock/snapshots/storageLayout/SystemConfig.json @@ -96,5 +96,12 @@ "offset": 4, "slot": "106", "type": "uint32" + }, + { + "bytes": "32", + "label": "l2ChainId", + "offset": 0, + "slot": "107", + "type": "uint256" } ] \ No newline at end of file diff --git a/packages/contracts-bedrock/snapshots/storageLayout/SystemConfigInterop.json b/packages/contracts-bedrock/snapshots/storageLayout/SystemConfigInterop.json index a6184a1f10dd5..3bbe34007506a 100644 --- a/packages/contracts-bedrock/snapshots/storageLayout/SystemConfigInterop.json +++ b/packages/contracts-bedrock/snapshots/storageLayout/SystemConfigInterop.json @@ -96,5 +96,12 @@ "offset": 4, "slot": "106", "type": "uint32" + }, + { + "bytes": "32", + "label": "l2ChainId", + "offset": 0, + "slot": "107", + "type": "uint256" } ] \ No newline at end of file diff --git a/packages/contracts-bedrock/src/L1/OPContractsManager.sol b/packages/contracts-bedrock/src/L1/OPContractsManager.sol index 272b611c4b918..d7778b7951540 100644 --- a/packages/contracts-bedrock/src/L1/OPContractsManager.sol +++ b/packages/contracts-bedrock/src/L1/OPContractsManager.sol @@ -17,7 +17,6 @@ import { IAnchorStateRegistry } from "interfaces/dispute/IAnchorStateRegistry.so import { IDisputeGame } from "interfaces/dispute/IDisputeGame.sol"; import { IAddressManager } from "interfaces/legacy/IAddressManager.sol"; import { IProxyAdmin } from "interfaces/universal/IProxyAdmin.sol"; -import { IDelayedWETH } from "interfaces/dispute/IDelayedWETH.sol"; import { IDisputeGameFactory } from "interfaces/dispute/IDisputeGameFactory.sol"; import { IFaultDisputeGame } from "interfaces/dispute/IFaultDisputeGame.sol"; import { IPermissionedDisputeGame } from "interfaces/dispute/IPermissionedDisputeGame.sol"; @@ -56,6 +55,7 @@ contract OPContractsManager is ISemver { string saltMixer; uint64 gasLimit; // Configurable dispute game parameters. + bool disputeGameUsesSuperRoots; GameType disputeGameType; Claim disputeAbsolutePrestate; uint256 disputeMaxGameDepth; @@ -121,6 +121,7 @@ contract OPContractsManager is ISemver { ISystemConfig systemConfigProxy; IProxyAdmin proxyAdmin; Claim absolutePrestate; + bool disputeGameUsesSuperRoots; } struct AddGameInput { @@ -226,8 +227,23 @@ contract OPContractsManager is ISemver { /// @notice Thrown when an invalid `l2ChainId` is provided to `deploy`. error InvalidChainId(); - /// @notice Thrown when a role's address is not valid. - error InvalidRoleAddress(string role); + /// @notice Thrown when a role's address is not valid (opChainProxyAdminOwner). + error InvalidRoleAddressPAO(); + + /// @notice Thrown when a role's address is not valid (systemConfigOwner). + error InvalidRoleAddressSCO(); + + /// @notice Thrown when a role's address is not valid (batcher). + error InvalidRoleAddressBatcher(); + + /// @notice Thrown when a role's address is not valid (unsafeBlockSigner). + error InvalidRoleAddressUBS(); + + /// @notice Thrown when a role's address is not valid (proposer). + error InvalidRoleAddressProposer(); + + /// @notice Thrown when a role's address is not valid (challenger). + error InvalidRoleAddressChallenger(); /// @notice Thrown when the latest release is not set upon initialization. error LatestReleaseNotSet(); @@ -388,7 +404,7 @@ contract OPContractsManager is ISemver { output.opChainProxyAdmin, address(output.l1ERC721BridgeProxy), implementation.l1ERC721BridgeImpl, data ); - data = encodeOptimismPortalInitializer(output); + data = encodeOptimismPortalInitializer(_input, output); upgradeToAndCall( output.opChainProxyAdmin, address(output.optimismPortalProxy), implementation.optimismPortalImpl, data ); @@ -542,9 +558,13 @@ contract OPContractsManager is ISemver { ) ) ); + // We're also going to need the l2ChainId below, so we cache it in the outer scope. uint256 l2ChainId = getL2ChainId(IFaultDisputeGame(address(permissionedDisputeGame))); + // Upgrade the SystemConfig to add the l2ChainId. + _opChainConfigs[i].systemConfigProxy.upgrade(l2ChainId); + // Replace the Anchor State Registry Proxy with a new Proxy and Implementation // For this upgrade, we are replacing the previous Anchor State Registry, thus we: // 1. deploy a new Anchor State Registry proxy @@ -590,7 +610,9 @@ contract OPContractsManager is ISemver { ); // Upgrade the OptimismPortal to have a reference to the new AnchorStateRegistry. - IOptimismPortal2(payable(opChainAddrs.optimismPortal)).upgrade(newAnchorStateRegistryProxy); + IOptimismPortal2(payable(opChainAddrs.optimismPortal)).upgrade( + newAnchorStateRegistryProxy, _opChainConfigs[i].disputeGameUsesSuperRoots + ); } // Deploy and set a new permissioned game to update its prestate @@ -649,6 +671,7 @@ contract OPContractsManager is ISemver { // This conversion is safe because the GameType is a uint32, which will always fit in an int256. int256 gameTypeInt = int256(uint256(gameConfig.disputeGameType.raw())); + // Ensure that the game configs are added in ascending order, and not duplicated. if (lastGameConfig >= gameTypeInt) revert InvalidGameConfigs(); lastGameConfig = gameTypeInt; @@ -660,20 +683,27 @@ contract OPContractsManager is ISemver { getGameImplementation(getDisputeGameFactory(gameConfig.systemConfig), GameTypes.PERMISSIONED_CANNON) ) ); + // Pull out the chain ID. uint256 l2ChainId = getL2ChainId(pdg); // Deploy a new DelayedWETH proxy for this game if one hasn't already been specified. Leaving /// gameConfig.delayedWETH as the zero address will cause a new DelayedWETH to be deployed for this game. if (address(gameConfig.delayedWETH) == address(0)) { - string memory contractName = string.concat( - "DelayedWETH-", - // This is a safe cast because GameType is a uint256 under the hood and no operation has been done - // on it at this point - Strings.toString(uint256(gameTypeInt)) - ); outputs[i].delayedWETH = IDelayedWETH( - payable(deployProxy(l2ChainId, gameConfig.proxyAdmin, gameConfig.saltMixer, contractName)) + payable( + deployProxy( + l2ChainId, + gameConfig.proxyAdmin, + gameConfig.saltMixer, + string.concat( + "DelayedWETH-", + // This is a safe cast because GameType is a uint256 under the hood + // and no operation has been done on it at this point + Strings.toString(uint256(gameTypeInt)) + ) + ) + ) ); // Initialize the proxy. @@ -770,12 +800,12 @@ contract OPContractsManager is ISemver { function assertValidInputs(DeployInput calldata _input) internal view { if (_input.l2ChainId == 0 || _input.l2ChainId == block.chainid) revert InvalidChainId(); - if (_input.roles.opChainProxyAdminOwner == address(0)) revert InvalidRoleAddress("opChainProxyAdminOwner"); - if (_input.roles.systemConfigOwner == address(0)) revert InvalidRoleAddress("systemConfigOwner"); - if (_input.roles.batcher == address(0)) revert InvalidRoleAddress("batcher"); - if (_input.roles.unsafeBlockSigner == address(0)) revert InvalidRoleAddress("unsafeBlockSigner"); - if (_input.roles.proposer == address(0)) revert InvalidRoleAddress("proposer"); - if (_input.roles.challenger == address(0)) revert InvalidRoleAddress("challenger"); + if (_input.roles.opChainProxyAdminOwner == address(0)) revert InvalidRoleAddressPAO(); + if (_input.roles.systemConfigOwner == address(0)) revert InvalidRoleAddressSCO(); + if (_input.roles.batcher == address(0)) revert InvalidRoleAddressBatcher(); + if (_input.roles.unsafeBlockSigner == address(0)) revert InvalidRoleAddressUBS(); + if (_input.roles.proposer == address(0)) revert InvalidRoleAddressProposer(); + if (_input.roles.challenger == address(0)) revert InvalidRoleAddressChallenger(); if (_input.startingAnchorRoot.length == 0) revert InvalidStartingAnchorRoot(); if (bytes32(_input.startingAnchorRoot) == bytes32(0)) revert InvalidStartingAnchorRoot(); @@ -845,14 +875,23 @@ contract OPContractsManager is ISemver { } /// @notice Helper method for encoding the OptimismPortal initializer data. - function encodeOptimismPortalInitializer(DeployOutput memory _output) + function encodeOptimismPortalInitializer( + DeployInput memory _input, + DeployOutput memory _output + ) internal view virtual returns (bytes memory) { return abi.encodeCall( - IOptimismPortal2.initialize, (_output.systemConfigProxy, superchainConfig, _output.anchorStateRegistryProxy) + IOptimismPortal2.initialize, + ( + _output.systemConfigProxy, + superchainConfig, + _output.anchorStateRegistryProxy, + _input.disputeGameUsesSuperRoots + ) ); } @@ -880,7 +919,8 @@ contract OPContractsManager is ISemver { _input.roles.unsafeBlockSigner, referenceResourceConfig, chainIdToBatchInboxAddress(_input.l2ChainId), - opChainAddrs + opChainAddrs, + _input.l2ChainId ) ); } diff --git a/packages/contracts-bedrock/src/L1/OPContractsManagerInterop.sol b/packages/contracts-bedrock/src/L1/OPContractsManagerInterop.sol index dcd98a3ed3d01..6358bc741d715 100644 --- a/packages/contracts-bedrock/src/L1/OPContractsManagerInterop.sol +++ b/packages/contracts-bedrock/src/L1/OPContractsManagerInterop.sol @@ -72,7 +72,8 @@ contract OPContractsManagerInterop is OPContractsManager { referenceResourceConfig, chainIdToBatchInboxAddress(_input.l2ChainId), opChainAddrs, - dependencyManager + dependencyManager, + _input.l2ChainId ) ); } diff --git a/packages/contracts-bedrock/src/L1/OptimismPortal2.sol b/packages/contracts-bedrock/src/L1/OptimismPortal2.sol index b567caf8eacc0..eff4f10c51931 100644 --- a/packages/contracts-bedrock/src/L1/OptimismPortal2.sol +++ b/packages/contracts-bedrock/src/L1/OptimismPortal2.sol @@ -4,6 +4,7 @@ pragma solidity 0.8.15; // Contracts import { Initializable } from "@openzeppelin/contracts/proxy/utils/Initializable.sol"; import { ResourceMetering } from "src/L1/ResourceMetering.sol"; +import { ReinitializableBase } from "src/universal/ReinitializableBase.sol"; // Libraries import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; @@ -31,7 +32,7 @@ import { IAnchorStateRegistry } from "interfaces/dispute/IAnchorStateRegistry.so /// @notice The OptimismPortal is a low-level contract responsible for passing messages between L1 /// 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 OptimismPortal2 is Initializable, ResourceMetering, ISemver { +contract OptimismPortal2 is Initializable, ResourceMetering, ReinitializableBase, ISemver { /// @notice Allows for interactions with non standard ERC20 tokens. using SafeERC20 for IERC20; @@ -122,6 +123,9 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ISemver { /// @notice Address of the AnchorStateRegistry contract. IAnchorStateRegistry public anchorStateRegistry; + /// @notice Whether the OptimismPortal is using Super Roots or Output Roots. + bool public superRootsActive; + /// @notice Emitted when a transaction is deposited from L1 to L2. The parameters of this event /// are read by the rollup node and used to derive deposit transactions on L2. /// @param from Address that triggered the deposit transaction. @@ -193,6 +197,18 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ISemver { /// @notice Thrown when a withdrawal has not been proven. error OptimismPortal_Unproven(); + /// @notice Thrown when the wrong proof method is used. + error OptimismPortal_WrongProofMethod(); + + /// @notice Thrown when a super root proof is invalid. + error OptimismPortal_InvalidSuperRootProof(); + + /// @notice Thrown when an output root index is invalid. + error OptimismPortal_InvalidOutputRootIndex(); + + /// @notice Thrown when an output root chain id is invalid. + error OptimismPortal_InvalidOutputRootChainId(); + /// @notice Reverts when paused. modifier whenNotPaused() { if (paused()) revert OptimismPortal_CallPaused(); @@ -206,7 +222,7 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ISemver { } /// @param _proofMaturityDelaySeconds The proof maturity delay in seconds. - constructor(uint256 _proofMaturityDelaySeconds) { + constructor(uint256 _proofMaturityDelaySeconds) ReinitializableBase(2) { PROOF_MATURITY_DELAY_SECONDS = _proofMaturityDelaySeconds; _disableInitializers(); } @@ -215,17 +231,20 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ISemver { /// @param _systemConfig Address of the SystemConfig. /// @param _superchainConfig Address of the SuperchainConfig. /// @param _anchorStateRegistry Address of the AnchorStateRegistry. + /// @param _superRootsActive Whether the OptimismPortal is using Super Roots or Output Roots. function initialize( ISystemConfig _systemConfig, ISuperchainConfig _superchainConfig, - IAnchorStateRegistry _anchorStateRegistry + IAnchorStateRegistry _anchorStateRegistry, + bool _superRootsActive ) external - reinitializer(2) + reinitializer(initVersion()) { systemConfig = _systemConfig; superchainConfig = _superchainConfig; anchorStateRegistry = _anchorStateRegistry; + superRootsActive = _superRootsActive; // Set the l2Sender slot, only if it is currently empty. This signals the first // initialization of the contract. @@ -238,8 +257,16 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ISemver { /// @notice Upgrades the OptimismPortal contract to have a reference to the AnchorStateRegistry. /// @param _anchorStateRegistry AnchorStateRegistry contract. - function upgrade(IAnchorStateRegistry _anchorStateRegistry) external reinitializer(2) { + /// @param _superRootsActive Whether the OptimismPortal is using Super Roots or Output Roots. + function upgrade( + IAnchorStateRegistry _anchorStateRegistry, + bool _superRootsActive + ) + external + reinitializer(initVersion()) + { anchorStateRegistry = _anchorStateRegistry; + superRootsActive = _superRootsActive; } /// @notice Getter for the current paused status. @@ -307,7 +334,38 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ISemver { // Intentionally empty. } - /// @notice Proves a withdrawal transaction. + /// @notice Proves a withdrawal transaction using a Super Root proof. Only callable when the + /// OptimismPortal is using Super Roots (superRootsActive flag is true). + /// @param _tx Withdrawal transaction to finalize. + /// @param _disputeGameProxy Address of the dispute game to prove the withdrawal against. + /// @param _outputRootIndex Index of the target Output Root within the Super Root. + /// @param _superRootProof Inclusion proof of the Output Root within the Super Root. + /// @param _outputRootProof Inclusion proof of the L2ToL1MessagePasser storage root. + /// @param _withdrawalProof Inclusion proof of the withdrawal within the L2ToL1MessagePasser. + function proveWithdrawalTransaction( + Types.WithdrawalTransaction memory _tx, + IDisputeGame _disputeGameProxy, + uint256 _outputRootIndex, + Types.SuperRootProof calldata _superRootProof, + Types.OutputRootProof calldata _outputRootProof, + bytes[] calldata _withdrawalProof + ) + external + whenNotPaused + { + // Make sure that the OptimismPortal is using Super Roots. + if (!superRootsActive) { + revert OptimismPortal_WrongProofMethod(); + } + + // Prove the transaction. + _proveWithdrawalTransaction( + _tx, _disputeGameProxy, _outputRootIndex, _superRootProof, _outputRootProof, _withdrawalProof + ); + } + + /// @notice Proves a withdrawal transaction using an Output Root proof. Only callable when the + /// OptimismPortal is using Output Roots (superRootsActive flag is false). /// @param _tx Withdrawal transaction to finalize. /// @param _disputeGameIndex Index of the dispute game to prove the withdrawal against. /// @param _outputRootProof Inclusion proof of the L2ToL1MessagePasser storage root. @@ -320,43 +378,100 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ISemver { ) external whenNotPaused + { + // Make sure that the OptimismPortal is using Output Roots. + if (superRootsActive) { + revert OptimismPortal_WrongProofMethod(); + } + + // Fetch the dispute game proxy from the `DisputeGameFactory` contract. + (,, IDisputeGame disputeGameProxy) = disputeGameFactory().gameAtIndex(_disputeGameIndex); + + // Create a dummy super root proof to pass into the internal function. Note that this is + // not a valid Super Root proof but it isn't used anywhere in the internal function when + // using Output Roots. + Types.SuperRootProof memory superRootProof; + + // Prove the transaction. + _proveWithdrawalTransaction(_tx, disputeGameProxy, 0, superRootProof, _outputRootProof, _withdrawalProof); + } + + /// @notice Internal function for proving a withdrawal transaction, used by both the Super Root + /// and Output Root proof functions. Will eventually be replaced with a single function + /// when the Output Root proof method is deprecated. + /// @param _tx Withdrawal transaction to prove. + /// @param _disputeGameProxy Address of the dispute game to prove the withdrawal against. + /// @param _outputRootIndex Index of the target Output Root within the Super Root. + /// @param _superRootProof Inclusion proof of the Output Root within the Super Root. + /// @param _outputRootProof Inclusion proof of the L2ToL1MessagePasser storage root. + /// @param _withdrawalProof Inclusion proof of the withdrawal within the L2ToL1MessagePasser. + function _proveWithdrawalTransaction( + Types.WithdrawalTransaction memory _tx, + IDisputeGame _disputeGameProxy, + uint256 _outputRootIndex, + Types.SuperRootProof memory _superRootProof, + Types.OutputRootProof memory _outputRootProof, + bytes[] memory _withdrawalProof + ) + internal { // Make sure that the target address is safe. if (_isUnsafeTarget(_tx.target)) { revert OptimismPortal_BadTarget(); } - // Fetch the dispute game proxy from the `DisputeGameFactory` contract. - (,, IDisputeGame gameProxy) = disputeGameFactory().gameAtIndex(_disputeGameIndex); - // Game must be a Proper Game. - if (!anchorStateRegistry.isGameProper(gameProxy)) { + if (!anchorStateRegistry.isGameProper(_disputeGameProxy)) { revert OptimismPortal_ImproperDisputeGame(); } // Game must have been respected game type when created. - if (!anchorStateRegistry.isGameRespected(gameProxy)) { + if (!anchorStateRegistry.isGameRespected(_disputeGameProxy)) { revert OptimismPortal_InvalidDisputeGame(); } // Game must not have resolved in favor of the Challenger (invalid root claim). - if (gameProxy.status() == GameStatus.CHALLENGER_WINS) { + if (_disputeGameProxy.status() == GameStatus.CHALLENGER_WINS) { revert OptimismPortal_InvalidDisputeGame(); } - // Verify that the output root can be generated with the elements in the proof. - if (gameProxy.rootClaim().raw() != Hashing.hashOutputRootProof(_outputRootProof)) { - revert OptimismPortal_InvalidOutputRootProof(); - } - // As a sanity check, we make sure that the current timestamp is not less than or equal to // the dispute game's creation timestamp. Not strictly necessary but extra layer of // safety against weird bugs. Note that this blocks withdrawals from being proven in the // same block that a dispute game is created. - if (block.timestamp <= gameProxy.createdAt().raw()) { + if (block.timestamp <= _disputeGameProxy.createdAt().raw()) { revert OptimismPortal_InvalidProofTimestamp(); } + // Validate the provided Output Root and/or Super Root proof depending on proof method. + if (superRootsActive) { + // Verify that the super root can be generated with the elements in the proof. + if (_disputeGameProxy.rootClaim().raw() != Hashing.hashSuperRootProof(_superRootProof)) { + revert OptimismPortal_InvalidSuperRootProof(); + } + + // Check that the index exists in the super root proof. + if (_outputRootIndex >= _superRootProof.outputRoots.length) { + revert OptimismPortal_InvalidOutputRootIndex(); + } + + // Check that the output root has the correct chain id. + Types.OutputRootWithChainId memory outputRoot = _superRootProof.outputRoots[_outputRootIndex]; + if (outputRoot.chainId != systemConfig.l2ChainId()) { + revert OptimismPortal_InvalidOutputRootChainId(); + } + + // Verify that the output root can be generated with the elements in the proof. + if (outputRoot.root != Hashing.hashOutputRootProof(_outputRootProof)) { + revert OptimismPortal_InvalidOutputRootProof(); + } + } else { + // Verify that the output root can be generated with the elements in the proof. + if (_disputeGameProxy.rootClaim().raw() != Hashing.hashOutputRootProof(_outputRootProof)) { + revert OptimismPortal_InvalidOutputRootProof(); + } + } + // Load the ProvenWithdrawal into memory, using the withdrawal hash as a unique identifier. bytes32 withdrawalHash = Hashing.hashWithdrawal(_tx); @@ -389,7 +504,7 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ISemver { // the provenWithdrawals mapping. A given user may re-prove a withdrawalHash multiple // times, but each proof will reset the proof timer. provenWithdrawals[withdrawalHash][msg.sender] = - ProvenWithdrawal({ disputeGameProxy: gameProxy, timestamp: uint64(block.timestamp) }); + ProvenWithdrawal({ disputeGameProxy: _disputeGameProxy, timestamp: uint64(block.timestamp) }); // Add the proof submitter to the list of proof submitters for this withdrawal hash. proofSubmitters[withdrawalHash].push(msg.sender); diff --git a/packages/contracts-bedrock/src/L1/SystemConfig.sol b/packages/contracts-bedrock/src/L1/SystemConfig.sol index d5cc62db1a6d5..d06c7efb3f434 100644 --- a/packages/contracts-bedrock/src/L1/SystemConfig.sol +++ b/packages/contracts-bedrock/src/L1/SystemConfig.sol @@ -3,6 +3,7 @@ pragma solidity 0.8.15; // Contracts import { OwnableUpgradeable } from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; +import { ReinitializableBase } from "src/universal/ReinitializableBase.sol"; // Libraries import { Storage } from "src/libraries/Storage.sol"; @@ -16,7 +17,7 @@ import { IResourceMetering } from "interfaces/L1/IResourceMetering.sol"; /// @notice The SystemConfig contract is used to manage configuration of an Optimism network. /// All configuration is stored on L1 and picked up by L2 as part of the derviation of /// the L2 chain. -contract SystemConfig is OwnableUpgradeable, ISemver { +contract SystemConfig is OwnableUpgradeable, ReinitializableBase, ISemver { /// @notice Enum representing different types of updates. /// @custom:value BATCHER Represents an update to the batcher hash. /// @custom:value FEE_SCALARS Represents an update to l1 data fee scalars. @@ -122,6 +123,9 @@ contract SystemConfig is OwnableUpgradeable, ISemver { /// @notice The EIP-1559 elasticity multiplier. uint32 public eip1559Elasticity; + /// @notice The L2 chain ID that this SystemConfig configures. + uint256 public l2ChainId; + /// @notice Emitted when configuration is updated. /// @param version SystemConfig version. /// @param updateType Type of update. @@ -129,15 +133,15 @@ contract SystemConfig is OwnableUpgradeable, ISemver { event ConfigUpdate(uint256 indexed version, UpdateType indexed updateType, bytes data); /// @notice Semantic version. - /// @custom:semver 2.4.0 + /// @custom:semver 2.5.0 function version() public pure virtual returns (string memory) { - return "2.4.0"; + return "2.5.0"; } /// @notice Constructs the SystemConfig contract. /// @dev START_BLOCK_SLOT is set to type(uint256).max here so that it will be a dead value /// in the singleton. - constructor() { + constructor() ReinitializableBase(2) { Storage.setUint(START_BLOCK_SLOT, type(uint256).max); _disableInitializers(); } @@ -154,6 +158,7 @@ contract SystemConfig is OwnableUpgradeable, ISemver { /// @param _batchInbox Batch inbox address. An identifier for the op-node to find /// canonical data. /// @param _addresses Set of L1 contract addresses. These should be the proxies. + /// @param _l2ChainId The L2 chain ID that this SystemConfig configures. function initialize( address _owner, uint32 _basefeeScalar, @@ -163,10 +168,11 @@ contract SystemConfig is OwnableUpgradeable, ISemver { address _unsafeBlockSigner, IResourceMetering.ResourceConfig memory _config, address _batchInbox, - SystemConfig.Addresses memory _addresses + SystemConfig.Addresses memory _addresses, + uint256 _l2ChainId ) public - initializer + reinitializer(initVersion()) { __Ownable_init(); transferOwnership(_owner); @@ -188,6 +194,14 @@ contract SystemConfig is OwnableUpgradeable, ISemver { _setStartBlock(); _setResourceConfig(_config); + + l2ChainId = _l2ChainId; + } + + /// @notice Upgrades the SystemConfig by setting the L2 chain ID variable. + /// @param _l2ChainId The L2 chain ID that this SystemConfig configures. + function upgrade(uint256 _l2ChainId) external reinitializer(initVersion()) { + l2ChainId = _l2ChainId; } /// @notice Returns the minimum L2 gas limit that can be safely set for the system to diff --git a/packages/contracts-bedrock/src/L1/SystemConfigInterop.sol b/packages/contracts-bedrock/src/L1/SystemConfigInterop.sol index e18ed7400f39c..dbb9a3dbb2562 100644 --- a/packages/contracts-bedrock/src/L1/SystemConfigInterop.sol +++ b/packages/contracts-bedrock/src/L1/SystemConfigInterop.sol @@ -46,7 +46,8 @@ contract SystemConfigInterop is SystemConfig { IResourceMetering.ResourceConfig memory _config, address _batchInbox, SystemConfig.Addresses memory _addresses, - address _dependencyManager + address _dependencyManager, + uint256 _l2ChainId ) external { @@ -60,14 +61,15 @@ contract SystemConfigInterop is SystemConfig { _unsafeBlockSigner: _unsafeBlockSigner, _config: _config, _batchInbox: _batchInbox, - _addresses: _addresses + _addresses: _addresses, + _l2ChainId: _l2ChainId }); Storage.setAddress(DEPENDENCY_MANAGER_SLOT, _dependencyManager); } - /// @custom:semver +interop + /// @custom:semver +interop.1 function version() public pure override returns (string memory) { - return string.concat(super.version(), "+interop"); + return string.concat(super.version(), "+interop.1"); } /// @notice Adds a chain to the interop dependency set. Can only be called by the dependency manager. diff --git a/packages/contracts-bedrock/src/libraries/Encoding.sol b/packages/contracts-bedrock/src/libraries/Encoding.sol index 5aa4ee7d3d8a4..44416f301de4c 100644 --- a/packages/contracts-bedrock/src/libraries/Encoding.sol +++ b/packages/contracts-bedrock/src/libraries/Encoding.sol @@ -9,6 +9,12 @@ import { RLPWriter } from "src/libraries/rlp/RLPWriter.sol"; /// @title Encoding /// @notice Encoding handles Optimism's various different encoding schemes. library Encoding { + /// @notice Thrown when a provided Super Root proof has an invalid version. + error Encoding_InvalidSuperRootVersion(); + + /// @notice Thrown when a provided Super Root proof has no Output Roots. + error Encoding_EmptySuperRoot(); + /// @notice RLP encodes the L2 transaction that would be generated when a given deposit is sent /// to the L2 system. Useful for searching for a deposit in the L2 system. The /// transaction is prefixed with 0x7e to identify its EIP-2718 type. @@ -216,4 +222,30 @@ library Encoding { _batcherHash ); } + + /// @notice Encodes a super root proof into the preimage of a Super Root. + /// @param _superRootProof Super root proof to encode. + /// @return Encoded super root proof. + function encodeSuperRootProof(Types.SuperRootProof memory _superRootProof) internal pure returns (bytes memory) { + // Version must match the expected version. + if (_superRootProof.version != 0x01) { + revert Encoding_InvalidSuperRootVersion(); + } + + // Output roots must not be empty. + if (_superRootProof.outputRoots.length == 0) { + revert Encoding_EmptySuperRoot(); + } + + // Start with version byte and timestamp. + bytes memory encoded = bytes.concat(bytes1(0x01), bytes8(_superRootProof.timestamp)); + + // Add each output root (chainId + root) + for (uint256 i = 0; i < _superRootProof.outputRoots.length; i++) { + Types.OutputRootWithChainId memory outputRoot = _superRootProof.outputRoots[i]; + encoded = bytes.concat(encoded, bytes32(outputRoot.chainId), outputRoot.root); + } + + return encoded; + } } diff --git a/packages/contracts-bedrock/src/libraries/Hashing.sol b/packages/contracts-bedrock/src/libraries/Hashing.sol index b736ad9e4b7e7..782bbbe4f9960 100644 --- a/packages/contracts-bedrock/src/libraries/Hashing.sol +++ b/packages/contracts-bedrock/src/libraries/Hashing.sol @@ -146,4 +146,11 @@ library Hashing { { return keccak256(abi.encode(_destination, _source, _nonce, _sender, _target, _message)); } + + /// @notice Hashes a Super Root proof into a Super Root. + /// @param _superRootProof Super Root proof to hash. + /// @return Hashed super root proof. + function hashSuperRootProof(Types.SuperRootProof memory _superRootProof) internal pure returns (bytes32) { + return keccak256(Encoding.encodeSuperRootProof(_superRootProof)); + } } diff --git a/packages/contracts-bedrock/src/libraries/Types.sol b/packages/contracts-bedrock/src/libraries/Types.sol index 7e9a65654bc19..3c18c4e1bc151 100644 --- a/packages/contracts-bedrock/src/libraries/Types.sol +++ b/packages/contracts-bedrock/src/libraries/Types.sol @@ -29,6 +29,24 @@ library Types { bytes32 latestBlockhash; } + /// @notice Struct representing an output root with a chain id. + /// @custom:field chainId The chain ID of the L2 chain that the output root commits to. + /// @custom:field root The output root. + struct OutputRootWithChainId { + uint256 chainId; + bytes32 root; + } + + /// @notice Struct representing a super root proof. + /// @custom:field version The version of the super root proof. + /// @custom:field timestamp The timestamp of the super root proof. + /// @custom:field outputRoots The output roots that are included in the super root proof. + struct SuperRootProof { + bytes1 version; + uint64 timestamp; + OutputRootWithChainId[] outputRoots; + } + /// @notice Struct representing a deposit transaction (L1 => L2 transaction) created by an end /// user (as opposed to a system deposit transaction generated by the system). /// @custom:field from Address of the sender of the transaction. diff --git a/packages/contracts-bedrock/src/universal/ReinitializableBase.sol b/packages/contracts-bedrock/src/universal/ReinitializableBase.sol new file mode 100644 index 0000000000000..056a15986e02a --- /dev/null +++ b/packages/contracts-bedrock/src/universal/ReinitializableBase.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.15; + +/// @title ReinitializableBase +/// @notice A base contract for reinitializable contracts that exposes a version number. +abstract contract ReinitializableBase { + /// @notice Thrown when the initialization version is zero. + error ReinitializableBase_ZeroInitVersion(); + + /// @notice Current initialization version. + uint8 internal immutable INIT_VERSION; + + /// @param _initVersion Current initialization version. + constructor(uint8 _initVersion) { + // Sanity check, we should never have a zero init version. + if (_initVersion == 0) revert ReinitializableBase_ZeroInitVersion(); + INIT_VERSION = _initVersion; + } + + /// @notice Getter for the current initialization version. + /// @return The current initialization version. + function initVersion() public view returns (uint8) { + return INIT_VERSION; + } +} diff --git a/packages/contracts-bedrock/test/L1/OPContractsManager.t.sol b/packages/contracts-bedrock/test/L1/OPContractsManager.t.sol index b597cd52497e8..6f3dd68fe22ec 100644 --- a/packages/contracts-bedrock/test/L1/OPContractsManager.t.sol +++ b/packages/contracts-bedrock/test/L1/OPContractsManager.t.sol @@ -130,6 +130,7 @@ contract OPContractsManager_Deploy_Test is DeployOPChain_TestBase { startingAnchorRoot: _doi.startingAnchorRoot(), saltMixer: _doi.saltMixer(), gasLimit: _doi.gasLimit(), + disputeGameUsesSuperRoots: _doi.disputeGameUsesSuperRoots(), disputeGameType: _doi.disputeGameType(), disputeAbsolutePrestate: _doi.disputeAbsolutePrestate(), disputeMaxGameDepth: _doi.disputeMaxGameDepth(), @@ -250,7 +251,8 @@ contract OPContractsManager_Upgrade_Harness is CommonTest { IOPContractsManager.OpChainConfig({ systemConfigProxy: systemConfig, proxyAdmin: proxyAdmin, - absolutePrestate: absolutePrestate + absolutePrestate: absolutePrestate, + disputeGameUsesSuperRoots: false }) ); @@ -668,6 +670,7 @@ contract OPContractsManager_AddGameType_Test is Test { l2ChainId: 100, saltMixer: "hello", gasLimit: 30_000_000, + disputeGameUsesSuperRoots: false, disputeGameType: GameType.wrap(1), disputeAbsolutePrestate: Claim.wrap( bytes32(hex"038512e02c4c3f7bdaec27d00edf55b7155e0905301e1a88083e4e0a6764d54c") diff --git a/packages/contracts-bedrock/test/L1/OPPrestateUpdater.t.sol b/packages/contracts-bedrock/test/L1/OPPrestateUpdater.t.sol index 56edac1f8fa2b..563e21d16be6c 100644 --- a/packages/contracts-bedrock/test/L1/OPPrestateUpdater.t.sol +++ b/packages/contracts-bedrock/test/L1/OPPrestateUpdater.t.sol @@ -167,6 +167,7 @@ contract OPPrestateUpdater_Test is Test { l2ChainId: 100, saltMixer: "hello", gasLimit: 30_000_000, + disputeGameUsesSuperRoots: false, disputeGameType: GameType.wrap(1), disputeAbsolutePrestate: Claim.wrap( bytes32(hex"038512e02c4c3f7bdaec27d00edf55b7155e0905301e1a88083e4e0a6764d54c") @@ -199,7 +200,10 @@ contract OPPrestateUpdater_Test is Test { function test_updatePrestate_pdgOnlyWithValidInput_succeeds() public { OPContractsManager.OpChainConfig[] memory inputs = new OPContractsManager.OpChainConfig[](1); inputs[0] = OPContractsManager.OpChainConfig( - chainDeployOutput.systemConfigProxy, chainDeployOutput.opChainProxyAdmin, Claim.wrap(bytes32(hex"ABBA")) + chainDeployOutput.systemConfigProxy, + chainDeployOutput.opChainProxyAdmin, + Claim.wrap(bytes32(hex"ABBA")), + false ); address proxyAdminOwner = chainDeployOutput.opChainProxyAdmin.owner(); @@ -230,7 +234,10 @@ contract OPPrestateUpdater_Test is Test { OPContractsManager.OpChainConfig[] memory inputs = new OPContractsManager.OpChainConfig[](1); inputs[0] = OPContractsManager.OpChainConfig( - chainDeployOutput.systemConfigProxy, chainDeployOutput.opChainProxyAdmin, Claim.wrap(bytes32(hex"ABBA")) + chainDeployOutput.systemConfigProxy, + chainDeployOutput.opChainProxyAdmin, + Claim.wrap(bytes32(hex"ABBA")), + false ); address proxyAdminOwner = chainDeployOutput.opChainProxyAdmin.owner(); @@ -267,7 +274,8 @@ contract OPPrestateUpdater_Test is Test { inputs[0] = OPContractsManager.OpChainConfig({ systemConfigProxy: chainDeployOutput.systemConfigProxy, proxyAdmin: chainDeployOutput.opChainProxyAdmin, - absolutePrestate: Claim.wrap(bytes32(0)) + absolutePrestate: Claim.wrap(bytes32(0)), + disputeGameUsesSuperRoots: false }); address proxyAdminOwner = chainDeployOutput.opChainProxyAdmin.owner(); @@ -295,6 +303,7 @@ contract OPPrestateUpdater_Test is Test { startingAnchorRoot: bytes(abi.encode(0)), saltMixer: "", gasLimit: 0, + disputeGameUsesSuperRoots: false, disputeGameType: GameType.wrap(0), disputeAbsolutePrestate: Claim.wrap(0), disputeMaxGameDepth: 0, @@ -312,7 +321,8 @@ contract OPPrestateUpdater_Test is Test { OPContractsManager.OpChainConfig({ systemConfigProxy: ISystemConfig(address(0)), proxyAdmin: IProxyAdmin(address(0)), - absolutePrestate: Claim.wrap(0) + absolutePrestate: Claim.wrap(0), + disputeGameUsesSuperRoots: false }) ); diff --git a/packages/contracts-bedrock/test/L1/OptimismPortal2.t.sol b/packages/contracts-bedrock/test/L1/OptimismPortal2.t.sol index a2b55af457bc9..e5fc247994fd5 100644 --- a/packages/contracts-bedrock/test/L1/OptimismPortal2.t.sol +++ b/packages/contracts-bedrock/test/L1/OptimismPortal2.t.sol @@ -1,12 +1,18 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.15; -// Testing +// Forge import { VmSafe } from "forge-std/Vm.sol"; +import { console2 as console } from "forge-std/console2.sol"; + +// Testing import { CommonTest } from "test/setup/CommonTest.sol"; import { NextImpl } from "test/mocks/NextImpl.sol"; import { EIP1967Helper } from "test/mocks/EIP1967Helper.sol"; +// Scripts +import { ForgeArtifacts, StorageSlot } from "scripts/libraries/ForgeArtifacts.sol"; + // Contracts import { SuperchainConfig } from "src/L1/SuperchainConfig.sol"; @@ -24,6 +30,7 @@ import { IOptimismPortal2 } from "interfaces/L1/IOptimismPortal2.sol"; import { IDisputeGame } from "interfaces/dispute/IDisputeGame.sol"; import { IFaultDisputeGame } from "interfaces/dispute/IFaultDisputeGame.sol"; import { IProxy } from "interfaces/universal/IProxy.sol"; +import { IAnchorStateRegistry } from "interfaces/dispute/IAnchorStateRegistry.sol"; contract OptimismPortal2_Test is CommonTest { address depositor; @@ -507,6 +514,24 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { assertFalse(optimismPortal2.finalizedWithdrawals(Hashing.hashWithdrawal(_defaultTx))); } + /// @notice Sets the supeRootsActive variable to the provided value. + /// @param _superRootsActive The value to set the superRootsActive variable to. + function setSuperRootsActive(bool _superRootsActive) public { + // Get the slot for superRootsActive. + StorageSlot memory slot = ForgeArtifacts.getSlot("OptimismPortal2", "superRootsActive"); + + // Load the existing storage slot value. + bytes32 existingValue = vm.load(address(optimismPortal2), bytes32(slot.slot)); + + // Inject the bool into the existing storage slot value with a bitwise OR. + // Shift the bool left by the offset of the storage slot and OR with existing value. + bytes32 newValue = + bytes32(uint256(uint8(_superRootsActive ? 1 : 0)) << slot.offset * 8 | uint256(existingValue)); + + // Store the new value at the correct slot/offset. + vm.store(address(optimismPortal2), bytes32(slot.slot), newValue); + } + /// @dev Tests that `proveWithdrawalTransaction` reverts when paused. function test_proveWithdrawalTransaction_paused_reverts() external { vm.prank(optimismPortal2.guardian()); @@ -748,6 +773,216 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { }); } + /// @dev Tests that `proveWithdrawalTransaction` reverts when using the Output Roots version of + /// `proveWithdrawalTransaction` when `superRootsActive` is true. + function test_proveWithdrawalTransaction_outputRootVersionWhenSuperRootsActive_reverts() external { + // Set superRootsActive to true. + setSuperRootsActive(true); + + // Should revert. + vm.expectRevert(IOptimismPortal2.OptimismPortal_WrongProofMethod.selector); + optimismPortal2.proveWithdrawalTransaction({ + _tx: _defaultTx, + _disputeGameIndex: _proposedGameIndex, + _outputRootProof: _outputRootProof, + _withdrawalProof: _withdrawalProof + }); + } + + /// @dev Tests that `proveWithdrawalTransaction` reverts when using the Super Roots version of + /// `proveWithdrawalTransaction` when `superRootsActive` is false. + function test_proveWithdrawalTransaction_superRootsVersionWhenSuperRootsInactive_reverts() external { + // Set up a dummy super root proof. + Types.OutputRootWithChainId[] memory outputRootWithChainIdArr = new Types.OutputRootWithChainId[](1); + outputRootWithChainIdArr[0] = + Types.OutputRootWithChainId({ root: _outputRoot, chainId: systemConfig.l2ChainId() }); + Types.SuperRootProof memory superRootProof = Types.SuperRootProof({ + version: 0x01, + timestamp: uint64(block.timestamp), + outputRoots: outputRootWithChainIdArr + }); + + // Should revert. + vm.expectRevert(IOptimismPortal2.OptimismPortal_WrongProofMethod.selector); + optimismPortal2.proveWithdrawalTransaction({ + _tx: _defaultTx, + _disputeGameProxy: game, + _outputRootIndex: 0, + _superRootProof: superRootProof, + _outputRootProof: _outputRootProof, + _withdrawalProof: _withdrawalProof + }); + } + + /// @dev Tests that `proveWithdrawalTransaction` reverts when using the Super Roots version of + /// `proveWithdrawalTransaction` when the provided proof is invalid. + function test_proveWithdrawalTransaction_superRootsVersionBadProof_reverts() external { + // Enable super roots. + setSuperRootsActive(true); + + // Set up a dummy super root proof. + Types.OutputRootWithChainId[] memory outputRootWithChainIdArr = new Types.OutputRootWithChainId[](1); + outputRootWithChainIdArr[0] = + Types.OutputRootWithChainId({ root: _outputRoot, chainId: systemConfig.l2ChainId() }); + Types.SuperRootProof memory superRootProof = Types.SuperRootProof({ + version: 0x01, + timestamp: uint64(block.timestamp), + outputRoots: outputRootWithChainIdArr + }); + + // Should revert because the proof is wrong. + vm.expectRevert(IOptimismPortal2.OptimismPortal_InvalidSuperRootProof.selector); + optimismPortal2.proveWithdrawalTransaction({ + _tx: _defaultTx, + _disputeGameProxy: game, + _outputRootIndex: 0, + _superRootProof: superRootProof, + _outputRootProof: _outputRootProof, + _withdrawalProof: _withdrawalProof + }); + } + + /// @dev Tests that `proveWithdrawalTransaction` reverts when using the Super Roots version of + /// `proveWithdrawalTransaction` when the provided proof is valid but the index is out of + /// bounds. + function test_proveWithdrawalTransaction_superRootsVersionBadIndex_reverts() external { + // Enable super roots. + setSuperRootsActive(true); + + // Set up a dummy super root proof. + Types.OutputRootWithChainId[] memory outputRootWithChainIdArr = new Types.OutputRootWithChainId[](1); + outputRootWithChainIdArr[0] = + Types.OutputRootWithChainId({ root: _outputRoot, chainId: systemConfig.l2ChainId() }); + Types.SuperRootProof memory superRootProof = Types.SuperRootProof({ + version: 0x01, + timestamp: uint64(block.timestamp), + outputRoots: outputRootWithChainIdArr + }); + + // Figure out what the right hash would be. + bytes32 expectedSuperRoot = Hashing.hashSuperRootProof(superRootProof); + + // Mock the game to return the expected super root. + vm.mockCall(address(game), abi.encodeCall(game.rootClaim, ()), abi.encode(expectedSuperRoot)); + + // Should revert because the proof is wrong. + vm.expectRevert(IOptimismPortal2.OptimismPortal_InvalidOutputRootIndex.selector); + optimismPortal2.proveWithdrawalTransaction({ + _tx: _defaultTx, + _disputeGameProxy: game, + _outputRootIndex: outputRootWithChainIdArr.length, // out of bounds + _superRootProof: superRootProof, + _outputRootProof: _outputRootProof, + _withdrawalProof: _withdrawalProof + }); + } + + /// @dev Tests that `proveWithdrawalTransaction` reverts when using the Super Roots version of + /// `proveWithdrawalTransaction` when the provided proof is valid, index is correct, but + /// the output root has the wrong chain id. + function test_proveWithdrawalTransaction_superRootsVersionBadChainId_reverts() external { + // Enable super roots. + setSuperRootsActive(true); + + // Set up a dummy super root proof. + Types.OutputRootWithChainId[] memory outputRootWithChainIdArr = new Types.OutputRootWithChainId[](1); + outputRootWithChainIdArr[0] = Types.OutputRootWithChainId({ + root: _outputRoot, + chainId: systemConfig.l2ChainId() + 1 // wrong chain id + }); + Types.SuperRootProof memory superRootProof = Types.SuperRootProof({ + version: 0x01, + timestamp: uint64(block.timestamp), + outputRoots: outputRootWithChainIdArr + }); + + // Figure out what the right hash would be. + bytes32 expectedSuperRoot = Hashing.hashSuperRootProof(superRootProof); + + // Mock the game to return the expected super root. + vm.mockCall(address(game), abi.encodeCall(game.rootClaim, ()), abi.encode(expectedSuperRoot)); + + // Should revert because the proof is wrong. + vm.expectRevert(IOptimismPortal2.OptimismPortal_InvalidOutputRootChainId.selector); + optimismPortal2.proveWithdrawalTransaction({ + _tx: _defaultTx, + _disputeGameProxy: game, + _outputRootIndex: 0, + _superRootProof: superRootProof, + _outputRootProof: _outputRootProof, + _withdrawalProof: _withdrawalProof + }); + } + + /// @dev Tests that `proveWithdrawalTransaction` reverts when using the Super Roots version of + /// `proveWithdrawalTransaction` when the provided proof is valid, index is correct, chain + /// id is correct, but the output root proof is invalid. + function test_proveWithdrawalTransaction_superRootsVersionBadOutputRootProof_reverts() external { + // Enable super roots. + setSuperRootsActive(true); + + // Set up a dummy super root proof. + Types.OutputRootWithChainId[] memory outputRootWithChainIdArr = new Types.OutputRootWithChainId[](1); + outputRootWithChainIdArr[0] = Types.OutputRootWithChainId({ + root: keccak256(abi.encode(_outputRoot)), // random root so the proof is wrong + chainId: systemConfig.l2ChainId() + }); + Types.SuperRootProof memory superRootProof = Types.SuperRootProof({ + version: 0x01, + timestamp: uint64(block.timestamp), + outputRoots: outputRootWithChainIdArr + }); + + // Figure out what the right hash would be. + bytes32 expectedSuperRoot = Hashing.hashSuperRootProof(superRootProof); + + // Mock the game to return the expected super root. + vm.mockCall(address(game), abi.encodeCall(game.rootClaim, ()), abi.encode(expectedSuperRoot)); + + // Should revert because the proof is wrong. + vm.expectRevert(IOptimismPortal2.OptimismPortal_InvalidOutputRootProof.selector); + optimismPortal2.proveWithdrawalTransaction({ + _tx: _defaultTx, + _disputeGameProxy: game, + _outputRootIndex: 0, + _superRootProof: superRootProof, + _outputRootProof: _outputRootProof, + _withdrawalProof: _withdrawalProof + }); + } + + /// @dev Tests that `proveWithdrawalTransaction` succeeds when all parameters are valid. + function test_proveWithdrawalTransaction_superRootsVersion_succeeds() external { + // Enable super roots. + setSuperRootsActive(true); + + // Set up a dummy super root proof. + Types.OutputRootWithChainId[] memory outputRootWithChainIdArr = new Types.OutputRootWithChainId[](1); + outputRootWithChainIdArr[0] = + Types.OutputRootWithChainId({ root: _outputRoot, chainId: systemConfig.l2ChainId() }); + Types.SuperRootProof memory superRootProof = Types.SuperRootProof({ + version: 0x01, + timestamp: uint64(block.timestamp), + outputRoots: outputRootWithChainIdArr + }); + + // Figure out what the right hash would be. + bytes32 expectedSuperRoot = Hashing.hashSuperRootProof(superRootProof); + + // Mock the game to return the expected super root. + vm.mockCall(address(game), abi.encodeCall(game.rootClaim, ()), abi.encode(expectedSuperRoot)); + + // Should succeed. + optimismPortal2.proveWithdrawalTransaction({ + _tx: _defaultTx, + _disputeGameProxy: game, + _outputRootIndex: 0, + _superRootProof: superRootProof, + _outputRootProof: _outputRootProof, + _withdrawalProof: _withdrawalProof + }); + } + /// @dev Tests that `proveWithdrawalTransaction` succeeds. function test_proveWithdrawalTransaction_validWithdrawalProof_succeeds() external { vm.expectEmit(true, true, true, true); @@ -1758,6 +1993,73 @@ contract OptimismPortal2_Upgradeable_Test is CommonTest { } } +/// @title OptimismPortal2_upgrade_Test +/// @notice Reusable test for the current upgrade() function in the OptimismPortal2 contract. If +/// the upgrade() function is changed, tests inside of this contract should be updated to +/// reflect the new function. If the upgrade() function is removed, remove the +/// corresponding tests but leave this contract in place so it's easy to add tests back +/// in the future. +contract OptimismPortal2_upgrade_Test is CommonTest { + function setUp() public override { + super.setUp(); + } + + /// @notice Tests that the upgrade() function succeeds. + function test_upgrade_succeeds() external { + // Get the slot for _initialized. + StorageSlot memory slot = ForgeArtifacts.getSlot("OptimismPortal2", "_initialized"); + + // Set the initialized slot to 0. + vm.store(address(optimismPortal2), bytes32(slot.slot), bytes32(0)); + + // Trigger upgrade(). + optimismPortal2.upgrade(IAnchorStateRegistry(address(0xdeadbeef)), true); + + // Verify that the initialized slot was updated. + bytes32 initializedSlotAfter = vm.load(address(optimismPortal2), bytes32(slot.slot)); + assertEq(initializedSlotAfter, bytes32(uint256(2))); + + // Verify that superRootsActive was set to true. + assertEq(optimismPortal2.superRootsActive(), true); + + // Verify that the AnchorStateRegistry was set. + assertEq(address(optimismPortal2.anchorStateRegistry()), address(0xdeadbeef)); + } + + /// @notice Tests that the upgrade() function reverts if called a second time. + function test_upgrade_upgradeTwice_reverts() external { + // Get the slot for _initialized. + StorageSlot memory slot = ForgeArtifacts.getSlot("OptimismPortal2", "_initialized"); + + // Set the initialized slot to 0. + vm.store(address(optimismPortal2), bytes32(slot.slot), bytes32(0)); + + // Trigger first upgrade. + optimismPortal2.upgrade(IAnchorStateRegistry(address(0xdeadbeef)), true); + + // Try to trigger second upgrade. + vm.expectRevert("Initializable: contract is already initialized"); + optimismPortal2.upgrade(IAnchorStateRegistry(address(0xdeadbeef)), true); + } + + /// @notice Tests that the upgrade() function reverts if called after initialization. + function test_upgrade_afterInitialization_reverts() external { + // Get the slot for _initialized. + StorageSlot memory slot = ForgeArtifacts.getSlot("OptimismPortal2", "_initialized"); + + // Slot value should be set to 2 (already initialized). + bytes32 initializedSlotBefore = vm.load(address(optimismPortal2), bytes32(slot.slot)); + assertEq(initializedSlotBefore, bytes32(uint256(2))); + + // AnchorStateRegistry address should be non-zero. + assertNotEq(address(optimismPortal2.anchorStateRegistry()), address(0)); + + // Try to trigger upgrade(). + vm.expectRevert("Initializable: contract is already initialized"); + optimismPortal2.upgrade(IAnchorStateRegistry(address(0xdeadbeef)), true); + } +} + /// @title OptimismPortal2_ResourceFuzz_Test /// @dev Test various values of the resource metering config to ensure that deposits cannot be /// broken by changing the config. diff --git a/packages/contracts-bedrock/test/L1/SystemConfig.t.sol b/packages/contracts-bedrock/test/L1/SystemConfig.t.sol index fbae6b41524b3..9fc8dfc974414 100644 --- a/packages/contracts-bedrock/test/L1/SystemConfig.t.sol +++ b/packages/contracts-bedrock/test/L1/SystemConfig.t.sol @@ -4,6 +4,9 @@ pragma solidity 0.8.15; // Testing import { CommonTest } from "test/setup/CommonTest.sol"; +// Scripts +import { ForgeArtifacts, StorageSlot } from "scripts/libraries/ForgeArtifacts.sol"; + // Libraries import { Constants } from "src/libraries/Constants.sol"; import { EIP1967Helper } from "test/mocks/EIP1967Helper.sol"; @@ -114,6 +117,7 @@ contract SystemConfig_Initialize_Test is SystemConfig_Init { assertEq(addrs.optimismPortal, address(optimismPortal2)); assertEq(address(systemConfig.optimismMintableERC20Factory()), address(optimismMintableERC20Factory)); assertEq(addrs.optimismMintableERC20Factory, address(optimismMintableERC20Factory)); + assertNotEq(systemConfig.l2ChainId(), 0); } } @@ -145,7 +149,8 @@ contract SystemConfig_Initialize_TestFail is SystemConfig_Initialize_Test { disputeGameFactory: address(0), optimismPortal: address(0), optimismMintableERC20Factory: address(0) - }) + }), + _l2ChainId: 1234 }); } @@ -174,7 +179,8 @@ contract SystemConfig_Initialize_TestFail is SystemConfig_Initialize_Test { disputeGameFactory: address(0), optimismPortal: address(0), optimismMintableERC20Factory: address(0) - }) + }), + _l2ChainId: 1234 }); assertEq(systemConfig.startBlock(), block.number); } @@ -204,7 +210,8 @@ contract SystemConfig_Initialize_TestFail is SystemConfig_Initialize_Test { disputeGameFactory: address(0), optimismPortal: address(0), optimismMintableERC20Factory: address(0) - }) + }), + _l2ChainId: 1234 }); assertEq(systemConfig.startBlock(), 1); } @@ -318,7 +325,8 @@ contract SystemConfig_Init_ResourceConfig is SystemConfig_Init { disputeGameFactory: address(0), optimismPortal: address(0), optimismMintableERC20Factory: address(0) - }) + }), + _l2ChainId: 1234 }); } } @@ -480,3 +488,63 @@ contract SystemConfig_Setters_Test is SystemConfig_Init { assertEq(systemConfig.eip1559Elasticity(), _elasticity); } } + +/// @title SystemConfig_upgrade_Test +/// @notice Reusable test for the current upgrade() function in the SystemConfig contract. If +/// the upgrade() function is changed, tests inside of this contract should be updated to +/// reflect the new function. If the upgrade() function is removed, remove the +/// corresponding tests but leave this contract in place so it's easy to add tests back +/// in the future. +contract SystemConfig_upgrade_Test is SystemConfig_Init { + /// @notice Tests that the upgrade() function succeeds. + function test_upgrade_succeeds() external { + // Get the slot for _initialized. + StorageSlot memory slot = ForgeArtifacts.getSlot("SystemConfig", "_initialized"); + + // Set the initialized slot to 0. + vm.store(address(systemConfig), bytes32(slot.slot), bytes32(0)); + + // Trigger upgrade(). + systemConfig.upgrade(1234); + + // Verify that the initialized slot was updated. + bytes32 initializedSlotAfter = vm.load(address(systemConfig), bytes32(slot.slot)); + assertEq(initializedSlotAfter, bytes32(uint256(2))); + + // Verify that the l2ChainId was updated. + assertEq(systemConfig.l2ChainId(), 1234); + } + + /// @notice Tests that the upgrade() function reverts if called a second time. + function test_upgrade_upgradeTwice_reverts() external { + // Get the slot for _initialized. + StorageSlot memory slot = ForgeArtifacts.getSlot("SystemConfig", "_initialized"); + + // Set the initialized slot to 0. + vm.store(address(systemConfig), bytes32(slot.slot), bytes32(0)); + + // Trigger first upgrade. + systemConfig.upgrade(1234); + + // Try to trigger second upgrade. + vm.expectRevert("Initializable: contract is already initialized"); + systemConfig.upgrade(1234); + } + + /// @notice Tests that the upgrade() function reverts if called after initialization. + function test_upgrade_afterInitialization_reverts() external { + // Get the slot for _initialized. + StorageSlot memory slot = ForgeArtifacts.getSlot("SystemConfig", "_initialized"); + + // Slot value should be set to 2 (already initialized). + bytes32 initializedSlotBefore = vm.load(address(systemConfig), bytes32(slot.slot)); + assertEq(initializedSlotBefore, bytes32(uint256(2))); + + // l2ChainId should be non-zero. + assertNotEq(systemConfig.l2ChainId(), 0); + + // Try to trigger upgrade(). + vm.expectRevert("Initializable: contract is already initialized"); + systemConfig.upgrade(1234); + } +} diff --git a/packages/contracts-bedrock/test/invariants/SystemConfig.t.sol b/packages/contracts-bedrock/test/invariants/SystemConfig.t.sol index 4ca84d96b0209..2107e15dc5021 100644 --- a/packages/contracts-bedrock/test/invariants/SystemConfig.t.sol +++ b/packages/contracts-bedrock/test/invariants/SystemConfig.t.sol @@ -45,7 +45,8 @@ contract SystemConfig_GasLimitBoundaries_Invariant is Test { disputeGameFactory: address(0), optimismPortal: address(0), optimismMintableERC20Factory: address(0) - }) + }), + 1234 // _l2ChainId ) ) ); diff --git a/packages/contracts-bedrock/test/libraries/Encoding.t.sol b/packages/contracts-bedrock/test/libraries/Encoding.t.sol index 277cce328dcdd..76ab3343d74c6 100644 --- a/packages/contracts-bedrock/test/libraries/Encoding.t.sol +++ b/packages/contracts-bedrock/test/libraries/Encoding.t.sol @@ -1,17 +1,24 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.15; -// Testing utilities +// Testing import { CommonTest } from "test/setup/CommonTest.sol"; // Libraries +import { Encoding } from "src/libraries/Encoding.sol"; import { Types } from "src/libraries/Types.sol"; import { LegacyCrossDomainUtils } from "src/libraries/LegacyCrossDomainUtils.sol"; -// Target contract -import { Encoding } from "src/libraries/Encoding.sol"; +contract Encoding_TestInit is CommonTest { + Encoding_Harness encoding; -contract Encoding_Test is CommonTest { + function setUp() public override { + super.setUp(); + encoding = new Encoding_Harness(); + } +} + +contract Encoding_Test is Encoding_TestInit { /// @dev Tests encoding and decoding a nonce and version. function testFuzz_nonceVersioning_succeeds(uint240 _nonce, uint16 _version) external pure { (uint240 nonce, uint16 version) = Encoding.decodeVersionedNonce(Encoding.encodeVersionedNonce(_nonce, _version)); @@ -77,8 +84,6 @@ contract Encoding_Test is CommonTest { uint256 minInvalidNonce = (uint256(type(uint240).max) + 1) * 2; nonce = bound(nonce, minInvalidNonce, type(uint256).max); - EncodingContract encoding = new EncodingContract(); - vm.expectRevert(bytes("Encoding: unknown cross domain message version")); encoding.encodeCrossDomainMessage(nonce, address(this), address(this), 1, 100, hex""); } @@ -107,7 +112,182 @@ contract Encoding_Test is CommonTest { } } -contract EncodingContract { +contract Encoding_encodeSuperRootProof_Test is Encoding_TestInit { + /// @notice Tests successful encoding of a valid super root proof + /// @param _timestamp The timestamp of the super root proof + /// @param _length The number of output roots in the super root proof + /// @param _seed The seed used to generate the output roots + function testFuzz_encodeSuperRootProof_succeeds(uint64 _timestamp, uint256 _length, uint256 _seed) external pure { + // Ensure at least 1 element and cap at a reasonable maximum to avoid gas issues + _length = uint256(bound(_length, 1, 50)); + + // Create output roots array + Types.OutputRootWithChainId[] memory outputRoots = new Types.OutputRootWithChainId[](_length); + + // Generate deterministic chain IDs and roots based on the seed + for (uint256 i = 0; i < _length; i++) { + // Use different derivations of the seed for each value + uint256 chainId = uint256(keccak256(abi.encode(_seed, "chainId", i))); + bytes32 root = keccak256(abi.encode(_seed, "root", i)); + + outputRoots[i] = Types.OutputRootWithChainId({ chainId: chainId, root: root }); + } + + // Create the super root proof + Types.SuperRootProof memory proof = + Types.SuperRootProof({ version: 0x01, timestamp: _timestamp, outputRoots: outputRoots }); + + // Encode the proof + bytes memory encoded = Encoding.encodeSuperRootProof(proof); + + // Verify encoding structure + assertEq(encoded[0], bytes1(0x01), "Version byte should be 0x01"); + + // Verify timestamp (bytes 1-8) + bytes8 encodedTimestamp; + for (uint256 i = 0; i < 8; i++) { + encodedTimestamp |= bytes8(encoded[i + 1]) >> (i * 8); + } + assertEq(uint64(encodedTimestamp), _timestamp, "Timestamp should match"); + + // Verify each chain ID and root is encoded correctly + uint256 offset = 9; // 1 byte version + 8 bytes timestamp + for (uint256 i = 0; i < _length; i++) { + // Extract chain ID (32 bytes) + uint256 encodedChainId; + assembly { + // Load 32 bytes from encoded at position offset + encodedChainId := mload(add(add(encoded, 32), offset)) + } + assertEq(encodedChainId, outputRoots[i].chainId, "Chain ID should match"); + offset += 32; + + // Extract root (32 bytes) + bytes32 encodedRoot; + assembly { + // Load 32 bytes from encoded at position offset + encodedRoot := mload(add(add(encoded, 32), offset)) + } + assertEq(encodedRoot, outputRoots[i].root, "Root should match"); + offset += 32; + } + + // Verify total length + assertEq(encoded.length, 9 + (_length * 64), "Encoded length should match expected"); + } + + /// @notice Tests encoding with a single output root + function test_encodeSuperRootProof_singleOutputRoot_succeeds() external pure { + // Create a single output root + Types.OutputRootWithChainId[] memory outputRoots = new Types.OutputRootWithChainId[](1); + outputRoots[0] = Types.OutputRootWithChainId({ chainId: 10, root: bytes32(uint256(0xdeadbeef)) }); + + // Create the super root proof + Types.SuperRootProof memory proof = + Types.SuperRootProof({ version: 0x01, timestamp: 1234567890, outputRoots: outputRoots }); + + // Encode the proof + bytes memory encoded = Encoding.encodeSuperRootProof(proof); + + // Expected: 1 byte version + 8 bytes timestamp + (32 bytes chainId + 32 bytes root) + assertEq(encoded.length, 1 + 8 + 64, "Encoded length should be 73 bytes"); + assertEq(encoded[0], bytes1(0x01), "First byte should be version 0x01"); + } + + /// @notice Tests encoding with multiple output roots + function test_encodeSuperRootProof_multipleOutputRoots_succeeds() external pure { + // Create multiple output roots + Types.OutputRootWithChainId[] memory outputRoots = new Types.OutputRootWithChainId[](3); + outputRoots[0] = Types.OutputRootWithChainId({ chainId: 10, root: bytes32(uint256(0xdeadbeef)) }); + outputRoots[1] = Types.OutputRootWithChainId({ chainId: 20, root: bytes32(uint256(0xbeefcafe)) }); + outputRoots[2] = Types.OutputRootWithChainId({ chainId: 30, root: bytes32(uint256(0xcafebabe)) }); + + // Create the super root proof + Types.SuperRootProof memory proof = + Types.SuperRootProof({ version: 0x01, timestamp: 1234567890, outputRoots: outputRoots }); + + // Encode the proof + bytes memory encoded = Encoding.encodeSuperRootProof(proof); + + // Expected: 1 byte version + 8 bytes timestamp + 3 * (32 bytes chainId + 32 bytes root) + assertEq(encoded.length, 1 + 8 + (3 * 64), "Encoded length should be 201 bytes"); + } + + /// @notice Tests that the Solidity impl of encodeSuperRootProof matches the FFI impl + /// @param _timestamp The timestamp of the super root proof + /// @param _length The number of output roots in the super root proof + /// @param _seed The seed used to generate the output roots + function testDiff_encodeSuperRootProof_succeeds(uint64 _timestamp, uint256 _length, uint256 _seed) external { + // Ensure at least 1 element and cap at a reasonable maximum to avoid gas issues + _length = uint256(bound(_length, 1, 50)); + + // Create output roots array + Types.OutputRootWithChainId[] memory outputRoots = new Types.OutputRootWithChainId[](_length); + + // Generate deterministic chain IDs and roots based on the seed + for (uint256 i = 0; i < _length; i++) { + // Use different derivations of the seed for each value + uint256 chainId = uint256(keccak256(abi.encode(_seed, "chainId", i))); + bytes32 root = keccak256(abi.encode(_seed, "root", i)); + + outputRoots[i] = Types.OutputRootWithChainId({ chainId: chainId, root: root }); + } + + // Create the super root proof + Types.SuperRootProof memory proof = + Types.SuperRootProof({ version: 0x01, timestamp: _timestamp, outputRoots: outputRoots }); + + // Encode using the Solidity implementation + bytes memory encoding1 = Encoding.encodeSuperRootProof(proof); + + // Encode using the FFI implementation + bytes memory encoding2 = ffi.encodeSuperRootProof(proof); + + // Compare the results + assertEq(encoding1, encoding2, "Solidity and FFI implementations should match"); + } +} + +contract Encoding_encodeSuperRootProof_TestFail is Encoding_TestInit { + /// @notice Tests that encoding fails when version is not 0x01 + /// @param _version The version to use for the super root proof + /// @param _timestamp The timestamp of the super root proof + function testFuzz_encodeSuperRootProof_invalidVersion_reverts(bytes1 _version, uint64 _timestamp) external { + // Ensure version is not 0x01 + if (_version == 0x01) { + _version = 0x02; + } + + // Create a minimal valid output roots array + Types.OutputRootWithChainId[] memory outputRoots = new Types.OutputRootWithChainId[](1); + outputRoots[0] = Types.OutputRootWithChainId({ chainId: 1, root: bytes32(uint256(1)) }); + + // Create the super root proof with invalid version + Types.SuperRootProof memory proof = + Types.SuperRootProof({ version: _version, timestamp: _timestamp, outputRoots: outputRoots }); + + // Expect revert when encoding + vm.expectRevert(Encoding.Encoding_InvalidSuperRootVersion.selector); + encoding.encodeSuperRootProof(proof); + } + + /// @notice Tests that encoding fails when output roots array is empty + /// @param _timestamp The timestamp of the super root proof + function testFuzz_encodeSuperRootProof_emptyOutputRoots_reverts(uint64 _timestamp) external { + // Create an empty output roots array + Types.OutputRootWithChainId[] memory outputRoots = new Types.OutputRootWithChainId[](0); + + // Create the super root proof with empty output roots + Types.SuperRootProof memory proof = + Types.SuperRootProof({ version: 0x01, timestamp: _timestamp, outputRoots: outputRoots }); + + // Expect revert when encoding + vm.expectRevert(Encoding.Encoding_EmptySuperRoot.selector); + encoding.encodeSuperRootProof(proof); + } +} + +contract Encoding_Harness { function encodeCrossDomainMessage( uint256 nonce, address sender, @@ -122,4 +302,8 @@ contract EncodingContract { { return Encoding.encodeCrossDomainMessage(nonce, sender, target, value, gasLimit, data); } + + function encodeSuperRootProof(Types.SuperRootProof memory proof) external pure returns (bytes memory) { + return Encoding.encodeSuperRootProof(proof); + } } diff --git a/packages/contracts-bedrock/test/libraries/Hashing.t.sol b/packages/contracts-bedrock/test/libraries/Hashing.t.sol index 45242acb088c6..73aef7cb22023 100644 --- a/packages/contracts-bedrock/test/libraries/Hashing.t.sol +++ b/packages/contracts-bedrock/test/libraries/Hashing.t.sol @@ -12,6 +12,12 @@ import { LegacyCrossDomainUtils } from "src/libraries/LegacyCrossDomainUtils.sol // Target contract import { Hashing } from "src/libraries/Hashing.sol"; +contract Hashing_Harness { + function hashSuperRootProof(Types.SuperRootProof memory _proof) external pure returns (bytes32) { + return Hashing.hashSuperRootProof(_proof); + } +} + contract Hashing_hashDepositSource_Test is CommonTest { /// @notice Tests that hashDepositSource returns the correct hash in a simple case. function test_hashDepositSource_succeeds() external pure { @@ -136,3 +142,51 @@ contract Hashing_hashDepositTransaction_Test is CommonTest { ); } } + +contract Hashing_hashSuperRootProof_Test is CommonTest { + Hashing_Harness internal harness; + + /// @notice Sets up the test. + function setUp() public override { + super.setUp(); + harness = new Hashing_Harness(); + } + + /// @notice Tests that the Solidity impl of hashSuperRootProof matches the FFI impl + /// @param _proof The super root proof to test. + function testDiff_hashSuperRootProof_succeeds(Types.SuperRootProof memory _proof) external { + // Make sure the proof has the right version. + _proof.version = 0x01; + + // Make sure the proof has at least one output root. + if (_proof.outputRoots.length == 0) { + _proof.outputRoots = new Types.OutputRootWithChainId[](1); + _proof.outputRoots[0] = Types.OutputRootWithChainId({ + chainId: vm.randomUint(0, type(uint64).max), + root: bytes32(vm.randomUint()) + }); + } + + // Encode using the Solidity implementation + bytes32 hash1 = harness.hashSuperRootProof(_proof); + + // Encode using the FFI implementation + bytes32 hash2 = ffi.hashSuperRootProof(_proof); + + // Compare the results + assertEq(hash1, hash2, "Solidity and FFI implementations should match"); + } + + /// @notice Tests that hashSuperRootProof reverts when the version is incorrect. + /// @param _proof The super root proof to test. + function testFuzz_hashSuperRootProof_wrongVersion_reverts(Types.SuperRootProof memory _proof) external { + // 0x01 is the correct version, so we need any other version. + if (_proof.version == 0x01) { + _proof.version = 0x00; + } + + // Should always revert when the version is incorrect. + vm.expectRevert(Encoding.Encoding_InvalidSuperRootVersion.selector); + harness.hashSuperRootProof(_proof); + } +} diff --git a/packages/contracts-bedrock/test/opcm/UpgradeOPChain.t.sol b/packages/contracts-bedrock/test/opcm/UpgradeOPChain.t.sol index 06e20c078c122..8b4944be7077a 100644 --- a/packages/contracts-bedrock/test/opcm/UpgradeOPChain.t.sol +++ b/packages/contracts-bedrock/test/opcm/UpgradeOPChain.t.sol @@ -55,7 +55,8 @@ contract UpgradeOPChainInput_Test is Test { configs[0] = OPContractsManager.OpChainConfig({ systemConfigProxy: ISystemConfig(systemConfig1), proxyAdmin: IProxyAdmin(proxyAdmin1), - absolutePrestate: Claim.wrap(bytes32(uint256(1))) + absolutePrestate: Claim.wrap(bytes32(uint256(1))), + disputeGameUsesSuperRoots: false }); // Setup mock addresses and contracts for second config @@ -67,7 +68,8 @@ contract UpgradeOPChainInput_Test is Test { configs[1] = OPContractsManager.OpChainConfig({ systemConfigProxy: ISystemConfig(systemConfig2), proxyAdmin: IProxyAdmin(proxyAdmin2), - absolutePrestate: Claim.wrap(bytes32(uint256(2))) + absolutePrestate: Claim.wrap(bytes32(uint256(2))), + disputeGameUsesSuperRoots: false }); input.set(input.opChainConfigs.selector, configs); @@ -111,7 +113,8 @@ contract UpgradeOPChainInput_Test is Test { configs[0] = OPContractsManager.OpChainConfig({ systemConfigProxy: ISystemConfig(mockSystemConfig), proxyAdmin: IProxyAdmin(mockProxyAdmin), - absolutePrestate: Claim.wrap(bytes32(uint256(1))) + absolutePrestate: Claim.wrap(bytes32(uint256(1))), + disputeGameUsesSuperRoots: false }); vm.expectRevert("UpgradeOPCMInput: unknown selector"); @@ -147,7 +150,8 @@ contract UpgradeOPChain_Test is Test { config = OPContractsManager.OpChainConfig({ systemConfigProxy: ISystemConfig(makeAddr("systemConfigProxy")), proxyAdmin: IProxyAdmin(makeAddr("proxyAdmin")), - absolutePrestate: Claim.wrap(keccak256("absolutePrestate")) + absolutePrestate: Claim.wrap(keccak256("absolutePrestate")), + disputeGameUsesSuperRoots: false }); OPContractsManager.OpChainConfig[] memory configs = new OPContractsManager.OpChainConfig[](1); configs[0] = config; diff --git a/packages/contracts-bedrock/test/setup/FFIInterface.sol b/packages/contracts-bedrock/test/setup/FFIInterface.sol index c1e1612da8e6b..97ed21cbc7fa6 100644 --- a/packages/contracts-bedrock/test/setup/FFIInterface.sol +++ b/packages/contracts-bedrock/test/setup/FFIInterface.sol @@ -188,6 +188,28 @@ contract FFIInterface { return abi.decode(result, (bytes)); } + function encodeSuperRootProof(Types.SuperRootProof calldata proof) external returns (bytes memory) { + string[] memory cmds = new string[](4); + cmds[0] = "scripts/go-ffi/go-ffi"; + cmds[1] = "diff"; + cmds[2] = "encodeSuperRootProof"; + cmds[3] = vm.toString(abi.encode(proof)); + + bytes memory result = Process.run(cmds); + return abi.decode(result, (bytes)); + } + + function hashSuperRootProof(Types.SuperRootProof calldata proof) external returns (bytes32) { + string[] memory cmds = new string[](4); + cmds[0] = "scripts/go-ffi/go-ffi"; + cmds[1] = "diff"; + cmds[2] = "hashSuperRootProof"; + cmds[3] = vm.toString(abi.encode(proof)); + + bytes memory result = Process.run(cmds); + return abi.decode(result, (bytes32)); + } + function decodeVersionedNonce(uint256 nonce) external returns (uint256, uint256) { string[] memory cmds = new string[](4); cmds[0] = "scripts/go-ffi/go-ffi"; diff --git a/packages/contracts-bedrock/test/setup/ForkLive.s.sol b/packages/contracts-bedrock/test/setup/ForkLive.s.sol index 45c3127342854..50c78b61588e7 100644 --- a/packages/contracts-bedrock/test/setup/ForkLive.s.sol +++ b/packages/contracts-bedrock/test/setup/ForkLive.s.sol @@ -174,7 +174,8 @@ contract ForkLive is Deployer { opChains[0] = IOPContractsManager.OpChainConfig({ systemConfigProxy: systemConfig, proxyAdmin: proxyAdmin, - absolutePrestate: Claim.wrap(bytes32(keccak256("absolutePrestate"))) + absolutePrestate: Claim.wrap(bytes32(keccak256("absolutePrestate"))), + disputeGameUsesSuperRoots: false }); // Temporarily replace the upgrader with a DelegateCaller so we can test the upgrade, diff --git a/packages/contracts-bedrock/test/universal/ReinitializableBase.t.sol b/packages/contracts-bedrock/test/universal/ReinitializableBase.t.sol new file mode 100644 index 0000000000000..1c3f7d93eb052 --- /dev/null +++ b/packages/contracts-bedrock/test/universal/ReinitializableBase.t.sol @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.15; + +// Testing +import { Test } from "forge-std/Test.sol"; + +// Contracts +import { ReinitializableBase } from "src/universal/ReinitializableBase.sol"; + +contract ReinitializableBase_Test is Test { + /// @notice Tests that the contract is created correctly and initVersion returns the right + /// value when the provided init version is non-zero. + /// @param _initVersion The init version to use when creating the contract. + function testFuzz_initVersion_validVersion_succeeds(uint8 _initVersion) public { + // Zero version not allowed. + _initVersion = uint8(bound(_initVersion, 1, type(uint8).max)); + + // Deploy the reinitializable contract. + ReinitializableBase_Harness harness = new ReinitializableBase_Harness(_initVersion); + + // Check the init version. + assertEq(harness.initVersion(), _initVersion); + } +} + +contract ReinitializableBase_TestFail is Test { + /// @notice Tests that the contract creation reverts when the init version is zero. + function test_initVersion_zeroVersion_reverts() public { + vm.expectRevert(ReinitializableBase.ReinitializableBase_ZeroInitVersion.selector); + new ReinitializableBase_Harness(0); + } +} + +contract ReinitializableBase_Harness is ReinitializableBase { + constructor(uint8 _initVersion) ReinitializableBase(_initVersion) { } +} diff --git a/packages/contracts-bedrock/test/universal/Specs.t.sol b/packages/contracts-bedrock/test/universal/Specs.t.sol index 92b2bef16a8cf..b650459d1f19e 100644 --- a/packages/contracts-bedrock/test/universal/Specs.t.sol +++ b/packages/contracts-bedrock/test/universal/Specs.t.sol @@ -226,14 +226,25 @@ contract Specification_Test is CommonTest { }); _addSpec({ _name: "OptimismPortalInterop", _sel: _getSel("finalizedWithdrawals(bytes32)") }); _addSpec({ _name: "OptimismPortalInterop", _sel: _getSel("guardian()") }); - _addSpec({ _name: "OptimismPortalInterop", _sel: _getSel("initialize(address,address,address)") }); + _addSpec({ _name: "OptimismPortalInterop", _sel: _getSel("initialize(address,address,address,bool)") }); _addSpec({ _name: "OptimismPortalInterop", _sel: _getSel("l2Sender()") }); _addSpec({ _name: "OptimismPortalInterop", _sel: _getSel("minimumGasLimit(uint64)") }); _addSpec({ _name: "OptimismPortalInterop", _sel: _getSel("params()") }); _addSpec({ _name: "OptimismPortalInterop", _sel: _getSel("paused()") }); + _addSpec({ _name: "OptimismPortalInterop", _sel: _getSel("initVersion()") }); + _addSpec({ _name: "OptimismPortalInterop", _sel: _getSel("superRootsActive()") }); _addSpec({ _name: "OptimismPortalInterop", - _sel: IOptimismPortal2.proveWithdrawalTransaction.selector, + _sel: _getSel( + "proveWithdrawalTransaction((uint256,address,address,uint256,uint256,bytes),uint256,(bytes32,bytes32,bytes32,bytes32),bytes[])" + ), + _pausable: true + }); + _addSpec({ + _name: "OptimismPortalInterop", + _sel: _getSel( + "proveWithdrawalTransaction((uint256,address,address,uint256,uint256,bytes),address,uint256,(bytes1,uint64,(uint256,bytes32)[]),(bytes32,bytes32,bytes32,bytes32),bytes[])" + ), _pausable: true }); _addSpec({ _name: "OptimismPortalInterop", _sel: _getSel("provenWithdrawals(bytes32,address)") }); @@ -247,7 +258,7 @@ contract Specification_Test is CommonTest { _addSpec({ _name: "OptimismPortalInterop", _sel: _getSel("respectedGameTypeUpdatedAt()") }); _addSpec({ _name: "OptimismPortalInterop", _sel: _getSel("proofSubmitters(bytes32,uint256)") }); _addSpec({ _name: "OptimismPortalInterop", _sel: _getSel("numProofSubmitters(bytes32)") }); - _addSpec({ _name: "OptimismPortalInterop", _sel: _getSel("upgrade(address)") }); + _addSpec({ _name: "OptimismPortalInterop", _sel: _getSel("upgrade(address,bool)") }); _addSpec({ _name: "OptimismPortalInterop", _sel: IOptimismPortalInterop.setConfig.selector, @@ -271,14 +282,25 @@ contract Specification_Test is CommonTest { }); _addSpec({ _name: "OptimismPortal2", _sel: _getSel("finalizedWithdrawals(bytes32)") }); _addSpec({ _name: "OptimismPortal2", _sel: _getSel("guardian()") }); - _addSpec({ _name: "OptimismPortal2", _sel: _getSel("initialize(address,address,address)") }); + _addSpec({ _name: "OptimismPortal2", _sel: _getSel("initialize(address,address,address,bool)") }); _addSpec({ _name: "OptimismPortal2", _sel: _getSel("l2Sender()") }); _addSpec({ _name: "OptimismPortal2", _sel: _getSel("minimumGasLimit(uint64)") }); _addSpec({ _name: "OptimismPortal2", _sel: _getSel("params()") }); _addSpec({ _name: "OptimismPortal2", _sel: _getSel("paused()") }); + _addSpec({ _name: "OptimismPortal2", _sel: _getSel("initVersion()") }); + _addSpec({ _name: "OptimismPortal2", _sel: _getSel("superRootsActive()") }); _addSpec({ _name: "OptimismPortal2", - _sel: IOptimismPortal2.proveWithdrawalTransaction.selector, + _sel: _getSel( + "proveWithdrawalTransaction((uint256,address,address,uint256,uint256,bytes),uint256,(bytes32,bytes32,bytes32,bytes32),bytes[])" + ), + _pausable: true + }); + _addSpec({ + _name: "OptimismPortal2", + _sel: _getSel( + "proveWithdrawalTransaction((uint256,address,address,uint256,uint256,bytes),address,uint256,(bytes1,uint64,(uint256,bytes32)[]),(bytes32,bytes32,bytes32,bytes32),bytes[])" + ), _pausable: true }); _addSpec({ _name: "OptimismPortal2", _sel: _getSel("provenWithdrawals(bytes32,address)") }); @@ -292,7 +314,7 @@ contract Specification_Test is CommonTest { _addSpec({ _name: "OptimismPortal2", _sel: _getSel("respectedGameTypeUpdatedAt()") }); _addSpec({ _name: "OptimismPortal2", _sel: _getSel("proofSubmitters(bytes32,uint256)") }); _addSpec({ _name: "OptimismPortal2", _sel: _getSel("numProofSubmitters(bytes32)") }); - _addSpec({ _name: "OptimismPortal2", _sel: _getSel("upgrade(address)") }); + _addSpec({ _name: "OptimismPortal2", _sel: _getSel("upgrade(address,bool)") }); // ProtocolVersions _addSpec({ _name: "ProtocolVersions", _sel: _getSel("RECOMMENDED_SLOT()") }); @@ -380,6 +402,9 @@ contract Specification_Test is CommonTest { _addSpec({ _name: "SystemConfig", _sel: _getSel("blobbasefeeScalar()") }); _addSpec({ _name: "SystemConfig", _sel: _getSel("maximumGasLimit()") }); _addSpec({ _name: "SystemConfig", _sel: _getSel("getAddresses()") }); + _addSpec({ _name: "SystemConfig", _sel: _getSel("l2ChainId()") }); + _addSpec({ _name: "SystemConfig", _sel: _getSel("upgrade(uint256)") }); + _addSpec({ _name: "SystemConfig", _sel: _getSel("initVersion()") }); // SystemConfigInterop _addSpec({ _name: "SystemConfigInterop", _sel: _getSel("UNSAFE_BLOCK_SIGNER_SLOT()") }); @@ -460,7 +485,9 @@ contract Specification_Test is CommonTest { }); _addSpec({ _name: "SystemConfigInterop", _sel: _getSel("dependencyManager()") }); _addSpec({ _name: "SystemConfigInterop", _sel: _getSel("getAddresses()") }); - + _addSpec({ _name: "SystemConfigInterop", _sel: _getSel("l2ChainId()") }); + _addSpec({ _name: "SystemConfigInterop", _sel: _getSel("upgrade(uint256)") }); + _addSpec({ _name: "SystemConfigInterop", _sel: _getSel("initVersion()") }); // ProxyAdmin _addSpec({ _name: "ProxyAdmin", _sel: _getSel("addressManager()") }); _addSpec({ diff --git a/packages/contracts-bedrock/test/vendor/Initializable.t.sol b/packages/contracts-bedrock/test/vendor/Initializable.t.sol index 8464682c88c45..fa48af8acf2ca 100644 --- a/packages/contracts-bedrock/test/vendor/Initializable.t.sol +++ b/packages/contracts-bedrock/test/vendor/Initializable.t.sol @@ -123,7 +123,7 @@ contract Initializer_Test is CommonTest { name: "OptimismPortal2Impl", target: EIP1967Helper.getImplementation(address(optimismPortal2)), initCalldata: abi.encodeCall( - optimismPortal2.initialize, (systemConfig, superchainConfig, anchorStateRegistry) + optimismPortal2.initialize, (systemConfig, superchainConfig, anchorStateRegistry, false) ) }) ); @@ -133,7 +133,7 @@ contract Initializer_Test is CommonTest { name: "OptimismPortal2Proxy", target: address(optimismPortal2), initCalldata: abi.encodeCall( - optimismPortal2.initialize, (systemConfig, superchainConfig, anchorStateRegistry) + optimismPortal2.initialize, (systemConfig, superchainConfig, anchorStateRegistry, false) ) }) ); @@ -167,7 +167,8 @@ contract Initializer_Test is CommonTest { disputeGameFactory: address(0), optimismPortal: address(0), optimismMintableERC20Factory: address(0) - }) + }), + 0 ) ) }) @@ -202,7 +203,8 @@ contract Initializer_Test is CommonTest { disputeGameFactory: address(0), optimismPortal: address(0), optimismMintableERC20Factory: address(0) - }) + }), + 0 ) ) }) From ccf9270141d41be74eba13af861989400b569e4c Mon Sep 17 00:00:00 2001 From: Disco <131301107+0xDiscotech@users.noreply.github.com> Date: Mon, 3 Mar 2025 17:28:02 -0300 Subject: [PATCH 10/50] fix: pr second comments (#295) * chore: undo initializable v5 test changes * chore: remove portal already authorized check * chore: remove portal already authorized check * refactor: rename pao to proxy admin owner --- .../interfaces/L1/IETHLockbox.sol | 7 +- .../interfaces/L1/IOptimismPortal2.sol | 4 +- .../interfaces/L1/IOptimismPortalInterop.sol | 4 +- .../interfaces/L1/IPAOBase.sol | 6 - .../interfaces/L1/IProxyAdminOwnerBase.sol | 6 + .../scripts/deploy/ChainAssertions.sol | 2 +- .../scripts/deploy/DeployOPChain.s.sol | 4 +- .../contracts-bedrock/src/L1/ETHLockbox.sol | 25 ++-- .../src/L1/OptimismPortal2.sol | 8 +- .../{PAOBase.sol => ProxyAdminOwnerBase.sol} | 10 +- .../test/L1/ETHLockbox.t.sol | 138 +++++++----------- .../test/L1/OptimismPortal2.t.sol | 16 +- .../test/universal/Specs.t.sol | 10 +- .../test/vendor/InitializableOZv5.t.sol | 26 +--- 14 files changed, 108 insertions(+), 158 deletions(-) delete mode 100644 packages/contracts-bedrock/interfaces/L1/IPAOBase.sol create mode 100644 packages/contracts-bedrock/interfaces/L1/IProxyAdminOwnerBase.sol rename packages/contracts-bedrock/src/L1/{PAOBase.sol => ProxyAdminOwnerBase.sol} (76%) diff --git a/packages/contracts-bedrock/interfaces/L1/IETHLockbox.sol b/packages/contracts-bedrock/interfaces/L1/IETHLockbox.sol index 849b47247a7d7..c7d2d5fdc2e2b 100644 --- a/packages/contracts-bedrock/interfaces/L1/IETHLockbox.sol +++ b/packages/contracts-bedrock/interfaces/L1/IETHLockbox.sol @@ -3,15 +3,14 @@ pragma solidity ^0.8.0; import { ISemver } from "interfaces/universal/ISemver.sol"; import { ISuperchainConfig } from "interfaces/L1/ISuperchainConfig.sol"; -import { IPAOBase } from "interfaces/L1/IPAOBase.sol"; +import { IProxyAdminOwnerBase } from "interfaces/L1/IProxyAdminOwnerBase.sol"; import { IOptimismPortal2 } from "interfaces/L1/IOptimismPortal2.sol"; -interface IETHLockbox is IPAOBase, ISemver { +interface IETHLockbox is IProxyAdminOwnerBase, ISemver { error ETHLockbox_Unauthorized(); error ETHLockbox_Paused(); error ETHLockbox_NoWithdrawalTransactions(); - error ETHLockbox_AlreadyAuthorized(); - error ETHLockbox_DifferentPAO(); + error ETHLockbox_DifferentProxyAdminOwner(); event Initialized(uint8 version); event ETHLocked(address indexed portal, uint256 amount); diff --git a/packages/contracts-bedrock/interfaces/L1/IOptimismPortal2.sol b/packages/contracts-bedrock/interfaces/L1/IOptimismPortal2.sol index 4db6c003da562..71e0ff751f992 100644 --- a/packages/contracts-bedrock/interfaces/L1/IOptimismPortal2.sol +++ b/packages/contracts-bedrock/interfaces/L1/IOptimismPortal2.sol @@ -8,10 +8,10 @@ import { IDisputeGameFactory } from "interfaces/dispute/IDisputeGameFactory.sol" import { ISystemConfig } from "interfaces/L1/ISystemConfig.sol"; import { ISuperchainConfig } from "interfaces/L1/ISuperchainConfig.sol"; import { IAnchorStateRegistry } from "interfaces/dispute/IAnchorStateRegistry.sol"; -import { IPAOBase } from "interfaces/L1/IPAOBase.sol"; +import { IProxyAdminOwnerBase } from "interfaces/L1/IProxyAdminOwnerBase.sol"; import { IETHLockbox } from "interfaces/L1/IETHLockbox.sol"; -interface IOptimismPortal2 is IPAOBase { +interface IOptimismPortal2 is IProxyAdminOwnerBase { error OptimismPortal_Unauthorized(); error ContentLengthMismatch(); error EmptyItem(); diff --git a/packages/contracts-bedrock/interfaces/L1/IOptimismPortalInterop.sol b/packages/contracts-bedrock/interfaces/L1/IOptimismPortalInterop.sol index 6876b3a715e25..5e1c3dc4261f1 100644 --- a/packages/contracts-bedrock/interfaces/L1/IOptimismPortalInterop.sol +++ b/packages/contracts-bedrock/interfaces/L1/IOptimismPortalInterop.sol @@ -10,9 +10,9 @@ import { ISuperchainConfig } from "interfaces/L1/ISuperchainConfig.sol"; import { ConfigType } from "interfaces/L2/IL1BlockInterop.sol"; import { IAnchorStateRegistry } from "interfaces/dispute/IAnchorStateRegistry.sol"; import { IETHLockbox } from "interfaces/L1/IETHLockbox.sol"; -import { IPAOBase } from "interfaces/L1/IPAOBase.sol"; +import { IProxyAdminOwnerBase } from "interfaces/L1/IProxyAdminOwnerBase.sol"; -interface IOptimismPortalInterop is IPAOBase { +interface IOptimismPortalInterop is IProxyAdminOwnerBase { error ContentLengthMismatch(); error EmptyItem(); error InvalidDataRemainder(); diff --git a/packages/contracts-bedrock/interfaces/L1/IPAOBase.sol b/packages/contracts-bedrock/interfaces/L1/IPAOBase.sol deleted file mode 100644 index edfc72606784c..0000000000000 --- a/packages/contracts-bedrock/interfaces/L1/IPAOBase.sol +++ /dev/null @@ -1,6 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; - -interface IPAOBase { - function PAO() external view returns (address); -} diff --git a/packages/contracts-bedrock/interfaces/L1/IProxyAdminOwnerBase.sol b/packages/contracts-bedrock/interfaces/L1/IProxyAdminOwnerBase.sol new file mode 100644 index 0000000000000..fa04187811c57 --- /dev/null +++ b/packages/contracts-bedrock/interfaces/L1/IProxyAdminOwnerBase.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +interface IProxyAdminOwnerBase { + function proxyAdminOwner() external view returns (address); +} diff --git a/packages/contracts-bedrock/scripts/deploy/ChainAssertions.sol b/packages/contracts-bedrock/scripts/deploy/ChainAssertions.sol index 19265324fc112..bb6091e84f19a 100644 --- a/packages/contracts-bedrock/scripts/deploy/ChainAssertions.sol +++ b/packages/contracts-bedrock/scripts/deploy/ChainAssertions.sol @@ -424,7 +424,7 @@ library ChainAssertions { if (_isProxy) { require(ethLockbox.superchainConfig() == superchainConfig, "CHECK-ELB-20"); require(ethLockbox.authorizedPortals(_contracts.OptimismPortal), "CHECK-ELB-30"); - require(ethLockbox.PAO() == _cfg.finalSystemOwner(), "CHECK-ELB-40"); + require(ethLockbox.proxyAdminOwner() == _cfg.finalSystemOwner(), "CHECK-ELB-40"); } else { require(address(ethLockbox.superchainConfig()) == address(0), "CHECK-ELB-50"); require(ethLockbox.authorizedPortals(_contracts.OptimismPortal) == false, "CHECK-ELB-60"); diff --git a/packages/contracts-bedrock/scripts/deploy/DeployOPChain.s.sol b/packages/contracts-bedrock/scripts/deploy/DeployOPChain.s.sol index 87d112bd134db..3dd574a956b3c 100644 --- a/packages/contracts-bedrock/scripts/deploy/DeployOPChain.s.sol +++ b/packages/contracts-bedrock/scripts/deploy/DeployOPChain.s.sol @@ -624,7 +624,7 @@ contract DeployOPChain is Script { // Check once the portal is updated to use the new lockbox. require(address(portal.ethLockbox()) == address(_doo.ethLockboxProxy()), "PORTAL-90"); - require(portal.PAO() == _doi.opChainProxyAdminOwner(), "PORTAL-100"); + require(portal.proxyAdminOwner() == _doi.opChainProxyAdminOwner(), "PORTAL-100"); } function assertValidETHLockbox(DeployOPChainInput _doi, DeployOPChainOutput _doo) internal { @@ -632,7 +632,7 @@ contract DeployOPChain is Script { require(address(lockbox.superchainConfig()) == address(_doi.opcm().superchainConfig()), "ETHLOCKBOX-10"); require(lockbox.authorizedPortals(address(_doo.optimismPortalProxy())), "ETHLOCKBOX-20"); - require(lockbox.PAO() == _doi.opChainProxyAdminOwner(), "ETHLOCKBOX-30"); + require(lockbox.proxyAdminOwner() == _doi.opChainProxyAdminOwner(), "ETHLOCKBOX-30"); } function assertValidDisputeGameFactory(DeployOPChainInput _doi, DeployOPChainOutput _doo) internal { diff --git a/packages/contracts-bedrock/src/L1/ETHLockbox.sol b/packages/contracts-bedrock/src/L1/ETHLockbox.sol index 82cd74246a939..5cd82db232674 100644 --- a/packages/contracts-bedrock/src/L1/ETHLockbox.sol +++ b/packages/contracts-bedrock/src/L1/ETHLockbox.sol @@ -2,7 +2,7 @@ pragma solidity 0.8.25; // Contracts -import { PAOBase } from "src/L1/PAOBase.sol"; +import { ProxyAdminOwnerBase } from "src/L1/ProxyAdminOwnerBase.sol"; import { Initializable } from "@openzeppelin/contracts/proxy/utils/Initializable.sol"; // Libraries @@ -18,21 +18,18 @@ import { IETHLockbox } from "interfaces/L1/IETHLockbox.sol"; /// @title ETHLockbox /// @notice Manages ETH liquidity locking and unlocking for authorized OptimismPortals, enabling unified ETH liquidity /// management across chains in the superchain cluster. -contract ETHLockbox is PAOBase, Initializable, ISemver { +contract ETHLockbox is ProxyAdminOwnerBase, Initializable, ISemver { /// @notice Thrown when the lockbox is paused. error ETHLockbox_Paused(); /// @notice Thrown when the caller is not authorized. error ETHLockbox_Unauthorized(); - /// @notice Thrown when an already authorized portal or lockbox attempts to be authorized again. - error ETHLockbox_AlreadyAuthorized(); - /// @notice Thrown when attempting to unlock ETH from the lockbox through a withdrawal transaction. error ETHLockbox_NoWithdrawalTransactions(); /// @notice Thrown when the admin owner of the lockbox is different from the admin owner of the proxy admin. - error ETHLockbox_DifferentPAO(); + error ETHLockbox_DifferentProxyAdminOwner(); /// @notice Emitted when ETH is locked in the lockbox by an authorized portal. /// @param portal The address of the portal that locked the ETH. @@ -99,7 +96,7 @@ contract ETHLockbox is PAOBase, Initializable, ISemver { /// @notice Authorizes a portal to lock and unlock ETH. /// @param _portal The address of the portal to authorize. function authorizePortal(IOptimismPortal _portal) external { - if (msg.sender != PAO()) revert ETHLockbox_Unauthorized(); + if (msg.sender != proxyAdminOwner()) revert ETHLockbox_Unauthorized(); _authorizePortal(address(_portal)); } @@ -141,9 +138,8 @@ contract ETHLockbox is PAOBase, Initializable, ISemver { /// @notice Authorizes an ETH lockbox to migrate its liquidity to the current ETH lockbox. /// @param _lockbox The address of the ETH lockbox to authorize. function authorizeLockbox(IETHLockbox _lockbox) external { - if (msg.sender != PAO()) revert ETHLockbox_Unauthorized(); - if (!_samePAO(address(_lockbox))) revert ETHLockbox_DifferentPAO(); - if (authorizedLockboxes[address(_lockbox)]) revert ETHLockbox_AlreadyAuthorized(); + if (msg.sender != proxyAdminOwner()) revert ETHLockbox_Unauthorized(); + if (!_sameproxyAdminOwner(address(_lockbox))) revert ETHLockbox_DifferentProxyAdminOwner(); authorizedLockboxes[address(_lockbox)] = true; emit LockboxAuthorized(address(_lockbox)); @@ -152,20 +148,17 @@ contract ETHLockbox is PAOBase, Initializable, ISemver { /// @notice Migrates liquidity from the current ETH lockbox to another. /// @param _lockbox The address of the ETH lockbox to migrate liquidity to. function migrateLiquidity(IETHLockbox _lockbox) external { - if (msg.sender != PAO()) revert ETHLockbox_Unauthorized(); - if (!_samePAO(address(_lockbox))) revert ETHLockbox_DifferentPAO(); + if (msg.sender != proxyAdminOwner()) revert ETHLockbox_Unauthorized(); + if (!_sameproxyAdminOwner(address(_lockbox))) revert ETHLockbox_DifferentProxyAdminOwner(); IETHLockbox(_lockbox).receiveLiquidity{ value: address(this).balance }(); - emit LiquidityMigrated(address(_lockbox)); } /// @notice Authorizes a portal to lock and unlock ETH. /// @param _portal The address of the portal to authorize. function _authorizePortal(address _portal) internal { - if (!_samePAO(_portal)) revert ETHLockbox_DifferentPAO(); - if (authorizedPortals[_portal]) revert ETHLockbox_AlreadyAuthorized(); - + if (!_sameproxyAdminOwner(_portal)) revert ETHLockbox_DifferentProxyAdminOwner(); authorizedPortals[_portal] = true; emit PortalAuthorized(_portal); } diff --git a/packages/contracts-bedrock/src/L1/OptimismPortal2.sol b/packages/contracts-bedrock/src/L1/OptimismPortal2.sol index df3d0630eb55b..84b8745f80108 100644 --- a/packages/contracts-bedrock/src/L1/OptimismPortal2.sol +++ b/packages/contracts-bedrock/src/L1/OptimismPortal2.sol @@ -2,7 +2,7 @@ pragma solidity 0.8.15; // Contracts -import { PAOBase } from "src/L1/PAOBase.sol"; +import { ProxyAdminOwnerBase } from "src/L1/ProxyAdminOwnerBase.sol"; import { Initializable } from "@openzeppelin/contracts/proxy/utils/Initializable.sol"; import { ResourceMetering } from "src/L1/ResourceMetering.sol"; @@ -31,7 +31,7 @@ import { IETHLockbox } from "interfaces/L1/IETHLockbox.sol"; /// @notice The OptimismPortal is a low-level contract responsible for passing messages between L1 /// 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 OptimismPortal2 is PAOBase, Initializable, ResourceMetering, ISemver { +contract OptimismPortal2 is ProxyAdminOwnerBase, Initializable, ResourceMetering, ISemver { /// @notice Represents a proven withdrawal. /// @custom:field disputeGameProxy Game that the withdrawal was proven against. /// @custom:field timestamp Timestamp at which the withdrawal was proven. @@ -328,7 +328,7 @@ contract OptimismPortal2 is PAOBase, Initializable, ResourceMetering, ISemver { /// @notice Updates the ETHLockbox contract. /// @param _newLockbox The address of the new ETHLockbox contract. function updateLockbox(IETHLockbox _newLockbox) external { - if (msg.sender != PAO()) revert OptimismPortal_Unauthorized(); + if (msg.sender != proxyAdminOwner()) revert OptimismPortal_Unauthorized(); address oldLockbox = address(ethLockbox); ethLockbox = IETHLockbox(_newLockbox); @@ -536,7 +536,7 @@ contract OptimismPortal2 is PAOBase, Initializable, ResourceMetering, ISemver { /// @notice Migrates the total ETH balance to the ETHLockbox. function migrateLiquidity() public { - if (msg.sender != PAO()) revert OptimismPortal_Unauthorized(); + if (msg.sender != proxyAdminOwner()) revert OptimismPortal_Unauthorized(); _migrateLiquidity(); } diff --git a/packages/contracts-bedrock/src/L1/PAOBase.sol b/packages/contracts-bedrock/src/L1/ProxyAdminOwnerBase.sol similarity index 76% rename from packages/contracts-bedrock/src/L1/PAOBase.sol rename to packages/contracts-bedrock/src/L1/ProxyAdminOwnerBase.sol index ecf5524d0c7ba..16883227e18cd 100644 --- a/packages/contracts-bedrock/src/L1/PAOBase.sol +++ b/packages/contracts-bedrock/src/L1/ProxyAdminOwnerBase.sol @@ -6,11 +6,11 @@ import { Storage } from "src/libraries/Storage.sol"; import { Constants } from "src/libraries/Constants.sol"; /// @notice Base contract for ProxyAdmin-owned contracts. It's main goal is to expose the ProxyAdmin owner address on -/// a function and also to check if the current contract and a given proxy have the same PAO. -abstract contract PAOBase { +/// a function and also to check if the current contract and a given proxy have the same ProxyAdmin owner. +abstract contract ProxyAdminOwnerBase { /// @notice Getter for the owner of the ProxyAdmin. /// The ProxyAdmin is the owner of the current proxy contract. - function PAO() public view returns (address) { + function proxyAdminOwner() public view returns (address) { // Get the proxy admin address reading for the reserved slot it has on the Proxy contract. IProxyAdmin proxyAdmin = IProxyAdmin(Storage.getAddress(Constants.PROXY_OWNER_ADDRESS)); // Return the owner of the proxy admin. @@ -20,7 +20,7 @@ abstract contract PAOBase { /// @notice Checks if the ProxyAdmin owner of the current contract is the same as the ProxyAdmin owner of the given /// proxy. /// @param _proxy The address of the proxy to check. - function _samePAO(address _proxy) internal view returns (bool) { - return PAO() == PAOBase(_proxy).PAO(); + function _sameproxyAdminOwner(address _proxy) internal view returns (bool) { + return proxyAdminOwner() == ProxyAdminOwnerBase(_proxy).proxyAdminOwner(); } } diff --git a/packages/contracts-bedrock/test/L1/ETHLockbox.t.sol b/packages/contracts-bedrock/test/L1/ETHLockbox.t.sol index eae5a819644f0..5640f02ab49ba 100644 --- a/packages/contracts-bedrock/test/L1/ETHLockbox.t.sol +++ b/packages/contracts-bedrock/test/L1/ETHLockbox.t.sol @@ -10,7 +10,7 @@ import { IOptimismPortal2 as IOptimismPortal } from "interfaces/L1/IOptimismPort import { IETHLockbox } from "interfaces/L1/IETHLockbox.sol"; import { ISuperchainConfig } from "interfaces/L1/ISuperchainConfig.sol"; -import { IPAOBase } from "interfaces/L1/IPAOBase.sol"; +import { IProxyAdminOwnerBase } from "interfaces/L1/IProxyAdminOwnerBase.sol"; import { IOptimismPortal2 } from "interfaces/L1/IOptimismPortal2.sol"; // Test @@ -30,11 +30,11 @@ contract ETHLockboxTest is CommonTest { event LiquidityReceived(address indexed lockbox); ProxyAdmin public proxyAdmin = ProxyAdmin(Predeploys.PROXY_ADMIN); - address public PAO; + address public proxyAdminOwner; function setUp() public virtual override { super.setUp(); - PAO = proxyAdmin.owner(); + proxyAdminOwner = proxyAdmin.owner(); } /// @notice Tests the superchain config was correctly set during initialization. @@ -51,8 +51,8 @@ contract ETHLockboxTest is CommonTest { } /// @notice Tests the proxy admin owner is correctly returned. - function test_proxyPAO_succeeds() public view { - assertEq(ethLockbox.PAO(), PAO); + function test_proxyProxyAdminOwner_succeeds() public view { + assertEq(ethLockbox.proxyAdminOwner(), proxyAdminOwner); } /// @notice Tests the paused status is correctly returned. @@ -76,11 +76,13 @@ contract ETHLockboxTest is CommonTest { deal(address(_lockbox), _value); // Mock the admin owner of the lockbox to be the same as the current lockbox proxy admin owner - vm.mockCall(address(_lockbox), abi.encodeCall(IPAOBase.PAO, ()), abi.encode(proxyAdmin.owner())); + vm.mockCall( + address(_lockbox), abi.encodeCall(IProxyAdminOwnerBase.proxyAdminOwner, ()), abi.encode(proxyAdmin.owner()) + ); // Authorize the lockbox if needed if (!ethLockbox.authorizedLockboxes(_lockbox)) { - vm.prank(PAO); + vm.prank(proxyAdminOwner); ethLockbox.authorizeLockbox(IETHLockbox(_lockbox)); } @@ -139,11 +141,13 @@ contract ETHLockboxTest is CommonTest { vm.assume(address(_portal) != address(ethLockbox)); // Mock the admin owner of the portal to be the same as the current lockbox proxy admin owner - vm.mockCall(address(_portal), abi.encodeCall(IPAOBase.PAO, ()), abi.encode(proxyAdmin.owner())); + vm.mockCall( + address(_portal), abi.encodeCall(IProxyAdminOwnerBase.proxyAdminOwner, ()), abi.encode(proxyAdmin.owner()) + ); // Set the portal as an authorized portal if needed if (!ethLockbox.authorizedPortals(address(_portal))) { - vm.prank(PAO); + vm.prank(proxyAdminOwner); ethLockbox.authorizePortal(_portal); } @@ -235,14 +239,16 @@ contract ETHLockboxTest is CommonTest { function testFuzz_unlockETH_multiplePortals_succeeds(IOptimismPortal2 _portal, uint256 _value) public { assumeNotForgeAddress(address(_portal)); - vm.assume(!ethLockbox.authorizedPortals(address(_portal))); - // Mock the admin owner of the portal to be the same as the current lockbox proxy admin owner - vm.mockCall(address(_portal), abi.encodeCall(IPAOBase.PAO, ()), abi.encode(proxyAdmin.owner())); + vm.mockCall( + address(_portal), abi.encodeCall(IProxyAdminOwnerBase.proxyAdminOwner, ()), abi.encode(proxyAdmin.owner()) + ); - // Set the portal as an authorized portal - vm.prank(PAO); - ethLockbox.authorizePortal(_portal); + // Set the portal as an authorized portal if needed + if (!ethLockbox.authorizedPortals(address(_portal))) { + vm.prank(proxyAdminOwner); + ethLockbox.authorizePortal(_portal); + } // Deal the ETH amount to the lockbox vm.deal(address(ethLockbox), _value); @@ -279,38 +285,17 @@ contract ETHLockboxTest is CommonTest { ethLockbox.authorizePortal(optimismPortal2); } - /// @notice Tests the `authorizePortal` function reverts when the portal is already authorized. - function testFuzz_authorizePortal_alreadyAuthorized_reverts(IOptimismPortal2 _portal) public { + /// @notice Tests the `authorizePortal` function reverts when the proxy admin owner of the portal is not the same as + /// the one of the lockbox. + function testFuzz_authorizePortal_differentProxyAdminOwner_reverts(IOptimismPortal2 _portal) public { assumeNotForgeAddress(address(_portal)); - // Authorize the portal - if (!ethLockbox.authorizedPortals(address(_portal))) { - // Mock the admin owner of the portal to be the same as the current lockbox proxy admin owner - vm.mockCall(address(_portal), abi.encodeCall(IPAOBase.PAO, ()), abi.encode(proxyAdmin.owner())); - - // Authorize the portal - vm.prank(PAO); - ethLockbox.authorizePortal(_portal); - } - - // Expect the revert with `AlreadyAuthorized` selector - vm.expectRevert(IETHLockbox.ETHLockbox_AlreadyAuthorized.selector); - - // Call the `authorizePortal` function with the portal - vm.prank(PAO); - ethLockbox.authorizePortal(_portal); - } - - /// @notice Tests the `authorizePortal` function reverts when the PAO of the portal is not the same as the PAO of - /// the lockbox. - function testFuzz_authorizePortal_differentPAO_reverts(IOptimismPortal2 _portal) public { - assumeNotForgeAddress(address(_portal)); - vm.mockCall(address(_portal), abi.encodeCall(IPAOBase.PAO, ()), abi.encode(address(0))); + vm.mockCall(address(_portal), abi.encodeCall(IProxyAdminOwnerBase.proxyAdminOwner, ()), abi.encode(address(0))); // Expect the revert with `DifferentOwner` selector - vm.expectRevert(IETHLockbox.ETHLockbox_DifferentPAO.selector); + vm.expectRevert(IETHLockbox.ETHLockbox_DifferentProxyAdminOwner.selector); // Call the `authorizePortal` function - vm.prank(PAO); + vm.prank(proxyAdminOwner); ethLockbox.authorizePortal(_portal); } @@ -329,7 +314,7 @@ contract ETHLockboxTest is CommonTest { emit PortalAuthorized(address(optimismPortal2)); // Call the `authorizePortal` function with the portal - vm.prank(PAO); + vm.prank(proxyAdminOwner); ethLockbox.authorizePortal(optimismPortal2); // Assert the portal is authorized @@ -339,17 +324,18 @@ contract ETHLockboxTest is CommonTest { /// @notice Tests the `authorizeLockbox` function succeeds function testFuzz_authorizePortal_succeeds(IOptimismPortal2 _portal) public { assumeNotForgeAddress(address(_portal)); - vm.assume(!ethLockbox.authorizedPortals(address(_portal))); // Mock the admin owner of the portal to be the same as the current lockbox proxy admin owner - vm.mockCall(address(_portal), abi.encodeCall(IPAOBase.PAO, ()), abi.encode(proxyAdmin.owner())); + vm.mockCall( + address(_portal), abi.encodeCall(IProxyAdminOwnerBase.proxyAdminOwner, ()), abi.encode(proxyAdmin.owner()) + ); // Expect the `PortalAuthorized` event to be emitted vm.expectEmit(address(ethLockbox)); emit PortalAuthorized(address(_portal)); // Call the `authorizePortal` function with the portal - vm.prank(PAO); + vm.prank(proxyAdminOwner); ethLockbox.authorizePortal(_portal); // Assert the portal is authorized @@ -368,56 +354,37 @@ contract ETHLockboxTest is CommonTest { ethLockbox.authorizeLockbox(ethLockbox); } - /// @notice Tests the `authorizeLockbox` function reverts when the lockbox is already authorized. - function testFuzz_authorizeLockbox_alreadyAuthorized_reverts(address _lockbox) public { - assumeNotForgeAddress(_lockbox); - - // Authorize the lockbox - if (!ethLockbox.authorizedLockboxes(_lockbox)) { - vm.mockCall(address(_lockbox), abi.encodeCall(IPAOBase.PAO, ()), abi.encode(proxyAdmin.owner())); - - vm.prank(PAO); - ethLockbox.authorizeLockbox(IETHLockbox(_lockbox)); - } - - // Call the `authorizeLockbox` function with the lockbox - vm.startPrank(ethLockbox.PAO()); - - // Expect the revert with `AlreadyAuthorized` selector - vm.expectRevert(IETHLockbox.ETHLockbox_AlreadyAuthorized.selector); - - ethLockbox.authorizeLockbox(IETHLockbox(_lockbox)); - } - - /// @notice Tests the `authorizeLockbox` function reverts when the PAO of the lockbox is not the same as the PAO of + /// @notice Tests the `authorizeLockbox` function reverts when the proxy admin owner of the lockbox is not the same + /// as the proxy admin owner of /// the proxy admin. - function testFuzz_authorizeLockbox_differentPAO_reverts(address _lockbox) public { + function testFuzz_authorizeLockbox_differentProxyAdminOwner_reverts(address _lockbox) public { assumeNotForgeAddress(_lockbox); - vm.mockCall(address(_lockbox), abi.encodeCall(IPAOBase.PAO, ()), abi.encode(address(0))); + vm.mockCall(address(_lockbox), abi.encodeCall(IProxyAdminOwnerBase.proxyAdminOwner, ()), abi.encode(address(0))); - // Expect the revert with `ETHLockbox_DifferentPAO` selector - vm.expectRevert(IETHLockbox.ETHLockbox_DifferentPAO.selector); + // Expect the revert with `ETHLockbox_DifferentProxyAdminOwner` selector + vm.expectRevert(IETHLockbox.ETHLockbox_DifferentProxyAdminOwner.selector); // Call the `authorizeLockbox` function with the lockbox - vm.prank(PAO); + vm.prank(proxyAdminOwner); ethLockbox.authorizeLockbox(IETHLockbox(_lockbox)); } /// @notice Tests the `authorizeLockbox` function succeeds function testFuzz_authorizeLockbox_succeeds(address _lockbox) public { assumeNotForgeAddress(_lockbox); - vm.assume(!ethLockbox.authorizedLockboxes(_lockbox)); // Mock the admin owner of the lockbox to be the same as the current lockbox proxy admin owner - vm.mockCall(address(_lockbox), abi.encodeCall(IPAOBase.PAO, ()), abi.encode(proxyAdmin.owner())); + vm.mockCall( + address(_lockbox), abi.encodeCall(IProxyAdminOwnerBase.proxyAdminOwner, ()), abi.encode(proxyAdmin.owner()) + ); // Expect the `LockboxAuthorized` event to be emitted vm.expectEmit(address(ethLockbox)); emit LockboxAuthorized(_lockbox); // Authorize the lockbox - vm.prank(PAO); + vm.prank(proxyAdminOwner); ethLockbox.authorizeLockbox(IETHLockbox(_lockbox)); // Assert the lockbox is authorized @@ -436,18 +403,19 @@ contract ETHLockboxTest is CommonTest { ethLockbox.migrateLiquidity(ethLockbox); } - /// @notice Tests the `migrateLiquidity` function reverts when the PAO of the lockbox is not the same as the PAO of + /// @notice Tests the `migrateLiquidity` function reverts when the proxy admin owner of the lockbox is not the same + /// as the proxy admin owner of /// the proxy admin. - function testFuzz_migrateLiquidity_differentPAO_reverts(address _lockbox) public { + function testFuzz_migrateLiquidity_differentProxyAdminOwner_reverts(address _lockbox) public { assumeNotForgeAddress(_lockbox); - vm.mockCall(address(_lockbox), abi.encodeCall(IPAOBase.PAO, ()), abi.encode(address(0))); + vm.mockCall(address(_lockbox), abi.encodeCall(IProxyAdminOwnerBase.proxyAdminOwner, ()), abi.encode(address(0))); - // Expect the revert with `ETHLockbox_DifferentPAO` selector - vm.expectRevert(IETHLockbox.ETHLockbox_DifferentPAO.selector); + // Expect the revert with `ETHLockbox_DifferentProxyAdminOwner` selector + vm.expectRevert(IETHLockbox.ETHLockbox_DifferentProxyAdminOwner.selector); // Call the `migrateLiquidity` function with the lockbox - vm.prank(PAO); + vm.prank(proxyAdminOwner); ethLockbox.migrateLiquidity(IETHLockbox(_lockbox)); } @@ -457,7 +425,9 @@ contract ETHLockboxTest is CommonTest { vm.assume(address(_lockbox) != address(ethLockbox)); // Mock on the lockbox that will receive the migration for it to succeed - vm.mockCall(address(_lockbox), abi.encodeCall(IPAOBase.PAO, ()), abi.encode(proxyAdmin.owner())); + vm.mockCall( + address(_lockbox), abi.encodeCall(IProxyAdminOwnerBase.proxyAdminOwner, ()), abi.encode(proxyAdmin.owner()) + ); vm.mockCall( address(_lockbox), abi.encodeCall(IETHLockbox.authorizedLockboxes, (address(ethLockbox))), abi.encode(true) ); @@ -475,7 +445,7 @@ contract ETHLockboxTest is CommonTest { uint256 newLockboxBalanceBefore = address(_lockbox).balance; // Call the `migrateLiquidity` function with the lockbox - vm.prank(PAO); + vm.prank(proxyAdminOwner); ethLockbox.migrateLiquidity(IETHLockbox(_lockbox)); // Assert the liquidity was migrated diff --git a/packages/contracts-bedrock/test/L1/OptimismPortal2.t.sol b/packages/contracts-bedrock/test/L1/OptimismPortal2.t.sol index 226b6f653841d..d99145002c017 100644 --- a/packages/contracts-bedrock/test/L1/OptimismPortal2.t.sol +++ b/packages/contracts-bedrock/test/L1/OptimismPortal2.t.sol @@ -467,9 +467,9 @@ contract OptimismPortal2_Test is CommonTest { assertEq(accountAccesses[2].storageAccesses.length, 0); } - /// @dev Tests that `updateLockbox` reverts if the caller is not the PAO. - function testFuzz_updateLockbox_notPAO_reverts(address _caller) external { - vm.assume(_caller != optimismPortal2.PAO()); + /// @dev Tests that `updateLockbox` reverts if the caller is not the proxy admin owner. + function testFuzz_updateLockbox_notProxyAdminOwner_reverts(address _caller) external { + vm.assume(_caller != optimismPortal2.proxyAdminOwner()); vm.expectRevert(IOptimismPortal.OptimismPortal_Unauthorized.selector); vm.prank(_caller); @@ -484,7 +484,7 @@ contract OptimismPortal2_Test is CommonTest { vm.expectEmit(address(optimismPortal2)); emit LockboxUpdated(oldLockbox, _newLockbox); - vm.prank(optimismPortal2.PAO()); + vm.prank(optimismPortal2.proxyAdminOwner()); optimismPortal2.updateLockbox(IETHLockbox(_newLockbox)); assertEq(address(optimismPortal2.ethLockbox()), _newLockbox); @@ -1859,8 +1859,8 @@ contract OptimismPortal2_LiquidityMigration_Test is CommonTest { } /// @notice Tests the liquidity migration from the portal to the lockbox reverts if not called by the admin owner. - function testFuzz_migrateLiquidity_notPAO_reverts(address _caller) external { - vm.assume(_caller != optimismPortal2.PAO()); + function testFuzz_migrateLiquidity_notProxyAdminOwner_reverts(address _caller) external { + vm.assume(_caller != optimismPortal2.proxyAdminOwner()); vm.expectRevert(IOptimismPortal.OptimismPortal_Unauthorized.selector); vm.prank(_caller); optimismPortal2.migrateLiquidity(); @@ -1871,14 +1871,14 @@ contract OptimismPortal2_LiquidityMigration_Test is CommonTest { vm.deal(address(optimismPortal2), _portalBalance); uint256 lockboxBalanceBefore = address(ethLockbox).balance; - address PAO = optimismPortal2.PAO(); + address proxyAdminOwner = optimismPortal2.proxyAdminOwner(); vm.expectCall(address(ethLockbox), _portalBalance, abi.encodeCall(ethLockbox.lockETH, ())); vm.expectEmit(address(optimismPortal2)); emit ETHMigrated(_portalBalance); - vm.prank(PAO); + vm.prank(proxyAdminOwner); optimismPortal2.migrateLiquidity(); assertEq(address(optimismPortal2).balance, 0); diff --git a/packages/contracts-bedrock/test/universal/Specs.t.sol b/packages/contracts-bedrock/test/universal/Specs.t.sol index 83a9a7e810e51..de6b5ca79d076 100644 --- a/packages/contracts-bedrock/test/universal/Specs.t.sol +++ b/packages/contracts-bedrock/test/universal/Specs.t.sol @@ -251,7 +251,7 @@ contract Specification_Test is CommonTest { _addSpec({ _name: "OptimismPortalInterop", _sel: _getSel("upgrade(address,address)") }); _addSpec({ _name: "OptimismPortalInterop", _sel: _getSel("ethLockbox()") }); _addSpec({ _name: "OptimismPortalInterop", _sel: _getSel("migrateLiquidity()") }); - _addSpec({ _name: "OptimismPortalInterop", _sel: _getSel("PAO()") }); + _addSpec({ _name: "OptimismPortalInterop", _sel: _getSel("proxyAdminOwner()") }); _addSpec({ _name: "OptimismPortalInterop", _sel: _getSel("setConfig(uint8,bytes)") }); // OptimismPortal2 @@ -296,10 +296,10 @@ contract Specification_Test is CommonTest { _addSpec({ _name: "OptimismPortal2", _sel: _getSel("upgrade(address,address)") }); _addSpec({ _name: "OptimismPortal2", _sel: _getSel("ethLockbox()") }); _addSpec({ _name: "OptimismPortal2", _sel: _getSel("migrateLiquidity()") }); - _addSpec({ _name: "OptimismPortal2", _sel: _getSel("PAO()") }); + _addSpec({ _name: "OptimismPortal2", _sel: _getSel("proxyAdminOwner()") }); - // PAOBase - _addSpec({ _name: "PAOBase", _sel: _getSel("PAO()") }); + // ProxyAdminOwnerBase + _addSpec({ _name: "ProxyAdminOwnerBase", _sel: _getSel("proxyAdminOwner()") }); // ProtocolVersions _addSpec({ _name: "ProtocolVersions", _sel: _getSel("RECOMMENDED_SLOT()") }); @@ -336,7 +336,7 @@ contract Specification_Test is CommonTest { _addSpec({ _name: "ETHLockbox", _sel: _getSel("authorizePortal(address)") }); _addSpec({ _name: "ETHLockbox", _sel: _getSel("authorizeLockbox(address)") }); _addSpec({ _name: "ETHLockbox", _sel: _getSel("migrateLiquidity(address)") }); - _addSpec({ _name: "ETHLockbox", _sel: _getSel("PAO()") }); + _addSpec({ _name: "ETHLockbox", _sel: _getSel("proxyAdminOwner()") }); // ResourceMetering _addSpec({ _name: "ResourceMetering", _sel: _getSel("params()") }); diff --git a/packages/contracts-bedrock/test/vendor/InitializableOZv5.t.sol b/packages/contracts-bedrock/test/vendor/InitializableOZv5.t.sol index eb0c1f8fa79dd..51c2fce266786 100644 --- a/packages/contracts-bedrock/test/vendor/InitializableOZv5.t.sol +++ b/packages/contracts-bedrock/test/vendor/InitializableOZv5.t.sol @@ -1,17 +1,15 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.15; +pragma solidity 0.8.25; -import { CommonTest } from "test/setup/CommonTest.sol"; +import { Test } from "forge-std/Test.sol"; 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 CommonTest { - error InvalidInitialization(); - +contract InitializerOZv5_Test is Test { /// @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; @@ -19,7 +17,6 @@ contract InitializerOZv5_Test is CommonTest { /// @notice Contains the address of an `Initializable` contract and the calldata /// used to initialize it. struct InitializeableContract { - string name; address target; bytes initCalldata; } @@ -28,18 +25,13 @@ contract InitializerOZv5_Test is CommonTest { /// used to initialize them. InitializeableContract[] contracts; - function setUp() public override { - super.enableInterop(); - super.enableAltDA(); - super.setUp(); - + function setUp() public { // 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", @@ -69,16 +61,12 @@ contract InitializerOZv5_Test is CommonTest { // Assert that the contract is already initialized. bytes32 slotVal = vm.load(_contract.target, INITIALIZABLE_STORAGE); uint64 initialized = uint64(uint256(slotVal)); - assertTrue( - // Either 1 for initialized or type(uint64).max for initializer disabled. - initialized == 1 || initialized == type(uint64).max, - "Initializable: contract is not initialized" - ); + assertEq(initialized, type(uint64).max); // 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), InvalidInitialization.selector); + assertEq(bytes4(returnData), Initializable.InvalidInitialization.selector); } } } From 1970d26a5a74424e02526414d2d229f8acec50cb Mon Sep 17 00:00:00 2001 From: agusduha Date: Tue, 4 Mar 2025 12:02:53 -0300 Subject: [PATCH 11/50] fix: pre pr --- .../snapshots/abi/ETHLockbox.json | 33 +++++++--------- .../snapshots/abi/OptimismPortal2.json | 38 ++++++++++++------- .../snapshots/abi/OptimismPortalInterop.json | 38 ++++++++++++------- .../snapshots/semver-lock.json | 24 ++++++------ .../storageLayout/OptimismPortal2.json | 11 +++++- .../storageLayout/OptimismPortalInterop.json | 11 +++++- .../src/L1/OPContractsManager.sol | 2 +- .../test/L1/OptimismPortal2.t.sol | 22 +++++------ .../test/universal/Specs.t.sol | 1 - 9 files changed, 104 insertions(+), 76 deletions(-) diff --git a/packages/contracts-bedrock/snapshots/abi/ETHLockbox.json b/packages/contracts-bedrock/snapshots/abi/ETHLockbox.json index 9bad5290bc5e4..8dc5b81723e45 100644 --- a/packages/contracts-bedrock/snapshots/abi/ETHLockbox.json +++ b/packages/contracts-bedrock/snapshots/abi/ETHLockbox.json @@ -4,19 +4,6 @@ "stateMutability": "nonpayable", "type": "constructor" }, - { - "inputs": [], - "name": "PAO", - "outputs": [ - { - "internalType": "address", - "name": "", - "type": "address" - } - ], - "stateMutability": "view", - "type": "function" - }, { "inputs": [ { @@ -132,6 +119,19 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [], + "name": "proxyAdminOwner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [], "name": "receiveLiquidity", @@ -283,12 +283,7 @@ }, { "inputs": [], - "name": "ETHLockbox_AlreadyAuthorized", - "type": "error" - }, - { - "inputs": [], - "name": "ETHLockbox_DifferentPAO", + "name": "ETHLockbox_DifferentProxyAdminOwner", "type": "error" }, { diff --git a/packages/contracts-bedrock/snapshots/abi/OptimismPortal2.json b/packages/contracts-bedrock/snapshots/abi/OptimismPortal2.json index 9b53972716cbd..ff90cc3ca3701 100644 --- a/packages/contracts-bedrock/snapshots/abi/OptimismPortal2.json +++ b/packages/contracts-bedrock/snapshots/abi/OptimismPortal2.json @@ -14,19 +14,6 @@ "stateMutability": "payable", "type": "receive" }, - { - "inputs": [], - "name": "PAO", - "outputs": [ - { - "internalType": "address", - "name": "", - "type": "address" - } - ], - "stateMutability": "view", - "type": "function" - }, { "inputs": [], "name": "anchorStateRegistry", @@ -294,6 +281,11 @@ "name": "_anchorStateRegistry", "type": "address" }, + { + "internalType": "contract IETHLockbox", + "name": "_ethLockbox", + "type": "address" + }, { "internalType": "bool", "name": "_superRootsActive", @@ -668,6 +660,19 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [], + "name": "proxyAdminOwner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [], "name": "respectedGameType", @@ -753,6 +758,11 @@ "name": "_anchorStateRegistry", "type": "address" }, + { + "internalType": "contract IETHLockbox", + "name": "_ethLockbox", + "type": "address" + }, { "internalType": "bool", "name": "_superRootsActive", @@ -1066,4 +1076,4 @@ "name": "UnexpectedString", "type": "error" } -] +] \ No newline at end of file diff --git a/packages/contracts-bedrock/snapshots/abi/OptimismPortalInterop.json b/packages/contracts-bedrock/snapshots/abi/OptimismPortalInterop.json index 2e6ada90299d8..b94c51d314cd7 100644 --- a/packages/contracts-bedrock/snapshots/abi/OptimismPortalInterop.json +++ b/packages/contracts-bedrock/snapshots/abi/OptimismPortalInterop.json @@ -14,19 +14,6 @@ "stateMutability": "payable", "type": "receive" }, - { - "inputs": [], - "name": "PAO", - "outputs": [ - { - "internalType": "address", - "name": "", - "type": "address" - } - ], - "stateMutability": "view", - "type": "function" - }, { "inputs": [], "name": "anchorStateRegistry", @@ -294,6 +281,11 @@ "name": "_anchorStateRegistry", "type": "address" }, + { + "internalType": "contract IETHLockbox", + "name": "_ethLockbox", + "type": "address" + }, { "internalType": "bool", "name": "_superRootsActive", @@ -668,6 +660,19 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [], + "name": "proxyAdminOwner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [], "name": "respectedGameType", @@ -771,6 +776,11 @@ "name": "_anchorStateRegistry", "type": "address" }, + { + "internalType": "contract IETHLockbox", + "name": "_ethLockbox", + "type": "address" + }, { "internalType": "bool", "name": "_superRootsActive", @@ -1084,4 +1094,4 @@ "name": "UnexpectedString", "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 cad5003da1fbe..f0c3a66ffcfc0 100644 --- a/packages/contracts-bedrock/snapshots/semver-lock.json +++ b/packages/contracts-bedrock/snapshots/semver-lock.json @@ -4,8 +4,8 @@ "sourceCodeHash": "0xe772f7db8033e4a738850cb28ac4849d3a454c93732135a8a10d4f7cb498088e" }, "src/L1/ETHLockbox.sol": { - "initCodeHash": "0xb5da33c92a6f0bc00eaaaec5d0b99c9c15a9dd9d7a25c416cd6740a96e7fc4d5", - "sourceCodeHash": "0x12af194d8fe23ff1c15c378151c7cb9e5a3854531b69c1153475432ef9f73d5e" + "initCodeHash": "0xd28e600379a190ca64a0f602d93a8a2850967a0df9da9d83b45f2501db53f17b", + "sourceCodeHash": "0x1b5229515bd0e4cd67d08185cb4aa88bab219ce8c1fd89575c9f5a50111cf2c1" }, "src/L1/L1CrossDomainMessenger.sol": { "initCodeHash": "0x03a3c0eb2418aba9f93bb89723ba2ee7cb9e1988ca00f380503c960149c85b7a", @@ -20,24 +20,24 @@ "sourceCodeHash": "0x7f614d0c5251365580c02dd898900a583ece2aa28579adea74a3808b575949b4" }, "src/L1/OPContractsManager.sol": { - "initCodeHash": "0xfc5622ed6411ec01e7bcfb3c9250b130b8ca25e74ee40e70e4017fefa34cddf7", - "sourceCodeHash": "0x1f53a1e174479aa244ad78fc171d4cbeb8529505a57af59f89435837e2803bcc" + "initCodeHash": "0xb68b46bb78e111809dae6e2a0404056691144796c006f988a9f17dfcafdd44e8", + "sourceCodeHash": "0x54667be25944539efec5d2ebedeb888081a224645f8e27ddecc1bc4a51407084" }, "src/L1/OPContractsManagerInterop.sol": { - "initCodeHash": "0x6855bfe2f22b4ce53c62fca0d32029cc39f0a2ab2ded43808bb6c493acadc781", + "initCodeHash": "0x47188fbb81c947426602099104046ce5ccd008292a233585493ce67e905a6082", "sourceCodeHash": "0x79b3ae68d83f021d9118f8a1943c1d0773e7e01a19f580f6b90ad4c00370cff5" }, "src/L1/OPPrestateUpdater.sol": { - "initCodeHash": "0x840b42954a8921e8216a93ace6d2a047e28ef8df8f426181b1b229d2d5818fc1", - "sourceCodeHash": "0x985c33cc1933d68de38d6c13afc1ac42ef7e455cb06bb255c1fed22b06b97a62" + "initCodeHash": "0x0b7626d038f385df1a51f862f6188292ce34aa624eaa5aca0df1d1452d8d5387", + "sourceCodeHash": "0xcdeb0f532ff90288ee13694a51a15aaee99392b7dfe2d46fffacf2af82cdb38f" }, "src/L1/OptimismPortal2.sol": { - "initCodeHash": "0xac11984d6c3e58fd02e33509540ee329c4f5caedb94e422f8ab3a7a95ce7a546", - "sourceCodeHash": "0x79652f4f6aebc11dd4ad201dcf9a436d94db76e2edef07b12486c3de9cd804d2" + "initCodeHash": "0xa66af9c53c197dad8a662a77573e371b55a3c23dac84a8febdd1d07084475b99", + "sourceCodeHash": "0x370650c5bf19add9561b6dc3bfe2c0bee5e5f1a31ab2de17cceccf0a36aabd2c" }, "src/L1/OptimismPortalInterop.sol": { - "initCodeHash": "0x8d76300f7a79686e522d4cac0582f4f7ec77b798f881747d0403762fa56b3a71", - "sourceCodeHash": "0x1b2ce1f2e3125bd449501b9d228b207d31f2bc39c7f23262c674e0d1f47c0d0b" + "initCodeHash": "0xb2b094a8a39f198cd947855dcdd470039059901f53253d867732ca04adb3ba52", + "sourceCodeHash": "0xd3db51a961120a97f8300a2587edda6311c862b48ba4178c4380f62368b07c09" }, "src/L1/ProtocolVersions.sol": { "initCodeHash": "0x5a76c8530cb24cf23d3baacc6eefaac226382af13f1e2a35535d2ec2b0573b29", @@ -243,4 +243,4 @@ "initCodeHash": "0x2bfce526f82622288333d53ca3f43a0a94306ba1bab99241daa845f8f4b18bd4", "sourceCodeHash": "0xf49d7b0187912a6bb67926a3222ae51121e9239495213c975b3b4b217ee57a1b" } -} +} \ No newline at end of file diff --git a/packages/contracts-bedrock/snapshots/storageLayout/OptimismPortal2.json b/packages/contracts-bedrock/snapshots/storageLayout/OptimismPortal2.json index 7fcae61d3f592..a66c4b71b4d16 100644 --- a/packages/contracts-bedrock/snapshots/storageLayout/OptimismPortal2.json +++ b/packages/contracts-bedrock/snapshots/storageLayout/OptimismPortal2.json @@ -132,11 +132,18 @@ "slot": "62", "type": "contract IAnchorStateRegistry" }, + { + "bytes": "20", + "label": "ethLockbox", + "offset": 0, + "slot": "63", + "type": "contract IETHLockbox" + }, { "bytes": "1", "label": "superRootsActive", "offset": 20, - "slot": "62", + "slot": "63", "type": "bool" } -] +] \ No newline at end of file diff --git a/packages/contracts-bedrock/snapshots/storageLayout/OptimismPortalInterop.json b/packages/contracts-bedrock/snapshots/storageLayout/OptimismPortalInterop.json index 7fcae61d3f592..a66c4b71b4d16 100644 --- a/packages/contracts-bedrock/snapshots/storageLayout/OptimismPortalInterop.json +++ b/packages/contracts-bedrock/snapshots/storageLayout/OptimismPortalInterop.json @@ -132,11 +132,18 @@ "slot": "62", "type": "contract IAnchorStateRegistry" }, + { + "bytes": "20", + "label": "ethLockbox", + "offset": 0, + "slot": "63", + "type": "contract IETHLockbox" + }, { "bytes": "1", "label": "superRootsActive", "offset": 20, - "slot": "62", + "slot": "63", "type": "bool" } -] +] \ No newline at end of file diff --git a/packages/contracts-bedrock/src/L1/OPContractsManager.sol b/packages/contracts-bedrock/src/L1/OPContractsManager.sol index 181a34f25a2a7..3db21a056d17e 100644 --- a/packages/contracts-bedrock/src/L1/OPContractsManager.sol +++ b/packages/contracts-bedrock/src/L1/OPContractsManager.sol @@ -920,7 +920,7 @@ contract OPContractsManager is ISemver { returns (bytes memory) { return abi.encodeCall( - IOptimismPortal2.initialize, + IOptimismPortal.initialize, ( _output.systemConfigProxy, superchainConfig, diff --git a/packages/contracts-bedrock/test/L1/OptimismPortal2.t.sol b/packages/contracts-bedrock/test/L1/OptimismPortal2.t.sol index 21f35fa30d59b..01218b376dc3e 100644 --- a/packages/contracts-bedrock/test/L1/OptimismPortal2.t.sol +++ b/packages/contracts-bedrock/test/L1/OptimismPortal2.t.sol @@ -105,7 +105,7 @@ contract OptimismPortal2_Test is CommonTest { // Call the upgrade function. vm.prank(Predeploys.PROXY_ADMIN); - optimismPortal2.upgrade(IAnchorStateRegistry(_newAnchorStateRegistry), IETHLockbox(ethLockbox)); + optimismPortal2.upgrade(IAnchorStateRegistry(_newAnchorStateRegistry), IETHLockbox(ethLockbox), true); // Assert the portal is properly upgraded. assertEq(address(optimismPortal2.ethLockbox()), address(ethLockbox)); @@ -874,7 +874,7 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { setSuperRootsActive(true); // Should revert. - vm.expectRevert(IOptimismPortal2.OptimismPortal_WrongProofMethod.selector); + vm.expectRevert(IOptimismPortal.OptimismPortal_WrongProofMethod.selector); optimismPortal2.proveWithdrawalTransaction({ _tx: _defaultTx, _disputeGameIndex: _proposedGameIndex, @@ -897,7 +897,7 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { }); // Should revert. - vm.expectRevert(IOptimismPortal2.OptimismPortal_WrongProofMethod.selector); + vm.expectRevert(IOptimismPortal.OptimismPortal_WrongProofMethod.selector); optimismPortal2.proveWithdrawalTransaction({ _tx: _defaultTx, _disputeGameProxy: game, @@ -925,7 +925,7 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { }); // Should revert because the proof is wrong. - vm.expectRevert(IOptimismPortal2.OptimismPortal_InvalidSuperRootProof.selector); + vm.expectRevert(IOptimismPortal.OptimismPortal_InvalidSuperRootProof.selector); optimismPortal2.proveWithdrawalTransaction({ _tx: _defaultTx, _disputeGameProxy: game, @@ -960,7 +960,7 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { vm.mockCall(address(game), abi.encodeCall(game.rootClaim, ()), abi.encode(expectedSuperRoot)); // Should revert because the proof is wrong. - vm.expectRevert(IOptimismPortal2.OptimismPortal_InvalidOutputRootIndex.selector); + vm.expectRevert(IOptimismPortal.OptimismPortal_InvalidOutputRootIndex.selector); optimismPortal2.proveWithdrawalTransaction({ _tx: _defaultTx, _disputeGameProxy: game, @@ -997,7 +997,7 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { vm.mockCall(address(game), abi.encodeCall(game.rootClaim, ()), abi.encode(expectedSuperRoot)); // Should revert because the proof is wrong. - vm.expectRevert(IOptimismPortal2.OptimismPortal_InvalidOutputRootChainId.selector); + vm.expectRevert(IOptimismPortal.OptimismPortal_InvalidOutputRootChainId.selector); optimismPortal2.proveWithdrawalTransaction({ _tx: _defaultTx, _disputeGameProxy: game, @@ -1034,7 +1034,7 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { vm.mockCall(address(game), abi.encodeCall(game.rootClaim, ()), abi.encode(expectedSuperRoot)); // Should revert because the proof is wrong. - vm.expectRevert(IOptimismPortal2.OptimismPortal_InvalidOutputRootProof.selector); + vm.expectRevert(IOptimismPortal.OptimismPortal_InvalidOutputRootProof.selector); optimismPortal2.proveWithdrawalTransaction({ _tx: _defaultTx, _disputeGameProxy: game, @@ -2140,7 +2140,7 @@ contract OptimismPortal2_upgrade_Test is CommonTest { vm.store(address(optimismPortal2), bytes32(slot.slot), bytes32(0)); // Trigger upgrade(). - optimismPortal2.upgrade(IAnchorStateRegistry(address(0xdeadbeef)), true); + optimismPortal2.upgrade(IAnchorStateRegistry(address(0xdeadbeef)), IETHLockbox(ethLockbox), true); // Verify that the initialized slot was updated. bytes32 initializedSlotAfter = vm.load(address(optimismPortal2), bytes32(slot.slot)); @@ -2162,11 +2162,11 @@ contract OptimismPortal2_upgrade_Test is CommonTest { vm.store(address(optimismPortal2), bytes32(slot.slot), bytes32(0)); // Trigger first upgrade. - optimismPortal2.upgrade(IAnchorStateRegistry(address(0xdeadbeef)), true); + optimismPortal2.upgrade(IAnchorStateRegistry(address(0xdeadbeef)), IETHLockbox(ethLockbox), true); // Try to trigger second upgrade. vm.expectRevert("Initializable: contract is already initialized"); - optimismPortal2.upgrade(IAnchorStateRegistry(address(0xdeadbeef)), true); + optimismPortal2.upgrade(IAnchorStateRegistry(address(0xdeadbeef)), IETHLockbox(ethLockbox), true); } /// @notice Tests that the upgrade() function reverts if called after initialization. @@ -2183,7 +2183,7 @@ contract OptimismPortal2_upgrade_Test is CommonTest { // Try to trigger upgrade(). vm.expectRevert("Initializable: contract is already initialized"); - optimismPortal2.upgrade(IAnchorStateRegistry(address(0xdeadbeef)), true); + optimismPortal2.upgrade(IAnchorStateRegistry(address(0xdeadbeef)), IETHLockbox(ethLockbox), true); } } diff --git a/packages/contracts-bedrock/test/universal/Specs.t.sol b/packages/contracts-bedrock/test/universal/Specs.t.sol index 1d4af602000d1..59bb5b7246dca 100644 --- a/packages/contracts-bedrock/test/universal/Specs.t.sol +++ b/packages/contracts-bedrock/test/universal/Specs.t.sol @@ -262,7 +262,6 @@ contract Specification_Test is CommonTest { _addSpec({ _name: "OptimismPortalInterop", _sel: _getSel("ethLockbox()") }); _addSpec({ _name: "OptimismPortalInterop", _sel: _getSel("migrateLiquidity()") }); _addSpec({ _name: "OptimismPortalInterop", _sel: _getSel("proxyAdminOwner()") }); - _addSpec({ _name: "OptimismPortalInterop", _sel: _getSel("setConfig(uint8,bytes)") }); _addSpec({ _name: "OptimismPortalInterop", _sel: _getSel("upgrade(address,address,bool)") }); _addSpec({ _name: "OptimismPortalInterop", From c28004e5eb397ac41fc6dfe8ebd27ec7b1971820 Mon Sep 17 00:00:00 2001 From: Disco <131301107+0xDiscotech@users.noreply.github.com> Date: Tue, 4 Mar 2025 12:53:01 -0300 Subject: [PATCH 12/50] fix: test upgrade fails (#296) * fix: test upgrade fails * feat: handle fork state on eth lockbox tests * chore: enhance comments * chore: run pre-pr * fix: semgrep * fix: comments * chore: address path where is not a fork to get the lockbox * chore: run pre pr --- .../test/L1/ETHLockbox.t.sol | 35 +++++++++++++------ .../test/L1/OPContractsManager.t.sol | 8 +++-- .../test/L1/OptimismPortal2.t.sol | 12 +++++++ .../test/setup/ForkLive.s.sol | 15 ++++++++ .../contracts-bedrock/test/setup/Setup.sol | 8 ++++- 5 files changed, 64 insertions(+), 14 deletions(-) diff --git a/packages/contracts-bedrock/test/L1/ETHLockbox.t.sol b/packages/contracts-bedrock/test/L1/ETHLockbox.t.sol index 5640f02ab49ba..ee95dabea5e32 100644 --- a/packages/contracts-bedrock/test/L1/ETHLockbox.t.sol +++ b/packages/contracts-bedrock/test/L1/ETHLockbox.t.sol @@ -16,7 +16,6 @@ import { IOptimismPortal2 } from "interfaces/L1/IOptimismPortal2.sol"; // Test import { CommonTest } from "test/setup/CommonTest.sol"; -import { Predeploys } from "src/libraries/Predeploys.sol"; import { ProxyAdmin } from "src/universal/ProxyAdmin.sol"; contract ETHLockboxTest is CommonTest { @@ -29,11 +28,16 @@ contract ETHLockboxTest is CommonTest { event LiquidityMigrated(address indexed lockbox); event LiquidityReceived(address indexed lockbox); - ProxyAdmin public proxyAdmin = ProxyAdmin(Predeploys.PROXY_ADMIN); + ProxyAdmin public proxyAdmin; address public proxyAdminOwner; function setUp() public virtual override { super.setUp(); + + // If not on the last upgrade network, we skip the test since the `ETHLockbox` won't be yet deployed + if (isForkTest() && !deploy.cfg().useUpgradedFork()) vm.skip(true); + + proxyAdmin = ProxyAdmin(artifacts.mustGetAddress("ProxyAdmin")); proxyAdminOwner = proxyAdmin.owner(); } @@ -69,6 +73,8 @@ contract ETHLockboxTest is CommonTest { /// @notice Tests the liquidity is correctly received. function testFuzz_receiveLiquidity_succeeds(address _lockbox, uint256 _value) public { + // Since on the fork the `_lockbox` fuzzed address doesn't exist, we skip the test + if (isForkTest()) vm.skip(true); assumeNotForgeAddress(_lockbox); vm.assume(address(_lockbox) != address(ethLockbox)); @@ -77,7 +83,7 @@ contract ETHLockboxTest is CommonTest { // Mock the admin owner of the lockbox to be the same as the current lockbox proxy admin owner vm.mockCall( - address(_lockbox), abi.encodeCall(IProxyAdminOwnerBase.proxyAdminOwner, ()), abi.encode(proxyAdmin.owner()) + address(_lockbox), abi.encodeCall(IProxyAdminOwnerBase.proxyAdminOwner, ()), abi.encode(proxyAdminOwner) ); // Authorize the lockbox if needed @@ -115,6 +121,9 @@ contract ETHLockboxTest is CommonTest { /// @notice Tests the ETH is correctly locked when the caller is an authorized portal. function testFuzz_lockETH_succeeds(uint256 _amount) public { + // Prevent overflow on an upgrade context + _amount = bound(_amount, 0, type(uint256).max - address(ethLockbox).balance); + // Deal the ETH amount to the portal vm.deal(address(optimismPortal2), _amount); @@ -137,12 +146,14 @@ contract ETHLockboxTest is CommonTest { /// @notice Tests the ETH is correctly locked when the caller is an authorized portal with different portals. function testFuzz_lockETH_multiplePortals_succeeds(IOptimismPortal2 _portal, uint256 _amount) public { + // Since on the fork the `_portal` fuzzed address doesn't exist, we skip the test + if (isForkTest()) vm.skip(true); assumeNotForgeAddress(address(_portal)); vm.assume(address(_portal) != address(ethLockbox)); // Mock the admin owner of the portal to be the same as the current lockbox proxy admin owner vm.mockCall( - address(_portal), abi.encodeCall(IProxyAdminOwnerBase.proxyAdminOwner, ()), abi.encode(proxyAdmin.owner()) + address(_portal), abi.encodeCall(IProxyAdminOwnerBase.proxyAdminOwner, ()), abi.encode(proxyAdminOwner) ); // Set the portal as an authorized portal if needed @@ -241,7 +252,7 @@ contract ETHLockboxTest is CommonTest { // Mock the admin owner of the portal to be the same as the current lockbox proxy admin owner vm.mockCall( - address(_portal), abi.encodeCall(IProxyAdminOwnerBase.proxyAdminOwner, ()), abi.encode(proxyAdmin.owner()) + address(_portal), abi.encodeCall(IProxyAdminOwnerBase.proxyAdminOwner, ()), abi.encode(proxyAdminOwner) ); // Set the portal as an authorized portal if needed @@ -275,7 +286,7 @@ contract ETHLockboxTest is CommonTest { /// @notice Tests the `authorizePortal` function reverts when the caller is not the proxy admin. function testFuzz_authorizePortal_unauthorized_reverts(address _caller) public { - vm.assume(_caller != proxyAdmin.owner()); + vm.assume(_caller != proxyAdminOwner); // Expect the revert with `Unauthorized` selector vm.expectRevert(IETHLockbox.ETHLockbox_Unauthorized.selector); @@ -327,7 +338,7 @@ contract ETHLockboxTest is CommonTest { // Mock the admin owner of the portal to be the same as the current lockbox proxy admin owner vm.mockCall( - address(_portal), abi.encodeCall(IProxyAdminOwnerBase.proxyAdminOwner, ()), abi.encode(proxyAdmin.owner()) + address(_portal), abi.encodeCall(IProxyAdminOwnerBase.proxyAdminOwner, ()), abi.encode(proxyAdminOwner) ); // Expect the `PortalAuthorized` event to be emitted @@ -344,7 +355,7 @@ contract ETHLockboxTest is CommonTest { /// @notice Tests the `authorizeLockbox` function reverts when the caller is not the proxy admin. function testFuzz_authorizeLockbox_unauthorized_reverts(address _caller) public { - vm.assume(_caller != proxyAdmin.owner()); + vm.assume(_caller != proxyAdminOwner); // Expect the revert with `Unauthorized` selector vm.expectRevert(IETHLockbox.ETHLockbox_Unauthorized.selector); @@ -376,7 +387,7 @@ contract ETHLockboxTest is CommonTest { // Mock the admin owner of the lockbox to be the same as the current lockbox proxy admin owner vm.mockCall( - address(_lockbox), abi.encodeCall(IProxyAdminOwnerBase.proxyAdminOwner, ()), abi.encode(proxyAdmin.owner()) + address(_lockbox), abi.encodeCall(IProxyAdminOwnerBase.proxyAdminOwner, ()), abi.encode(proxyAdminOwner) ); // Expect the `LockboxAuthorized` event to be emitted @@ -393,7 +404,7 @@ contract ETHLockboxTest is CommonTest { /// @notice Tests the `migrateLiquidity` function reverts when the caller is not the proxy admin. function testFuzz_migrateLiquidity_unauthorized_reverts(address _caller) public { - vm.assume(_caller != proxyAdmin.owner()); + vm.assume(_caller != proxyAdminOwner); // Expect the revert with `Unauthorized` selector vm.expectRevert(IETHLockbox.ETHLockbox_Unauthorized.selector); @@ -421,12 +432,14 @@ contract ETHLockboxTest is CommonTest { /// @notice Tests the `migrateLiquidity` function succeeds function testFuzz_migrateLiquidity_succeeds(uint256 _balance, address _lockbox) public { + // Since on the fork the `_lockbox` fuzzed address doesn't exist, we skip the test + if (isForkTest()) vm.skip(true); assumeNotForgeAddress(_lockbox); vm.assume(address(_lockbox) != address(ethLockbox)); // Mock on the lockbox that will receive the migration for it to succeed vm.mockCall( - address(_lockbox), abi.encodeCall(IProxyAdminOwnerBase.proxyAdminOwner, ()), abi.encode(proxyAdmin.owner()) + address(_lockbox), abi.encodeCall(IProxyAdminOwnerBase.proxyAdminOwner, ()), abi.encode(proxyAdminOwner) ); vm.mockCall( address(_lockbox), abi.encodeCall(IETHLockbox.authorizedLockboxes, (address(ethLockbox))), abi.encode(true) diff --git a/packages/contracts-bedrock/test/L1/OPContractsManager.t.sol b/packages/contracts-bedrock/test/L1/OPContractsManager.t.sol index c9ebdcdfa3fbf..ace949f85d299 100644 --- a/packages/contracts-bedrock/test/L1/OPContractsManager.t.sol +++ b/packages/contracts-bedrock/test/L1/OPContractsManager.t.sol @@ -301,7 +301,9 @@ contract OPContractsManager_Upgrade_Harness is CommonTest { expectEmitUpgraded(impls.l1ERC721BridgeImpl, address(l1ERC721Bridge)); expectEmitUpgraded(impls.disputeGameFactoryImpl, address(disputeGameFactory)); expectEmitUpgraded(impls.optimismPortalImpl, address(optimismPortal2)); - expectEmitUpgraded(impls.ethLockboxImpl, address(ethLockbox)); + if (deploy.cfg().useUpgradedFork()) { + expectEmitUpgraded(impls.ethLockboxImpl, address(ethLockbox)); + } expectEmitUpgraded(impls.optimismMintableERC20FactoryImpl, address(l1OptimismMintableERC20Factory)); vm.expectEmit(address(newAnchorStateRegistryProxy)); emit AdminChanged(address(0), address(proxyAdmin)); @@ -343,7 +345,9 @@ contract OPContractsManager_Upgrade_Harness is CommonTest { assertEq(impls.l1ERC721BridgeImpl, EIP1967Helper.getImplementation(address(l1ERC721Bridge))); assertEq(impls.disputeGameFactoryImpl, EIP1967Helper.getImplementation(address(disputeGameFactory))); assertEq(impls.optimismPortalImpl, EIP1967Helper.getImplementation(address(optimismPortal2))); - assertEq(impls.ethLockboxImpl, EIP1967Helper.getImplementation(address(ethLockbox))); + if (deploy.cfg().useUpgradedFork()) { + assertEq(impls.ethLockboxImpl, EIP1967Helper.getImplementation(address(ethLockbox))); + } assertEq( impls.optimismMintableERC20FactoryImpl, EIP1967Helper.getImplementation(address(l1OptimismMintableERC20Factory)) diff --git a/packages/contracts-bedrock/test/L1/OptimismPortal2.t.sol b/packages/contracts-bedrock/test/L1/OptimismPortal2.t.sol index 01218b376dc3e..30c2111305f96 100644 --- a/packages/contracts-bedrock/test/L1/OptimismPortal2.t.sol +++ b/packages/contracts-bedrock/test/L1/OptimismPortal2.t.sol @@ -93,6 +93,9 @@ contract OptimismPortal2_Test is CommonTest { } function testFuzz_upgrade_succeeds(address _newAnchorStateRegistry, uint256 _balance) external { + // Prevent overflow on an upgrade context + _balance = bound(_balance, 0, type(uint256).max - address(ethLockbox).balance); + // Set the initialize state of the portal to false. vm.store(address(optimismPortal2), bytes32(uint256(0)), bytes32(uint256(0))); @@ -177,6 +180,8 @@ contract OptimismPortal2_Test is CommonTest { /// @dev Tests that `receive` successdully deposits ETH. function testFuzz_receive_succeeds(uint256 _value) external { + // Prevent overflow on an upgrade context + _value = bound(_value, 0, type(uint256).max - address(ethLockbox).balance); uint256 balanceBefore = address(optimismPortal2).balance; uint256 lockboxBalanceBefore = address(ethLockbox).balance; _value = bound(_value, 0, type(uint256).max - balanceBefore); @@ -280,6 +285,8 @@ contract OptimismPortal2_Test is CommonTest { ) external { + // Prevent overflow on an upgrade context + _mint = bound(_mint, 0, type(uint256).max - address(ethLockbox).balance); _gasLimit = uint64( bound( _gasLimit, @@ -335,6 +342,8 @@ contract OptimismPortal2_Test is CommonTest { external { assumeNotForgeAddress(_7702Target); + // Prevent overflow on an upgrade context + _mint = bound(_mint, 0, type(uint256).max - address(ethLockbox).balance); _gasLimit = uint64( bound( @@ -388,6 +397,8 @@ contract OptimismPortal2_Test is CommonTest { ) external { + // Prevent overflow on an upgrade context + _mint = bound(_mint, 0, type(uint256).max - address(ethLockbox).balance); _gasLimit = uint64( bound( _gasLimit, @@ -2102,6 +2113,7 @@ contract OptimismPortal2_LiquidityMigration_Test is CommonTest { /// @notice Tests that the liquidity migration from the portal to the lockbox succeeds. function test_migrateLiquidity_succeeds(uint256 _portalBalance) external { + _portalBalance = uint256(bound(_portalBalance, 0, type(uint256).max - address(ethLockbox).balance)); vm.deal(address(optimismPortal2), _portalBalance); uint256 lockboxBalanceBefore = address(ethLockbox).balance; diff --git a/packages/contracts-bedrock/test/setup/ForkLive.s.sol b/packages/contracts-bedrock/test/setup/ForkLive.s.sol index 50c78b61588e7..818b8e5047923 100644 --- a/packages/contracts-bedrock/test/setup/ForkLive.s.sol +++ b/packages/contracts-bedrock/test/setup/ForkLive.s.sol @@ -24,6 +24,8 @@ import { ISystemConfig } from "interfaces/L1/ISystemConfig.sol"; import { IProxyAdmin } from "interfaces/universal/IProxyAdmin.sol"; import { IOPContractsManager } from "interfaces/L1/IOPContractsManager.sol"; import { IAnchorStateRegistry } from "interfaces/dispute/IAnchorStateRegistry.sol"; +import { IETHLockbox } from "interfaces/L1/IETHLockbox.sol"; +import { IOptimismPortal2 } from "interfaces/L1/IOptimismPortal2.sol"; /// @title ForkLive /// @notice This script is called by Setup.sol as a preparation step for the foundry test suite, and is run as an @@ -116,6 +118,14 @@ contract ForkLive is Deployer { artifacts.save("OptimismPortalProxy", optimismPortal); artifacts.save("OptimismPortal2Impl", EIP1967Helper.getImplementation(optimismPortal)); + // Get the lockbox address from the portal, and save it + try IOptimismPortal2(payable(optimismPortal)).ethLockbox() returns (IETHLockbox ethLockbox_) { + console.log("ForkLive: ETHLockboxProxy found: %s", address(ethLockbox_)); + artifacts.save("ETHLockboxProxy", address(ethLockbox_)); + } catch { + console.log("ForkLive: ETHLockboxProxy not found"); + } + address addressManager = vm.parseTomlAddress(opToml, ".addresses.AddressManager"); artifacts.save("AddressManager", addressManager); artifacts.save( @@ -200,6 +210,11 @@ contract ForkLive is Deployer { IAnchorStateRegistry newAnchorStateRegistry = IPermissionedDisputeGame(permissionedDisputeGame).anchorStateRegistry(); artifacts.save("AnchorStateRegistryProxy", address(newAnchorStateRegistry)); + + // Get the lockbox address from the portal, and save it + IOptimismPortal2 portal = IOptimismPortal2(artifacts.mustGetAddress("OptimismPortalProxy")); + address lockboxAddress = address(portal.ethLockbox()); + artifacts.save("ETHLockboxProxy", lockboxAddress); } /// @notice Saves the proxy and implementation addresses for a contract name diff --git a/packages/contracts-bedrock/test/setup/Setup.sol b/packages/contracts-bedrock/test/setup/Setup.sol index aea2760508e90..9a2582e241a4b 100644 --- a/packages/contracts-bedrock/test/setup/Setup.sol +++ b/packages/contracts-bedrock/test/setup/Setup.sol @@ -229,7 +229,13 @@ contract Setup { console.log("Setup: completed L1 deployment, registering addresses now"); optimismPortal2 = IOptimismPortal(artifacts.mustGetAddress("OptimismPortalProxy")); - ethLockbox = IETHLockbox(artifacts.mustGetAddress("ETHLockboxProxy")); + + if (isForkTest() && deploy.cfg().useUpgradedFork()) { + // If we are on the last upgrade network, we can use the predeployed ETHLockbox for the fork test + ethLockbox = IETHLockbox(artifacts.mustGetAddress("ETHLockboxProxy")); + } else if (!isForkTest()) { + ethLockbox = IETHLockbox(artifacts.mustGetAddress("ETHLockboxProxy")); + } systemConfig = ISystemConfig(artifacts.mustGetAddress("SystemConfigProxy")); l1StandardBridge = IL1StandardBridge(artifacts.mustGetAddress("L1StandardBridgeProxy")); l1CrossDomainMessenger = IL1CrossDomainMessenger(artifacts.mustGetAddress("L1CrossDomainMessengerProxy")); From 884b84aa9682a3a42030d1fe69da587ce149a312 Mon Sep 17 00:00:00 2001 From: Disco <131301107+0xDiscotech@users.noreply.github.com> Date: Wed, 5 Mar 2025 11:29:00 -0300 Subject: [PATCH 13/50] fix: pr fixes (#298) * chore: enhance opcm comment * fix: same proxy admin owner typo * feat: add insufficient balance check * chore: add natspec comments on lockbox migration process * chore: pre pr --- .../contracts-bedrock/interfaces/L1/IETHLockbox.sol | 1 + .../contracts-bedrock/snapshots/abi/ETHLockbox.json | 5 +++++ .../contracts-bedrock/snapshots/semver-lock.json | 8 ++++---- packages/contracts-bedrock/src/L1/ETHLockbox.sol | 13 ++++++++++--- .../contracts-bedrock/src/L1/OPContractsManager.sol | 6 ++---- .../contracts-bedrock/src/L1/OptimismPortal2.sol | 3 +++ .../src/L1/ProxyAdminOwnerBase.sol | 2 +- packages/contracts-bedrock/test/L1/ETHLockbox.t.sol | 13 +++++++++++++ 8 files changed, 39 insertions(+), 12 deletions(-) diff --git a/packages/contracts-bedrock/interfaces/L1/IETHLockbox.sol b/packages/contracts-bedrock/interfaces/L1/IETHLockbox.sol index c7d2d5fdc2e2b..27df1600dd686 100644 --- a/packages/contracts-bedrock/interfaces/L1/IETHLockbox.sol +++ b/packages/contracts-bedrock/interfaces/L1/IETHLockbox.sol @@ -9,6 +9,7 @@ import { IOptimismPortal2 } from "interfaces/L1/IOptimismPortal2.sol"; interface IETHLockbox is IProxyAdminOwnerBase, ISemver { error ETHLockbox_Unauthorized(); error ETHLockbox_Paused(); + error ETHLockbox_InsufficientBalance(); error ETHLockbox_NoWithdrawalTransactions(); error ETHLockbox_DifferentProxyAdminOwner(); diff --git a/packages/contracts-bedrock/snapshots/abi/ETHLockbox.json b/packages/contracts-bedrock/snapshots/abi/ETHLockbox.json index 8dc5b81723e45..4b7bd30e568ef 100644 --- a/packages/contracts-bedrock/snapshots/abi/ETHLockbox.json +++ b/packages/contracts-bedrock/snapshots/abi/ETHLockbox.json @@ -286,6 +286,11 @@ "name": "ETHLockbox_DifferentProxyAdminOwner", "type": "error" }, + { + "inputs": [], + "name": "ETHLockbox_InsufficientBalance", + "type": "error" + }, { "inputs": [], "name": "ETHLockbox_NoWithdrawalTransactions", diff --git a/packages/contracts-bedrock/snapshots/semver-lock.json b/packages/contracts-bedrock/snapshots/semver-lock.json index f0c3a66ffcfc0..738b63d4c25a8 100644 --- a/packages/contracts-bedrock/snapshots/semver-lock.json +++ b/packages/contracts-bedrock/snapshots/semver-lock.json @@ -4,8 +4,8 @@ "sourceCodeHash": "0xe772f7db8033e4a738850cb28ac4849d3a454c93732135a8a10d4f7cb498088e" }, "src/L1/ETHLockbox.sol": { - "initCodeHash": "0xd28e600379a190ca64a0f602d93a8a2850967a0df9da9d83b45f2501db53f17b", - "sourceCodeHash": "0x1b5229515bd0e4cd67d08185cb4aa88bab219ce8c1fd89575c9f5a50111cf2c1" + "initCodeHash": "0x4ad4f1719b2092cf224d7083c4816c2ab9fa39331eb016557d17aa2ff43ef909", + "sourceCodeHash": "0x45c7ee45398ab4be3a59d93d36b897bbcc1c12adb71c002e17fd6142ce95ef52" }, "src/L1/L1CrossDomainMessenger.sol": { "initCodeHash": "0x03a3c0eb2418aba9f93bb89723ba2ee7cb9e1988ca00f380503c960149c85b7a", @@ -21,7 +21,7 @@ }, "src/L1/OPContractsManager.sol": { "initCodeHash": "0xb68b46bb78e111809dae6e2a0404056691144796c006f988a9f17dfcafdd44e8", - "sourceCodeHash": "0x54667be25944539efec5d2ebedeb888081a224645f8e27ddecc1bc4a51407084" + "sourceCodeHash": "0x7c5ad88de15e2f2910558ccddb58b6a5ba523fb70087c4683e8418034b2dc11d" }, "src/L1/OPContractsManagerInterop.sol": { "initCodeHash": "0x47188fbb81c947426602099104046ce5ccd008292a233585493ce67e905a6082", @@ -33,7 +33,7 @@ }, "src/L1/OptimismPortal2.sol": { "initCodeHash": "0xa66af9c53c197dad8a662a77573e371b55a3c23dac84a8febdd1d07084475b99", - "sourceCodeHash": "0x370650c5bf19add9561b6dc3bfe2c0bee5e5f1a31ab2de17cceccf0a36aabd2c" + "sourceCodeHash": "0xf735d2c64c3675b178fc2ea334aa0e3689622a5e33a5f9aeba6844682b2be05b" }, "src/L1/OptimismPortalInterop.sol": { "initCodeHash": "0xb2b094a8a39f198cd947855dcdd470039059901f53253d867732ca04adb3ba52", diff --git a/packages/contracts-bedrock/src/L1/ETHLockbox.sol b/packages/contracts-bedrock/src/L1/ETHLockbox.sol index 5cd82db232674..e08ac219c8cf6 100644 --- a/packages/contracts-bedrock/src/L1/ETHLockbox.sol +++ b/packages/contracts-bedrock/src/L1/ETHLockbox.sol @@ -25,6 +25,9 @@ contract ETHLockbox is ProxyAdminOwnerBase, Initializable, ISemver { /// @notice Thrown when the caller is not authorized. error ETHLockbox_Unauthorized(); + /// @notice Thrown when the value to unlock is greater than the balance of the lockbox. + error ETHLockbox_InsufficientBalance(); + /// @notice Thrown when attempting to unlock ETH from the lockbox through a withdrawal transaction. error ETHLockbox_NoWithdrawalTransactions(); @@ -125,6 +128,7 @@ contract ETHLockbox is ProxyAdminOwnerBase, Initializable, ISemver { function unlockETH(uint256 _value) external { if (paused()) revert ETHLockbox_Paused(); if (!authorizedPortals[msg.sender]) revert ETHLockbox_Unauthorized(); + if (_value > address(this).balance) revert ETHLockbox_InsufficientBalance(); /// NOTE: Check l2Sender is not set to avoid this function to be called as a target on a withdrawal transaction if (IOptimismPortal(payable(msg.sender)).l2Sender() != Constants.DEFAULT_L2_SENDER) { revert ETHLockbox_NoWithdrawalTransactions(); @@ -139,17 +143,20 @@ contract ETHLockbox is ProxyAdminOwnerBase, Initializable, ISemver { /// @param _lockbox The address of the ETH lockbox to authorize. function authorizeLockbox(IETHLockbox _lockbox) external { if (msg.sender != proxyAdminOwner()) revert ETHLockbox_Unauthorized(); - if (!_sameproxyAdminOwner(address(_lockbox))) revert ETHLockbox_DifferentProxyAdminOwner(); + if (!_sameProxyAdminOwner(address(_lockbox))) revert ETHLockbox_DifferentProxyAdminOwner(); authorizedLockboxes[address(_lockbox)] = true; emit LockboxAuthorized(address(_lockbox)); } /// @notice Migrates liquidity from the current ETH lockbox to another. + /// @dev Must be called atomically with `OptimismPortal.updateLockbox()` in the same + /// transaction batch, or otherwise the OptimismPortal may not be able to unlock ETH + /// from the ETHLockbox on finalized withdrawals. /// @param _lockbox The address of the ETH lockbox to migrate liquidity to. function migrateLiquidity(IETHLockbox _lockbox) external { if (msg.sender != proxyAdminOwner()) revert ETHLockbox_Unauthorized(); - if (!_sameproxyAdminOwner(address(_lockbox))) revert ETHLockbox_DifferentProxyAdminOwner(); + if (!_sameProxyAdminOwner(address(_lockbox))) revert ETHLockbox_DifferentProxyAdminOwner(); IETHLockbox(_lockbox).receiveLiquidity{ value: address(this).balance }(); emit LiquidityMigrated(address(_lockbox)); @@ -158,7 +165,7 @@ contract ETHLockbox is ProxyAdminOwnerBase, Initializable, ISemver { /// @notice Authorizes a portal to lock and unlock ETH. /// @param _portal The address of the portal to authorize. function _authorizePortal(address _portal) internal { - if (!_sameproxyAdminOwner(_portal)) revert ETHLockbox_DifferentProxyAdminOwner(); + if (!_sameProxyAdminOwner(_portal)) revert ETHLockbox_DifferentProxyAdminOwner(); authorizedPortals[_portal] = true; emit PortalAuthorized(_portal); } diff --git a/packages/contracts-bedrock/src/L1/OPContractsManager.sol b/packages/contracts-bedrock/src/L1/OPContractsManager.sol index 3db21a056d17e..75c70ee97336a 100644 --- a/packages/contracts-bedrock/src/L1/OPContractsManager.sol +++ b/packages/contracts-bedrock/src/L1/OPContractsManager.sol @@ -619,11 +619,9 @@ contract OPContractsManager is ISemver { ); } - // Deploy the ETHLockbox contract implementation and proxy. - // Initialize the ETHLockbox, and then set the OptimismPortal as an authorized portal. + // Deploy the ETHLockbox proxy. IETHLockbox ethLockbox; { - // Deploy the ETHLockbox contract proxy. ethLockbox = IETHLockbox( deployProxy({ _l2ChainId: l2ChainId, @@ -633,7 +631,7 @@ contract OPContractsManager is ISemver { }) ); - // Initialize the ETHLockbox. + // Initialize the ETHLockbox setting the OptimismPortal as an authorized portal. IOptimismPortal[] memory portals = new IOptimismPortal[](1); portals[0] = IOptimismPortal(payable(opChainAddrs.optimismPortal)); upgradeToAndCall( diff --git a/packages/contracts-bedrock/src/L1/OptimismPortal2.sol b/packages/contracts-bedrock/src/L1/OptimismPortal2.sol index d9398cfb43cd8..2a083f7787ca8 100644 --- a/packages/contracts-bedrock/src/L1/OptimismPortal2.sol +++ b/packages/contracts-bedrock/src/L1/OptimismPortal2.sol @@ -354,6 +354,9 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ReinitializableBase } /// @notice Updates the ETHLockbox contract. + /// @dev This function MUST be called atomically with `ETHLockbox.migrateLiquidity()` + /// in the same transaction batch, or otherwise the OptimismPortal may not be able to + /// unlock ETH from the ETHLockbox on finalized withdrawals. /// @param _newLockbox The address of the new ETHLockbox contract. function updateLockbox(IETHLockbox _newLockbox) external { if (msg.sender != proxyAdminOwner()) revert OptimismPortal_Unauthorized(); diff --git a/packages/contracts-bedrock/src/L1/ProxyAdminOwnerBase.sol b/packages/contracts-bedrock/src/L1/ProxyAdminOwnerBase.sol index 16883227e18cd..9f5861034e566 100644 --- a/packages/contracts-bedrock/src/L1/ProxyAdminOwnerBase.sol +++ b/packages/contracts-bedrock/src/L1/ProxyAdminOwnerBase.sol @@ -20,7 +20,7 @@ abstract contract ProxyAdminOwnerBase { /// @notice Checks if the ProxyAdmin owner of the current contract is the same as the ProxyAdmin owner of the given /// proxy. /// @param _proxy The address of the proxy to check. - function _sameproxyAdminOwner(address _proxy) internal view returns (bool) { + function _sameProxyAdminOwner(address _proxy) internal view returns (bool) { return proxyAdminOwner() == ProxyAdminOwnerBase(_proxy).proxyAdminOwner(); } } diff --git a/packages/contracts-bedrock/test/L1/ETHLockbox.t.sol b/packages/contracts-bedrock/test/L1/ETHLockbox.t.sol index ee95dabea5e32..02c8fd2e1050b 100644 --- a/packages/contracts-bedrock/test/L1/ETHLockbox.t.sol +++ b/packages/contracts-bedrock/test/L1/ETHLockbox.t.sol @@ -205,9 +205,22 @@ contract ETHLockboxTest is CommonTest { ethLockbox.unlockETH(_value); } + /// @notice Tests `unlockETH` reverts when the `_value` input is greater than the balance of the lockbox. + function testFuzz_unlockETH_insufficientBalance_reverts(uint256 _value) public { + _value = bound(_value, address(ethLockbox).balance + 1, type(uint256).max); + + // Expect the revert with `InsufficientBalance` selector + vm.expectRevert(IETHLockbox.ETHLockbox_InsufficientBalance.selector); + + // Call the `unlockETH` function with the portal + vm.prank(address(optimismPortal2)); + ethLockbox.unlockETH(_value); + } + /// @notice Tests `unlockETH` reverts when the portal is not the L2 sender to prevent unlocking ETH from the lockbox /// through a withdrawal transaction. function testFuzz_unlockETH_withdrawalTransaction_reverts(uint256 _value, address _l2Sender) public { + _value = bound(_value, 0, address(ethLockbox).balance); vm.assume(_l2Sender != Constants.DEFAULT_L2_SENDER); // Mock the L2 sender From 0ab40c5f014a44b76411594d3c9370a557f3e705 Mon Sep 17 00:00:00 2001 From: Disco <131301107+0xDiscotech@users.noreply.github.com> Date: Wed, 5 Mar 2025 17:21:29 -0300 Subject: [PATCH 14/50] fix: add lockbox on implementations struct to fix go test (#300) --- op-deployer/pkg/deployer/opcm/implementations.go | 1 + 1 file changed, 1 insertion(+) diff --git a/op-deployer/pkg/deployer/opcm/implementations.go b/op-deployer/pkg/deployer/opcm/implementations.go index 7e8d70bf53525..4aaa670db6447 100644 --- a/op-deployer/pkg/deployer/opcm/implementations.go +++ b/op-deployer/pkg/deployer/opcm/implementations.go @@ -44,6 +44,7 @@ type DeployImplementationsOutput struct { AnchorStateRegistryImpl common.Address SuperchainConfigImpl common.Address ProtocolVersionsImpl common.Address + EthLockboxImpl common.Address } func (output *DeployImplementationsOutput) CheckOutput(input common.Address) error { From 1e55785964d9cda2615f64be8ed9c2e587baf48a Mon Sep 17 00:00:00 2001 From: Kelvin Fichter Date: Thu, 16 Jan 2025 18:01:15 -0600 Subject: [PATCH 15/50] feat: AnchorStateRegistry as source of truth Updates the OptimismPortal to use the AnchorStateRegistry as the source of truth for the validity of Dispute Game contracts. --- .../deployer/integration_test/apply_test.go | 2 +- .../pkg/deployer/opcm/dispute_game_factory.go | 1 - .../pkg/deployer/pipeline/dispute_games.go | 9 +- op-e2e/actions/helpers/user_test.go | 6 + op-e2e/system/bridge/validity_test.go | 6 + op-e2e/system/helpers/withdrawal_helper.go | 7 + packages/contracts-bedrock/foundry.toml | 8 + .../interfaces/L1/IOptimismPortal2.sol | 48 +- .../interfaces/L1/IOptimismPortalInterop.sol | 47 +- .../dispute/IAnchorStateRegistry.sol | 26 +- .../interfaces/safe/IDeputyGuardianModule.sol | 13 +- .../scripts/deploy/ChainAssertions.sol | 3 +- .../scripts/deploy/Deploy.s.sol | 3 +- .../deploy/DeployImplementations.s.sol | 28 +- .../scripts/deploy/DeployOPChain.s.sol | 15 +- .../scripts/deploy/SetDisputeGameImpl.s.sol | 27 +- .../snapshots/abi/AnchorStateRegistry.json | 136 ++++- .../snapshots/abi/DeputyGuardianModule.json | 54 +- .../snapshots/abi/OptimismPortal2.json | 150 ++---- .../snapshots/abi/OptimismPortalInterop.json | 151 ++---- .../snapshots/semver-lock.json | 10 +- .../storageLayout/AnchorStateRegistry.json | 32 +- .../storageLayout/OptimismPortal2.json | 19 +- .../storageLayout/OptimismPortalInterop.json | 19 +- .../src/L1/OPContractsManagerInterop.sol | 79 +++ .../src/L1/OPPrestateUpdater.sol | 172 +++++++ .../src/L1/OptimismPortal2.sol | 467 +++++++++--------- .../src/L1/OptimismPortalInterop.sol | 14 +- .../src/dispute/AnchorStateRegistry.sol | 106 +++- .../src/libraries/PortalErrors.sol | 40 -- .../src/safe/DeputyGuardianModule.sol | 67 +-- .../test/L1/OPContractsManager.t.sol | 4 +- .../test/L1/OPPrestateUpdater.t.sol | 385 +++++++++++++++ .../test/L1/OptimismPortal2.t.sol | 378 ++++++++------ .../test/L1/OptimismPortalInterop.t.sol | 5 +- .../test/dispute/AnchorStateRegistry.t.sol | 362 +++++++++++--- .../test/dispute/FaultDisputeGame.t.sol | 6 + .../test/dispute/SuperFaultDisputeGame.t.sol | 7 +- .../test/invariants/OptimismPortal2.t.sol | 7 +- .../test/kontrol/proofs/OptimismPortal2.k.sol | 5 +- .../test/opcm/DeployImplementations.t.sol | 4 +- .../test/opcm/SetDisputeGameImpl.t.sol | 29 +- .../test/safe/DeputyGuardianModule.t.sol | 165 ++++--- .../test/safe/DeputyPauseModule.t.sol | 3 +- .../test/universal/Specs.t.sol | 53 +- .../test/vendor/Initializable.t.sol | 25 +- 46 files changed, 2090 insertions(+), 1113 deletions(-) create mode 100644 packages/contracts-bedrock/src/L1/OPContractsManagerInterop.sol create mode 100644 packages/contracts-bedrock/src/L1/OPPrestateUpdater.sol delete mode 100644 packages/contracts-bedrock/src/libraries/PortalErrors.sol create mode 100644 packages/contracts-bedrock/test/L1/OPPrestateUpdater.t.sol diff --git a/op-deployer/pkg/deployer/integration_test/apply_test.go b/op-deployer/pkg/deployer/integration_test/apply_test.go index 83884e07bac73..c8a7d624f87c3 100644 --- a/op-deployer/pkg/deployer/integration_test/apply_test.go +++ b/op-deployer/pkg/deployer/integration_test/apply_test.go @@ -297,7 +297,7 @@ func TestProofParamOverrides(t *testing.T) { { "disputeGameFinalityDelaySeconds", uint64Caster, - st.ImplementationsDeployment.OptimismPortalImplAddress, + st.ImplementationsDeployment.AnchorStateRegistryImplAddress, }, { "faultGameAbsolutePrestate", diff --git a/op-deployer/pkg/deployer/opcm/dispute_game_factory.go b/op-deployer/pkg/deployer/opcm/dispute_game_factory.go index 6e79f0683a945..61480523510c4 100644 --- a/op-deployer/pkg/deployer/opcm/dispute_game_factory.go +++ b/op-deployer/pkg/deployer/opcm/dispute_game_factory.go @@ -8,7 +8,6 @@ import ( type SetDisputeGameImplInput struct { Factory common.Address Impl common.Address - Portal common.Address AnchorStateRegistry common.Address GameType uint32 } diff --git a/op-deployer/pkg/deployer/pipeline/dispute_games.go b/op-deployer/pkg/deployer/pipeline/dispute_games.go index dd95398556cdc..5363ba7c49796 100644 --- a/op-deployer/pkg/deployer/pipeline/dispute_games.go +++ b/op-deployer/pkg/deployer/pipeline/dispute_games.go @@ -127,13 +127,12 @@ func deployDisputeGame( lgr.Info("setting dispute game impl on factory", "respected", game.MakeRespected) sdgiInput := opcm.SetDisputeGameImplInput{ - Factory: thisState.DisputeGameFactoryProxyAddress, - Impl: out.DisputeGameImpl, - GameType: game.DisputeGameType, - AnchorStateRegistry: thisState.AnchorStateRegistryProxyAddress, + Factory: thisState.DisputeGameFactoryProxyAddress, + Impl: out.DisputeGameImpl, + GameType: game.DisputeGameType, } if game.MakeRespected { - sdgiInput.Portal = thisState.OptimismPortalProxyAddress + sdgiInput.AnchorStateRegistry = thisState.AnchorStateRegistryProxyAddress } if err := opcm.SetDisputeGameImpl( env.L1ScriptHost, diff --git a/op-e2e/actions/helpers/user_test.go b/op-e2e/actions/helpers/user_test.go index 00b27dea1fe47..2c5822f988942 100644 --- a/op-e2e/actions/helpers/user_test.go +++ b/op-e2e/actions/helpers/user_test.go @@ -308,6 +308,12 @@ func runCrossLayerUserTest(gt *testing.T, test hardforkScheduledTest) { require.Equal(t, types.ReceiptStatusSuccessful, receipt.Status, "proposal failed") } + // Mine an empty block so that the timestamp is updated. Otherwise ActProveWithdrawal will fail + // because it tries to estimate gas based on the current timestamp, which is the same timestamp + // as the dispute game creation timestamp, which causes proveWithdrawalTransaction to revert. + miner.ActL1StartBlock(12)(t) + miner.ActL1EndBlock(t) + // prove our withdrawal on L1 alice.ActProveWithdrawal(t) // include proved withdrawal in new L1 block diff --git a/op-e2e/system/bridge/validity_test.go b/op-e2e/system/bridge/validity_test.go index 4c4ceff07805f..ebc141436a4de 100644 --- a/op-e2e/system/bridge/validity_test.go +++ b/op-e2e/system/bridge/validity_test.go @@ -449,6 +449,12 @@ func testMixedWithdrawalValidity(t *testing.T, allocType config.AllocType) { receiptCl := ethclient.NewClient(rpcClient) blockCl := ethclient.NewClient(rpcClient) + // Mine an empty block so that the timestamp is updated. Otherwise, + // proveWithdrawalTransaction gas estimation may fail because the current timestamp is + // the same as the dispute game creation timestamp. + err = wait.ForNextBlock(ctx, l1Client) + require.NoError(t, err) + // Now create the withdrawal params, err := helpers.ProveWithdrawalParameters(context.Background(), proofCl, receiptCl, blockCl, tx.Hash(), header, l2OutputOracle, disputeGameFactory, optimismPortal2, cfg.AllocType) require.Nil(t, err) diff --git a/op-e2e/system/helpers/withdrawal_helper.go b/op-e2e/system/helpers/withdrawal_helper.go index a2a57c9e3bb66..787495e4aa579 100644 --- a/op-e2e/system/helpers/withdrawal_helper.go +++ b/op-e2e/system/helpers/withdrawal_helper.go @@ -127,12 +127,19 @@ func ProveWithdrawal(t *testing.T, cfg e2esys.SystemConfig, clients ClientProvid require.NoError(t, err) } + // Wait for another block to be mined so that the timestamp increases. Otherwise, + // proveWithdrawalTransaction gas estimation may fail because the current timestamp is the same + // as the dispute game creation timestamp. + err = wait.ForNextBlock(ctx, l1Client) + require.NoError(t, err) + receiptCl := clients.NodeClient(l2NodeName) headerCl := clients.NodeClient(l2NodeName) proofCl := gethclient.New(receiptCl.Client()) ctx, cancel = context.WithTimeout(context.Background(), 30*time.Second) defer cancel() + // Get the latest header header, err := receiptCl.HeaderByNumber(ctx, new(big.Int).SetUint64(blockNumber)) require.NoError(t, err) diff --git a/packages/contracts-bedrock/foundry.toml b/packages/contracts-bedrock/foundry.toml index aafcc6c22b3d6..782d9ec587bc4 100644 --- a/packages/contracts-bedrock/foundry.toml +++ b/packages/contracts-bedrock/foundry.toml @@ -27,7 +27,10 @@ compilation_restrictions = [ { paths = "src/dispute/SuperFaultDisputeGame.sol", optimizer_runs = 5000 }, { paths = "src/dispute/SuperPermissionedDisputeGame.sol", optimizer_runs = 5000 }, { paths = "src/L1/OPContractsManager.sol", optimizer_runs = 5000 }, + { paths = "src/L1/OPContractsManagerInterop.sol", optimizer_runs = 5000 }, { paths = "src/L1/StandardValidator.sol", optimizer_runs = 5000 }, + { paths = "src/L1/OptimismPortal2.sol", optimizer_runs = 5000 }, + { paths = "src/L1/OptimismPortalInterop.sol", optimizer_runs = 5000 }, ] extra_output = ['devdoc', 'userdoc', 'metadata', 'storageLayout'] @@ -138,8 +141,13 @@ additional_compiler_profiles = [ compilation_restrictions = [ { paths = "src/dispute/FaultDisputeGame.sol", optimizer_runs = 0 }, { paths = "src/dispute/PermissionedDisputeGame.sol", optimizer_runs = 0 }, + { paths = "src/dispute/SuperFaultDisputeGame.sol", optimizer_runs = 0 }, + { paths = "src/dispute/SuperPermissionedDisputeGame.sol", optimizer_runs = 0 }, { paths = "src/L1/OPContractsManager.sol", optimizer_runs = 0 }, + { paths = "src/L1/OPContractsManagerInterop.sol", optimizer_runs = 0 }, { paths = "src/L1/StandardValidator.sol", optimizer_runs = 0 }, + { paths = "src/L1/OptimismPortal2.sol", optimizer_runs = 0 }, + { paths = "src/L1/OptimismPortalInterop.sol", optimizer_runs = 0 }, ] ################################################################ diff --git a/packages/contracts-bedrock/interfaces/L1/IOptimismPortal2.sol b/packages/contracts-bedrock/interfaces/L1/IOptimismPortal2.sol index bbf12c3fce4dc..76b0111f171e2 100644 --- a/packages/contracts-bedrock/interfaces/L1/IOptimismPortal2.sol +++ b/packages/contracts-bedrock/interfaces/L1/IOptimismPortal2.sol @@ -2,40 +2,38 @@ pragma solidity ^0.8.0; import { Types } from "src/libraries/Types.sol"; -import { GameType, Timestamp } from "src/dispute/lib/LibUDT.sol"; +import { GameType } from "src/dispute/lib/LibUDT.sol"; 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 { IAnchorStateRegistry } from "interfaces/dispute/IAnchorStateRegistry.sol"; interface IOptimismPortal2 { - error AlreadyFinalized(); - error BadTarget(); - error Blacklisted(); - error CallPaused(); error ContentLengthMismatch(); error EmptyItem(); - error GasEstimation(); error InvalidDataRemainder(); - error InvalidDisputeGame(); - error InvalidGameType(); error InvalidHeader(); - error InvalidMerkleProof(); - error InvalidProof(); - error LargeCalldata(); - error NonReentrant(); + error OptimismPortal_AlreadyFinalized(); + error OptimismPortal_BadTarget(); + error OptimismPortal_CallPaused(); + error OptimismPortal_CalldataTooLarge(); + error OptimismPortal_GasEstimation(); + error OptimismPortal_GasLimitTooLow(); + error OptimismPortal_ImproperDisputeGame(); + error OptimismPortal_InvalidDisputeGame(); + error OptimismPortal_InvalidMerkleProof(); + error OptimismPortal_InvalidOutputRootProof(); + error OptimismPortal_InvalidProofTimestamp(); + error OptimismPortal_InvalidRootClaim(); + error OptimismPortal_NoReentrancy(); + error OptimismPortal_ProofNotOldEnough(); + error OptimismPortal_Unproven(); error OutOfGas(); - error ProposalNotValidated(); - error SmallGasLimit(); - error Unauthorized(); error UnexpectedList(); error UnexpectedString(); - error Unproven(); - error LegacyGame(); - event DisputeGameBlacklisted(IDisputeGame indexed disputeGame); event Initialized(uint8 version); - event RespectedGameTypeSet(GameType indexed newGameType, Timestamp indexed updatedAt); event TransactionDeposited(address indexed from, address indexed to, uint256 indexed version, bytes opaqueData); event WithdrawalFinalized(bytes32 indexed withdrawalHash, bool success); event WithdrawalProven(bytes32 indexed withdrawalHash, address indexed from, address indexed to); @@ -43,7 +41,7 @@ interface IOptimismPortal2 { receive() external payable; - function blacklistDisputeGame(IDisputeGame _disputeGame) external; + function anchorStateRegistry() external view returns (IAnchorStateRegistry); function checkWithdrawal(bytes32 _withdrawalHash, address _proofSubmitter) external view; function depositTransaction( address _to, @@ -54,7 +52,6 @@ interface IOptimismPortal2 { ) external payable; - function disputeGameBlacklist(IDisputeGame) external view returns (bool); function disputeGameFactory() external view returns (IDisputeGameFactory); function disputeGameFinalityDelaySeconds() external view returns (uint256); function donateETH() external payable; @@ -67,10 +64,9 @@ interface IOptimismPortal2 { function finalizedWithdrawals(bytes32) external view returns (bool); function guardian() external view returns (address); function initialize( - IDisputeGameFactory _disputeGameFactory, ISystemConfig _systemConfig, ISuperchainConfig _superchainConfig, - GameType _initialRespectedGameType + IAnchorStateRegistry _anchorStateRegistry ) external; function l2Sender() external view returns (address); @@ -93,13 +89,13 @@ interface IOptimismPortal2 { ) external view - returns (IDisputeGame disputeGameProxy, uint64 timestamp); // nosemgrep + returns (IDisputeGame disputeGameProxy, uint64 timestamp); function respectedGameType() external view returns (GameType); function respectedGameTypeUpdatedAt() external view returns (uint64); - function setRespectedGameType(GameType _gameType) external; function superchainConfig() external view returns (ISuperchainConfig); function systemConfig() external view returns (ISystemConfig); + function upgrade(IAnchorStateRegistry _anchorStateRegistry) external; function version() external pure returns (string memory); - function __constructor__(uint256 _proofMaturityDelaySeconds, uint256 _disputeGameFinalityDelaySeconds) external; + function __constructor__(uint256 _proofMaturityDelaySeconds) external; } diff --git a/packages/contracts-bedrock/interfaces/L1/IOptimismPortalInterop.sol b/packages/contracts-bedrock/interfaces/L1/IOptimismPortalInterop.sol index 6dec13a8335c9..716b42f41985b 100644 --- a/packages/contracts-bedrock/interfaces/L1/IOptimismPortalInterop.sol +++ b/packages/contracts-bedrock/interfaces/L1/IOptimismPortalInterop.sol @@ -2,41 +2,40 @@ pragma solidity ^0.8.0; import { Types } from "src/libraries/Types.sol"; -import { GameType, Timestamp } from "src/dispute/lib/LibUDT.sol"; +import { GameType } from "src/dispute/lib/LibUDT.sol"; 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 { ConfigType } from "interfaces/L2/IL1BlockInterop.sol"; +import { IAnchorStateRegistry } from "interfaces/dispute/IAnchorStateRegistry.sol"; interface IOptimismPortalInterop { - error AlreadyFinalized(); - error BadTarget(); - error Blacklisted(); - error CallPaused(); error ContentLengthMismatch(); error EmptyItem(); - error GasEstimation(); error InvalidDataRemainder(); - error InvalidDisputeGame(); - error InvalidGameType(); error InvalidHeader(); - error InvalidMerkleProof(); - error InvalidProof(); - error LargeCalldata(); - error NonReentrant(); + error OptimismPortal_AlreadyFinalized(); + error OptimismPortal_BadTarget(); + error OptimismPortal_CallPaused(); + error OptimismPortal_CalldataTooLarge(); + error OptimismPortal_GasEstimation(); + error OptimismPortal_GasLimitTooLow(); + error OptimismPortal_ImproperDisputeGame(); + error OptimismPortal_InvalidDisputeGame(); + error OptimismPortal_InvalidMerkleProof(); + error OptimismPortal_InvalidOutputRootProof(); + error OptimismPortal_InvalidProofTimestamp(); + error OptimismPortal_InvalidRootClaim(); + error OptimismPortal_NoReentrancy(); + error OptimismPortal_ProofNotOldEnough(); + error OptimismPortal_Unauthorized(); + error OptimismPortal_Unproven(); error OutOfGas(); - error ProposalNotValidated(); - error SmallGasLimit(); - error Unauthorized(); error UnexpectedList(); error UnexpectedString(); - error Unproven(); - error LegacyGame(); - event DisputeGameBlacklisted(IDisputeGame indexed disputeGame); event Initialized(uint8 version); - event RespectedGameTypeSet(GameType indexed newGameType, Timestamp indexed updatedAt); event TransactionDeposited(address indexed from, address indexed to, uint256 indexed version, bytes opaqueData); event WithdrawalFinalized(bytes32 indexed withdrawalHash, bool success); event WithdrawalProven(bytes32 indexed withdrawalHash, address indexed from, address indexed to); @@ -44,7 +43,7 @@ interface IOptimismPortalInterop { receive() external payable; - function blacklistDisputeGame(IDisputeGame _disputeGame) external; + function anchorStateRegistry() external view returns (IAnchorStateRegistry); function checkWithdrawal(bytes32 _withdrawalHash, address _proofSubmitter) external view; function depositTransaction( address _to, @@ -55,7 +54,6 @@ interface IOptimismPortalInterop { ) external payable; - function disputeGameBlacklist(IDisputeGame) external view returns (bool); function disputeGameFactory() external view returns (IDisputeGameFactory); function disputeGameFinalityDelaySeconds() external view returns (uint256); function donateETH() external payable; @@ -68,10 +66,9 @@ interface IOptimismPortalInterop { function finalizedWithdrawals(bytes32) external view returns (bool); function guardian() external view returns (address); function initialize( - IDisputeGameFactory _disputeGameFactory, ISystemConfig _systemConfig, ISuperchainConfig _superchainConfig, - GameType _initialRespectedGameType + IAnchorStateRegistry _anchorStateRegistry ) external; function l2Sender() external view returns (address); @@ -98,10 +95,10 @@ interface IOptimismPortalInterop { function respectedGameType() external view returns (GameType); function respectedGameTypeUpdatedAt() external view returns (uint64); function setConfig(ConfigType _type, bytes memory _value) external; - function setRespectedGameType(GameType _gameType) external; function superchainConfig() external view returns (ISuperchainConfig); function systemConfig() external view returns (ISystemConfig); + function upgrade(IAnchorStateRegistry _anchorStateRegistry) external; function version() external pure returns (string memory); - function __constructor__(uint256 _proofMaturityDelaySeconds, uint256 _disputeGameFinalityDelaySeconds) external; + function __constructor__(uint256 _proofMaturityDelaySeconds) external; } diff --git a/packages/contracts-bedrock/interfaces/dispute/IAnchorStateRegistry.sol b/packages/contracts-bedrock/interfaces/dispute/IAnchorStateRegistry.sol index a6f7d3a45c430..d6c96648e63d1 100644 --- a/packages/contracts-bedrock/interfaces/dispute/IAnchorStateRegistry.sol +++ b/packages/contracts-bedrock/interfaces/dispute/IAnchorStateRegistry.sol @@ -5,30 +5,33 @@ import { IDisputeGame } from "interfaces/dispute/IDisputeGame.sol"; import { IFaultDisputeGame } from "interfaces/dispute/IFaultDisputeGame.sol"; import { IDisputeGameFactory } from "interfaces/dispute/IDisputeGameFactory.sol"; import { ISuperchainConfig } from "interfaces/L1/ISuperchainConfig.sol"; -import { IOptimismPortal2 } from "interfaces/L1/IOptimismPortal2.sol"; import { GameType, Hash, OutputRoot } from "src/dispute/lib/Types.sol"; interface IAnchorStateRegistry { - error AnchorStateRegistry_Unauthorized(); - error AnchorStateRegistry_InvalidAnchorGame(); error AnchorStateRegistry_AnchorGameBlacklisted(); + error AnchorStateRegistry_InvalidAnchorGame(); + error AnchorStateRegistry_Unauthorized(); - event AnchorNotUpdated(IFaultDisputeGame indexed game); event AnchorUpdated(IFaultDisputeGame indexed game); + event DisputeGameBlacklisted(IDisputeGame indexed disputeGame); event Initialized(uint8 version); + event RespectedGameTypeSet(GameType gameType); + event RetirementTimestampSet(uint256 timestamp); function anchorGame() external view returns (IFaultDisputeGame); function anchors(GameType) external view returns (Hash, uint256); + function blacklistDisputeGame(IDisputeGame _disputeGame) external; + function disputeGameBlacklist(IDisputeGame) external view returns (bool); function getAnchorRoot() external view returns (Hash, uint256); + function disputeGameFinalityDelaySeconds() external view returns (uint256); function disputeGameFactory() external view returns (IDisputeGameFactory); function initialize( ISuperchainConfig _superchainConfig, IDisputeGameFactory _disputeGameFactory, - IOptimismPortal2 _portal, - OutputRoot memory _startingAnchorRoot + OutputRoot memory _startingAnchorRoot, + GameType _startingRespectedGameType ) external; - function isGameBlacklisted(IDisputeGame _game) external view returns (bool); function isGameProper(IDisputeGame _game) external view returns (bool); function isGameRegistered(IDisputeGame _game) external view returns (bool); @@ -37,11 +40,16 @@ interface IAnchorStateRegistry { function isGameRetired(IDisputeGame _game) external view returns (bool); function isGameFinalized(IDisputeGame _game) external view returns (bool); function isGameClaimValid(IDisputeGame _game) external view returns (bool); - function portal() external view returns (IOptimismPortal2); + function paused() external view returns (bool); function respectedGameType() external view returns (GameType); + function retirementTimestamp() external view returns (uint64); function setAnchorState(IDisputeGame _game) external; + function setRespectedGameType(GameType _gameType) external; function superchainConfig() external view returns (ISuperchainConfig); + function updateRetirementTimestamp() external; function version() external view returns (string memory); - function __constructor__() external; + function __constructor__( + uint256 _disputeGameFinalityDelaySeconds + ) external; } diff --git a/packages/contracts-bedrock/interfaces/safe/IDeputyGuardianModule.sol b/packages/contracts-bedrock/interfaces/safe/IDeputyGuardianModule.sol index a5c0e33130279..d4d76649a50c7 100644 --- a/packages/contracts-bedrock/interfaces/safe/IDeputyGuardianModule.sol +++ b/packages/contracts-bedrock/interfaces/safe/IDeputyGuardianModule.sol @@ -2,22 +2,21 @@ pragma solidity ^0.8.0; import { IAnchorStateRegistry } from "interfaces/dispute/IAnchorStateRegistry.sol"; -import { IFaultDisputeGame } from "interfaces/dispute/IFaultDisputeGame.sol"; import { ISuperchainConfig } from "interfaces/L1/ISuperchainConfig.sol"; -import { IOptimismPortal2 } from "interfaces/L1/IOptimismPortal2.sol"; import { IDisputeGame } from "interfaces/dispute/IDisputeGame.sol"; import { ISemver } from "interfaces/universal/ISemver.sol"; import { GameType, Timestamp } from "src/dispute/lib/Types.sol"; import { GnosisSafe as Safe } from "safe-contracts/GnosisSafe.sol"; interface IDeputyGuardianModule is ISemver { - error ExecutionFailed(string); - error Unauthorized(); + error DeputyGuardianModule_ExecutionFailed(string); + error DeputyGuardianModule_Unauthorized(); event Paused(string identifier); event Unpaused(); event DisputeGameBlacklisted(IDisputeGame indexed game); event RespectedGameTypeSet(GameType indexed gameType, Timestamp indexed updatedAt); + event RetirementTimestampUpdated(Timestamp indexed updatedAt); function version() external view returns (string memory); function __constructor__(Safe _safe, ISuperchainConfig _superchainConfig, address _deputyGuardian) external; @@ -26,7 +25,7 @@ interface IDeputyGuardianModule is ISemver { function deputyGuardian() external view returns (address deputyGuardian_); function pause() external; function unpause() external; - function setAnchorState(IAnchorStateRegistry _registry, IFaultDisputeGame _game) external; - function blacklistDisputeGame(IOptimismPortal2 _portal, IDisputeGame _game) external; - function setRespectedGameType(IOptimismPortal2 _portal, GameType _gameType) external; + function blacklistDisputeGame(IAnchorStateRegistry _anchorStateRegistry, IDisputeGame _game) external; + function setRespectedGameType(IAnchorStateRegistry _anchorStateRegistry, GameType _gameType) external; + function updateRetirementTimestamp(IAnchorStateRegistry _anchorStateRegistry) external; } diff --git a/packages/contracts-bedrock/scripts/deploy/ChainAssertions.sol b/packages/contracts-bedrock/scripts/deploy/ChainAssertions.sol index 026cdb178f96f..b73d9e069a271 100644 --- a/packages/contracts-bedrock/scripts/deploy/ChainAssertions.sol +++ b/packages/contracts-bedrock/scripts/deploy/ChainAssertions.sol @@ -385,13 +385,14 @@ library ChainAssertions { if (_isProxy) { require(address(portal.disputeGameFactory()) == _contracts.DisputeGameFactory, "CHECK-OP2-20"); + require(address(portal.anchorStateRegistry()) == _contracts.AnchorStateRegistry, "CHECK-OP2-25"); require(address(portal.systemConfig()) == _contracts.SystemConfig, "CHECK-OP2-30"); require(portal.guardian() == guardian, "CHECK-OP2-40"); require(address(portal.superchainConfig()) == address(_contracts.SuperchainConfig), "CHECK-OP2-50"); require(portal.paused() == ISuperchainConfig(_contracts.SuperchainConfig).paused(), "CHECK-OP2-60"); require(portal.l2Sender() == Constants.DEFAULT_L2_SENDER, "CHECK-OP2-70"); } else { - require(address(portal.disputeGameFactory()) == address(0), "CHECK-OP2-80"); + require(address(portal.anchorStateRegistry()) == address(0), "CHECK-OP2-80"); require(address(portal.systemConfig()) == address(0), "CHECK-OP2-90"); require(address(portal.superchainConfig()) == address(0), "CHECK-OP2-100"); require(portal.l2Sender() == address(0), "CHECK-OP2-110"); diff --git a/packages/contracts-bedrock/scripts/deploy/Deploy.s.sol b/packages/contracts-bedrock/scripts/deploy/Deploy.s.sol index f4638d208011d..006d91b5b072a 100644 --- a/packages/contracts-bedrock/scripts/deploy/Deploy.s.sol +++ b/packages/contracts-bedrock/scripts/deploy/Deploy.s.sol @@ -35,7 +35,6 @@ import { GameType, Claim, GameTypes, OutputRoot, Hash } from "src/dispute/lib/Ty import { IOPContractsManager } from "interfaces/L1/IOPContractsManager.sol"; import { IProxy } from "interfaces/universal/IProxy.sol"; import { IProxyAdmin } from "interfaces/universal/IProxyAdmin.sol"; -import { IOptimismPortal2 } from "interfaces/L1/IOptimismPortal2.sol"; import { ISuperchainConfig } from "interfaces/L1/ISuperchainConfig.sol"; import { ISystemConfig } from "interfaces/L1/ISystemConfig.sol"; import { IDataAvailabilityChallenge } from "interfaces/L1/IDataAvailabilityChallenge.sol"; @@ -206,7 +205,7 @@ contract Deploy is Deployer { // Set the respected game type according to the deploy config vm.startPrank(ISuperchainConfig(artifacts.mustGetAddress("SuperchainConfigProxy")).guardian()); - IOptimismPortal2(artifacts.mustGetAddress("OptimismPortalProxy")).setRespectedGameType( + IAnchorStateRegistry(artifacts.mustGetAddress("AnchorStateRegistryProxy")).setRespectedGameType( GameType.wrap(uint32(cfg.respectedGameType())) ); vm.stopPrank(); diff --git a/packages/contracts-bedrock/scripts/deploy/DeployImplementations.s.sol b/packages/contracts-bedrock/scripts/deploy/DeployImplementations.s.sol index 5cd0414734547..8b8f979a073bc 100644 --- a/packages/contracts-bedrock/scripts/deploy/DeployImplementations.s.sol +++ b/packages/contracts-bedrock/scripts/deploy/DeployImplementations.s.sol @@ -343,7 +343,7 @@ contract DeployImplementationsOutput is BaseDeployIO { DeployUtils.assertInitialized({ _contractAddress: address(portal), _isProxy: false, _slot: 0, _offset: 0 }); - require(address(portal.disputeGameFactory()) == address(0), "PORTAL-10"); + require(address(portal.anchorStateRegistry()) == address(0), "PORTAL-10"); require(address(portal.systemConfig()) == address(0), "PORTAL-20"); require(address(portal.superchainConfig()) == address(0), "PORTAL-30"); require(portal.l2Sender() == address(0), "PORTAL-40"); @@ -489,7 +489,7 @@ contract DeployImplementations is Script { deployPreimageOracleSingleton(_dii, _dio); deployMipsSingleton(_dii, _dio); deployDisputeGameFactoryImpl(_dio); - deployAnchorStateRegistryImpl(_dio); + deployAnchorStateRegistryImpl(_dii, _dio); // Deploy the OP Contracts Manager with the new implementations set. deployOPContractsManager(_dii, _dio); @@ -725,14 +725,11 @@ contract DeployImplementations is Script { virtual { uint256 proofMaturityDelaySeconds = _dii.proofMaturityDelaySeconds(); - uint256 disputeGameFinalityDelaySeconds = _dii.disputeGameFinalityDelaySeconds(); IOptimismPortal2 impl = IOptimismPortal2( DeployUtils.createDeterministic({ _name: "OptimismPortal2", _args: DeployUtils.encodeConstructor( - abi.encodeCall( - IOptimismPortal2.__constructor__, (proofMaturityDelaySeconds, disputeGameFinalityDelaySeconds) - ) + abi.encodeCall(IOptimismPortal2.__constructor__, (proofMaturityDelaySeconds)) ), _salt: _salt }) @@ -810,11 +807,20 @@ contract DeployImplementations is Script { _dio.set(_dio.disputeGameFactoryImpl.selector, address(impl)); } - function deployAnchorStateRegistryImpl(DeployImplementationsOutput _dio) public virtual { + function deployAnchorStateRegistryImpl( + DeployImplementationsInput _dii, + DeployImplementationsOutput _dio + ) + public + virtual + { + uint256 disputeGameFinalityDelaySeconds = _dii.disputeGameFinalityDelaySeconds(); IAnchorStateRegistry impl = IAnchorStateRegistry( DeployUtils.createDeterministic({ _name: "AnchorStateRegistry", - _args: DeployUtils.encodeConstructor(abi.encodeCall(IAnchorStateRegistry.__constructor__, ())), + _args: DeployUtils.encodeConstructor( + abi.encodeCall(IAnchorStateRegistry.__constructor__, (disputeGameFinalityDelaySeconds)) + ), _salt: _salt }) ); @@ -963,19 +969,15 @@ contract DeployImplementationsInterop is DeployImplementations { override { uint256 proofMaturityDelaySeconds = _dii.proofMaturityDelaySeconds(); - uint256 disputeGameFinalityDelaySeconds = _dii.disputeGameFinalityDelaySeconds(); IOptimismPortalInterop impl = IOptimismPortalInterop( DeployUtils.createDeterministic({ _name: "OptimismPortalInterop", _args: DeployUtils.encodeConstructor( - abi.encodeCall( - IOptimismPortalInterop.__constructor__, (proofMaturityDelaySeconds, disputeGameFinalityDelaySeconds) - ) + abi.encodeCall(IOptimismPortalInterop.__constructor__, (proofMaturityDelaySeconds)) ), _salt: _salt }) ); - vm.label(address(impl), "OptimismPortalImpl"); _dio.set(_dio.optimismPortalImpl.selector, address(impl)); } diff --git a/packages/contracts-bedrock/scripts/deploy/DeployOPChain.s.sol b/packages/contracts-bedrock/scripts/deploy/DeployOPChain.s.sol index 9541db0bbab5a..8af54e4224123 100644 --- a/packages/contracts-bedrock/scripts/deploy/DeployOPChain.s.sol +++ b/packages/contracts-bedrock/scripts/deploy/DeployOPChain.s.sol @@ -612,16 +612,17 @@ contract DeployOPChain is Script { IOptimismPortal2 portal = _doo.optimismPortalProxy(); ISuperchainConfig superchainConfig = ISuperchainConfig(address(_doi.opcm().superchainConfig())); - require(address(portal.disputeGameFactory()) == address(_doo.disputeGameFactoryProxy()), "PORTAL-10"); - require(address(portal.systemConfig()) == address(_doo.systemConfigProxy()), "PORTAL-20"); - require(address(portal.superchainConfig()) == address(superchainConfig), "PORTAL-30"); - require(portal.guardian() == superchainConfig.guardian(), "PORTAL-40"); - require(portal.paused() == superchainConfig.paused(), "PORTAL-50"); - require(portal.l2Sender() == Constants.DEFAULT_L2_SENDER, "PORTAL-60"); + require(address(portal.anchorStateRegistry()) == address(_doo.anchorStateRegistryProxy()), "PORTAL-10"); + require(address(portal.disputeGameFactory()) == address(_doo.disputeGameFactoryProxy()), "PORTAL-20"); + require(address(portal.systemConfig()) == address(_doo.systemConfigProxy()), "PORTAL-30"); + require(address(portal.superchainConfig()) == address(superchainConfig), "PORTAL-40"); + require(portal.guardian() == superchainConfig.guardian(), "PORTAL-50"); + require(portal.paused() == superchainConfig.paused(), "PORTAL-60"); + require(portal.l2Sender() == Constants.DEFAULT_L2_SENDER, "PORTAL-70"); // This slot is the custom gas token _balance and this check ensures // that it stays unset for forwards compatibility with custom gas token. - require(vm.load(address(portal), bytes32(uint256(61))) == bytes32(0), "PORTAL-70"); + require(vm.load(address(portal), bytes32(uint256(61))) == bytes32(0), "PORTAL-80"); } function assertValidDisputeGameFactory(DeployOPChainInput _doi, DeployOPChainOutput _doo) internal { diff --git a/packages/contracts-bedrock/scripts/deploy/SetDisputeGameImpl.s.sol b/packages/contracts-bedrock/scripts/deploy/SetDisputeGameImpl.s.sol index 17e3d5a99dce2..77d4880f02aa5 100644 --- a/packages/contracts-bedrock/scripts/deploy/SetDisputeGameImpl.s.sol +++ b/packages/contracts-bedrock/scripts/deploy/SetDisputeGameImpl.s.sol @@ -7,11 +7,11 @@ import { IFaultDisputeGame } from "interfaces/dispute/IFaultDisputeGame.sol"; import { IDisputeGameFactory } from "interfaces/dispute/IDisputeGameFactory.sol"; import { BaseDeployIO } from "scripts/deploy/BaseDeployIO.sol"; import { GameType } from "src/dispute/lib/Types.sol"; -import { IOptimismPortal2 } from "interfaces/L1/IOptimismPortal2.sol"; +import { IAnchorStateRegistry } from "interfaces/dispute/IAnchorStateRegistry.sol"; contract SetDisputeGameImplInput is BaseDeployIO { IDisputeGameFactory internal _factory; - IOptimismPortal2 internal _portal; + IAnchorStateRegistry internal _anchorStateRegistry; IFaultDisputeGame internal _impl; uint32 internal _gameType; @@ -20,7 +20,7 @@ contract SetDisputeGameImplInput is BaseDeployIO { require(_addr != address(0), "SetDisputeGameImplInput: cannot set zero address"); if (_sel == this.factory.selector) _factory = IDisputeGameFactory(_addr); - else if (_sel == this.portal.selector) _portal = IOptimismPortal2(payable(_addr)); + else if (_sel == this.anchorStateRegistry.selector) _anchorStateRegistry = IAnchorStateRegistry(_addr); else if (_sel == this.impl.selector) _impl = IFaultDisputeGame(_addr); else revert("SetDisputeGameImplInput: unknown selector"); } @@ -36,8 +36,8 @@ contract SetDisputeGameImplInput is BaseDeployIO { return _factory; } - function portal() public view returns (IOptimismPortal2) { - return _portal; + function anchorStateRegistry() public view returns (IAnchorStateRegistry) { + return _anchorStateRegistry; } function impl() public view returns (IFaultDisputeGame) { @@ -57,15 +57,15 @@ contract SetDisputeGameImpl is Script { require(address(factory.gameImpls(gameType)) == address(0), "SDGI-10"); IFaultDisputeGame impl = _input.impl(); - IOptimismPortal2 portal = _input.portal(); + IAnchorStateRegistry anchorStateRegistry = _input.anchorStateRegistry(); vm.broadcast(msg.sender); factory.setImplementation(gameType, impl); - if (address(portal) != address(0)) { - require(address(portal.disputeGameFactory()) == address(factory), "SDGI-20"); + if (address(anchorStateRegistry) != address(0)) { + require(address(anchorStateRegistry.disputeGameFactory()) == address(factory), "SDGI-20"); vm.broadcast(msg.sender); - portal.setRespectedGameType(gameType); + anchorStateRegistry.setRespectedGameType(gameType); } assertValid(_input); @@ -75,9 +75,12 @@ contract SetDisputeGameImpl is Script { GameType gameType = GameType.wrap(_input.gameType()); require(address(_input.factory().gameImpls(gameType)) == address(_input.impl()), "SDGI-30"); - if (address(_input.portal()) != address(0)) { - require(address(_input.portal().disputeGameFactory()) == address(_input.factory()), "SDGI-40"); - require(GameType.unwrap(_input.portal().respectedGameType()) == GameType.unwrap(gameType), "SDGI-50"); + if (address(_input.anchorStateRegistry()) != address(0)) { + require(address(_input.anchorStateRegistry().disputeGameFactory()) == address(_input.factory()), "SDGI-40"); + require( + GameType.unwrap(_input.anchorStateRegistry().respectedGameType()) == GameType.unwrap(gameType), + "SDGI-50" + ); } } } diff --git a/packages/contracts-bedrock/snapshots/abi/AnchorStateRegistry.json b/packages/contracts-bedrock/snapshots/abi/AnchorStateRegistry.json index 25919d1d4bfb1..3aa3c26096c60 100644 --- a/packages/contracts-bedrock/snapshots/abi/AnchorStateRegistry.json +++ b/packages/contracts-bedrock/snapshots/abi/AnchorStateRegistry.json @@ -1,6 +1,12 @@ [ { - "inputs": [], + "inputs": [ + { + "internalType": "uint256", + "name": "_disputeGameFinalityDelaySeconds", + "type": "uint256" + } + ], "stateMutability": "nonpayable", "type": "constructor" }, @@ -41,6 +47,38 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [ + { + "internalType": "contract IDisputeGame", + "name": "_disputeGame", + "type": "address" + } + ], + "name": "blacklistDisputeGame", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract IDisputeGame", + "name": "", + "type": "address" + } + ], + "name": "disputeGameBlacklist", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [], "name": "disputeGameFactory", @@ -54,6 +92,19 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [], + "name": "disputeGameFinalityDelaySeconds", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [], "name": "getAnchorRoot", @@ -84,11 +135,6 @@ "name": "_disputeGameFactory", "type": "address" }, - { - "internalType": "contract IOptimismPortal2", - "name": "_portal", - "type": "address" - }, { "components": [ { @@ -105,6 +151,11 @@ "internalType": "struct OutputRoot", "name": "_startingAnchorRoot", "type": "tuple" + }, + { + "internalType": "GameType", + "name": "_startingRespectedGameType", + "type": "uint32" } ], "name": "initialize", @@ -266,12 +317,12 @@ }, { "inputs": [], - "name": "portal", + "name": "paused", "outputs": [ { - "internalType": "contract IOptimismPortal2", + "internalType": "bool", "name": "", - "type": "address" + "type": "bool" } ], "stateMutability": "view", @@ -290,6 +341,19 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [], + "name": "retirementTimestamp", + "outputs": [ + { + "internalType": "uint64", + "name": "", + "type": "uint64" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [ { @@ -303,6 +367,19 @@ "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [ + { + "internalType": "GameType", + "name": "_gameType", + "type": "uint32" + } + ], + "name": "setRespectedGameType", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [], "name": "superchainConfig", @@ -316,6 +393,13 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [], + "name": "updateRetirementTimestamp", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [], "name": "version", @@ -339,7 +423,7 @@ "type": "address" } ], - "name": "AnchorNotUpdated", + "name": "AnchorUpdated", "type": "event" }, { @@ -347,12 +431,12 @@ "inputs": [ { "indexed": true, - "internalType": "contract IFaultDisputeGame", - "name": "game", + "internalType": "contract IDisputeGame", + "name": "disputeGame", "type": "address" } ], - "name": "AnchorUpdated", + "name": "DisputeGameBlacklisted", "type": "event" }, { @@ -368,6 +452,32 @@ "name": "Initialized", "type": "event" }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "GameType", + "name": "gameType", + "type": "uint32" + } + ], + "name": "RespectedGameTypeSet", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "timestamp", + "type": "uint256" + } + ], + "name": "RetirementTimestampSet", + "type": "event" + }, { "inputs": [], "name": "AnchorStateRegistry_AnchorGameBlacklisted", diff --git a/packages/contracts-bedrock/snapshots/abi/DeputyGuardianModule.json b/packages/contracts-bedrock/snapshots/abi/DeputyGuardianModule.json index f1006749351ea..ab662e4ee49fc 100644 --- a/packages/contracts-bedrock/snapshots/abi/DeputyGuardianModule.json +++ b/packages/contracts-bedrock/snapshots/abi/DeputyGuardianModule.json @@ -23,8 +23,8 @@ { "inputs": [ { - "internalType": "contract IOptimismPortal2", - "name": "_portal", + "internalType": "contract IAnchorStateRegistry", + "name": "_anchorStateRegistry", "type": "address" }, { @@ -75,25 +75,7 @@ "inputs": [ { "internalType": "contract IAnchorStateRegistry", - "name": "_registry", - "type": "address" - }, - { - "internalType": "contract IFaultDisputeGame", - "name": "_game", - "type": "address" - } - ], - "name": "setAnchorState", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "contract IOptimismPortal2", - "name": "_portal", + "name": "_anchorStateRegistry", "type": "address" }, { @@ -127,6 +109,19 @@ "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [ + { + "internalType": "contract IAnchorStateRegistry", + "name": "_anchorStateRegistry", + "type": "address" + } + ], + "name": "updateRetirementTimestamp", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [], "name": "version", @@ -185,6 +180,19 @@ "name": "RespectedGameTypeSet", "type": "event" }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "Timestamp", + "name": "updatedAt", + "type": "uint64" + } + ], + "name": "RetirementTimestampUpdated", + "type": "event" + }, { "anonymous": false, "inputs": [], @@ -199,12 +207,12 @@ "type": "string" } ], - "name": "ExecutionFailed", + "name": "DeputyGuardianModule_ExecutionFailed", "type": "error" }, { "inputs": [], - "name": "Unauthorized", + "name": "DeputyGuardianModule_Unauthorized", "type": "error" } ] \ 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 71b8677f7dd85..ebc1b9132de40 100644 --- a/packages/contracts-bedrock/snapshots/abi/OptimismPortal2.json +++ b/packages/contracts-bedrock/snapshots/abi/OptimismPortal2.json @@ -5,11 +5,6 @@ "internalType": "uint256", "name": "_proofMaturityDelaySeconds", "type": "uint256" - }, - { - "internalType": "uint256", - "name": "_disputeGameFinalityDelaySeconds", - "type": "uint256" } ], "stateMutability": "nonpayable", @@ -20,16 +15,16 @@ "type": "receive" }, { - "inputs": [ + "inputs": [], + "name": "anchorStateRegistry", + "outputs": [ { - "internalType": "contract IDisputeGame", - "name": "_disputeGame", + "internalType": "contract IAnchorStateRegistry", + "name": "", "type": "address" } ], - "name": "blacklistDisputeGame", - "outputs": [], - "stateMutability": "nonpayable", + "stateMutability": "view", "type": "function" }, { @@ -83,25 +78,6 @@ "stateMutability": "payable", "type": "function" }, - { - "inputs": [ - { - "internalType": "contract IDisputeGame", - "name": "", - "type": "address" - } - ], - "name": "disputeGameBlacklist", - "outputs": [ - { - "internalType": "bool", - "name": "", - "type": "bool" - } - ], - "stateMutability": "view", - "type": "function" - }, { "inputs": [], "name": "disputeGameFactory", @@ -264,11 +240,6 @@ }, { "inputs": [ - { - "internalType": "contract IDisputeGameFactory", - "name": "_disputeGameFactory", - "type": "address" - }, { "internalType": "contract ISystemConfig", "name": "_systemConfig", @@ -280,9 +251,9 @@ "type": "address" }, { - "internalType": "GameType", - "name": "_initialRespectedGameType", - "type": "uint32" + "internalType": "contract IAnchorStateRegistry", + "name": "_anchorStateRegistry", + "type": "address" } ], "name": "initialize", @@ -551,19 +522,6 @@ "stateMutability": "view", "type": "function" }, - { - "inputs": [ - { - "internalType": "GameType", - "name": "_gameType", - "type": "uint32" - } - ], - "name": "setRespectedGameType", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, { "inputs": [], "name": "superchainConfig", @@ -590,6 +548,19 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [ + { + "internalType": "contract IAnchorStateRegistry", + "name": "_anchorStateRegistry", + "type": "address" + } + ], + "name": "upgrade", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [], "name": "version", @@ -603,19 +574,6 @@ "stateMutability": "pure", "type": "function" }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "contract IDisputeGame", - "name": "disputeGame", - "type": "address" - } - ], - "name": "DisputeGameBlacklisted", - "type": "event" - }, { "anonymous": false, "inputs": [ @@ -629,25 +587,6 @@ "name": "Initialized", "type": "event" }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "GameType", - "name": "newGameType", - "type": "uint32" - }, - { - "indexed": true, - "internalType": "Timestamp", - "name": "updatedAt", - "type": "uint64" - } - ], - "name": "RespectedGameTypeSet", - "type": "event" - }, { "anonymous": false, "inputs": [ @@ -744,102 +683,102 @@ }, { "inputs": [], - "name": "AlreadyFinalized", + "name": "ContentLengthMismatch", "type": "error" }, { "inputs": [], - "name": "BadTarget", + "name": "EmptyItem", "type": "error" }, { "inputs": [], - "name": "Blacklisted", + "name": "InvalidDataRemainder", "type": "error" }, { "inputs": [], - "name": "CallPaused", + "name": "InvalidHeader", "type": "error" }, { "inputs": [], - "name": "ContentLengthMismatch", + "name": "OptimismPortal_AlreadyFinalized", "type": "error" }, { "inputs": [], - "name": "EmptyItem", + "name": "OptimismPortal_BadTarget", "type": "error" }, { "inputs": [], - "name": "GasEstimation", + "name": "OptimismPortal_CallPaused", "type": "error" }, { "inputs": [], - "name": "InvalidDataRemainder", + "name": "OptimismPortal_CalldataTooLarge", "type": "error" }, { "inputs": [], - "name": "InvalidDisputeGame", + "name": "OptimismPortal_GasEstimation", "type": "error" }, { "inputs": [], - "name": "InvalidGameType", + "name": "OptimismPortal_GasLimitTooLow", "type": "error" }, { "inputs": [], - "name": "InvalidHeader", + "name": "OptimismPortal_ImproperDisputeGame", "type": "error" }, { "inputs": [], - "name": "InvalidMerkleProof", + "name": "OptimismPortal_InvalidDisputeGame", "type": "error" }, { "inputs": [], - "name": "InvalidProof", + "name": "OptimismPortal_InvalidMerkleProof", "type": "error" }, { "inputs": [], - "name": "LargeCalldata", + "name": "OptimismPortal_InvalidOutputRootProof", "type": "error" }, { "inputs": [], - "name": "LegacyGame", + "name": "OptimismPortal_InvalidProofTimestamp", "type": "error" }, { "inputs": [], - "name": "NonReentrant", + "name": "OptimismPortal_InvalidRootClaim", "type": "error" }, { "inputs": [], - "name": "OutOfGas", + "name": "OptimismPortal_NoReentrancy", "type": "error" }, { "inputs": [], - "name": "ProposalNotValidated", + "name": "OptimismPortal_ProofNotOldEnough", "type": "error" }, { "inputs": [], - "name": "SmallGasLimit", + "name": "OptimismPortal_Unproven", "type": "error" }, { "inputs": [], - "name": "Unauthorized", + "name": "OutOfGas", "type": "error" }, { @@ -851,10 +790,5 @@ "inputs": [], "name": "UnexpectedString", "type": "error" - }, - { - "inputs": [], - "name": "Unproven", - "type": "error" } ] \ No newline at end of file diff --git a/packages/contracts-bedrock/snapshots/abi/OptimismPortalInterop.json b/packages/contracts-bedrock/snapshots/abi/OptimismPortalInterop.json index 2dd27e68eaff9..4a6c342105f2c 100644 --- a/packages/contracts-bedrock/snapshots/abi/OptimismPortalInterop.json +++ b/packages/contracts-bedrock/snapshots/abi/OptimismPortalInterop.json @@ -5,11 +5,6 @@ "internalType": "uint256", "name": "_proofMaturityDelaySeconds", "type": "uint256" - }, - { - "internalType": "uint256", - "name": "_disputeGameFinalityDelaySeconds", - "type": "uint256" } ], "stateMutability": "nonpayable", @@ -20,16 +15,16 @@ "type": "receive" }, { - "inputs": [ + "inputs": [], + "name": "anchorStateRegistry", + "outputs": [ { - "internalType": "contract IDisputeGame", - "name": "_disputeGame", + "internalType": "contract IAnchorStateRegistry", + "name": "", "type": "address" } ], - "name": "blacklistDisputeGame", - "outputs": [], - "stateMutability": "nonpayable", + "stateMutability": "view", "type": "function" }, { @@ -83,25 +78,6 @@ "stateMutability": "payable", "type": "function" }, - { - "inputs": [ - { - "internalType": "contract IDisputeGame", - "name": "", - "type": "address" - } - ], - "name": "disputeGameBlacklist", - "outputs": [ - { - "internalType": "bool", - "name": "", - "type": "bool" - } - ], - "stateMutability": "view", - "type": "function" - }, { "inputs": [], "name": "disputeGameFactory", @@ -264,11 +240,6 @@ }, { "inputs": [ - { - "internalType": "contract IDisputeGameFactory", - "name": "_disputeGameFactory", - "type": "address" - }, { "internalType": "contract ISystemConfig", "name": "_systemConfig", @@ -280,9 +251,9 @@ "type": "address" }, { - "internalType": "GameType", - "name": "_initialRespectedGameType", - "type": "uint32" + "internalType": "contract IAnchorStateRegistry", + "name": "_anchorStateRegistry", + "type": "address" } ], "name": "initialize", @@ -569,19 +540,6 @@ "stateMutability": "nonpayable", "type": "function" }, - { - "inputs": [ - { - "internalType": "GameType", - "name": "_gameType", - "type": "uint32" - } - ], - "name": "setRespectedGameType", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, { "inputs": [], "name": "superchainConfig", @@ -608,6 +566,19 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [ + { + "internalType": "contract IAnchorStateRegistry", + "name": "_anchorStateRegistry", + "type": "address" + } + ], + "name": "upgrade", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [], "name": "version", @@ -621,19 +592,6 @@ "stateMutability": "pure", "type": "function" }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "contract IDisputeGame", - "name": "disputeGame", - "type": "address" - } - ], - "name": "DisputeGameBlacklisted", - "type": "event" - }, { "anonymous": false, "inputs": [ @@ -647,25 +605,6 @@ "name": "Initialized", "type": "event" }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "GameType", - "name": "newGameType", - "type": "uint32" - }, - { - "indexed": true, - "internalType": "Timestamp", - "name": "updatedAt", - "type": "uint64" - } - ], - "name": "RespectedGameTypeSet", - "type": "event" - }, { "anonymous": false, "inputs": [ @@ -762,117 +701,117 @@ }, { "inputs": [], - "name": "AlreadyFinalized", + "name": "ContentLengthMismatch", "type": "error" }, { "inputs": [], - "name": "BadTarget", + "name": "EmptyItem", "type": "error" }, { "inputs": [], - "name": "Blacklisted", + "name": "InvalidDataRemainder", "type": "error" }, { "inputs": [], - "name": "CallPaused", + "name": "InvalidHeader", "type": "error" }, { "inputs": [], - "name": "ContentLengthMismatch", + "name": "OptimismPortal_AlreadyFinalized", "type": "error" }, { "inputs": [], - "name": "EmptyItem", + "name": "OptimismPortal_BadTarget", "type": "error" }, { "inputs": [], - "name": "GasEstimation", + "name": "OptimismPortal_CallPaused", "type": "error" }, { "inputs": [], - "name": "InvalidDataRemainder", + "name": "OptimismPortal_CalldataTooLarge", "type": "error" }, { "inputs": [], - "name": "InvalidDisputeGame", + "name": "OptimismPortal_GasEstimation", "type": "error" }, { "inputs": [], - "name": "InvalidGameType", + "name": "OptimismPortal_GasLimitTooLow", "type": "error" }, { "inputs": [], - "name": "InvalidHeader", + "name": "OptimismPortal_ImproperDisputeGame", "type": "error" }, { "inputs": [], - "name": "InvalidMerkleProof", + "name": "OptimismPortal_InvalidDisputeGame", "type": "error" }, { "inputs": [], - "name": "InvalidProof", + "name": "OptimismPortal_InvalidMerkleProof", "type": "error" }, { "inputs": [], - "name": "LargeCalldata", + "name": "OptimismPortal_InvalidOutputRootProof", "type": "error" }, { "inputs": [], - "name": "LegacyGame", + "name": "OptimismPortal_InvalidProofTimestamp", "type": "error" }, { "inputs": [], - "name": "NonReentrant", + "name": "OptimismPortal_InvalidRootClaim", "type": "error" }, { "inputs": [], - "name": "OutOfGas", + "name": "OptimismPortal_NoReentrancy", "type": "error" }, { "inputs": [], - "name": "ProposalNotValidated", + "name": "OptimismPortal_ProofNotOldEnough", "type": "error" }, { "inputs": [], - "name": "SmallGasLimit", + "name": "OptimismPortal_Unauthorized", "type": "error" }, { "inputs": [], - "name": "Unauthorized", + "name": "OptimismPortal_Unproven", "type": "error" }, { "inputs": [], - "name": "UnexpectedList", + "name": "OutOfGas", "type": "error" }, { "inputs": [], - "name": "UnexpectedString", + "name": "UnexpectedList", "type": "error" }, { "inputs": [], - "name": "Unproven", + "name": "UnexpectedString", "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 8a31cd06dc979..f56df9990471d 100644 --- a/packages/contracts-bedrock/snapshots/semver-lock.json +++ b/packages/contracts-bedrock/snapshots/semver-lock.json @@ -156,8 +156,8 @@ "sourceCodeHash": "0x03c160168986ffc8d26a90c37366e7ad6da03f49d83449e1f8b3de0f4b590f6f" }, "src/dispute/AnchorStateRegistry.sol": { - "initCodeHash": "0x08cc5a5e41eadb6c411fa6387ddc0cf12be360855599dd622cce84c0ba081e77", - "sourceCodeHash": "0xe0aaa79f7184724ff0fba2e92e85f652f936fecd099288edb0a0f6b0e0240f34" + "initCodeHash": "0xb905a31a816dc7354e9153a6cbf08d968c6d631e5383bd64c5ff1825bf284825", + "sourceCodeHash": "0xf785b369133782f614ed792766f5ec05d79b639db29abc2af2a4074f8600fb36" }, "src/dispute/DelayedWETH.sol": { "initCodeHash": "0xdd0b5e523f3b53563fe0b6e6165fb73605b14910ffa32a7cbed855cdebab47c6", @@ -196,8 +196,8 @@ "sourceCodeHash": "0x62c9a6182d82692fb9c173ddb0d7978bcff2d1d4dc8cd2f10625e1e65bda6888" }, "src/safe/DeputyGuardianModule.sol": { - "initCodeHash": "0x5eaf823d81995ce1f703f26e31049c54c1d4902dd9873a0b4645d470f2f459a2", - "sourceCodeHash": "0x17236a91c4171ae9525eae0e59fa65bb2dc320d62677cfc7d7eb942f182619fb" + "initCodeHash": "0xf0ccc9997a120b2642bf4b15b5c12b041219c5e748c0c5e24af36f9cde62f175", + "sourceCodeHash": "0xa7dc83c6278118b9a7b4633633eeaa7e5b466bf24fdd184925ab620555caf0aa" }, "src/safe/DeputyPauseModule.sol": { "initCodeHash": "0xa3b7bf0c93b41f39ebc18a81322b90127a633d684ae9f86c2f2a1c48fe7f1372", @@ -235,4 +235,4 @@ "initCodeHash": "0x2bfce526f82622288333d53ca3f43a0a94306ba1bab99241daa845f8f4b18bd4", "sourceCodeHash": "0xf49d7b0187912a6bb67926a3222ae51121e9239495213c975b3b4b217ee57a1b" } -} \ No newline at end of file +} diff --git a/packages/contracts-bedrock/snapshots/storageLayout/AnchorStateRegistry.json b/packages/contracts-bedrock/snapshots/storageLayout/AnchorStateRegistry.json index fac376a90d126..4d981001f8fba 100644 --- a/packages/contracts-bedrock/snapshots/storageLayout/AnchorStateRegistry.json +++ b/packages/contracts-bedrock/snapshots/storageLayout/AnchorStateRegistry.json @@ -27,25 +27,39 @@ "slot": "1", "type": "contract IDisputeGameFactory" }, - { - "bytes": "20", - "label": "portal", - "offset": 0, - "slot": "2", - "type": "contract IOptimismPortal2" - }, { "bytes": "20", "label": "anchorGame", "offset": 0, - "slot": "3", + "slot": "2", "type": "contract IFaultDisputeGame" }, { "bytes": "64", "label": "startingAnchorRoot", "offset": 0, - "slot": "4", + "slot": "3", "type": "struct OutputRoot" + }, + { + "bytes": "32", + "label": "disputeGameBlacklist", + "offset": 0, + "slot": "5", + "type": "mapping(contract IDisputeGame => bool)" + }, + { + "bytes": "4", + "label": "respectedGameType", + "offset": 0, + "slot": "6", + "type": "GameType" + }, + { + "bytes": "8", + "label": "retirementTimestamp", + "offset": 4, + "slot": "6", + "type": "uint64" } ] \ No newline at end of file diff --git a/packages/contracts-bedrock/snapshots/storageLayout/OptimismPortal2.json b/packages/contracts-bedrock/snapshots/storageLayout/OptimismPortal2.json index 654695522e049..ccb5d970d342e 100644 --- a/packages/contracts-bedrock/snapshots/storageLayout/OptimismPortal2.json +++ b/packages/contracts-bedrock/snapshots/storageLayout/OptimismPortal2.json @@ -78,10 +78,10 @@ }, { "bytes": "20", - "label": "disputeGameFactory", + "label": "spacer_56_0_20", "offset": 0, "slot": "56", - "type": "contract IDisputeGameFactory" + "type": "address" }, { "bytes": "32", @@ -92,21 +92,21 @@ }, { "bytes": "32", - "label": "disputeGameBlacklist", + "label": "spacer_58_0_32", "offset": 0, "slot": "58", - "type": "mapping(contract IDisputeGame => bool)" + "type": "bytes32" }, { "bytes": "4", - "label": "respectedGameType", + "label": "spacer_59_0_4", "offset": 0, "slot": "59", "type": "GameType" }, { "bytes": "8", - "label": "respectedGameTypeUpdatedAt", + "label": "spacer_59_4_8", "offset": 4, "slot": "59", "type": "uint64" @@ -124,5 +124,12 @@ "offset": 0, "slot": "61", "type": "uint256" + }, + { + "bytes": "20", + "label": "anchorStateRegistry", + "offset": 0, + "slot": "62", + "type": "contract IAnchorStateRegistry" } ] \ No newline at end of file diff --git a/packages/contracts-bedrock/snapshots/storageLayout/OptimismPortalInterop.json b/packages/contracts-bedrock/snapshots/storageLayout/OptimismPortalInterop.json index 654695522e049..ccb5d970d342e 100644 --- a/packages/contracts-bedrock/snapshots/storageLayout/OptimismPortalInterop.json +++ b/packages/contracts-bedrock/snapshots/storageLayout/OptimismPortalInterop.json @@ -78,10 +78,10 @@ }, { "bytes": "20", - "label": "disputeGameFactory", + "label": "spacer_56_0_20", "offset": 0, "slot": "56", - "type": "contract IDisputeGameFactory" + "type": "address" }, { "bytes": "32", @@ -92,21 +92,21 @@ }, { "bytes": "32", - "label": "disputeGameBlacklist", + "label": "spacer_58_0_32", "offset": 0, "slot": "58", - "type": "mapping(contract IDisputeGame => bool)" + "type": "bytes32" }, { "bytes": "4", - "label": "respectedGameType", + "label": "spacer_59_0_4", "offset": 0, "slot": "59", "type": "GameType" }, { "bytes": "8", - "label": "respectedGameTypeUpdatedAt", + "label": "spacer_59_4_8", "offset": 4, "slot": "59", "type": "uint64" @@ -124,5 +124,12 @@ "offset": 0, "slot": "61", "type": "uint256" + }, + { + "bytes": "20", + "label": "anchorStateRegistry", + "offset": 0, + "slot": "62", + "type": "contract IAnchorStateRegistry" } ] \ No newline at end of file diff --git a/packages/contracts-bedrock/src/L1/OPContractsManagerInterop.sol b/packages/contracts-bedrock/src/L1/OPContractsManagerInterop.sol new file mode 100644 index 0000000000000..dcd98a3ed3d01 --- /dev/null +++ b/packages/contracts-bedrock/src/L1/OPContractsManagerInterop.sol @@ -0,0 +1,79 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.15; + +// Contracts +import { OPContractsManager } from "src/L1/OPContractsManager.sol"; + +// Interfaces +import { ISuperchainConfig } from "interfaces/L1/ISuperchainConfig.sol"; +import { IProtocolVersions } from "interfaces/L1/IProtocolVersions.sol"; +import { IResourceMetering } from "interfaces/L1/IResourceMetering.sol"; +import { ISystemConfig } from "interfaces/L1/ISystemConfig.sol"; +import { ISystemConfigInterop } from "interfaces/L1/ISystemConfigInterop.sol"; +import { IProxyAdmin } from "interfaces/universal/IProxyAdmin.sol"; + +contract OPContractsManagerInterop is OPContractsManager { + /// @custom:semver +interop.11 + function version() public pure override returns (string memory) { + return string.concat(super.version(), "+interop.11"); + } + + constructor( + ISuperchainConfig _superchainConfig, + IProtocolVersions _protocolVersions, + IProxyAdmin _superchainProxyAdmin, + string memory _l1ContractsRelease, + Blueprints memory _blueprints, + Implementations memory _implementations, + address _upgradeController + ) + OPContractsManager( + _superchainConfig, + _protocolVersions, + _superchainProxyAdmin, + _l1ContractsRelease, + _blueprints, + _implementations, + _upgradeController + ) + { } + + // The `SystemConfigInterop` contract has an extra `address _dependencyManager` argument + // that we must account for. + function encodeSystemConfigInitializer( + DeployInput memory _input, + DeployOutput memory _output + ) + internal + view + virtual + override + returns (bytes memory) + { + (IResourceMetering.ResourceConfig memory referenceResourceConfig, ISystemConfig.Addresses memory opChainAddrs) = + defaultSystemConfigParams(_input, _output); + + // TODO For now we assume that the dependency manager is the same as system config owner. + // This is currently undefined since it's not part of the standard config, so we may need + // to update where this value is pulled from in the future. To support a different dependency + // manager in this contract without an invasive change of redefining the `Roles` struct, + // we will make the change described in https://github.com/ethereum-optimism/optimism/issues/11783. + address dependencyManager = address(_input.roles.systemConfigOwner); + + return abi.encodeCall( + ISystemConfigInterop.initialize, + ( + _input.roles.systemConfigOwner, + _input.basefeeScalar, + _input.blobBasefeeScalar, + bytes32(uint256(uint160(_input.roles.batcher))), // batcherHash + _input.gasLimit, + _input.roles.unsafeBlockSigner, + referenceResourceConfig, + chainIdToBatchInboxAddress(_input.l2ChainId), + opChainAddrs, + dependencyManager + ) + ); + } +} diff --git a/packages/contracts-bedrock/src/L1/OPPrestateUpdater.sol b/packages/contracts-bedrock/src/L1/OPPrestateUpdater.sol new file mode 100644 index 0000000000000..dd6f50989fec0 --- /dev/null +++ b/packages/contracts-bedrock/src/L1/OPPrestateUpdater.sol @@ -0,0 +1,172 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.15; + +// Contracts +import { OPContractsManager } from "src/L1/OPContractsManager.sol"; + +// Interfaces +import { ISuperchainConfig } from "interfaces/L1/ISuperchainConfig.sol"; +import { IProtocolVersions } from "interfaces/L1/IProtocolVersions.sol"; +import { IPermissionedDisputeGame } from "interfaces/dispute/IPermissionedDisputeGame.sol"; +import { IDisputeGameFactory } from "interfaces/dispute/IDisputeGameFactory.sol"; +import { IFaultDisputeGame } from "interfaces/dispute/IFaultDisputeGame.sol"; +import { IDelayedWETH } from "interfaces/dispute/IDelayedWETH.sol"; +import { IProxyAdmin } from "interfaces/universal/IProxyAdmin.sol"; + +// Libraries +import { Claim, GameTypes } from "src/dispute/lib/Types.sol"; + +/// @title OPPrestateUpdater +/// @notice A custom implementation of OPContractsManager that enables updating the prestate hash +/// for the permissioned and fault dispute games on a set of chains. +contract OPPrestateUpdater is OPContractsManager { + /// @notice Thrown when a function from the parent (OPCM) is not implemented. + error NotImplemented(); + + /// @notice Thrown when the prestate of a permissioned disputed game is 0. + error PrestateRequired(); + + // @return Version string + /// @custom:semver 1.7.1 + function version() public pure override returns (string memory) { + return "1.7.1"; + } + + // @notice Constructs the OPPrestateUpdater contract + // @param _superchainConfig Address of the SuperchainConfig contract + // @param _protocolVersions Address of the ProtocolVersions contract + // @param _blueprints Addresses of Blueprint contracts + constructor( + ISuperchainConfig _superchainConfig, + IProtocolVersions _protocolVersions, + Blueprints memory _blueprints + ) + OPContractsManager( + _superchainConfig, + _protocolVersions, + IProxyAdmin(address(0)), + "", + _blueprints, + Implementations( + address(0), //superchainConfigImpl + address(0), //protocolVersionsImpl + address(0), //l1ERC721BridgeImpl + address(0), //optimismPortalImpl + address(0), //systemConfigImpl + address(0), //optimismMintableERC20FactoryImpl + address(0), //l1CrossDomainMessengerImpl + address(0), //l1StandardBridgeImpl + address(0), //disputeGameFactoryImpl + address(0), //anchorStateRegistryImpl + address(0), //delayedWETHImpl + address(0) // mipsImpl + ), + address(0) + ) + { } + + /// @notice Overrides the l1ContractsRelease function to return "none", as this OPCM + /// is not releasing new contracts. + function l1ContractsRelease() external pure override returns (string memory) { + return "none"; + } + + function deploy(DeployInput memory _input) external pure override returns (DeployOutput memory) { + _input; // Silence warning + revert NotImplemented(); + } + + function upgrade(OpChainConfig[] memory _opChainConfigs) external pure override { + _opChainConfigs; // Silence warning + revert NotImplemented(); + } + + function addGameType(AddGameInput[] memory _gameConfigs) public pure override returns (AddGameOutput[] memory) { + _gameConfigs; // Silence warning + revert NotImplemented(); + } + + /// @notice Updates the prestate hash for a new game type while keeping all other parameters the same + /// @param _prestateUpdateInputs The new prestate hash to use + function updatePrestate(OpChainConfig[] memory _prestateUpdateInputs) external { + // Loop through each chain and prestate hash + for (uint256 i = 0; i < _prestateUpdateInputs.length; i++) { + if (Claim.unwrap(_prestateUpdateInputs[i].absolutePrestate) == bytes32(0)) { + revert PrestateRequired(); + } + + // Get the DisputeGameFactory and existing game implementations + IDisputeGameFactory dgf = + IDisputeGameFactory(_prestateUpdateInputs[i].systemConfigProxy.disputeGameFactory()); + IFaultDisputeGame fdg = IFaultDisputeGame(address(getGameImplementation(dgf, GameTypes.CANNON))); + IPermissionedDisputeGame pdg = + IPermissionedDisputeGame(address(getGameImplementation(dgf, GameTypes.PERMISSIONED_CANNON))); + + // All chains must have a permissioned game, but not all chains must have a fault dispute game. + // Whether a chain has a fault dispute game determines how many AddGameInput objects are needed. + bool hasFDG = address(fdg) != address(0); + + AddGameInput[] memory inputs = new AddGameInput[](hasFDG ? 2 : 1); + AddGameInput memory pdgInput; + AddGameInput memory fdgInput; + + // Get the existing game parameters and init bond for the permissioned game + IFaultDisputeGame.GameConstructorParams memory pdgParams = + getGameConstructorParams(IFaultDisputeGame(address(pdg))); + uint256 initBond = dgf.initBonds(GameTypes.PERMISSIONED_CANNON); + + string memory saltMixer = reusableSaltMixer(_prestateUpdateInputs[i]); + // Create game input with updated prestate but same other params + pdgInput = AddGameInput({ + disputeAbsolutePrestate: _prestateUpdateInputs[i].absolutePrestate, + saltMixer: saltMixer, + systemConfig: _prestateUpdateInputs[i].systemConfigProxy, + proxyAdmin: _prestateUpdateInputs[i].proxyAdmin, + delayedWETH: IDelayedWETH(payable(address(pdgParams.weth))), + disputeGameType: pdgParams.gameType, + disputeMaxGameDepth: pdgParams.maxGameDepth, + disputeSplitDepth: pdgParams.splitDepth, + disputeClockExtension: pdgParams.clockExtension, + disputeMaxClockDuration: pdgParams.maxClockDuration, + initialBond: initBond, + vm: pdgParams.vm, + permissioned: true + }); + + // If a fault dispute game exists, create a new game with the same parameters but updated prestate. + if (hasFDG) { + // Get the existing game parameters and init bond for the fault dispute game + IFaultDisputeGame.GameConstructorParams memory fdgParams = + getGameConstructorParams(IFaultDisputeGame(address(fdg))); + initBond = dgf.initBonds(GameTypes.CANNON); + + // Create game input with updated prestate but same other params + fdgInput = AddGameInput({ + disputeAbsolutePrestate: _prestateUpdateInputs[i].absolutePrestate, + saltMixer: saltMixer, + systemConfig: _prestateUpdateInputs[i].systemConfigProxy, + proxyAdmin: _prestateUpdateInputs[i].proxyAdmin, + delayedWETH: IDelayedWETH(payable(address(fdgParams.weth))), + disputeGameType: fdgParams.gameType, + disputeMaxGameDepth: fdgParams.maxGameDepth, + disputeSplitDepth: fdgParams.splitDepth, + disputeClockExtension: fdgParams.clockExtension, + disputeMaxClockDuration: fdgParams.maxClockDuration, + initialBond: initBond, + vm: fdgParams.vm, + permissioned: false + }); + } + + // Game inputs must be ordered with increasing game type values. So FDG is first if it exists. + if (hasFDG) { + inputs[0] = fdgInput; + inputs[1] = pdgInput; + } else { + inputs[0] = pdgInput; + } + // Add the new game type with updated prestate + super.addGameType(inputs); + } + } +} diff --git a/packages/contracts-bedrock/src/L1/OptimismPortal2.sol b/packages/contracts-bedrock/src/L1/OptimismPortal2.sol index 0b43241ea4f9f..3125d92e4a054 100644 --- a/packages/contracts-bedrock/src/L1/OptimismPortal2.sol +++ b/packages/contracts-bedrock/src/L1/OptimismPortal2.sol @@ -14,25 +14,7 @@ import { Types } from "src/libraries/Types.sol"; import { Hashing } from "src/libraries/Hashing.sol"; import { SecureMerkleTrie } from "src/libraries/trie/SecureMerkleTrie.sol"; import { AddressAliasHelper } from "src/vendor/AddressAliasHelper.sol"; -import { - BadTarget, - LargeCalldata, - SmallGasLimit, - Unauthorized, - CallPaused, - GasEstimation, - NonReentrant, - InvalidProof, - InvalidGameType, - InvalidDisputeGame, - InvalidMerkleProof, - Blacklisted, - Unproven, - ProposalNotValidated, - AlreadyFinalized, - LegacyGame -} from "src/libraries/PortalErrors.sol"; -import { GameStatus, GameType, Claim, Timestamp } from "src/dispute/lib/Types.sol"; +import { GameStatus, GameType } from "src/dispute/lib/Types.sol"; // Interfaces import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; @@ -42,6 +24,7 @@ import { IResourceMetering } from "interfaces/L1/IResourceMetering.sol"; import { ISuperchainConfig } from "interfaces/L1/ISuperchainConfig.sol"; import { IDisputeGameFactory } from "interfaces/dispute/IDisputeGameFactory.sol"; import { IDisputeGame } from "interfaces/dispute/IDisputeGame.sol"; +import { IAnchorStateRegistry } from "interfaces/dispute/IAnchorStateRegistry.sol"; /// @custom:proxied true /// @title OptimismPortal2 @@ -53,20 +36,16 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ISemver { using SafeERC20 for IERC20; /// @notice Represents a proven withdrawal. - /// @custom:field disputeGameProxy The address of the dispute game proxy that the withdrawal was proven against. + /// @custom:field disputeGameProxy Game that the withdrawal was proven against. /// @custom:field timestamp Timestamp at which the withdrawal was proven. struct ProvenWithdrawal { IDisputeGame disputeGameProxy; uint64 timestamp; } - /// @notice The delay between when a withdrawal transaction is proven and when it may be finalized. + /// @notice The delay between when a withdrawal is proven and when it may be finalized. uint256 internal immutable PROOF_MATURITY_DELAY_SECONDS; - /// @notice The delay between when a dispute game is resolved and when a withdrawal proven against it may be - /// finalized. - uint256 internal immutable DISPUTE_GAME_FINALITY_DELAY_SECONDS; - /// @notice Version of the deposit event. uint256 internal constant DEPOSIT_VERSION = 0; @@ -94,7 +73,7 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ISemver { /// @notice Spacer for backwards compatibility. bool private spacer_53_0_1; - /// @notice Contract of the Superchain Config. + /// @notice Address of the SuperchainConfig contract. ISuperchainConfig public superchainConfig; /// @custom:legacy @@ -102,25 +81,30 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ISemver { /// @notice Spacer taking up the legacy `l2Oracle` address slot. address private spacer_54_0_20; - /// @notice Contract of the SystemConfig. + /// @notice Address of the SystemConfig contract. /// @custom:network-specific ISystemConfig public systemConfig; - /// @notice Address of the DisputeGameFactory. /// @custom:network-specific - IDisputeGameFactory public disputeGameFactory; + /// @custom:legacy + /// @custom:spacer disputeGameFactory + /// @notice Spacer taking up the legacy `disputeGameFactory` address slot. + address private spacer_56_0_20; - /// @notice A mapping of withdrawal hashes to proof submitters to `ProvenWithdrawal` data. + /// @notice A mapping of withdrawal hashes to proof submitters to ProvenWithdrawal data. mapping(bytes32 => mapping(address => ProvenWithdrawal)) public provenWithdrawals; - /// @notice A mapping of dispute game addresses to whether or not they are blacklisted. - mapping(IDisputeGame => bool) public disputeGameBlacklist; + /// @custom:legacy + /// @custom:spacer disputeGameBlacklist + bytes32 private spacer_58_0_32; - /// @notice The game type that the OptimismPortal consults for output proposals. - GameType public respectedGameType; + /// @custom:legacy + /// @custom:spacer respectedGameType + GameType private spacer_59_0_4; - /// @notice The timestamp at which the respected game type was last updated. - uint64 public respectedGameTypeUpdatedAt; + /// @custom:legacy + /// @custom:spacer respectedGameTypeUpdatedAt + uint64 private spacer_59_4_8; /// @notice Mapping of withdrawal hashes to addresses that have submitted a proof for the /// withdrawal. Original OptimismPortal contract only allowed one proof to be submitted @@ -133,12 +117,13 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ISemver { /// @custom:legacy /// @custom:spacer _balance - /// @notice Spacer taking up the legacy `_balance` slot. uint256 private spacer_61_0_32; - /// @notice Emitted when a transaction is deposited from L1 to L2. - /// The parameters of this event are read by the rollup node and used to derive deposit - /// transactions on L2. + /// @notice Address of the AnchorStateRegistry contract. + IAnchorStateRegistry public anchorStateRegistry; + + /// @notice Emitted when a transaction is deposited from L1 to L2. The parameters of this event + /// are read by the rollup node and used to derive deposit transactions on L2. /// @param from Address that triggered the deposit transaction. /// @param to Address that the deposit transaction is directed to. /// @param version Version of this deposit transaction event. @@ -151,8 +136,9 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ISemver { /// @param to Address that the withdrawal transaction is directed to. event WithdrawalProven(bytes32 indexed withdrawalHash, address indexed from, address indexed to); - /// @notice Emitted when a withdrawal transaction is proven. Exists as a separate event to allow for backwards - /// compatibility for tooling that observes the `WithdrawalProven` event. + /// @notice Emitted when a withdrawal transaction is proven. Exists as a separate event to + /// allow for backwards compatibility for tooling that observes the WithdrawalProven + /// event. /// @param withdrawalHash Hash of the withdrawal transaction. /// @param proofSubmitter Address of the proof submitter. event WithdrawalProvenExtension1(bytes32 indexed withdrawalHash, address indexed proofSubmitter); @@ -162,74 +148,98 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ISemver { /// @param success Whether the withdrawal transaction was successful. event WithdrawalFinalized(bytes32 indexed withdrawalHash, bool success); - /// @notice Emitted when a dispute game is blacklisted by the Guardian. - /// @param disputeGame Address of the dispute game that was blacklisted. - event DisputeGameBlacklisted(IDisputeGame indexed disputeGame); + /// @notice Thrown when a withdrawal has already been finalized. + error OptimismPortal_AlreadyFinalized(); + + /// @notice Thrown when the target of a withdrawal is unsafe. + error OptimismPortal_BadTarget(); + + /// @notice Thrown when the calldata for a deposit is too large. + error OptimismPortal_CalldataTooLarge(); + + /// @notice Thrown when the portal is paused. + error OptimismPortal_CallPaused(); + + /// @notice Thrown when a gas estimation transaction is being executed. + error OptimismPortal_GasEstimation(); + + /// @notice Thrown when the gas limit for a deposit is too low. + error OptimismPortal_GasLimitTooLow(); + + /// @notice Thrown when the target of a withdrawal is not a proper dispute game. + error OptimismPortal_ImproperDisputeGame(); + + /// @notice Thrown when a withdrawal has not been proven against a valid dispute game. + error OptimismPortal_InvalidDisputeGame(); + + /// @notice Thrown when a withdrawal has not been proven against a valid merkle proof. + error OptimismPortal_InvalidMerkleProof(); + + /// @notice Thrown when a withdrawal has not been proven against a valid output root proof. + error OptimismPortal_InvalidOutputRootProof(); + + /// @notice Thrown when a withdrawal's timestamp is not greater than the dispute game's creation timestamp. + error OptimismPortal_InvalidProofTimestamp(); + + /// @notice Thrown when the root claim of a dispute game is invalid. + error OptimismPortal_InvalidRootClaim(); + + /// @notice Thrown when a withdrawal is being finalized by a reentrant call. + error OptimismPortal_NoReentrancy(); - /// @notice Emitted when the Guardian changes the respected game type in the portal. - /// @param newGameType The new respected game type. - /// @param updatedAt The timestamp at which the respected game type was updated. - event RespectedGameTypeSet(GameType indexed newGameType, Timestamp indexed updatedAt); + /// @notice Thrown when a withdrawal has not been proven for long enough. + error OptimismPortal_ProofNotOldEnough(); + + /// @notice Thrown when a withdrawal has not been proven. + error OptimismPortal_Unproven(); /// @notice Reverts when paused. modifier whenNotPaused() { - if (paused()) revert CallPaused(); + if (paused()) revert OptimismPortal_CallPaused(); _; } /// @notice Semantic version. - /// @custom:semver 3.14.0 + /// @custom:semver 4.0.0 function version() public pure virtual returns (string memory) { - return "3.14.0"; + return "4.0.0"; } - /// @notice Constructs the OptimismPortal contract. - constructor(uint256 _proofMaturityDelaySeconds, uint256 _disputeGameFinalityDelaySeconds) { + /// @param _proofMaturityDelaySeconds The proof maturity delay in seconds. + constructor(uint256 _proofMaturityDelaySeconds) { PROOF_MATURITY_DELAY_SECONDS = _proofMaturityDelaySeconds; - DISPUTE_GAME_FINALITY_DELAY_SECONDS = _disputeGameFinalityDelaySeconds; - _disableInitializers(); } /// @notice Initializer. - /// @param _disputeGameFactory Contract of the DisputeGameFactory. - /// @param _systemConfig Contract of the SystemConfig. - /// @param _superchainConfig Contract of the SuperchainConfig. + /// @param _systemConfig Address of the SystemConfig. + /// @param _superchainConfig Address of the SuperchainConfig. + /// @param _anchorStateRegistry Address of the AnchorStateRegistry. function initialize( - IDisputeGameFactory _disputeGameFactory, ISystemConfig _systemConfig, ISuperchainConfig _superchainConfig, - GameType _initialRespectedGameType + IAnchorStateRegistry _anchorStateRegistry ) external - initializer + reinitializer(2) { - disputeGameFactory = _disputeGameFactory; systemConfig = _systemConfig; superchainConfig = _superchainConfig; + anchorStateRegistry = _anchorStateRegistry; - // Set the `l2Sender` slot, only if it is currently empty. This signals the first initialization of the - // contract. + // Set the l2Sender slot, only if it is currently empty. This signals the first + // initialization of the contract. if (l2Sender == address(0)) { l2Sender = Constants.DEFAULT_L2_SENDER; - - // Set the `respectedGameTypeUpdatedAt` timestamp, to ignore all games of the respected type prior - // to this operation. - respectedGameTypeUpdatedAt = uint64(block.timestamp); - - // Set the initial respected game type - respectedGameType = _initialRespectedGameType; } __ResourceMetering_init(); } - /// @notice Getter function for the address of the guardian. - /// Public getter is legacy and will be removed in the future. Use `SuperchainConfig.guardian()` instead. - /// @return Address of the guardian. - /// @custom:legacy - function guardian() public view returns (address) { - return superchainConfig.guardian(); + /// @notice Upgrades the OptimismPortal contract to have a reference to the AnchorStateRegistry. + /// @param _anchorStateRegistry AnchorStateRegistry contract. + function upgrade(IAnchorStateRegistry _anchorStateRegistry) external reinitializer(2) { + anchorStateRegistry = _anchorStateRegistry; } /// @notice Getter for the current paused status. @@ -242,9 +252,33 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ISemver { return PROOF_MATURITY_DELAY_SECONDS; } + /// @notice Getter for the address of the DisputeGameFactory contract. + function disputeGameFactory() public view returns (IDisputeGameFactory) { + return anchorStateRegistry.disputeGameFactory(); + } + + /// @custom:legacy + /// @notice Getter function for the address of the guardian. + function guardian() public view returns (address) { + return superchainConfig.guardian(); + } + + /// @custom:legacy /// @notice Getter for the dispute game finality delay. - function disputeGameFinalityDelaySeconds() public view returns (uint256) { - return DISPUTE_GAME_FINALITY_DELAY_SECONDS; + function disputeGameFinalityDelaySeconds() external view returns (uint256) { + return anchorStateRegistry.disputeGameFinalityDelaySeconds(); + } + + /// @custom:legacy + /// @notice Getter for the respected game type. + function respectedGameType() external view returns (GameType) { + return anchorStateRegistry.respectedGameType(); + } + + /// @custom:legacy + /// @notice Getter for the timestamp at which the respected game type was updated. + function respectedGameTypeUpdatedAt() external view returns (uint64) { + return anchorStateRegistry.retirementTimestamp(); } /// @notice Computes the minimum gas limit for a deposit. @@ -273,22 +307,11 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ISemver { // Intentionally empty. } - /// @notice Getter for the resource config. - /// Used internally by the ResourceMetering contract. - /// The SystemConfig is the source of truth for the resource config. - /// @return config_ ResourceMetering ResourceConfig - function _resourceConfig() internal view override returns (ResourceMetering.ResourceConfig memory config_) { - IResourceMetering.ResourceConfig memory config = systemConfig.resourceConfig(); - assembly ("memory-safe") { - config_ := config - } - } - /// @notice Proves a withdrawal transaction. /// @param _tx Withdrawal transaction to finalize. /// @param _disputeGameIndex Index of the dispute game to prove the withdrawal against. - /// @param _outputRootProof Inclusion proof of the L2ToL1MessagePasser contract's storage root. - /// @param _withdrawalProof Inclusion proof of the withdrawal in L2ToL1MessagePasser contract. + /// @param _outputRootProof Inclusion proof of the L2ToL1MessagePasser storage root. + /// @param _withdrawalProof Inclusion proof of the withdrawal within the L2ToL1MessagePasser. function proveWithdrawalTransaction( Types.WithdrawalTransaction memory _tx, uint256 _disputeGameIndex, @@ -298,47 +321,45 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ISemver { external whenNotPaused { - // Prevent users from creating a deposit transaction where this address is the message - // sender on L2. Because this is checked here, we do not need to check again in - // `finalizeWithdrawalTransaction`. - if (_tx.target == address(this)) revert BadTarget(); + // Make sure that the target address is safe. + if (_isUnsafeTarget(_tx.target)) { + revert OptimismPortal_BadTarget(); + } // Fetch the dispute game proxy from the `DisputeGameFactory` contract. - (GameType gameType,, IDisputeGame gameProxy) = disputeGameFactory.gameAtIndex(_disputeGameIndex); - Claim outputRoot = gameProxy.rootClaim(); - - // The game type of the dispute game must be the respected game type. - if (gameType.raw() != respectedGameType.raw()) revert InvalidGameType(); - - // The game type of the DisputeGame must have been the respected game type at creation. - // eip150-safe - try gameProxy.wasRespectedGameTypeWhenCreated() returns (bool wasRespected_) { - if (!wasRespected_) revert InvalidGameType(); - } catch { - revert LegacyGame(); + (,, IDisputeGame gameProxy) = disputeGameFactory().gameAtIndex(_disputeGameIndex); + + // Game must be a Proper Game. + if (!anchorStateRegistry.isGameProper(gameProxy)) { + revert OptimismPortal_ImproperDisputeGame(); } - // Game must have been created after the respected game type was updated. This check is a - // strict inequality because we want to prevent users from being able to prove or finalize - // withdrawals against games that were created in the same block that the retirement - // timestamp was set. If the retirement timestamp and game type are changed in the same - // block, such games could still be considered valid even if they used the old game type - // that we intended to invalidate. - require( - gameProxy.createdAt().raw() > respectedGameTypeUpdatedAt, - "OptimismPortal: dispute game created before respected game type was updated" - ); + // Game must have been respected game type when created. + if (!anchorStateRegistry.isGameRespected(gameProxy)) { + revert OptimismPortal_InvalidDisputeGame(); + } + + // Game must not have resolved in favor of the Challenger (invalid root claim). + if (gameProxy.status() == GameStatus.CHALLENGER_WINS) { + revert OptimismPortal_InvalidDisputeGame(); + } // Verify that the output root can be generated with the elements in the proof. - if (outputRoot.raw() != Hashing.hashOutputRootProof(_outputRootProof)) revert InvalidProof(); + if (gameProxy.rootClaim().raw() != Hashing.hashOutputRootProof(_outputRootProof)) { + revert OptimismPortal_InvalidOutputRootProof(); + } + + // As a sanity check, we make sure that the current timestamp is not less than or equal to + // the dispute game's creation timestamp. Not strictly necessary but extra layer of + // safety against weird bugs. Note that this blocks withdrawals from being proven in the + // same block that a dispute game is created. + if (block.timestamp <= gameProxy.createdAt().raw()) { + revert OptimismPortal_InvalidProofTimestamp(); + } // Load the ProvenWithdrawal into memory, using the withdrawal hash as a unique identifier. bytes32 withdrawalHash = Hashing.hashWithdrawal(_tx); - // We do not allow for proving withdrawals against dispute games that have resolved against the favor - // of the root claim. - if (gameProxy.status() == GameStatus.CHALLENGER_WINS) revert InvalidDisputeGame(); - // Compute the storage slot of the withdrawal hash in the L2ToL1MessagePasser contract. // Refer to the Solidity documentation for more information on how storage layouts are // computed for mappings. @@ -360,21 +381,22 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ISemver { _proof: _withdrawalProof, _root: _outputRootProof.messagePasserStorageRoot }) == false - ) revert InvalidMerkleProof(); + ) { + revert OptimismPortal_InvalidMerkleProof(); + } - // Designate the withdrawalHash as proven by storing the `disputeGameProxy` & `timestamp` in the - // `provenWithdrawals` mapping. A `withdrawalHash` can only be proven once unless the dispute game it proved - // against resolves against the favor of the root claim. + // Designate the withdrawalHash as proven by storing the disputeGameProxy and timestamp in + // the provenWithdrawals mapping. A given user may re-prove a withdrawalHash multiple + // times, but each proof will reset the proof timer. provenWithdrawals[withdrawalHash][msg.sender] = ProvenWithdrawal({ disputeGameProxy: gameProxy, timestamp: uint64(block.timestamp) }); - // Emit a `WithdrawalProven` event. - emit WithdrawalProven(withdrawalHash, _tx.sender, _tx.target); - // Emit a `WithdrawalProvenExtension1` event. - emit WithdrawalProvenExtension1(withdrawalHash, msg.sender); - // Add the proof submitter to the list of proof submitters for this withdrawal hash. proofSubmitters[withdrawalHash].push(msg.sender); + + // Emit a WithdrawalProven events. + emit WithdrawalProven(withdrawalHash, _tx.sender, _tx.target); + emit WithdrawalProvenExtension1(withdrawalHash, msg.sender); } /// @notice Finalizes a withdrawal transaction. @@ -396,9 +418,16 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ISemver { // Make sure that the l2Sender has not yet been set. The l2Sender is set to a value other // than the default value when a withdrawal transaction is being finalized. This check is // a defacto reentrancy guard. - if (l2Sender != Constants.DEFAULT_L2_SENDER) revert NonReentrant(); + if (l2Sender != Constants.DEFAULT_L2_SENDER) { + revert OptimismPortal_NoReentrancy(); + } + + // Make sure that the target address is safe. + if (_isUnsafeTarget(_tx.target)) { + revert OptimismPortal_BadTarget(); + } - // Compute the withdrawal hash. + // Grab the withdrawal. bytes32 withdrawalHash = Hashing.hashWithdrawal(_tx); // Check that the withdrawal can be finalized. @@ -430,7 +459,46 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ISemver { // sub call to the target contract if the minimum gas limit specified by the user would not // be sufficient to execute the sub call. if (!success && tx.origin == Constants.ESTIMATION_ADDRESS) { - revert GasEstimation(); + revert OptimismPortal_GasEstimation(); + } + } + + /// @notice Checks that a withdrawal has been proven and is ready to be finalized. + /// @param _withdrawalHash Hash of the withdrawal. + /// @param _proofSubmitter Address of the proof submitter. + function checkWithdrawal(bytes32 _withdrawalHash, address _proofSubmitter) public view { + // Grab the withdrawal and dispute game proxy. + ProvenWithdrawal memory provenWithdrawal = provenWithdrawals[_withdrawalHash][_proofSubmitter]; + IDisputeGame disputeGameProxy = provenWithdrawal.disputeGameProxy; + + // Check that this withdrawal has not already been finalized, this is replay protection. + if (finalizedWithdrawals[_withdrawalHash]) { + revert OptimismPortal_AlreadyFinalized(); + } + + // A withdrawal can only be finalized if it has been proven. We know that a withdrawal has + // been proven at least once when its timestamp is non-zero. Unproven withdrawals will have + // a timestamp of zero. + if (provenWithdrawal.timestamp == 0) { + revert OptimismPortal_Unproven(); + } + + // As a sanity check, we make sure that the proven withdrawal's timestamp is greater than + // starting timestamp inside the Dispute Game. Not strictly necessary but extra layer of + // safety against weird bugs in the proving step. Note that this blocks withdrawals that + // are proven in the same block that a dispute game is created. + if (provenWithdrawal.timestamp <= disputeGameProxy.createdAt().raw()) { + revert OptimismPortal_InvalidProofTimestamp(); + } + + // A proven withdrawal must wait at least `PROOF_MATURITY_DELAY_SECONDS` before finalizing. + if (block.timestamp - provenWithdrawal.timestamp <= PROOF_MATURITY_DELAY_SECONDS) { + revert OptimismPortal_ProofNotOldEnough(); + } + + // Check that the root claim is valid. + if (!anchorStateRegistry.isGameClaimValid(disputeGameProxy)) { + revert OptimismPortal_InvalidRootClaim(); } } @@ -456,17 +524,23 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ISemver { { // Just to be safe, make sure that people specify address(0) as the target when doing // contract creations. - if (_isCreation && _to != address(0)) revert BadTarget(); + if (_isCreation && _to != address(0)) { + revert OptimismPortal_BadTarget(); + } // Prevent depositing transactions that have too small of a gas limit. Users should pay // more for more resource usage. - if (_gasLimit < minimumGasLimit(uint64(_data.length))) revert SmallGasLimit(); + if (_gasLimit < minimumGasLimit(uint64(_data.length))) { + revert OptimismPortal_GasLimitTooLow(); + } // Prevent the creation of deposit transactions that have too much calldata. This gives an // upper limit on the size of unsafe blocks over the p2p network. 120kb is chosen to ensure // that the transaction can fit into the p2p network policy of 128kb even though deposit // transactions are not gossipped over the p2p network. - if (_data.length > 120_000) revert LargeCalldata(); + if (_data.length > 120_000) { + revert OptimismPortal_CalldataTooLarge(); + } // Transform the from-address to its alias if the caller is a contract. address from = msg.sender; @@ -484,108 +558,27 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ISemver { emit TransactionDeposited(from, _to, DEPOSIT_VERSION, opaqueData); } - /// @notice Blacklists a dispute game. Should only be used in the event that a dispute game resolves incorrectly. - /// @param _disputeGame Dispute game to blacklist. - function blacklistDisputeGame(IDisputeGame _disputeGame) external { - if (msg.sender != guardian()) revert Unauthorized(); - disputeGameBlacklist[_disputeGame] = true; - emit DisputeGameBlacklisted(_disputeGame); - } - - /// @notice Sets the respected game type. Changing this value can alter the security properties of the system, - /// depending on the new game's behavior. - /// @param _gameType The game type to consult for output proposals. - function setRespectedGameType(GameType _gameType) external { - if (msg.sender != guardian()) revert Unauthorized(); - // respectedGameTypeUpdatedAt is now no longer set by default. We want to avoid modifying - // this function's signature as that would result in changes to the DeputyGuardianModule. - // We use type(uint32).max as a temporary solution to allow us to update the - // respectedGameTypeUpdatedAt timestamp without modifying this function's signature. - if (_gameType.raw() == type(uint32).max) { - respectedGameTypeUpdatedAt = uint64(block.timestamp); - } else { - respectedGameType = _gameType; - } - emit RespectedGameTypeSet(respectedGameType, Timestamp.wrap(respectedGameTypeUpdatedAt)); - } - - /// @notice Checks if a withdrawal can be finalized. This function will revert if the withdrawal cannot be - /// finalized, and otherwise has no side-effects. - /// @param _withdrawalHash Hash of the withdrawal to check. - /// @param _proofSubmitter The submitter of the proof for the withdrawal hash - function checkWithdrawal(bytes32 _withdrawalHash, address _proofSubmitter) public view { - ProvenWithdrawal memory provenWithdrawal = provenWithdrawals[_withdrawalHash][_proofSubmitter]; - IDisputeGame disputeGameProxy = provenWithdrawal.disputeGameProxy; - - // The dispute game must not be blacklisted. - if (disputeGameBlacklist[disputeGameProxy]) revert Blacklisted(); - - // A withdrawal can only be finalized if it has been proven. We know that a withdrawal has - // been proven at least once when its timestamp is non-zero. Unproven withdrawals will have - // a timestamp of zero. - if (provenWithdrawal.timestamp == 0) revert Unproven(); - - // Grab the createdAt timestamp once. - uint64 createdAt = disputeGameProxy.createdAt().raw(); - - // As a sanity check, we make sure that the proven withdrawal's timestamp is greater than - // starting timestamp inside the Dispute Game. Not strictly necessary but extra layer of - // safety against weird bugs in the proving step. - require( - provenWithdrawal.timestamp > createdAt, - "OptimismPortal: withdrawal timestamp less than dispute game creation timestamp" - ); - - // A proven withdrawal must wait at least `PROOF_MATURITY_DELAY_SECONDS` before finalizing. - require( - block.timestamp - provenWithdrawal.timestamp > PROOF_MATURITY_DELAY_SECONDS, - "OptimismPortal: proven withdrawal has not matured yet" - ); - - // A proven withdrawal must wait until the dispute game it was proven against has been - // resolved in favor of the root claim (the output proposal). This is to prevent users - // from finalizing withdrawals proven against non-finalized output roots. - if (disputeGameProxy.status() != GameStatus.DEFENDER_WINS) revert ProposalNotValidated(); - - // The game type of the dispute game must have been the respected game type at creation - // time. We check that the game type is the respected game type at proving time, but it's - // possible that the respected game type has since changed. Users can still use this game - // to finalize a withdrawal as long as it has not been otherwise invalidated. - // The game type of the DisputeGame must have been the respected game type at creation. - // eip150-safe - try disputeGameProxy.wasRespectedGameTypeWhenCreated() returns (bool wasRespected_) { - if (!wasRespected_) revert InvalidGameType(); - } catch { - revert LegacyGame(); - } - - // Game must have been created after the respected game type was updated. This check is a - // strict inequality because we want to prevent users from being able to prove or finalize - // withdrawals against games that were created in the same block that the retirement - // timestamp was set. If the retirement timestamp and game type are changed in the same - // block, such games could still be considered valid even if they used the old game type - // that we intended to invalidate. - require( - createdAt > respectedGameTypeUpdatedAt, - "OptimismPortal: dispute game created before respected game type was updated" - ); - - // Before a withdrawal can be finalized, the dispute game it was proven against must have been - // resolved for at least `DISPUTE_GAME_FINALITY_DELAY_SECONDS`. This is to allow for manual - // intervention in the event that a dispute game is resolved incorrectly. - require( - block.timestamp - disputeGameProxy.resolvedAt().raw() > DISPUTE_GAME_FINALITY_DELAY_SECONDS, - "OptimismPortal: output proposal in air-gap" - ); - - // Check that this withdrawal has not already been finalized, this is replay protection. - if (finalizedWithdrawals[_withdrawalHash]) revert AlreadyFinalized(); - } - /// @notice External getter for the number of proof submitters for a withdrawal hash. /// @param _withdrawalHash Hash of the withdrawal. /// @return The number of proof submitters for the withdrawal hash. function numProofSubmitters(bytes32 _withdrawalHash) external view returns (uint256) { return proofSubmitters[_withdrawalHash].length; } + + /// @notice Checks if a target address is unsafe. + function _isUnsafeTarget(address _target) internal view virtual returns (bool) { + // Prevent users from creating a deposit transaction where this address is the message + // sender on L2. + return _target == address(this); + } + + /// @notice Getter for the resource config. Used internally by the ResourceMetering contract. + /// The SystemConfig is the source of truth for the resource config. + /// @return config_ ResourceMetering ResourceConfig + function _resourceConfig() internal view override returns (ResourceMetering.ResourceConfig memory config_) { + IResourceMetering.ResourceConfig memory config = systemConfig.resourceConfig(); + assembly ("memory-safe") { + config_ := config + } + } } diff --git a/packages/contracts-bedrock/src/L1/OptimismPortalInterop.sol b/packages/contracts-bedrock/src/L1/OptimismPortalInterop.sol index 7920a4931b165..532f67ac0be5a 100644 --- a/packages/contracts-bedrock/src/L1/OptimismPortalInterop.sol +++ b/packages/contracts-bedrock/src/L1/OptimismPortalInterop.sol @@ -7,7 +7,6 @@ import { OptimismPortal2 } from "src/L1/OptimismPortal2.sol"; // Libraries import { Predeploys } from "src/libraries/Predeploys.sol"; import { Constants } from "src/libraries/Constants.sol"; -import { Unauthorized } from "src/libraries/PortalErrors.sol"; // Interfaces import { IL1BlockInterop, ConfigType } from "interfaces/L2/IL1BlockInterop.sol"; @@ -18,12 +17,11 @@ 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 { - constructor( - uint256 _proofMaturityDelaySeconds, - uint256 _disputeGameFinalityDelaySeconds - ) - OptimismPortal2(_proofMaturityDelaySeconds, _disputeGameFinalityDelaySeconds) - { } + /// @notice Thrown when the caller is not the system config. + error OptimismPortal_Unauthorized(); + + /// @param _proofMaturityDelaySeconds The proof maturity delay in seconds. + constructor(uint256 _proofMaturityDelaySeconds) OptimismPortal2(_proofMaturityDelaySeconds) { } /// @custom:semver +interop.3 function version() public pure override returns (string memory) { @@ -34,7 +32,7 @@ contract OptimismPortalInterop is OptimismPortal2 { /// @param _type Type of configuration to set. /// @param _value Encoded value of the configuration. function setConfig(ConfigType _type, bytes memory _value) external { - if (msg.sender != address(systemConfig)) revert Unauthorized(); + if (msg.sender != address(systemConfig)) revert OptimismPortal_Unauthorized(); // Set L2 deposit gas as used without paying burning gas. Ensures that deposits cannot use too much L2 gas. // This value must be large enough to cover the cost of calling `L1Block.setConfig`. diff --git a/packages/contracts-bedrock/src/dispute/AnchorStateRegistry.sol b/packages/contracts-bedrock/src/dispute/AnchorStateRegistry.sol index 60cd9ba31e12a..35b9d2736fbc7 100644 --- a/packages/contracts-bedrock/src/dispute/AnchorStateRegistry.sol +++ b/packages/contracts-bedrock/src/dispute/AnchorStateRegistry.sol @@ -13,7 +13,6 @@ import { IFaultDisputeGame } from "interfaces/dispute/IFaultDisputeGame.sol"; import { IDisputeGame } from "interfaces/dispute/IDisputeGame.sol"; import { IDisputeGameFactory } from "interfaces/dispute/IDisputeGameFactory.sol"; import { ISuperchainConfig } from "interfaces/L1/ISuperchainConfig.sol"; -import { IOptimismPortal2 } from "interfaces/L1/IOptimismPortal2.sol"; /// @custom:proxied true /// @title AnchorStateRegistry @@ -23,8 +22,11 @@ import { IOptimismPortal2 } from "interfaces/L1/IOptimismPortal2.sol"; /// be initialized with a more recent starting state which reduces the amount of required offchain computation. contract AnchorStateRegistry is Initializable, ISemver { /// @notice Semantic version. - /// @custom:semver 2.2.2 - string public constant version = "2.2.2"; + /// @custom:semver 3.0.0 + string public constant version = "3.0.0"; + + /// @notice The dispute game finality delay in seconds. + uint256 internal immutable DISPUTE_GAME_FINALITY_DELAY_SECONDS; /// @notice Address of the SuperchainConfig contract. ISuperchainConfig public superchainConfig; @@ -32,61 +34,105 @@ contract AnchorStateRegistry is Initializable, ISemver { /// @notice Address of the DisputeGameFactory contract. IDisputeGameFactory public disputeGameFactory; - /// @notice Address of the OptimismPortal contract. - IOptimismPortal2 public portal; - /// @notice The game whose claim is currently being used as the anchor state. IFaultDisputeGame public anchorGame; /// @notice The starting anchor root. OutputRoot internal startingAnchorRoot; - /// @notice Emitted when an anchor state is not updated. - /// @param game Game that was not used as the new anchor game. - event AnchorNotUpdated(IFaultDisputeGame indexed game); + /// @notice Mapping of blacklisted dispute games. + mapping(IDisputeGame => bool) public disputeGameBlacklist; + + /// @notice The respected game type. + GameType public respectedGameType; + + /// @notice The retirement timestamp. All games created before or at this timestamp are + /// considered retired and are therefore not valid games. Retirement is used as a + /// blanket invalidation mechanism if games resolve incorrectly. + uint64 public retirementTimestamp; /// @notice Emitted when an anchor state is updated. /// @param game Game that was used as the new anchor game. event AnchorUpdated(IFaultDisputeGame indexed game); - /// @notice Thrown when an unauthorized caller attempts to set the anchor state. - error AnchorStateRegistry_Unauthorized(); + /// @notice Emitted when the respected game type is set. + /// @param gameType The new respected game type. + event RespectedGameTypeSet(GameType gameType); - /// @notice Thrown when an invalid anchor game is provided. - error AnchorStateRegistry_InvalidAnchorGame(); + /// @notice Emitted when the retirement timestamp is set. + /// @param timestamp The new retirement timestamp. + event RetirementTimestampSet(uint256 timestamp); + + /// @notice Emitted when a dispute game is blacklisted. + /// @param disputeGame The dispute game that was blacklisted. + event DisputeGameBlacklisted(IDisputeGame indexed disputeGame); /// @notice Thrown when the anchor root is requested, but the anchor game is blacklisted. error AnchorStateRegistry_AnchorGameBlacklisted(); - /// @notice Constructor to disable initializers. - constructor() { + /// @notice Thrown when an invalid anchor game is provided. + error AnchorStateRegistry_InvalidAnchorGame(); + + /// @notice Thrown when an unauthorized caller attempts to set the anchor state. + error AnchorStateRegistry_Unauthorized(); + + /// @param _disputeGameFinalityDelaySeconds The dispute game finality delay in seconds. + constructor(uint256 _disputeGameFinalityDelaySeconds) { + DISPUTE_GAME_FINALITY_DELAY_SECONDS = _disputeGameFinalityDelaySeconds; _disableInitializers(); } /// @notice Initializes the contract. /// @param _superchainConfig The address of the SuperchainConfig contract. /// @param _disputeGameFactory The address of the DisputeGameFactory contract. - /// @param _portal The address of the OptimismPortal contract. /// @param _startingAnchorRoot The starting anchor root. function initialize( ISuperchainConfig _superchainConfig, IDisputeGameFactory _disputeGameFactory, - IOptimismPortal2 _portal, - OutputRoot memory _startingAnchorRoot + OutputRoot memory _startingAnchorRoot, + GameType _startingRespectedGameType ) external initializer { superchainConfig = _superchainConfig; disputeGameFactory = _disputeGameFactory; - portal = _portal; startingAnchorRoot = _startingAnchorRoot; + respectedGameType = _startingRespectedGameType; + retirementTimestamp = uint64(block.timestamp); + } + + /// @notice Returns whether the contract is paused. + function paused() public view returns (bool) { + return superchainConfig.paused(); } - /// @notice Returns the respected game type. - /// @return The respected game type. - function respectedGameType() public view returns (GameType) { - return portal.respectedGameType(); + /// @notice Returns the dispute game finality delay in seconds. + function disputeGameFinalityDelaySeconds() external view returns (uint256) { + return DISPUTE_GAME_FINALITY_DELAY_SECONDS; + } + + /// @notice Allows the Guardian to set the respected game type. + /// @param _gameType The new respected game type. + function setRespectedGameType(GameType _gameType) external { + if (msg.sender != superchainConfig.guardian()) revert AnchorStateRegistry_Unauthorized(); + respectedGameType = _gameType; + emit RespectedGameTypeSet(_gameType); + } + + /// @notice Allows the Guardian to update the retirement timestamp. + function updateRetirementTimestamp() external { + if (msg.sender != superchainConfig.guardian()) revert AnchorStateRegistry_Unauthorized(); + retirementTimestamp = uint64(block.timestamp); + emit RetirementTimestampSet(block.timestamp); + } + + /// @notice Allows the Guardian to blacklist a dispute game. + /// @param _disputeGame Dispute game to blacklist. + function blacklistDisputeGame(IDisputeGame _disputeGame) external { + if (msg.sender != superchainConfig.guardian()) revert AnchorStateRegistry_Unauthorized(); + disputeGameBlacklist[_disputeGame] = true; + emit DisputeGameBlacklisted(_disputeGame); } /// @custom:legacy @@ -129,6 +175,9 @@ contract AnchorStateRegistry is Initializable, ISemver { /// @param _game The game to check. /// @return Whether the game is of a respected game type. function isGameRespected(IDisputeGame _game) public view returns (bool) { + // We don't do a try/catch here for legacy games because by the time this code is live on + // mainnet, users won't be using legacy games anymore. Avoiding the try/catch simplifies + // the logic. return _game.wasRespectedGameTypeWhenCreated(); } @@ -136,7 +185,7 @@ contract AnchorStateRegistry is Initializable, ISemver { /// @param _game The game to check. /// @return Whether the game is blacklisted. function isGameBlacklisted(IDisputeGame _game) public view returns (bool) { - return portal.disputeGameBlacklist(_game); + return disputeGameBlacklist[_game]; } /// @notice Determines whether a game is retired. @@ -146,7 +195,7 @@ contract AnchorStateRegistry is Initializable, ISemver { // Must be created after the respectedGameTypeUpdatedAt timestamp. Note that this means all // games created in the same block as the respectedGameTypeUpdatedAt timestamp are // considered retired. - return _game.createdAt().raw() <= portal.respectedGameTypeUpdatedAt(); + return _game.createdAt().raw() <= retirementTimestamp; } /// @notice Returns whether a game is resolved. @@ -186,6 +235,11 @@ contract AnchorStateRegistry is Initializable, ISemver { return false; } + // Must not be paused, temporarily causes game to be considered improper. + if (paused()) { + return false; + } + return true; } @@ -200,7 +254,7 @@ contract AnchorStateRegistry is Initializable, ISemver { // Game must be beyond the "airgap period" - time since resolution must be at least // "dispute game finality delay" seconds in the past. - if (block.timestamp - _game.resolvedAt().raw() <= portal.disputeGameFinalityDelaySeconds()) { + if (block.timestamp - _game.resolvedAt().raw() <= DISPUTE_GAME_FINALITY_DELAY_SECONDS) { return false; } diff --git a/packages/contracts-bedrock/src/libraries/PortalErrors.sol b/packages/contracts-bedrock/src/libraries/PortalErrors.sol deleted file mode 100644 index 9096a2938fdc1..0000000000000 --- a/packages/contracts-bedrock/src/libraries/PortalErrors.sol +++ /dev/null @@ -1,40 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; - -/// @notice Error for when a deposit or withdrawal is to a bad target. -error BadTarget(); -/// @notice Error for when a deposit has too much calldata. -error LargeCalldata(); -/// @notice Error for when a deposit has too small of a gas limit. -error SmallGasLimit(); -/// @notice Error for when a withdrawal transfer fails. -error TransferFailed(); -/// @notice Error for when a method cannot be called with non zero CALLVALUE. -error NoValue(); -/// @notice Error for an unauthorized CALLER. -error Unauthorized(); -/// @notice Error for when a method cannot be called when paused. This could be renamed -/// to `Paused` in the future, but it collides with the `Paused` event. -error CallPaused(); -/// @notice Error for special gas estimation. -error GasEstimation(); -/// @notice Error for when a method is being reentered. -error NonReentrant(); -/// @notice Error for invalid proof. -error InvalidProof(); -/// @notice Error for invalid game type. -error InvalidGameType(); -/// @notice Error for an invalid dispute game. -error InvalidDisputeGame(); -/// @notice Error for an invalid merkle proof. -error InvalidMerkleProof(); -/// @notice Error for when a dispute game has been blacklisted. -error Blacklisted(); -/// @notice Error for when trying to withdrawal without first proven. -error Unproven(); -/// @notice Error for when a proposal is not validated. -error ProposalNotValidated(); -/// @notice Error for when a withdrawal has already been finalized. -error AlreadyFinalized(); -/// @notice Error for when a game is a legacy game. -error LegacyGame(); diff --git a/packages/contracts-bedrock/src/safe/DeputyGuardianModule.sol b/packages/contracts-bedrock/src/safe/DeputyGuardianModule.sol index a742c452ef0ba..2a0be015c0f8b 100644 --- a/packages/contracts-bedrock/src/safe/DeputyGuardianModule.sol +++ b/packages/contracts-bedrock/src/safe/DeputyGuardianModule.sol @@ -6,14 +6,11 @@ import { GnosisSafe as Safe } from "safe-contracts/GnosisSafe.sol"; import { Enum } from "safe-contracts/common/Enum.sol"; // Libraries -import { Unauthorized } from "src/libraries/PortalErrors.sol"; import { GameType, Timestamp } from "src/dispute/lib/Types.sol"; // Interfaces import { IAnchorStateRegistry } from "interfaces/dispute/IAnchorStateRegistry.sol"; -import { IFaultDisputeGame } from "interfaces/dispute/IFaultDisputeGame.sol"; import { ISuperchainConfig } from "interfaces/L1/ISuperchainConfig.sol"; -import { IOptimismPortal2 } from "interfaces/L1/IOptimismPortal2.sol"; import { IDisputeGame } from "interfaces/dispute/IDisputeGame.sol"; import { ISemver } from "interfaces/universal/ISemver.sol"; @@ -24,7 +21,10 @@ import { ISemver } from "interfaces/universal/ISemver.sol"; /// authorization at any time by disabling this module. contract DeputyGuardianModule is ISemver { /// @notice Error message for failed transaction execution - error ExecutionFailed(string); + error DeputyGuardianModule_ExecutionFailed(string); + + /// @notice Thrown when the caller is not the deputy guardian. + error DeputyGuardianModule_Unauthorized(); /// @notice Emitted when the SuperchainConfig is paused event Paused(string identifier); @@ -38,6 +38,9 @@ contract DeputyGuardianModule is ISemver { /// @notice Emitted when the respected game type is set event RespectedGameTypeSet(GameType indexed gameType, Timestamp indexed updatedAt); + /// @notice Emitted when the retirement timestamp is updated + event RetirementTimestampUpdated(Timestamp indexed updatedAt); + /// @notice The Safe contract instance Safe internal immutable SAFE; @@ -48,8 +51,8 @@ contract DeputyGuardianModule is ISemver { address internal immutable DEPUTY_GUARDIAN; /// @notice Semantic version. - /// @custom:semver 2.0.1-beta.5 - string public constant version = "2.0.1-beta.5"; + /// @custom:semver 3.0.0 + string public constant version = "3.0.0"; // Constructor to initialize the Safe and baseModule instances constructor(Safe _safe, ISuperchainConfig _superchainConfig, address _deputyGuardian) { @@ -79,7 +82,7 @@ contract DeputyGuardianModule is ISemver { /// @notice Internal function to ensure that only the deputy guardian can call certain functions. function _onlyDeputyGuardian() internal view { if (msg.sender != DEPUTY_GUARDIAN) { - revert Unauthorized(); + revert DeputyGuardianModule_Unauthorized(); } } @@ -93,7 +96,7 @@ contract DeputyGuardianModule is ISemver { (bool success, bytes memory returnData) = SAFE.execTransactionFromModuleReturnData(address(SUPERCHAIN_CONFIG), 0, data, Enum.Operation.Call); if (!success) { - revert ExecutionFailed(string(returnData)); + revert DeputyGuardianModule_ExecutionFailed(string(returnData)); } emit Paused("Deputy Guardian"); } @@ -108,58 +111,58 @@ contract DeputyGuardianModule is ISemver { (bool success, bytes memory returnData) = SAFE.execTransactionFromModuleReturnData(address(SUPERCHAIN_CONFIG), 0, data, Enum.Operation.Call); if (!success) { - revert ExecutionFailed(string(returnData)); + revert DeputyGuardianModule_ExecutionFailed(string(returnData)); } emit Unpaused(); } /// @notice Calls the Security Council Safe's `execTransactionFromModuleReturnData()`, with the arguments - /// necessary to call `setAnchorState()` on the `AnchorStateRegistry` contract. + /// necessary to call `blacklistDisputeGame()` on the `AnchorStateRegistry` contract. /// Only the deputy guardian can call this function. - /// @param _registry The `IAnchorStateRegistry` contract instance. - /// @param _game The `IFaultDisputeGame` contract instance. - function setAnchorState(IAnchorStateRegistry _registry, IFaultDisputeGame _game) external { + /// @param _anchorStateRegistry The `AnchorStateRegistry` contract instance. + /// @param _game The `IDisputeGame` contract instance. + function blacklistDisputeGame(IAnchorStateRegistry _anchorStateRegistry, IDisputeGame _game) external { _onlyDeputyGuardian(); - bytes memory data = abi.encodeCall(IAnchorStateRegistry.setAnchorState, (_game)); + bytes memory data = abi.encodeCall(IAnchorStateRegistry.blacklistDisputeGame, (_game)); (bool success, bytes memory returnData) = - SAFE.execTransactionFromModuleReturnData(address(_registry), 0, data, Enum.Operation.Call); + SAFE.execTransactionFromModuleReturnData(address(_anchorStateRegistry), 0, data, Enum.Operation.Call); if (!success) { - revert ExecutionFailed(string(returnData)); + revert DeputyGuardianModule_ExecutionFailed(string(returnData)); } + emit DisputeGameBlacklisted(_game); } /// @notice Calls the Security Council Safe's `execTransactionFromModuleReturnData()`, with the arguments - /// necessary to call `blacklistDisputeGame()` on the `OptimismPortal2` contract. + /// necessary to call `setRespectedGameType()` on the `AnchorStateRegistry` contract. /// Only the deputy guardian can call this function. - /// @param _portal The `OptimismPortal2` contract instance. - /// @param _game The `IDisputeGame` contract instance. - function blacklistDisputeGame(IOptimismPortal2 _portal, IDisputeGame _game) external { + /// @param _anchorStateRegistry The `AnchorStateRegistry` contract instance. + /// @param _gameType The `GameType` to set as the respected game type. + function setRespectedGameType(IAnchorStateRegistry _anchorStateRegistry, GameType _gameType) external { _onlyDeputyGuardian(); - bytes memory data = abi.encodeCall(IOptimismPortal2.blacklistDisputeGame, (_game)); + bytes memory data = abi.encodeCall(IAnchorStateRegistry.setRespectedGameType, (_gameType)); (bool success, bytes memory returnData) = - SAFE.execTransactionFromModuleReturnData(address(_portal), 0, data, Enum.Operation.Call); + SAFE.execTransactionFromModuleReturnData(address(_anchorStateRegistry), 0, data, Enum.Operation.Call); if (!success) { - revert ExecutionFailed(string(returnData)); + revert DeputyGuardianModule_ExecutionFailed(string(returnData)); } - emit DisputeGameBlacklisted(_game); + emit RespectedGameTypeSet(_gameType, Timestamp.wrap(uint64(block.timestamp))); } /// @notice Calls the Security Council Safe's `execTransactionFromModuleReturnData()`, with the arguments - /// necessary to call `setRespectedGameType()` on the `OptimismPortal2` contract. + /// necessary to call `updateRetirementTimestamp()` on the `AnchorStateRegistry` contract. /// Only the deputy guardian can call this function. - /// @param _portal The `OptimismPortal2` contract instance. - /// @param _gameType The `GameType` to set as the respected game type. - function setRespectedGameType(IOptimismPortal2 _portal, GameType _gameType) external { + /// @param _anchorStateRegistry The `AnchorStateRegistry` contract instance. + function updateRetirementTimestamp(IAnchorStateRegistry _anchorStateRegistry) external { _onlyDeputyGuardian(); - bytes memory data = abi.encodeCall(IOptimismPortal2.setRespectedGameType, (_gameType)); + bytes memory data = abi.encodeCall(IAnchorStateRegistry.updateRetirementTimestamp, ()); (bool success, bytes memory returnData) = - SAFE.execTransactionFromModuleReturnData(address(_portal), 0, data, Enum.Operation.Call); + SAFE.execTransactionFromModuleReturnData(address(_anchorStateRegistry), 0, data, Enum.Operation.Call); if (!success) { - revert ExecutionFailed(string(returnData)); + revert DeputyGuardianModule_ExecutionFailed(string(returnData)); } - emit RespectedGameTypeSet(_gameType, Timestamp.wrap(uint64(block.timestamp))); + emit RetirementTimestampUpdated(Timestamp.wrap(uint64(block.timestamp))); } } diff --git a/packages/contracts-bedrock/test/L1/OPContractsManager.t.sol b/packages/contracts-bedrock/test/L1/OPContractsManager.t.sol index 6f742180c67a8..492b608da6d6e 100644 --- a/packages/contracts-bedrock/test/L1/OPContractsManager.t.sol +++ b/packages/contracts-bedrock/test/L1/OPContractsManager.t.sol @@ -646,7 +646,7 @@ contract OPContractsManager_AddGameType_Test is Test { }), optimismPortalImpl: DeployUtils.create1({ _name: "OptimismPortal2", - _args: DeployUtils.encodeConstructor(abi.encodeCall(IOptimismPortal2.__constructor__, (1, 1))) + _args: DeployUtils.encodeConstructor(abi.encodeCall(IOptimismPortal2.__constructor__, (1))) }), systemConfigImpl: DeployUtils.create1({ _name: "SystemConfig", @@ -670,7 +670,7 @@ contract OPContractsManager_AddGameType_Test is Test { }), anchorStateRegistryImpl: DeployUtils.create1({ _name: "AnchorStateRegistry", - _args: DeployUtils.encodeConstructor(abi.encodeCall(IAnchorStateRegistry.__constructor__, ())) + _args: DeployUtils.encodeConstructor(abi.encodeCall(IAnchorStateRegistry.__constructor__, (1))) }), delayedWETHImpl: DeployUtils.create1({ _name: "DelayedWETH", diff --git a/packages/contracts-bedrock/test/L1/OPPrestateUpdater.t.sol b/packages/contracts-bedrock/test/L1/OPPrestateUpdater.t.sol new file mode 100644 index 0000000000000..56edac1f8fa2b --- /dev/null +++ b/packages/contracts-bedrock/test/L1/OPPrestateUpdater.t.sol @@ -0,0 +1,385 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.15; + +// Testing +import { Test } from "forge-std/Test.sol"; +import { DelegateCaller } from "test/mocks/Callers.sol"; + +// Scripts +import { DeployUtils } from "scripts/libraries/DeployUtils.sol"; + +// Libraries +import { Blueprint } from "src/libraries/Blueprint.sol"; + +// Interfaces +import { IL1ERC721Bridge } from "interfaces/L1/IL1ERC721Bridge.sol"; +import { IL1StandardBridge } from "interfaces/L1/IL1StandardBridge.sol"; +import { IL1CrossDomainMessenger } from "interfaces/L1/IL1CrossDomainMessenger.sol"; +import { IOptimismMintableERC20Factory } from "interfaces/universal/IOptimismMintableERC20Factory.sol"; +import { IAnchorStateRegistry } from "interfaces/dispute/IAnchorStateRegistry.sol"; +import { IMIPS } from "interfaces/cannon/IMIPS.sol"; +import { IOptimismPortal2 } from "interfaces/L1/IOptimismPortal2.sol"; +import { IProxyAdmin } from "interfaces/universal/IProxyAdmin.sol"; +import { ISuperchainConfig } from "interfaces/L1/ISuperchainConfig.sol"; +import { IProtocolVersions } from "interfaces/L1/IProtocolVersions.sol"; +import { IPreimageOracle } from "interfaces/cannon/IPreimageOracle.sol"; +import { IPermissionedDisputeGame } from "interfaces/dispute/IPermissionedDisputeGame.sol"; +import { IDelayedWETH } from "interfaces/dispute/IDelayedWETH.sol"; +import { IOPContractsManager } from "interfaces/L1/IOPContractsManager.sol"; +import { IDisputeGameFactory } from "interfaces/dispute/IDisputeGameFactory.sol"; +import { ISystemConfig } from "interfaces/L1/ISystemConfig.sol"; +import { IOPPrestateUpdater } from "interfaces/L1/IOPPrestateUpdater.sol"; + +// Contracts +import { OPContractsManager } from "src/L1/OPContractsManager.sol"; +import { OPPrestateUpdater } from "src/L1/OPPrestateUpdater.sol"; +import { Blueprint } from "src/libraries/Blueprint.sol"; +import { IBigStepper } from "interfaces/dispute/IBigStepper.sol"; +import { GameType, Duration, Hash, Claim } from "src/dispute/lib/LibUDT.sol"; +import { OutputRoot, GameTypes } from "src/dispute/lib/Types.sol"; + +contract OPPrestateUpdater_Test is Test { + IOPContractsManager internal opcm; + OPPrestateUpdater internal prestateUpdater; + + OPContractsManager.OpChainConfig[] internal opChainConfigs; + OPContractsManager.AddGameInput[] internal gameInput; + + IOPContractsManager.DeployOutput internal chainDeployOutput; + + function setUp() public { + IProxyAdmin superchainProxyAdmin = IProxyAdmin(makeAddr("superchainProxyAdmin")); + ISuperchainConfig superchainConfigProxy = ISuperchainConfig(makeAddr("superchainConfig")); + IProtocolVersions protocolVersionsProxy = IProtocolVersions(makeAddr("protocolVersions")); + bytes32 salt = hex"01"; + IOPContractsManager.Blueprints memory blueprints; + + (blueprints.addressManager,) = Blueprint.create(vm.getCode("AddressManager"), salt); + (blueprints.proxy,) = Blueprint.create(vm.getCode("Proxy"), salt); + (blueprints.proxyAdmin,) = Blueprint.create(vm.getCode("ProxyAdmin"), salt); + (blueprints.l1ChugSplashProxy,) = Blueprint.create(vm.getCode("L1ChugSplashProxy"), salt); + (blueprints.resolvedDelegateProxy,) = Blueprint.create(vm.getCode("ResolvedDelegateProxy"), salt); + (blueprints.permissionedDisputeGame1, blueprints.permissionedDisputeGame2) = + Blueprint.create(vm.getCode("PermissionedDisputeGame"), salt); + (blueprints.permissionlessDisputeGame1, blueprints.permissionlessDisputeGame2) = + Blueprint.create(vm.getCode("FaultDisputeGame"), salt); + + IPreimageOracle oracle = IPreimageOracle( + DeployUtils.create1({ + _name: "PreimageOracle", + _args: DeployUtils.encodeConstructor(abi.encodeCall(IPreimageOracle.__constructor__, (126000, 86400))) + }) + ); + + IOPContractsManager.Implementations memory impls = IOPContractsManager.Implementations({ + superchainConfigImpl: DeployUtils.create1({ + _name: "SuperchainConfig", + _args: DeployUtils.encodeConstructor(abi.encodeCall(ISuperchainConfig.__constructor__, ())) + }), + protocolVersionsImpl: DeployUtils.create1({ + _name: "ProtocolVersions", + _args: DeployUtils.encodeConstructor(abi.encodeCall(IProtocolVersions.__constructor__, ())) + }), + l1ERC721BridgeImpl: DeployUtils.create1({ + _name: "L1ERC721Bridge", + _args: DeployUtils.encodeConstructor(abi.encodeCall(IL1ERC721Bridge.__constructor__, ())) + }), + optimismPortalImpl: DeployUtils.create1({ + _name: "OptimismPortal2", + _args: DeployUtils.encodeConstructor(abi.encodeCall(IOptimismPortal2.__constructor__, (1))) + }), + systemConfigImpl: DeployUtils.create1({ + _name: "SystemConfig", + _args: DeployUtils.encodeConstructor(abi.encodeCall(ISystemConfig.__constructor__, ())) + }), + optimismMintableERC20FactoryImpl: DeployUtils.create1({ + _name: "OptimismMintableERC20Factory", + _args: DeployUtils.encodeConstructor(abi.encodeCall(IOptimismMintableERC20Factory.__constructor__, ())) + }), + l1CrossDomainMessengerImpl: DeployUtils.create1({ + _name: "L1CrossDomainMessenger", + _args: DeployUtils.encodeConstructor(abi.encodeCall(IL1CrossDomainMessenger.__constructor__, ())) + }), + l1StandardBridgeImpl: DeployUtils.create1({ + _name: "L1StandardBridge", + _args: DeployUtils.encodeConstructor(abi.encodeCall(IL1StandardBridge.__constructor__, ())) + }), + disputeGameFactoryImpl: DeployUtils.create1({ + _name: "DisputeGameFactory", + _args: DeployUtils.encodeConstructor(abi.encodeCall(IDisputeGameFactory.__constructor__, ())) + }), + anchorStateRegistryImpl: DeployUtils.create1({ + _name: "AnchorStateRegistry", + _args: DeployUtils.encodeConstructor(abi.encodeCall(IAnchorStateRegistry.__constructor__, (1))) + }), + delayedWETHImpl: DeployUtils.create1({ + _name: "DelayedWETH", + _args: DeployUtils.encodeConstructor(abi.encodeCall(IDelayedWETH.__constructor__, (3))) + }), + mipsImpl: DeployUtils.create1({ + _name: "MIPS", + _args: DeployUtils.encodeConstructor(abi.encodeCall(IMIPS.__constructor__, (oracle))) + }) + }); + + vm.etch(address(superchainConfigProxy), hex"01"); + vm.etch(address(protocolVersionsProxy), hex"01"); + + opcm = IOPContractsManager( + DeployUtils.createDeterministic({ + _name: "OPContractsManager", + _args: DeployUtils.encodeConstructor( + abi.encodeCall( + IOPContractsManager.__constructor__, + ( + superchainConfigProxy, + protocolVersionsProxy, + superchainProxyAdmin, + "dev", + blueprints, + impls, + address(this) + ) + ) + ), + _salt: DeployUtils.DEFAULT_SALT + }) + ); + + chainDeployOutput = opcm.deploy( + IOPContractsManager.DeployInput({ + roles: IOPContractsManager.Roles({ + opChainProxyAdminOwner: address(this), + systemConfigOwner: address(this), + batcher: address(this), + unsafeBlockSigner: address(this), + proposer: address(this), + challenger: address(this) + }), + basefeeScalar: 1, + blobBasefeeScalar: 1, + startingAnchorRoot: abi.encode( + OutputRoot({ + root: Hash.wrap(0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef), + l2BlockNumber: 0 + }) + ), + l2ChainId: 100, + saltMixer: "hello", + gasLimit: 30_000_000, + disputeGameType: GameType.wrap(1), + disputeAbsolutePrestate: Claim.wrap( + bytes32(hex"038512e02c4c3f7bdaec27d00edf55b7155e0905301e1a88083e4e0a6764d54c") + ), + disputeMaxGameDepth: 73, + disputeSplitDepth: 30, + disputeClockExtension: Duration.wrap(10800), + disputeMaxClockDuration: Duration.wrap(302400) + }) + ); + + prestateUpdater = OPPrestateUpdater( + DeployUtils.createDeterministic({ + _name: "OPPrestateUpdater", + _args: DeployUtils.encodeConstructor( + abi.encodeCall( + IOPPrestateUpdater.__constructor__, + (ISuperchainConfig(address(this)), IProtocolVersions(address(this)), blueprints) + ) + ), + _salt: DeployUtils.DEFAULT_SALT + }) + ); + } + + function test_semver_works() public view { + assertNotEq(abi.encode(prestateUpdater.version()), abi.encode(0)); + } + + function test_updatePrestate_pdgOnlyWithValidInput_succeeds() public { + OPContractsManager.OpChainConfig[] memory inputs = new OPContractsManager.OpChainConfig[](1); + inputs[0] = OPContractsManager.OpChainConfig( + chainDeployOutput.systemConfigProxy, chainDeployOutput.opChainProxyAdmin, Claim.wrap(bytes32(hex"ABBA")) + ); + address proxyAdminOwner = chainDeployOutput.opChainProxyAdmin.owner(); + + vm.etch(address(proxyAdminOwner), vm.getDeployedCode("test/mocks/Callers.sol:DelegateCaller")); + DelegateCaller(proxyAdminOwner).dcForward( + address(prestateUpdater), abi.encodeCall(OPPrestateUpdater.updatePrestate, (inputs)) + ); + + IPermissionedDisputeGame pdg = IPermissionedDisputeGame( + address( + IDisputeGameFactory(chainDeployOutput.systemConfigProxy.disputeGameFactory()).gameImpls( + GameTypes.PERMISSIONED_CANNON + ) + ) + ); + + assertEq(pdg.absolutePrestate().raw(), inputs[0].absolutePrestate.raw(), "pdg prestate mismatch"); + + // Ensure that the WETH contract is not reverting + pdg.weth().balanceOf(address(0)); + } + + function test_updatePrestate_bothGamesWithValidInput_succeeds() public { + // Also add a permissionless game + IOPContractsManager.AddGameInput memory input = newGameInputFactory({ permissioned: false }); + input.disputeGameType = GameTypes.CANNON; + addGameType(input); + + OPContractsManager.OpChainConfig[] memory inputs = new OPContractsManager.OpChainConfig[](1); + inputs[0] = OPContractsManager.OpChainConfig( + chainDeployOutput.systemConfigProxy, chainDeployOutput.opChainProxyAdmin, Claim.wrap(bytes32(hex"ABBA")) + ); + address proxyAdminOwner = chainDeployOutput.opChainProxyAdmin.owner(); + + vm.etch(address(proxyAdminOwner), vm.getDeployedCode("test/mocks/Callers.sol:DelegateCaller")); + DelegateCaller(proxyAdminOwner).dcForward( + address(prestateUpdater), abi.encodeCall(OPPrestateUpdater.updatePrestate, (inputs)) + ); + + IPermissionedDisputeGame pdg = IPermissionedDisputeGame( + address( + IDisputeGameFactory(chainDeployOutput.systemConfigProxy.disputeGameFactory()).gameImpls( + GameTypes.PERMISSIONED_CANNON + ) + ) + ); + IPermissionedDisputeGame fdg = IPermissionedDisputeGame( + address( + IDisputeGameFactory(chainDeployOutput.systemConfigProxy.disputeGameFactory()).gameImpls( + GameTypes.CANNON + ) + ) + ); + + assertEq(pdg.absolutePrestate().raw(), inputs[0].absolutePrestate.raw(), "pdg prestate mismatch"); + assertEq(fdg.absolutePrestate().raw(), inputs[0].absolutePrestate.raw(), "fdg prestate mismatch"); + + // Ensure that the WETH contracts are not reverting + pdg.weth().balanceOf(address(0)); + fdg.weth().balanceOf(address(0)); + } + + function test_updatePrestate_whenPDGPrestateIsZero_reverts() public { + OPPrestateUpdater.OpChainConfig[] memory inputs = new OPPrestateUpdater.OpChainConfig[](1); + inputs[0] = OPContractsManager.OpChainConfig({ + systemConfigProxy: chainDeployOutput.systemConfigProxy, + proxyAdmin: chainDeployOutput.opChainProxyAdmin, + absolutePrestate: Claim.wrap(bytes32(0)) + }); + + address proxyAdminOwner = chainDeployOutput.opChainProxyAdmin.owner(); + vm.etch(address(proxyAdminOwner), vm.getDeployedCode("test/mocks/Callers.sol:DelegateCaller")); + + vm.expectRevert(OPPrestateUpdater.PrestateRequired.selector); + DelegateCaller(proxyAdminOwner).dcForward( + address(prestateUpdater), abi.encodeCall(OPPrestateUpdater.updatePrestate, (inputs)) + ); + } + + function test_deploy_notImplemented_reverts() public { + OPContractsManager.DeployInput memory input = OPContractsManager.DeployInput({ + roles: OPContractsManager.Roles({ + opChainProxyAdminOwner: address(0), + systemConfigOwner: address(0), + batcher: address(0), + unsafeBlockSigner: address(0), + proposer: address(0), + challenger: address(0) + }), + basefeeScalar: 0, + blobBasefeeScalar: 0, + l2ChainId: 0, + startingAnchorRoot: bytes(abi.encode(0)), + saltMixer: "", + gasLimit: 0, + disputeGameType: GameType.wrap(0), + disputeAbsolutePrestate: Claim.wrap(0), + disputeMaxGameDepth: 0, + disputeSplitDepth: 0, + disputeClockExtension: Duration.wrap(0), + disputeMaxClockDuration: Duration.wrap(0) + }); + + vm.expectRevert(OPPrestateUpdater.NotImplemented.selector); + prestateUpdater.deploy(input); + } + + function test_upgrade_notImplemented_reverts() public { + opChainConfigs.push( + OPContractsManager.OpChainConfig({ + systemConfigProxy: ISystemConfig(address(0)), + proxyAdmin: IProxyAdmin(address(0)), + absolutePrestate: Claim.wrap(0) + }) + ); + + vm.expectRevert(OPPrestateUpdater.NotImplemented.selector); + prestateUpdater.upgrade(opChainConfigs); + } + + function test_addGameType_notImplemented_reverts() public { + gameInput.push( + OPContractsManager.AddGameInput({ + saltMixer: "hello", + systemConfig: ISystemConfig(address(0)), + proxyAdmin: IProxyAdmin(address(0)), + delayedWETH: IDelayedWETH(payable(address(0))), + disputeGameType: GameType.wrap(2000), + disputeAbsolutePrestate: Claim.wrap(bytes32(hex"deadbeef1234")), + disputeMaxGameDepth: 73, + disputeSplitDepth: 30, + disputeClockExtension: Duration.wrap(10800), + disputeMaxClockDuration: Duration.wrap(302400), + initialBond: 1 ether, + vm: IBigStepper(address(0)), + permissioned: true + }) + ); + + vm.expectRevert(OPPrestateUpdater.NotImplemented.selector); + prestateUpdater.addGameType(gameInput); + } + + function test_l1ContractsRelease_works() public view { + string memory result = "none"; + + assertEq(result, prestateUpdater.l1ContractsRelease()); + } + + function addGameType(IOPContractsManager.AddGameInput memory input) + internal + returns (IOPContractsManager.AddGameOutput memory) + { + IOPContractsManager.AddGameInput[] memory inputs = new IOPContractsManager.AddGameInput[](1); + inputs[0] = input; + + (bool success, bytes memory rawGameOut) = + address(opcm).delegatecall(abi.encodeCall(IOPContractsManager.addGameType, (inputs))); + assertTrue(success, "addGameType failed"); + + IOPContractsManager.AddGameOutput[] memory addGameOutAll = + abi.decode(rawGameOut, (IOPContractsManager.AddGameOutput[])); + return addGameOutAll[0]; + } + + function newGameInputFactory(bool permissioned) internal view returns (IOPContractsManager.AddGameInput memory) { + return IOPContractsManager.AddGameInput({ + saltMixer: "hello", + systemConfig: chainDeployOutput.systemConfigProxy, + proxyAdmin: chainDeployOutput.opChainProxyAdmin, + delayedWETH: IDelayedWETH(payable(address(0))), + disputeGameType: GameType.wrap(2000), + disputeAbsolutePrestate: Claim.wrap(bytes32(hex"deadbeef1234")), + disputeMaxGameDepth: 73, + disputeSplitDepth: 30, + disputeClockExtension: Duration.wrap(10800), + disputeMaxClockDuration: Duration.wrap(302400), + initialBond: 1 ether, + vm: IBigStepper(address(opcm.implementations().mipsImpl)), + permissioned: permissioned + }); + } +} diff --git a/packages/contracts-bedrock/test/L1/OptimismPortal2.t.sol b/packages/contracts-bedrock/test/L1/OptimismPortal2.t.sol index 6b6f60bf778b6..b1d1882e84a3b 100644 --- a/packages/contracts-bedrock/test/L1/OptimismPortal2.t.sol +++ b/packages/contracts-bedrock/test/L1/OptimismPortal2.t.sol @@ -17,7 +17,6 @@ import { Constants } from "src/libraries/Constants.sol"; import { AddressAliasHelper } from "src/vendor/AddressAliasHelper.sol"; import { EIP1967Helper } from "test/mocks/EIP1967Helper.sol"; import "src/dispute/lib/Types.sol"; -import "src/libraries/PortalErrors.sol"; // Interfaces import { IResourceMetering } from "interfaces/L1/IResourceMetering.sol"; @@ -45,17 +44,25 @@ contract OptimismPortal2_Test is CommonTest { /// test/kontrol/deployment/DeploymentSummary.t.sol function test_constructor_succeeds() external virtual { IOptimismPortal2 opImpl = IOptimismPortal2(payable(EIP1967Helper.getImplementation(address(optimismPortal2)))); - assertEq(address(opImpl.disputeGameFactory()), address(0)); + assertEq(address(opImpl.anchorStateRegistry()), address(0)); assertEq(address(opImpl.systemConfig()), address(0)); assertEq(address(opImpl.superchainConfig()), address(0)); assertEq(opImpl.respectedGameType().raw(), deploy.cfg().respectedGameType()); + + // TODO(opcm upgrades): remove skip once upgrade path is implemented + returnIfForkTest("OptimismPortal2_Test: l2Sender is nonzero on OP mainnet"); assertEq(opImpl.l2Sender(), address(0)); + + // TODO(opcm upgrades): remove skip once upgrade path is implemented + returnIfForkTest("OptimismPortal2_Test: anchorStateRegistry is nonzero on OP mainnet"); + assertEq(address(opImpl.anchorStateRegistry()), address(0)); } /// @dev Tests that the initializer sets the correct values. /// @notice Marked virtual to be overridden in /// test/kontrol/deployment/DeploymentSummary.t.sol function test_initialize_succeeds() external virtual { + assertEq(address(optimismPortal2.anchorStateRegistry()), address(anchorStateRegistry)); assertEq(address(optimismPortal2.disputeGameFactory()), address(disputeGameFactory)); assertEq(address(optimismPortal2.superchainConfig()), address(superchainConfig)); assertEq(optimismPortal2.l2Sender(), Constants.DEFAULT_L2_SENDER); @@ -162,17 +169,21 @@ contract OptimismPortal2_Test is CommonTest { /// @dev Tests that `depositTransaction` reverts when the destination address is non-zero /// for a contract creation deposit. function test_depositTransaction_contractCreation_reverts() external { + // TODO(opcm upgrades): remove skip once upgrade path is implemented + skipIfForkTest("OptimismPortal2_Test: error is different on OP Mainnet"); // contract creation must have a target of address(0) - vm.expectRevert(BadTarget.selector); + vm.expectRevert(IOptimismPortal2.OptimismPortal_BadTarget.selector); optimismPortal2.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 { + // TODO(opcm upgrades): remove skip once upgrade path is implemented + skipIfForkTest("OptimismPortal2_Test: error is different on OP Mainnet"); uint256 size = 120_001; uint64 gasLimit = optimismPortal2.minimumGasLimit(uint64(size)); - vm.expectRevert(LargeCalldata.selector); + vm.expectRevert(IOptimismPortal2.OptimismPortal_CalldataTooLarge.selector); optimismPortal2.depositTransaction({ _to: address(0), _value: 0, @@ -184,7 +195,9 @@ contract OptimismPortal2_Test is CommonTest { /// @dev Tests that `depositTransaction` reverts when the gas limit is too small. function test_depositTransaction_smallGasLimit_reverts() external { - vm.expectRevert(SmallGasLimit.selector); + // TODO(opcm upgrades): remove skip once upgrade path is implemented + skipIfForkTest("OptimismPortal2_Test: error is different on OP Mainnet"); + vm.expectRevert(IOptimismPortal2.OptimismPortal_GasLimitTooLow.selector); optimismPortal2.depositTransaction({ _to: address(1), _value: 0, _gasLimit: 0, _isCreation: false, _data: hex"" }); } @@ -194,7 +207,9 @@ contract OptimismPortal2_Test is CommonTest { uint64 gasLimit = optimismPortal2.minimumGasLimit(uint64(_data.length)); if (_shouldFail) { gasLimit = uint64(bound(gasLimit, 0, gasLimit - 1)); - vm.expectRevert(SmallGasLimit.selector); + // TODO(opcm upgrades): remove skip once upgrade path is implemented + skipIfForkTest("OptimismPortal2_Test: error is different on OP Mainnet"); + vm.expectRevert(IOptimismPortal2.OptimismPortal_GasLimitTooLow.selector); } optimismPortal2.depositTransaction({ @@ -405,7 +420,6 @@ contract OptimismPortal2_Test is CommonTest { contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { // Reusable default values for a test withdrawal Types.WithdrawalTransaction _defaultTx; - IFaultDisputeGame game; uint256 _proposedGameIndex; uint256 _proposedBlockNumber; @@ -428,6 +442,7 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { 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); @@ -455,12 +470,13 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { address(disputeGameFactory), keccak256(abi.encode(GameType.wrap(0), uint256(102))), bytes32(uint256(0)) ); } else { - // Warp forward in time to ensure that the game is created after the retirement timestamp. - vm.warp(optimismPortal2.respectedGameTypeUpdatedAt() + 1 seconds); - // Set up the dummy game. _proposedBlockNumber = 0xFF; } + + // Warp forward in time to ensure that the game is created after the retirement timestamp. + vm.warp(anchorStateRegistry.retirementTimestamp() + 1); + GameType respectedGameType = optimismPortal2.respectedGameType(); game = IFaultDisputeGame( payable( @@ -484,7 +500,7 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { /// @dev Asserts that the reentrant call will revert. function callPortalAndExpectRevert() external payable { - vm.expectRevert(NonReentrant.selector); + vm.expectRevert(IOptimismPortal2.OptimismPortal_NoReentrancy.selector); // Arguments here don't matter, as the require check is the first thing that happens. // We assume that this has already been proven. optimismPortal2.finalizeWithdrawalTransaction(_defaultTx); @@ -492,70 +508,12 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { assertFalse(optimismPortal2.finalizedWithdrawals(Hashing.hashWithdrawal(_defaultTx))); } - /// @dev Tests that `blacklistDisputeGame` reverts when called by a non-guardian. - function testFuzz_blacklist_onlyGuardian_reverts(address _act) external { - vm.assume(_act != address(optimismPortal2.guardian())); - - vm.expectRevert(Unauthorized.selector); - optimismPortal2.blacklistDisputeGame(IDisputeGame(address(0xdead))); - } - - /// @dev Tests that the guardian role can blacklist any dispute game. - function testFuzz_blacklist_guardian_succeeds(IDisputeGame _addr) external { - vm.expectEmit(address(optimismPortal2)); - emit DisputeGameBlacklisted(_addr); - - vm.prank(optimismPortal2.guardian()); - optimismPortal2.blacklistDisputeGame(_addr); - - assertTrue(optimismPortal2.disputeGameBlacklist(_addr)); - } - - /// @dev Tests that `setRespectedGameType` reverts when called by a non-guardian. - function testFuzz_setRespectedGameType_onlyGuardian_reverts(address _act, GameType _ty) external { - vm.assume(_act != address(optimismPortal2.guardian())); - - vm.prank(_act); - vm.expectRevert(Unauthorized.selector); - optimismPortal2.setRespectedGameType(_ty); - } - - /// @dev Tests that the guardian role can set the respected game type to anything they want. - function testFuzz_setRespectedGameType_guardianCanSetRespectedGameType_succeeds(GameType _ty) external { - vm.assume(_ty.raw() != type(uint32).max); - uint64 respectedGameTypeUpdatedAt = optimismPortal2.respectedGameTypeUpdatedAt(); - vm.expectEmit(address(optimismPortal2)); - emit RespectedGameTypeSet(_ty, Timestamp.wrap(respectedGameTypeUpdatedAt)); - vm.prank(optimismPortal2.guardian()); - optimismPortal2.setRespectedGameType(_ty); - // GameType changes, but the timestamp doesn't. - assertEq(optimismPortal2.respectedGameType().raw(), _ty.raw()); - assertEq(optimismPortal2.respectedGameTypeUpdatedAt(), respectedGameTypeUpdatedAt); - } - - /// @dev Tests that the guardian can set the `respectedGameTypeUpdatedAt` timestamp to current timestamp. - function testFuzz_setRespectedGameType_guardianCanSetRespectedGameTypeUpdatedAt_succeeds(uint64 _elapsed) - external - { - _elapsed = uint64(bound(_elapsed, 0, type(uint64).max - uint64(block.timestamp))); - GameType _ty = GameType.wrap(type(uint32).max); - uint64 _newRespectedGameTypeUpdatedAt = uint64(block.timestamp) + _elapsed; - GameType _existingGameType = optimismPortal2.respectedGameType(); - vm.warp(_newRespectedGameTypeUpdatedAt); - emit RespectedGameTypeSet(_existingGameType, Timestamp.wrap(_newRespectedGameTypeUpdatedAt)); - vm.prank(optimismPortal2.guardian()); - optimismPortal2.setRespectedGameType(_ty); - // GameType doesn't change, but the timestamp does. - assertEq(optimismPortal2.respectedGameType().raw(), _existingGameType.raw()); - assertEq(optimismPortal2.respectedGameTypeUpdatedAt(), _newRespectedGameTypeUpdatedAt); - } - /// @dev Tests that `proveWithdrawalTransaction` reverts when paused. function test_proveWithdrawalTransaction_paused_reverts() external { vm.prank(optimismPortal2.guardian()); superchainConfig.pause("identifier"); - vm.expectRevert(CallPaused.selector); + vm.expectRevert(IOptimismPortal2.OptimismPortal_CallPaused.selector); optimismPortal2.proveWithdrawalTransaction({ _tx: _defaultTx, _disputeGameIndex: _proposedGameIndex, @@ -567,7 +525,26 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { /// @dev Tests that `proveWithdrawalTransaction` reverts when the target is the portal contract. function test_proveWithdrawalTransaction_onSelfCall_reverts() external { _defaultTx.target = address(optimismPortal2); - vm.expectRevert(BadTarget.selector); + vm.expectRevert(IOptimismPortal2.OptimismPortal_BadTarget.selector); + optimismPortal2.proveWithdrawalTransaction({ + _tx: _defaultTx, + _disputeGameIndex: _proposedGameIndex, + _outputRootProof: _outputRootProof, + _withdrawalProof: _withdrawalProof + }); + } + + /// @dev Tests that `proveWithdrawalTransaction` reverts when the current timestamp is less + /// than or equal to the creation timestamp of the dispute game. + function testFuzz_proveWithdrawalTransaction_timestampLessThanOrEqualToCreation_reverts(uint64 _timestamp) + external + { + // Set the timestamp to be less than or equal to the creation timestamp of the dispute game. + _timestamp = uint64(bound(_timestamp, 0, game.createdAt().raw())); + vm.warp(_timestamp); + + // Should revert. + vm.expectRevert(IOptimismPortal2.OptimismPortal_InvalidProofTimestamp.selector); optimismPortal2.proveWithdrawalTransaction({ _tx: _defaultTx, _disputeGameIndex: _proposedGameIndex, @@ -580,7 +557,7 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { function test_proveWithdrawalTransaction_onInvalidOutputRootProof_reverts() external { // Modify the version to invalidate the withdrawal proof. _outputRootProof.version = bytes32(uint256(1)); - vm.expectRevert(InvalidProof.selector); + vm.expectRevert(IOptimismPortal2.OptimismPortal_InvalidOutputRootProof.selector); optimismPortal2.proveWithdrawalTransaction({ _tx: _defaultTx, _disputeGameIndex: _proposedGameIndex, @@ -624,25 +601,7 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { 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); - optimismPortal2.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); + vm.expectRevert(IOptimismPortal2.OptimismPortal_InvalidDisputeGame.selector); optimismPortal2.proveWithdrawalTransaction({ _tx: _defaultTx, _disputeGameIndex: _proposedGameIndex, @@ -654,7 +613,7 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { /// @dev Tests that `proveWithdrawalTransaction` reverts if the game was not the respected game type when created. function test_proveWithdrawalTransaction_wasNotRespectedGameTypeWhenCreated_reverts() external { vm.mockCall(address(game), abi.encodeCall(game.wasRespectedGameTypeWhenCreated, ()), abi.encode(false)); - vm.expectRevert(InvalidGameType.selector); + vm.expectRevert(IOptimismPortal2.OptimismPortal_InvalidDisputeGame.selector); optimismPortal2.proveWithdrawalTransaction({ _tx: _defaultTx, _disputeGameIndex: _proposedGameIndex, @@ -667,7 +626,7 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { /// `wasRespectedGameTypeWhenCreated`. function test_proveWithdrawalTransaction_legacyGame_reverts() external { vm.mockCallRevert(address(game), abi.encodeCall(game.wasRespectedGameTypeWhenCreated, ()), ""); - vm.expectRevert(LegacyGame.selector); + vm.expectRevert(); // nosemgrep: sol-safety-expectrevert-no-args optimismPortal2.proveWithdrawalTransaction({ _tx: _defaultTx, _disputeGameIndex: _proposedGameIndex, @@ -679,7 +638,8 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { /// @dev Tests that `proveWithdrawalTransaction` succeeds if the game was created after the /// game retirement timestamp. function testFuzz_proveWithdrawalTransaction_createdAfterRetirementTimestamp_succeeds(uint64 _createdAt) external { - _createdAt = uint64(bound(_createdAt, optimismPortal2.respectedGameTypeUpdatedAt() + 1, type(uint64).max)); + _createdAt = uint64(bound(_createdAt, optimismPortal2.respectedGameTypeUpdatedAt() + 1, type(uint64).max - 1)); + vm.warp(_createdAt + 1); vm.mockCall(address(game), abi.encodeCall(game.createdAt, ()), abi.encode(uint64(_createdAt))); optimismPortal2.proveWithdrawalTransaction({ _tx: _defaultTx, @@ -696,45 +656,7 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { { _createdAt = uint64(bound(_createdAt, 0, optimismPortal2.respectedGameTypeUpdatedAt())); vm.mockCall(address(game), abi.encodeCall(game.createdAt, ()), abi.encode(uint64(_createdAt))); - vm.expectRevert("OptimismPortal: dispute game created before respected game type was updated"); - optimismPortal2.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)); - optimismPortal2.proveWithdrawalTransaction({ - _tx: _defaultTx, - _disputeGameIndex: _proposedGameIndex, - _outputRootProof: _outputRootProof, - _withdrawalProof: _withdrawalProof - }); - - // Blacklist the dispute dispute game. - vm.prank(optimismPortal2.guardian()); - optimismPortal2.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( - optimismPortal2.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)); + vm.expectRevert(IOptimismPortal2.OptimismPortal_ImproperDisputeGame.selector); optimismPortal2.proveWithdrawalTransaction({ _tx: _defaultTx, _disputeGameIndex: _proposedGameIndex, @@ -759,12 +681,16 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { // 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( optimismPortal2.respectedGameType(), Claim.wrap(_outputRoot), abi.encode(_proposedBlockNumber + 1) ); _proposedGameIndex = disputeGameFactory.gameCount() - 1; + // Warp 1 second into the future so we're not in the same block as the dispute game. + vm.warp(block.timestamp + 1 seconds); + vm.expectEmit(true, true, true, true); emit WithdrawalProven(_withdrawalHash, alice, bob); vm.expectEmit(true, true, true, true); @@ -798,7 +724,7 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { // Update the respected game type to 0xbeef. vm.prank(optimismPortal2.guardian()); - optimismPortal2.setRespectedGameType(GameType.wrap(0xbeef)); + anchorStateRegistry.setRespectedGameType(GameType.wrap(0xbeef)); // Create a new game and mock the game type as 0xbeef in the factory. vm.mockCall( @@ -807,6 +733,9 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { abi.encode(GameType.wrap(0xbeef), Timestamp.wrap(uint64(block.timestamp)), IDisputeGame(address(newGame))) ); + // Warp 1 second into the future so we're not in the same block as the dispute game. + vm.warp(block.timestamp + 1 seconds); + // Re-proving should be successful against the new game. vm.expectEmit(true, true, true, true); emit WithdrawalProven(_withdrawalHash, alice, bob); @@ -873,7 +802,7 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { emit WithdrawalFinalized(_withdrawalHash, true); optimismPortal2.finalizeWithdrawalTransactionExternalProof(_defaultTx, address(0xb0b)); - vm.expectRevert(AlreadyFinalized.selector); + vm.expectRevert(IOptimismPortal2.OptimismPortal_AlreadyFinalized.selector); optimismPortal2.finalizeWithdrawalTransactionExternalProof(_defaultTx, address(this)); assert(address(bob).balance == bobBalanceBefore + 100); @@ -895,7 +824,7 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { vm.warp(block.timestamp + optimismPortal2.proofMaturityDelaySeconds() + 1 seconds); vm.startPrank(alice, Constants.ESTIMATION_ADDRESS); - vm.expectRevert(GasEstimation.selector); + vm.expectRevert(IOptimismPortal2.OptimismPortal_GasEstimation.selector); optimismPortal2.finalizeWithdrawalTransaction(_defaultTx); } @@ -1005,7 +934,7 @@ 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); + vm.warp(block.timestamp + 1); // Prove the withdrawal transaction against the invalid dispute game, as 0xb0b. vm.expectEmit(true, true, true, true); @@ -1044,7 +973,7 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { // Ensure both proofs are registered successfully. assertEq(optimismPortal2.numProofSubmitters(_withdrawalHash), 2); - vm.expectRevert(ProposalNotValidated.selector); + vm.expectRevert(IOptimismPortal2.OptimismPortal_InvalidRootClaim.selector); vm.prank(address(0xb0b)); optimismPortal2.finalizeWithdrawalTransaction(_defaultTx); @@ -1060,7 +989,7 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { vm.prank(optimismPortal2.guardian()); superchainConfig.pause("identifier"); - vm.expectRevert(CallPaused.selector); + vm.expectRevert(IOptimismPortal2.OptimismPortal_CallPaused.selector); optimismPortal2.finalizeWithdrawalTransaction(_defaultTx); } @@ -1068,7 +997,7 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { function test_finalizeWithdrawalTransaction_ifWithdrawalNotProven_reverts() external { uint256 bobBalanceBefore = address(bob).balance; - vm.expectRevert(Unproven.selector); + vm.expectRevert(IOptimismPortal2.OptimismPortal_Unproven.selector); optimismPortal2.finalizeWithdrawalTransaction(_defaultTx); assert(address(bob).balance == bobBalanceBefore); @@ -1090,7 +1019,7 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { _withdrawalProof: _withdrawalProof }); - vm.expectRevert("OptimismPortal: proven withdrawal has not matured yet"); + vm.expectRevert(IOptimismPortal2.OptimismPortal_ProofNotOldEnough.selector); optimismPortal2.finalizeWithdrawalTransaction(_defaultTx); assert(address(bob).balance == bobBalanceBefore); @@ -1120,7 +1049,7 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { 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"); + vm.expectRevert(IOptimismPortal2.OptimismPortal_InvalidProofTimestamp.selector); optimismPortal2.finalizeWithdrawalTransaction(_defaultTx); // Ensure that bob's balance has remained the same @@ -1148,7 +1077,7 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { vm.warp(block.timestamp + optimismPortal2.proofMaturityDelaySeconds() + 1); // Attempt to finalize the withdrawal - vm.expectRevert(ProposalNotValidated.selector); + vm.expectRevert(IOptimismPortal2.OptimismPortal_InvalidRootClaim.selector); optimismPortal2.finalizeWithdrawalTransaction(_defaultTx); // Ensure that bob's balance has remained the same @@ -1206,7 +1135,7 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { emit WithdrawalFinalized(_withdrawalHash, true); optimismPortal2.finalizeWithdrawalTransaction(_defaultTx); - vm.expectRevert(AlreadyFinalized.selector); + vm.expectRevert(IOptimismPortal2.OptimismPortal_AlreadyFinalized.selector); optimismPortal2.finalizeWithdrawalTransaction(_defaultTx); } @@ -1455,7 +1384,7 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { // Change the respectedGameType vm.prank(optimismPortal2.guardian()); - optimismPortal2.setRespectedGameType(_newGameType); + anchorStateRegistry.setRespectedGameType(_newGameType); // Withdrawal transaction still finalizable vm.expectCallMinGas(_tx.target, _tx.value, uint64(_tx.gasLimit), _tx.data); @@ -1481,11 +1410,11 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { game.resolve(); vm.prank(optimismPortal2.guardian()); - optimismPortal2.blacklistDisputeGame(IDisputeGame(address(game))); + anchorStateRegistry.blacklistDisputeGame(IDisputeGame(address(game))); vm.warp(block.timestamp + optimismPortal2.proofMaturityDelaySeconds() + 1); - vm.expectRevert(Blacklisted.selector); + vm.expectRevert(IOptimismPortal2.OptimismPortal_InvalidRootClaim.selector); optimismPortal2.finalizeWithdrawalTransaction(_defaultTx); } @@ -1511,7 +1440,7 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { game.resolve(); // Attempt to finalize the withdrawal directly after the game resolves. This should fail. - vm.expectRevert("OptimismPortal: output proposal in air-gap"); + vm.expectRevert(IOptimismPortal2.OptimismPortal_InvalidRootClaim.selector); optimismPortal2.finalizeWithdrawalTransaction(_defaultTx); // Finalize the withdrawal transaction. This should succeed. @@ -1546,9 +1475,10 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { // Set respectedGameTypeUpdatedAt. vm.prank(optimismPortal2.guardian()); - optimismPortal2.setRespectedGameType(GameType.wrap(type(uint32).max)); + anchorStateRegistry.updateRetirementTimestamp(); - vm.expectRevert("OptimismPortal: dispute game created before respected game type was updated"); + // Should revert. + vm.expectRevert(IOptimismPortal2.OptimismPortal_InvalidRootClaim.selector); optimismPortal2.finalizeWithdrawalTransaction(_defaultTx); } @@ -1578,7 +1508,7 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { vm.mockCall(address(game), abi.encodeCall(game.wasRespectedGameTypeWhenCreated, ()), abi.encode(false)); - vm.expectRevert(InvalidGameType.selector); + vm.expectRevert(IOptimismPortal2.OptimismPortal_InvalidRootClaim.selector); optimismPortal2.finalizeWithdrawalTransaction(_defaultTx); } @@ -1607,9 +1537,11 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { // Warp past the dispute game finality delay. vm.warp(block.timestamp + optimismPortal2.disputeGameFinalityDelaySeconds() + 1); + // Mock the wasRespectedGameTypeWhenCreated call to revert. vm.mockCallRevert(address(game), abi.encodeCall(game.wasRespectedGameTypeWhenCreated, ()), ""); - vm.expectRevert(LegacyGame.selector); + // Should revert. + vm.expectRevert(); // nosemgrep: sol-safety-expectrevert-no-args optimismPortal2.finalizeWithdrawalTransaction(_defaultTx); } @@ -1629,13 +1561,13 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { // Attempt to finalize the withdrawal transaction 1 second before the proof has matured. This should fail. vm.warp(block.timestamp + optimismPortal2.proofMaturityDelaySeconds()); - vm.expectRevert("OptimismPortal: proven withdrawal has not matured yet"); + vm.expectRevert(IOptimismPortal2.OptimismPortal_ProofNotOldEnough.selector); optimismPortal2.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); + vm.expectRevert(IOptimismPortal2.OptimismPortal_InvalidRootClaim.selector); optimismPortal2.finalizeWithdrawalTransaction(_defaultTx); // Finalize the dispute game and attempt to finalize the withdrawal again. This should also fail, since the @@ -1643,7 +1575,7 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { game.resolveClaim(0, 0); game.resolve(); vm.warp(block.timestamp + optimismPortal2.disputeGameFinalityDelaySeconds()); - vm.expectRevert("OptimismPortal: output proposal in air-gap"); + vm.expectRevert(IOptimismPortal2.OptimismPortal_InvalidRootClaim.selector); optimismPortal2.finalizeWithdrawalTransaction(_defaultTx); // Warp 1 second in the future, past the air gap dispute game delay, and attempt to finalize the withdrawal. @@ -1652,6 +1584,140 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { optimismPortal2.finalizeWithdrawalTransaction(_defaultTx); assertTrue(optimismPortal2.finalizedWithdrawals(_withdrawalHash)); } + + /// @notice Tests that checkWithdrawal succeeds if the withdrawal has been proven, the dispute + /// game has been finalized, and the root claim is valid. + function test_checkWithdrawal_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)); + optimismPortal2.proveWithdrawalTransaction({ + _tx: _defaultTx, + _disputeGameIndex: _proposedGameIndex, + _outputRootProof: _outputRootProof, + _withdrawalProof: _withdrawalProof + }); + + // Warp past the finalization period. + vm.warp(block.timestamp + optimismPortal2.proofMaturityDelaySeconds() + 1); + + // Mark the dispute game as CHALLENGER_WINS. + vm.mockCall(address(game), abi.encodeCall(game.status, ()), abi.encode(GameStatus.CHALLENGER_WINS)); + + // Mock isGameClaimValid to return true. + vm.mockCall( + address(anchorStateRegistry), abi.encodeCall(anchorStateRegistry.isGameClaimValid, (game)), abi.encode(true) + ); + + // Should succeed. + optimismPortal2.checkWithdrawal(_withdrawalHash, address(this)); + } + + /// @notice Tests that checkWithdrawal reverts if the withdrawal has already been finalized. + function test_checkWithdrawal_ifAlreadyFinalized_reverts() 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)); + optimismPortal2.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 + optimismPortal2.proofMaturityDelaySeconds() + 1); + + // Finalize the withdrawal. + optimismPortal2.finalizeWithdrawalTransaction(_defaultTx); + + // Should revert. + vm.expectRevert(IOptimismPortal2.OptimismPortal_AlreadyFinalized.selector); + optimismPortal2.checkWithdrawal(_withdrawalHash, address(this)); + } + + /// @notice Tests that checkWithdrawal reverts if the withdrawal has not been proven. + function test_checkWithdrawal_ifUnproven_reverts() external { + // Don't prove the withdrawal transaction. + // Should revert. + vm.expectRevert(IOptimismPortal2.OptimismPortal_Unproven.selector); + optimismPortal2.checkWithdrawal(_withdrawalHash, address(this)); + } + + /// @notice Tests that checkWithdrawal reverts if the proof timestamp is greater than the game + /// creation timestamp. + function testFuzz_checkWithdrawal_ifInvalidProofTimestamp_reverts(uint64 _createdAt) external { + // Prove the withdrawal transaction. + optimismPortal2.proveWithdrawalTransaction({ + _tx: _defaultTx, + _disputeGameIndex: _proposedGameIndex, + _outputRootProof: _outputRootProof, + _withdrawalProof: _withdrawalProof + }); + + // Mock the game creation timestamp to be greater than the proof timestamp. + _createdAt = uint64(bound(_createdAt, block.timestamp, type(uint64).max)); + vm.mockCall(address(game), abi.encodeCall(game.createdAt, ()), abi.encode(_createdAt)); + + // Warp beyond the proof maturity delay. + vm.warp(block.timestamp + optimismPortal2.proofMaturityDelaySeconds() + 1); + + // Mark the dispute game as CHALLENGER_WINS. + vm.mockCall(address(game), abi.encodeCall(game.status, ()), abi.encode(GameStatus.CHALLENGER_WINS)); + + // Mock isGameClaimValid to return true. + vm.mockCall( + address(anchorStateRegistry), abi.encodeCall(anchorStateRegistry.isGameClaimValid, (game)), abi.encode(true) + ); + + // Should revert. + vm.expectRevert(IOptimismPortal2.OptimismPortal_InvalidProofTimestamp.selector); + optimismPortal2.checkWithdrawal(_withdrawalHash, address(this)); + } + + /// @notice Tests that checkWithdrawal reverts if the proof timestamp is less than the proof + /// maturity delay. + function test_checkWithdrawal_ifProofNotOldEnough_reverts() external { + // Prove but don't warp ahead past the proof maturity delay. + optimismPortal2.proveWithdrawalTransaction({ + _tx: _defaultTx, + _disputeGameIndex: _proposedGameIndex, + _outputRootProof: _outputRootProof, + _withdrawalProof: _withdrawalProof + }); + + // Should revert. + vm.warp(block.timestamp + optimismPortal2.proofMaturityDelaySeconds() - 1); + vm.expectRevert(IOptimismPortal2.OptimismPortal_ProofNotOldEnough.selector); + optimismPortal2.checkWithdrawal(_withdrawalHash, address(this)); + } + + /// @notice Tests that checkWithdrawal reverts if the root claim is invalid. + function test_checkWithdrawal_ifInvalidRootClaim_reverts() external { + // Prove the withdrawal. + optimismPortal2.proveWithdrawalTransaction({ + _tx: _defaultTx, + _disputeGameIndex: _proposedGameIndex, + _outputRootProof: _outputRootProof, + _withdrawalProof: _withdrawalProof + }); + + // Warp past the proof maturity delay. + vm.warp(block.timestamp + optimismPortal2.proofMaturityDelaySeconds() + 1); + + // Mock the game to have CHALLENGER_WINS status + vm.mockCall(address(game), abi.encodeCall(game.status, ()), abi.encode(GameStatus.CHALLENGER_WINS)); + + // Should revert. + vm.expectRevert(IOptimismPortal2.OptimismPortal_InvalidRootClaim.selector); + optimismPortal2.checkWithdrawal(_withdrawalHash, address(this)); + } } contract OptimismPortal2_Upgradeable_Test is CommonTest { @@ -1682,7 +1748,7 @@ contract OptimismPortal2_Upgradeable_Test is CommonTest { // The value passed to the initialize must be larger than the last value // that initialize was called with. IProxy(payable(address(optimismPortal2))).upgradeToAndCall( - address(nextImpl), abi.encodeCall(NextImpl.initialize, (2)) + address(nextImpl), abi.encodeCall(NextImpl.initialize, (3)) ); assertEq(IProxy(payable(address(optimismPortal2))).implementation(), address(nextImpl)); diff --git a/packages/contracts-bedrock/test/L1/OptimismPortalInterop.t.sol b/packages/contracts-bedrock/test/L1/OptimismPortalInterop.t.sol index df9100ce8817f..3a8c9973e62dc 100644 --- a/packages/contracts-bedrock/test/L1/OptimismPortalInterop.t.sol +++ b/packages/contracts-bedrock/test/L1/OptimismPortalInterop.t.sol @@ -7,7 +7,6 @@ import { CommonTest } from "test/setup/CommonTest.sol"; // Libraries import { Constants } from "src/libraries/Constants.sol"; import { Predeploys } from "src/libraries/Predeploys.sol"; -import "src/libraries/PortalErrors.sol"; // Interfaces import { IL1BlockInterop, ConfigType } from "interfaces/L2/IL1BlockInterop.sol"; @@ -46,7 +45,7 @@ contract OptimismPortalInterop_Test is CommonTest { /// @dev Tests that setting the add dependency config as not the system config reverts. function testFuzz_setConfig_addDependencyButNotSystemConfig_reverts(bytes calldata _value) public { - vm.expectRevert(Unauthorized.selector); + vm.expectRevert(IOptimismPortalInterop.OptimismPortal_Unauthorized.selector); _optimismPortalInterop().setConfig(ConfigType.ADD_DEPENDENCY, _value); } @@ -69,7 +68,7 @@ contract OptimismPortalInterop_Test is CommonTest { /// @dev Tests that setting the remove dependency config as not the system config reverts. function testFuzz_setConfig_removeDependencyButNotSystemConfig_reverts(bytes calldata _value) public { - vm.expectRevert(Unauthorized.selector); + vm.expectRevert(IOptimismPortalInterop.OptimismPortal_Unauthorized.selector); _optimismPortalInterop().setConfig(ConfigType.REMOVE_DEPENDENCY, _value); } diff --git a/packages/contracts-bedrock/test/dispute/AnchorStateRegistry.t.sol b/packages/contracts-bedrock/test/dispute/AnchorStateRegistry.t.sol index b3c7bf06400c3..c171d97737eb8 100644 --- a/packages/contracts-bedrock/test/dispute/AnchorStateRegistry.t.sol +++ b/packages/contracts-bedrock/test/dispute/AnchorStateRegistry.t.sol @@ -8,6 +8,7 @@ import { FaultDisputeGame_Init, _changeClaimStatus } from "test/dispute/FaultDis import { GameType, GameStatus, Hash, Claim, VMStatuses, OutputRoot } from "src/dispute/lib/Types.sol"; // Interfaces +import { IDisputeGame } from "interfaces/dispute/IDisputeGame.sol"; import { IFaultDisputeGame } from "interfaces/dispute/IFaultDisputeGame.sol"; import { IAnchorStateRegistry } from "interfaces/dispute/IAnchorStateRegistry.sol"; @@ -15,8 +16,9 @@ contract AnchorStateRegistry_Init is FaultDisputeGame_Init { /// @dev A valid l2BlockNumber that comes after the current anchor root block. uint256 validL2BlockNumber; - event AnchorNotUpdated(IFaultDisputeGame indexed game); event AnchorUpdated(IFaultDisputeGame indexed game); + event RespectedGameTypeSet(GameType gameType); + event RetirementTimestampSet(uint256 timestamp); function setUp() public virtual override { // Duplicating the initialization/setup logic of FaultDisputeGame_Test. @@ -46,7 +48,6 @@ contract AnchorStateRegistry_Initialize_Test is AnchorStateRegistry_Init { // Verify contract addresses. assert(anchorStateRegistry.superchainConfig() == superchainConfig); assert(anchorStateRegistry.disputeGameFactory() == disputeGameFactory); - assert(anchorStateRegistry.portal() == optimismPortal2); } } @@ -57,11 +58,11 @@ contract AnchorStateRegistry_Initialize_TestFail is AnchorStateRegistry_Init { anchorStateRegistry.initialize( superchainConfig, disputeGameFactory, - optimismPortal2, OutputRoot({ root: Hash.wrap(0xDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEF), l2BlockNumber: 0 - }) + }), + GameType.wrap(0) ); } } @@ -73,6 +74,25 @@ contract AnchorStateRegistry_Version_Test is AnchorStateRegistry_Init { } } +contract AnchorStateRegistry_Paused_Test is AnchorStateRegistry_Init { + /// @notice Tests that paused() will return the correct value. + function test_paused_succeeds() public { + // Pause the superchain. + vm.prank(superchainConfig.guardian()); + superchainConfig.pause("testing"); + + // Paused should return true. + assertTrue(anchorStateRegistry.paused()); + + // Unpause the superchain. + vm.prank(superchainConfig.guardian()); + superchainConfig.unpause(); + + // Paused should return false. + assertFalse(anchorStateRegistry.paused()); + } +} + contract AnchorStateRegistry_GetAnchorRoot_Test is AnchorStateRegistry_Init { /// @notice Tests that getAnchorRoot will return the value of the starting anchor root when no /// anchor game exists yet. @@ -106,6 +126,29 @@ contract AnchorStateRegistry_GetAnchorRoot_Test is AnchorStateRegistry_Init { assertEq(l2BlockNumber, gameProxy.l2BlockNumber()); } + /// @notice Tests that getAnchorRoot will return the latest anchor root even if the superchain + /// is paused. + function test_getAnchorRoot_superchainPaused_succeeds() public { + // Mock the game to be resolved. + vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.resolvedAt, ()), abi.encode(block.timestamp)); + vm.warp(block.timestamp + optimismPortal2.disputeGameFinalityDelaySeconds() + 1); + + // Mock the game to be the defender wins. + vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.status, ()), abi.encode(GameStatus.DEFENDER_WINS)); + + // Set the anchor game to the game proxy. + anchorStateRegistry.setAnchorState(gameProxy); + + // Pause the superchain. + vm.prank(superchainConfig.guardian()); + superchainConfig.pause("testing"); + + // We should get the anchor root back. + (Hash root, uint256 l2BlockNumber) = anchorStateRegistry.getAnchorRoot(); + assertEq(root.raw(), gameProxy.rootClaim().raw()); + assertEq(l2BlockNumber, gameProxy.l2BlockNumber()); + } + /// @notice Tests that getAnchorRoot returns even if the anchor game is blacklisted. function test_getAnchorRoot_blacklistedGame_succeeds() public { // Mock the game to be resolved. @@ -118,12 +161,9 @@ contract AnchorStateRegistry_GetAnchorRoot_Test is AnchorStateRegistry_Init { // Set the anchor game to the game proxy. anchorStateRegistry.setAnchorState(gameProxy); - // Mock the disputeGameBlacklist call to return true. - vm.mockCall( - address(optimismPortal2), - abi.encodeCall(optimismPortal2.disputeGameBlacklist, (gameProxy)), - abi.encode(true) - ); + // Blacklist the game. + vm.prank(superchainConfig.guardian()); + anchorStateRegistry.blacklistDisputeGame(gameProxy); // Get the anchor root. (Hash root, uint256 l2BlockNumber) = anchorStateRegistry.getAnchorRoot(); @@ -172,12 +212,11 @@ contract AnchorStateRegistry_IsGameRegistered_Test is AnchorStateRegistry_Init { contract AnchorStateRegistry_IsGameBlacklisted_Test is AnchorStateRegistry_Init { /// @notice Tests that isGameBlacklisted will return true if the game is blacklisted. function test_isGameBlacklisted_isActuallyBlacklisted_succeeds() public { - // Mock the disputeGameBlacklist call to return true. - vm.mockCall( - address(optimismPortal2), - abi.encodeCall(optimismPortal2.disputeGameBlacklist, (gameProxy)), - abi.encode(true) - ); + // Blacklist the game. + vm.prank(superchainConfig.guardian()); + anchorStateRegistry.blacklistDisputeGame(gameProxy); + + // Should return true. assertTrue(anchorStateRegistry.isGameBlacklisted(gameProxy)); } @@ -185,8 +224,8 @@ contract AnchorStateRegistry_IsGameBlacklisted_Test is AnchorStateRegistry_Init function test_isGameBlacklisted_isNotBlacklisted_succeeds() public { // Mock the disputeGameBlacklist call to return false. vm.mockCall( - address(optimismPortal2), - abi.encodeCall(optimismPortal2.disputeGameBlacklist, (gameProxy)), + address(anchorStateRegistry), + abi.encodeCall(anchorStateRegistry.disputeGameBlacklist, (gameProxy)), abi.encode(false) ); assertFalse(anchorStateRegistry.isGameBlacklisted(gameProxy)); @@ -214,34 +253,35 @@ contract AnchorStateRegistry_IsGameRespected_Test is AnchorStateRegistry_Init { contract AnchorStateRegistry_IsGameRetired_Test is AnchorStateRegistry_Init { /// @notice Tests that isGameRetired will return true if the game is retired. - /// @param _retirementTimestamp The retirement timestamp to use for the test. - function testFuzz_isGameRetired_isRetired_succeeds(uint64 _retirementTimestamp) public { - // Make sure retirement timestamp is greater than or equal to the game's creation time. - _retirementTimestamp = uint64(bound(_retirementTimestamp, gameProxy.createdAt().raw(), type(uint64).max)); + /// @param _createdAtTimestamp The createdAt timestamp to use for the test. + function testFuzz_isGameRetired_isRetired_succeeds(uint64 _createdAtTimestamp) public { + // Set the retirement timestamp to now. + vm.prank(superchainConfig.guardian()); + anchorStateRegistry.updateRetirementTimestamp(); + + // Make sure createdAt timestamp is less than or equal to the retirementTimestamp. + _createdAtTimestamp = uint64(bound(_createdAtTimestamp, 0, anchorStateRegistry.retirementTimestamp())); // Mock the respectedGameTypeUpdatedAt call. - vm.mockCall( - address(optimismPortal2), - abi.encodeCall(optimismPortal2.respectedGameTypeUpdatedAt, ()), - abi.encode(_retirementTimestamp) - ); + vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.createdAt, ()), abi.encode(_createdAtTimestamp)); // Game should be retired. assertTrue(anchorStateRegistry.isGameRetired(gameProxy)); } /// @notice Tests that isGameRetired will return false if the game is not retired. - /// @param _retirementTimestamp The retirement timestamp to use for the test. - function testFuzz_isGameRetired_isNotRetired_succeeds(uint64 _retirementTimestamp) public { - // Make sure retirement timestamp is earlier than the game's creation time. - _retirementTimestamp = uint64(bound(_retirementTimestamp, 0, gameProxy.createdAt().raw() - 1)); + /// @param _createdAtTimestamp The createdAt timestamp to use for the test. + function testFuzz_isGameRetired_isNotRetired_succeeds(uint64 _createdAtTimestamp) public { + // Set the retirement timestamp to now. + vm.prank(superchainConfig.guardian()); + anchorStateRegistry.updateRetirementTimestamp(); - // Mock the respectedGameTypeUpdatedAt call to be earlier than the game's creation time. - vm.mockCall( - address(optimismPortal2), - abi.encodeCall(optimismPortal2.respectedGameTypeUpdatedAt, ()), - abi.encode(_retirementTimestamp) - ); + // Make sure createdAt timestamp is greater than the retirementTimestamp. + _createdAtTimestamp = + uint64(bound(_createdAtTimestamp, anchorStateRegistry.retirementTimestamp() + 1, type(uint64).max)); + + // Mock the call to createdAt. + vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.createdAt, ()), abi.encode(_createdAtTimestamp)); // Game should not be retired. assertFalse(anchorStateRegistry.isGameRetired(gameProxy)); @@ -287,29 +327,38 @@ contract AnchorStateRegistry_IsGameProper_Test is AnchorStateRegistry_Init { /// @notice Tests that isGameProper will return false if the game is blacklisted. function test_isGameProper_isBlacklisted_succeeds() public { - // Mock the disputeGameBlacklist call to return true. - vm.mockCall( - address(optimismPortal2), - abi.encodeCall(optimismPortal2.disputeGameBlacklist, (gameProxy)), - abi.encode(true) - ); + // Blacklist the game. + vm.prank(superchainConfig.guardian()); + anchorStateRegistry.blacklistDisputeGame(gameProxy); + + // Should return false. + assertFalse(anchorStateRegistry.isGameProper(gameProxy)); + } + + /// @notice Tests that isGameProper will return false if the superchain is paused. + function test_isGameProper_superchainPaused_succeeds() public { + // Pause the superchain. + vm.prank(superchainConfig.guardian()); + superchainConfig.pause("testing"); + // Game should not be proper. assertFalse(anchorStateRegistry.isGameProper(gameProxy)); } /// @notice Tests that isGameProper will return false if the game is retired. - /// @param _retirementTimestamp The retirement timestamp to use for the test. - function testFuzz_isGameProper_isRetired_succeeds(uint64 _retirementTimestamp) public { - // Make sure retirement timestamp is later than the game's creation time. - _retirementTimestamp = uint64(bound(_retirementTimestamp, gameProxy.createdAt().raw() + 1, type(uint64).max)); + /// @param _createdAtTimestamp The createdAt timestamp to use for the test. + function testFuzz_isGameProper_isRetired_succeeds(uint64 _createdAtTimestamp) public { + // Set the retirement timestamp to now. + vm.prank(superchainConfig.guardian()); + anchorStateRegistry.updateRetirementTimestamp(); - // Mock the respectedGameTypeUpdatedAt call to be later than the game's creation time. - vm.mockCall( - address(optimismPortal2), - abi.encodeCall(optimismPortal2.respectedGameTypeUpdatedAt, ()), - abi.encode(_retirementTimestamp) - ); + // Make sure createdAt timestamp is less than or equal to the retirementTimestamp. + _createdAtTimestamp = uint64(bound(_createdAtTimestamp, 0, anchorStateRegistry.retirementTimestamp())); + // Mock the call to createdAt. + vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.createdAt, ()), abi.encode(_createdAtTimestamp)); + + // Game should not be proper. assertFalse(anchorStateRegistry.isGameProper(gameProxy)); } } @@ -476,8 +525,8 @@ contract AnchorStateRegistry_IsGameClaimValid_Test is AnchorStateRegistry_Init { function testFuzz_isGameClaimValid_isBlacklisted_succeeds() public { // Mock the disputeGameBlacklist call to return true. vm.mockCall( - address(optimismPortal2), - abi.encodeCall(optimismPortal2.disputeGameBlacklist, (gameProxy)), + address(anchorStateRegistry), + abi.encodeCall(anchorStateRegistry.disputeGameBlacklist, (gameProxy)), abi.encode(true) ); @@ -486,17 +535,17 @@ contract AnchorStateRegistry_IsGameClaimValid_Test is AnchorStateRegistry_Init { } /// @notice Tests that isGameClaimValid will return false if the game is retired. - /// @param _resolvedAtTimestamp The resolvedAt timestamp to use for the test. - function testFuzz_isGameClaimValid_isRetired_succeeds(uint256 _resolvedAtTimestamp) public { - // Make sure retirement timestamp is later than the game's creation time. - _resolvedAtTimestamp = uint64(bound(_resolvedAtTimestamp, gameProxy.createdAt().raw() + 1, type(uint64).max)); + /// @param _createdAtTimestamp The createdAt timestamp to use for the test. + function testFuzz_isGameClaimValid_isRetired_succeeds(uint256 _createdAtTimestamp) public { + // Set the retirement timestamp to now. + vm.prank(superchainConfig.guardian()); + anchorStateRegistry.updateRetirementTimestamp(); - // Mock the respectedGameTypeUpdatedAt call to be later than the game's creation time. - vm.mockCall( - address(optimismPortal2), - abi.encodeCall(optimismPortal2.respectedGameTypeUpdatedAt, ()), - abi.encode(_resolvedAtTimestamp) - ); + // Make sure createdAt timestamp is less than or equal to the retirementTimestamp. + _createdAtTimestamp = uint64(bound(_createdAtTimestamp, 0, anchorStateRegistry.retirementTimestamp())); + + // Mock the call to createdAt. + vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.createdAt, ()), abi.encode(_createdAtTimestamp)); // Claim should not be valid. assertFalse(anchorStateRegistry.isGameClaimValid(gameProxy)); @@ -528,6 +577,16 @@ contract AnchorStateRegistry_IsGameClaimValid_Test is AnchorStateRegistry_Init { // Claim should not be valid. assertFalse(anchorStateRegistry.isGameClaimValid(gameProxy)); } + + /// @notice Tests that isGameClaimValid will return false if the superchain is paused. + function test_isGameClaimValid_superchainPaused_succeeds() public { + // Pause the superchain. + vm.prank(superchainConfig.guardian()); + superchainConfig.pause("testing"); + + // Game should not be valid. + assertFalse(anchorStateRegistry.isGameClaimValid(gameProxy)); + } } contract AnchorStateRegistry_SetAnchorState_Test is AnchorStateRegistry_Init { @@ -768,12 +827,9 @@ contract AnchorStateRegistry_SetAnchorState_TestFail is AnchorStateRegistry_Init vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.resolvedAt, ()), abi.encode(block.timestamp)); vm.warp(block.timestamp + optimismPortal2.disputeGameFinalityDelaySeconds() + 1); - // Mock the disputeGameBlacklist call to return true. - vm.mockCall( - address(optimismPortal2), - abi.encodeCall(optimismPortal2.disputeGameBlacklist, (gameProxy)), - abi.encode(true) - ); + // Blacklist the game. + vm.prank(superchainConfig.guardian()); + anchorStateRegistry.blacklistDisputeGame(gameProxy); // Update the anchor state. vm.prank(address(gameProxy)); @@ -786,8 +842,7 @@ contract AnchorStateRegistry_SetAnchorState_TestFail is AnchorStateRegistry_Init assertEq(updatedRoot.raw(), root.raw()); } - /// @notice Tests that setAnchorState will revert if the game is valid and the game is - /// retired. + /// @notice Tests that setAnchorState will revert if the game is retired. /// @param _l2BlockNumber The L2 block number to use for the game. function testFuzz_setAnchorState_retiredGame_fails(uint256 _l2BlockNumber) public { // Grab block number of the existing anchor root. @@ -802,11 +857,15 @@ contract AnchorStateRegistry_SetAnchorState_TestFail is AnchorStateRegistry_Init // Mock that the game was respected. vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.wasRespectedGameTypeWhenCreated, ()), abi.encode(true)); - // Mock the respectedGameTypeUpdatedAt call to be later than the game's creation time. + // Set the retirement timestamp. + vm.prank(superchainConfig.guardian()); + anchorStateRegistry.updateRetirementTimestamp(); + + // Mock the call to createdAt. vm.mockCall( - address(optimismPortal2), - abi.encodeCall(optimismPortal2.respectedGameTypeUpdatedAt, ()), - abi.encode(gameProxy.createdAt().raw() + 1) + address(gameProxy), + abi.encodeCall(gameProxy.createdAt, ()), + abi.encode(anchorStateRegistry.retirementTimestamp() - 1) ); // Update the anchor state. @@ -819,4 +878,153 @@ contract AnchorStateRegistry_SetAnchorState_TestFail is AnchorStateRegistry_Init assertEq(updatedL2BlockNumber, l2BlockNumber); assertEq(updatedRoot.raw(), root.raw()); } + + /// @notice Tests that setAnchorState will revert if the superchain is paused. + function test_setAnchorState_superchainPaused_fails() public { + // Pause the superchain. + vm.prank(superchainConfig.guardian()); + superchainConfig.pause("testing"); + + // Update the anchor state. + vm.prank(address(gameProxy)); + vm.expectRevert(IAnchorStateRegistry.AnchorStateRegistry_InvalidAnchorGame.selector); + anchorStateRegistry.setAnchorState(gameProxy); + } +} + +contract AnchorStateRegistry_setRespectedGameType_Test is AnchorStateRegistry_Init { + /// @notice Tests that setRespectedGameType succeeds when called by the guardian + /// @param _gameType The game type to set as respected + function testFuzz_setRespectedGameType_succeeds(GameType _gameType) public { + // Call as guardian + vm.prank(superchainConfig.guardian()); + vm.expectEmit(address(anchorStateRegistry)); + emit RespectedGameTypeSet(_gameType); + anchorStateRegistry.setRespectedGameType(_gameType); + + // Verify the game type was set + assertEq(anchorStateRegistry.respectedGameType().raw(), _gameType.raw()); + } +} + +contract AnchorStateRegistry_setRespectedGameType_TestFail is AnchorStateRegistry_Init { + /// @notice Tests that setRespectedGameType reverts when not called by the guardian + /// @param _gameType The game type to attempt to set + /// @param _caller The address attempting to call the function + function testFuzz_setRespectedGameType_notGuardian_reverts(GameType _gameType, address _caller) public { + // Ensure caller is not the guardian + vm.assume(_caller != superchainConfig.guardian()); + + // Attempt to call as non-guardian + vm.prank(_caller); + vm.expectRevert(IAnchorStateRegistry.AnchorStateRegistry_Unauthorized.selector); + anchorStateRegistry.setRespectedGameType(_gameType); + } +} + +contract AnchorStateRegistry_updateRetirementTimestamp_Test is AnchorStateRegistry_Init { + /// @notice Tests that updateRetirementTimestamp succeeds when called by the guardian + function test_updateRetirementTimestamp_succeeds() public { + // Call as guardian + vm.prank(superchainConfig.guardian()); + vm.expectEmit(address(anchorStateRegistry)); + emit RetirementTimestampSet(block.timestamp); + anchorStateRegistry.updateRetirementTimestamp(); + + // Verify the timestamp was set + assertEq(anchorStateRegistry.retirementTimestamp(), block.timestamp); + } + + /// @notice Tests that updateRetirementTimestamp can be called multiple times by the guardian + function test_updateRetirementTimestamp_multipleUpdates_succeeds() public { + // First update + vm.prank(superchainConfig.guardian()); + anchorStateRegistry.updateRetirementTimestamp(); + uint64 firstTimestamp = anchorStateRegistry.retirementTimestamp(); + + // Warp forward and update again + vm.warp(block.timestamp + 1000); + vm.prank(superchainConfig.guardian()); + vm.expectEmit(address(anchorStateRegistry)); + emit RetirementTimestampSet(block.timestamp); + anchorStateRegistry.updateRetirementTimestamp(); + + // Verify the timestamp was updated + assertEq(anchorStateRegistry.retirementTimestamp(), block.timestamp); + assertGt(anchorStateRegistry.retirementTimestamp(), firstTimestamp); + } +} + +contract AnchorStateRegistry_updateRetirementTimestamp_TestFail is AnchorStateRegistry_Init { + /// @notice Tests that updateRetirementTimestamp reverts when not called by the guardian + /// @param _caller The address attempting to call the function + function testFuzz_updateRetirementTimestamp_notGuardian_reverts(address _caller) public { + // Ensure caller is not the guardian + vm.assume(_caller != superchainConfig.guardian()); + + // Attempt to call as non-guardian + vm.prank(_caller); + vm.expectRevert(IAnchorStateRegistry.AnchorStateRegistry_Unauthorized.selector); + anchorStateRegistry.updateRetirementTimestamp(); + } +} + +contract AnchorStateRegistry_blacklistDisputeGame_Test is AnchorStateRegistry_Init { + /// @notice Tests that blacklistDisputeGame succeeds when called by the guardian + function test_blacklistDisputeGame_succeeds() public { + // Call as guardian + vm.prank(superchainConfig.guardian()); + vm.expectEmit(address(anchorStateRegistry)); + emit DisputeGameBlacklisted(gameProxy); + anchorStateRegistry.blacklistDisputeGame(gameProxy); + + // Verify the game was blacklisted + assertTrue(anchorStateRegistry.disputeGameBlacklist(gameProxy)); + } + + /// @notice Tests that multiple games can be blacklisted + function test_blacklistDisputeGame_multipleGames_succeeds() public { + // Create a second game proxy + IDisputeGame secondGame = IDisputeGame(address(0x123)); + + // Blacklist both games + vm.startPrank(superchainConfig.guardian()); + anchorStateRegistry.blacklistDisputeGame(gameProxy); + anchorStateRegistry.blacklistDisputeGame(secondGame); + vm.stopPrank(); + + // Verify both games are blacklisted + assertTrue(anchorStateRegistry.disputeGameBlacklist(gameProxy)); + assertTrue(anchorStateRegistry.disputeGameBlacklist(secondGame)); + } +} + +contract AnchorStateRegistry_blacklistDisputeGame_TestFail is AnchorStateRegistry_Init { + /// @notice Tests that blacklistDisputeGame reverts when not called by the guardian + /// @param _caller The address attempting to call the function + function testFuzz_blacklistDisputeGame_notGuardian_reverts(address _caller) public { + // Ensure caller is not the guardian + vm.assume(_caller != superchainConfig.guardian()); + + // Attempt to call as non-guardian + vm.prank(_caller); + vm.expectRevert(IAnchorStateRegistry.AnchorStateRegistry_Unauthorized.selector); + anchorStateRegistry.blacklistDisputeGame(gameProxy); + } + + /// @notice Tests that blacklisting a game twice succeeds but doesn't change state + function test_blacklistDisputeGame_twice_succeeds() public { + // Blacklist the game + vm.startPrank(superchainConfig.guardian()); + anchorStateRegistry.blacklistDisputeGame(gameProxy); + + // Blacklist again - should emit event but not change state + vm.expectEmit(address(anchorStateRegistry)); + emit DisputeGameBlacklisted(gameProxy); + anchorStateRegistry.blacklistDisputeGame(gameProxy); + vm.stopPrank(); + + // Verify the game is still blacklisted + assertTrue(anchorStateRegistry.disputeGameBlacklist(gameProxy)); + } } diff --git a/packages/contracts-bedrock/test/dispute/FaultDisputeGame.t.sol b/packages/contracts-bedrock/test/dispute/FaultDisputeGame.t.sol index a90f62ea68f8a..a655fcf12f3b8 100644 --- a/packages/contracts-bedrock/test/dispute/FaultDisputeGame.t.sol +++ b/packages/contracts-bedrock/test/dispute/FaultDisputeGame.t.sol @@ -96,6 +96,12 @@ contract FaultDisputeGame_Init is DisputeGameFactory_Init { // Register the game implementation with the factory. disputeGameFactory.setImplementation(GAME_TYPE, gameImpl); uint256 bondAmount = disputeGameFactory.initBonds(GAME_TYPE); + + // Warp ahead of the game retirement timestamp if needed. + if (block.timestamp <= anchorStateRegistry.retirementTimestamp()) { + vm.warp(anchorStateRegistry.retirementTimestamp() + 1); + } + // Create a new game. gameProxy = IFaultDisputeGame( payable(address(disputeGameFactory.create{ value: bondAmount }(GAME_TYPE, rootClaim, extraData))) diff --git a/packages/contracts-bedrock/test/dispute/SuperFaultDisputeGame.t.sol b/packages/contracts-bedrock/test/dispute/SuperFaultDisputeGame.t.sol index 099a85e56c010..919deaa2c960b 100644 --- a/packages/contracts-bedrock/test/dispute/SuperFaultDisputeGame.t.sol +++ b/packages/contracts-bedrock/test/dispute/SuperFaultDisputeGame.t.sol @@ -98,7 +98,12 @@ contract SuperFaultDisputeGame_Init is DisputeGameFactory_Init { uint256 bondAmount = disputeGameFactory.initBonds(GAME_TYPE); vm.prank(superchainConfig.guardian()); - optimismPortal2.setRespectedGameType(GAME_TYPE); + anchorStateRegistry.setRespectedGameType(GAME_TYPE); + + // Warp ahead of the game retirement timestamp if needed. + if (block.timestamp <= anchorStateRegistry.retirementTimestamp()) { + vm.warp(anchorStateRegistry.retirementTimestamp() + 1); + } // Create a new game. gameProxy = IFaultDisputeGame( diff --git a/packages/contracts-bedrock/test/invariants/OptimismPortal2.t.sol b/packages/contracts-bedrock/test/invariants/OptimismPortal2.t.sol index 15ce33c252b18..dbb8f231b28c7 100644 --- a/packages/contracts-bedrock/test/invariants/OptimismPortal2.t.sol +++ b/packages/contracts-bedrock/test/invariants/OptimismPortal2.t.sol @@ -14,7 +14,6 @@ import { ResourceMetering } from "src/L1/ResourceMetering.sol"; import { Constants } from "src/libraries/Constants.sol"; import { Types } from "src/libraries/Types.sol"; import "src/dispute/lib/Types.sol"; -import "src/libraries/PortalErrors.sol"; // Interfaces import { IOptimismPortal2 } from "interfaces/L1/IOptimismPortal2.sol"; @@ -119,7 +118,7 @@ contract OptimismPortal2_Invariant_Harness is CommonTest { }); // Warp forward in time to ensure that the game is created after the retirement timestamp. - vm.warp(optimismPortal2.respectedGameTypeUpdatedAt() + 1 seconds); + vm.warp(anchorStateRegistry.retirementTimestamp() + 1); // Create a dispute game with the output root we've proposed. _proposedBlockNumber = 0xFF; @@ -188,7 +187,7 @@ contract OptimismPortal2_CannotTimeTravel is OptimismPortal2_Invariant_Harness { /// A withdrawal that has been proven should not be able to be finalized /// until after the proof maturity period has elapsed. function invariant_cannotFinalizeBeforePeriodHasPassed() external { - vm.expectRevert("OptimismPortal: proven withdrawal has not matured yet"); + vm.expectRevert(IOptimismPortal2.OptimismPortal_ProofNotOldEnough.selector); optimismPortal2.finalizeWithdrawalTransaction(_defaultTx); } } @@ -217,7 +216,7 @@ contract OptimismPortal2_CannotFinalizeTwice is OptimismPortal2_Invariant_Harnes /// Ensures that there is no chain of calls that can be made that allows a withdrawal to be /// finalized twice. function invariant_cannotFinalizeTwice() external { - vm.expectRevert(AlreadyFinalized.selector); + vm.expectRevert(IOptimismPortal2.OptimismPortal_AlreadyFinalized.selector); optimismPortal2.finalizeWithdrawalTransaction(_defaultTx); } } diff --git a/packages/contracts-bedrock/test/kontrol/proofs/OptimismPortal2.k.sol b/packages/contracts-bedrock/test/kontrol/proofs/OptimismPortal2.k.sol index 7d7b49e3a0c4a..756b225751e90 100644 --- a/packages/contracts-bedrock/test/kontrol/proofs/OptimismPortal2.k.sol +++ b/packages/contracts-bedrock/test/kontrol/proofs/OptimismPortal2.k.sol @@ -6,7 +6,6 @@ import { KontrolUtils } from "./utils/KontrolUtils.sol"; import { Types } from "src/libraries/Types.sol"; import { IOptimismPortal2 as OptimismPortal } from "interfaces/L1/IOptimismPortal2.sol"; import { ISuperchainConfig as SuperchainConfig } from "interfaces/L1/ISuperchainConfig.sol"; -import "src/libraries/PortalErrors.sol"; contract OptimismPortal2Kontrol is DeploymentSummaryFaultProofs, KontrolUtils { OptimismPortal optimismPortal; @@ -26,7 +25,7 @@ contract OptimismPortal2Kontrol is DeploymentSummaryFaultProofs, KontrolUtils { vm.prank(optimismPortal.guardian()); superchainConfig.pause("identifier"); - vm.expectRevert(CallPaused.selector); + vm.expectRevert(OptimismPortal.OptimismPortal_CallPaused.selector); optimismPortal.finalizeWithdrawalTransaction(_tx); } @@ -47,7 +46,7 @@ contract OptimismPortal2Kontrol is DeploymentSummaryFaultProofs, KontrolUtils { vm.prank(optimismPortal.guardian()); superchainConfig.pause("identifier"); - vm.expectRevert(CallPaused.selector); + vm.expectRevert(OptimismPortal.OptimismPortal_CallPaused.selector); optimismPortal.proveWithdrawalTransaction(_tx, _l2OutputIndex, _outputRootProof, _withdrawalProof); } diff --git a/packages/contracts-bedrock/test/opcm/DeployImplementations.t.sol b/packages/contracts-bedrock/test/opcm/DeployImplementations.t.sol index 85680343ccc95..8e3093e838c15 100644 --- a/packages/contracts-bedrock/test/opcm/DeployImplementations.t.sol +++ b/packages/contracts-bedrock/test/opcm/DeployImplementations.t.sol @@ -288,7 +288,7 @@ contract DeployImplementations_Test is Test { deployImplementations.deployPreimageOracleSingleton(dii, dio); deployImplementations.deployMipsSingleton(dii, dio); deployImplementations.deployDisputeGameFactoryImpl(dio); - deployImplementations.deployAnchorStateRegistryImpl(dio); + deployImplementations.deployAnchorStateRegistryImpl(dii, dio); deployImplementations.deployOPContractsManager(dii, dio); // Store the original addresses. @@ -316,7 +316,7 @@ contract DeployImplementations_Test is Test { deployImplementations.deployPreimageOracleSingleton(dii, dio); deployImplementations.deployMipsSingleton(dii, dio); deployImplementations.deployDisputeGameFactoryImpl(dio); - deployImplementations.deployAnchorStateRegistryImpl(dio); + deployImplementations.deployAnchorStateRegistryImpl(dii, dio); deployImplementations.deployOPContractsManager(dii, dio); // Assert that the addresses did not change. diff --git a/packages/contracts-bedrock/test/opcm/SetDisputeGameImpl.t.sol b/packages/contracts-bedrock/test/opcm/SetDisputeGameImpl.t.sol index 2dca24b486bdc..1ece4e242bb9e 100644 --- a/packages/contracts-bedrock/test/opcm/SetDisputeGameImpl.t.sol +++ b/packages/contracts-bedrock/test/opcm/SetDisputeGameImpl.t.sol @@ -4,15 +4,14 @@ pragma solidity ^0.8.0; import { Test } from "forge-std/Test.sol"; import { IDisputeGame } from "interfaces/dispute/IDisputeGame.sol"; import { IDisputeGameFactory } from "interfaces/dispute/IDisputeGameFactory.sol"; -import { GameType } from "src/dispute/lib/Types.sol"; +import { GameType, OutputRoot, Hash } from "src/dispute/lib/Types.sol"; import { SetDisputeGameImpl, SetDisputeGameImplInput } from "scripts/deploy/SetDisputeGameImpl.s.sol"; import { DisputeGameFactory } from "src/dispute/DisputeGameFactory.sol"; import { Proxy } from "src/universal/Proxy.sol"; -import { OptimismPortal2 } from "src/L1/OptimismPortal2.sol"; -import { IOptimismPortal2 } from "interfaces/L1/IOptimismPortal2.sol"; -import { ISystemConfig } from "interfaces/L1/ISystemConfig.sol"; import { ISuperchainConfig } from "interfaces/L1/ISuperchainConfig.sol"; import { SuperchainConfig } from "src/L1/SuperchainConfig.sol"; +import { AnchorStateRegistry } from "src/dispute/AnchorStateRegistry.sol"; +import { IAnchorStateRegistry } from "interfaces/dispute/IAnchorStateRegistry.sol"; contract SetDisputeGameImplInput_Test is Test { SetDisputeGameImplInput input; @@ -70,7 +69,7 @@ contract SetDisputeGameImpl_Test is Test { SetDisputeGameImpl script; SetDisputeGameImplInput input; IDisputeGameFactory factory; - IOptimismPortal2 portal; + IAnchorStateRegistry anchorStateRegistry; address mockImpl; uint32 gameType; @@ -78,8 +77,8 @@ contract SetDisputeGameImpl_Test is Test { script = new SetDisputeGameImpl(); input = new SetDisputeGameImplInput(); DisputeGameFactory dgfImpl = new DisputeGameFactory(); - OptimismPortal2 portalImpl = new OptimismPortal2(0, 0); SuperchainConfig supConfigImpl = new SuperchainConfig(); + AnchorStateRegistry anchorStateRegistryImpl = new AnchorStateRegistry(0); Proxy supConfigProxy = new Proxy(address(1)); vm.prank(address(1)); @@ -92,21 +91,21 @@ contract SetDisputeGameImpl_Test is Test { factoryProxy.upgradeToAndCall(address(dgfImpl), abi.encodeCall(dgfImpl.initialize, (address(this)))); factory = IDisputeGameFactory(address(factoryProxy)); - Proxy portalProxy = new Proxy(address(1)); + Proxy anchorStateRegistryProxy = new Proxy(address(1)); vm.prank(address(1)); - portalProxy.upgradeToAndCall( - address(portalImpl), + anchorStateRegistryProxy.upgradeToAndCall( + address(anchorStateRegistryImpl), abi.encodeCall( - portalImpl.initialize, + anchorStateRegistryImpl.initialize, ( - factory, - ISystemConfig(makeAddr("sysConfig")), ISuperchainConfig(address(supConfigProxy)), + factory, + OutputRoot({ root: Hash.wrap(0), l2BlockNumber: 0 }), GameType.wrap(100) ) ) ); - portal = IOptimismPortal2(payable(address(portalProxy))); + anchorStateRegistry = IAnchorStateRegistry(address(anchorStateRegistryProxy)); mockImpl = makeAddr("impl"); gameType = 999; @@ -115,7 +114,7 @@ contract SetDisputeGameImpl_Test is Test { function test_run_succeeds() public { input.set(input.factory.selector, address(factory)); input.set(input.impl.selector, mockImpl); - input.set(input.portal.selector, address(portal)); + input.set(input.anchorStateRegistry.selector, address(anchorStateRegistry)); input.set(input.gameType.selector, gameType); script.run(input); @@ -124,7 +123,7 @@ contract SetDisputeGameImpl_Test is Test { function test_run_whenImplAlreadySet_reverts() public { input.set(input.factory.selector, address(factory)); input.set(input.impl.selector, mockImpl); - input.set(input.portal.selector, address(portal)); + input.set(input.anchorStateRegistry.selector, address(anchorStateRegistry)); input.set(input.gameType.selector, gameType); // First run should succeed diff --git a/packages/contracts-bedrock/test/safe/DeputyGuardianModule.t.sol b/packages/contracts-bedrock/test/safe/DeputyGuardianModule.t.sol index 593173acffedb..cdecf1110fa74 100644 --- a/packages/contracts-bedrock/test/safe/DeputyGuardianModule.t.sol +++ b/packages/contracts-bedrock/test/safe/DeputyGuardianModule.t.sol @@ -14,17 +14,14 @@ import "src/dispute/lib/Types.sol"; // Interfaces import { IDisputeGame } from "interfaces/dispute/IDisputeGame.sol"; -import { IFaultDisputeGame } from "interfaces/dispute/IFaultDisputeGame.sol"; import { IAnchorStateRegistry } from "interfaces/dispute/IAnchorStateRegistry.sol"; import { DeployUtils } from "scripts/libraries/DeployUtils.sol"; contract DeputyGuardianModule_TestInit is CommonTest, SafeTestTools { using SafeTestLib for SafeInstance; - error Unauthorized(); - error ExecutionFailed(string); - event ExecutionFromModuleSuccess(address indexed); + event RetirementTimestampUpdated(Timestamp indexed); IDeputyGuardianModule deputyGuardianModule; SafeInstance safeInstance; @@ -91,7 +88,7 @@ contract DeputyGuardianModule_Pause_Test is DeputyGuardianModule_TestInit { contract DeputyGuardianModule_Pause_TestFail is DeputyGuardianModule_TestInit { /// @dev Tests that `pause` reverts when called by a non deputy guardian. function test_pause_notDeputyGuardian_reverts() external { - vm.expectRevert(abi.encodeWithSelector(Unauthorized.selector)); + vm.expectRevert(abi.encodeWithSelector(IDeputyGuardianModule.DeputyGuardianModule_Unauthorized.selector)); deputyGuardianModule.pause(); } @@ -104,7 +101,12 @@ contract DeputyGuardianModule_Pause_TestFail is DeputyGuardianModule_TestInit { ); vm.prank(address(deputyGuardian)); - vm.expectRevert(abi.encodeWithSelector(ExecutionFailed.selector, "SuperchainConfig: pause() reverted")); + vm.expectRevert( + abi.encodeWithSelector( + IDeputyGuardianModule.DeputyGuardianModule_ExecutionFailed.selector, + "SuperchainConfig: pause() reverted" + ) + ); deputyGuardianModule.pause(); } } @@ -140,7 +142,7 @@ contract DeputyGuardianModule_Unpause_Test is DeputyGuardianModule_TestInit { contract DeputyGuardianModule_Unpause_TestFail is DeputyGuardianModule_Unpause_Test { /// @dev Tests that `unpause` reverts when called by a non deputy guardian. function test_unpause_notDeputyGuardian_reverts() external { - vm.expectRevert(abi.encodeWithSelector(Unauthorized.selector)); + vm.expectRevert(abi.encodeWithSelector(IDeputyGuardianModule.DeputyGuardianModule_Unauthorized.selector)); deputyGuardianModule.unpause(); assertTrue(superchainConfig.paused()); } @@ -153,42 +155,14 @@ contract DeputyGuardianModule_Unpause_TestFail is DeputyGuardianModule_Unpause_T "SuperchainConfig: unpause reverted" ); - vm.prank(address(deputyGuardian)); - vm.expectRevert(abi.encodeWithSelector(ExecutionFailed.selector, "SuperchainConfig: unpause reverted")); - deputyGuardianModule.unpause(); - } -} - -contract DeputyGuardianModule_SetAnchorState_TestFail is DeputyGuardianModule_TestInit { - function test_setAnchorState_notDeputyGuardian_reverts() external { - IAnchorStateRegistry asr = IAnchorStateRegistry(makeAddr("asr")); - vm.expectRevert(abi.encodeWithSelector(Unauthorized.selector)); - deputyGuardianModule.setAnchorState(asr, IFaultDisputeGame(address(0))); - } - - function test_setAnchorState_targetReverts_reverts() external { - IAnchorStateRegistry asr = IAnchorStateRegistry(makeAddr("asr")); - vm.mockCallRevert( - address(asr), abi.encodePacked(asr.setAnchorState.selector), "AnchorStateRegistry: setAnchorState reverted" - ); vm.prank(address(deputyGuardian)); vm.expectRevert( - abi.encodeWithSelector(ExecutionFailed.selector, "AnchorStateRegistry: setAnchorState reverted") - ); - deputyGuardianModule.setAnchorState(asr, IFaultDisputeGame(address(0))); - } -} - -contract DeputyGuardianModule_SetAnchorState_Test is DeputyGuardianModule_TestInit { - function test_setAnchorState_succeeds() external { - IAnchorStateRegistry asr = IAnchorStateRegistry(makeAddr("asr")); - vm.mockCall( - address(asr), abi.encodeCall(IAnchorStateRegistry.setAnchorState, (IFaultDisputeGame(address(0)))), "" + abi.encodeWithSelector( + IDeputyGuardianModule.DeputyGuardianModule_ExecutionFailed.selector, + "SuperchainConfig: unpause reverted" + ) ); - vm.expectEmit(address(safeInstance.safe)); - emit ExecutionFromModuleSuccess(address(deputyGuardianModule)); - vm.prank(address(deputyGuardian)); - deputyGuardianModule.setAnchorState(asr, IFaultDisputeGame(address(0))); + deputyGuardianModule.unpause(); } } @@ -205,8 +179,8 @@ contract DeputyGuardianModule_BlacklistDisputeGame_Test is DeputyGuardianModule_ emit DisputeGameBlacklisted(game); vm.prank(address(deputyGuardian)); - deputyGuardianModule.blacklistDisputeGame(optimismPortal2, game); - assertTrue(optimismPortal2.disputeGameBlacklist(game)); + deputyGuardianModule.blacklistDisputeGame(anchorStateRegistry, game); + assertTrue(anchorStateRegistry.disputeGameBlacklist(game)); } } @@ -214,25 +188,28 @@ contract DeputyGuardianModule_BlacklistDisputeGame_TestFail is DeputyGuardianMod /// @dev Tests that `blacklistDisputeGame` reverts when called by a non deputy guardian. function test_blacklistDisputeGame_notDeputyGuardian_reverts() external { IDisputeGame game = IDisputeGame(makeAddr("game")); - vm.expectRevert(abi.encodeWithSelector(Unauthorized.selector)); - deputyGuardianModule.blacklistDisputeGame(optimismPortal2, game); - assertFalse(optimismPortal2.disputeGameBlacklist(game)); + vm.expectRevert(abi.encodeWithSelector(IDeputyGuardianModule.DeputyGuardianModule_Unauthorized.selector)); + deputyGuardianModule.blacklistDisputeGame(anchorStateRegistry, game); + assertFalse(anchorStateRegistry.disputeGameBlacklist(game)); } /// @dev Tests that when the call from the Safe reverts, the error message is returned. function test_blacklistDisputeGame_targetReverts_reverts() external { vm.mockCallRevert( - address(optimismPortal2), - abi.encodePacked(optimismPortal2.blacklistDisputeGame.selector), - "OptimismPortal2: blacklistDisputeGame reverted" + address(anchorStateRegistry), + abi.encodePacked(anchorStateRegistry.blacklistDisputeGame.selector), + "AnchorStateRegistry: blacklistDisputeGame reverted" ); IDisputeGame game = IDisputeGame(makeAddr("game")); vm.prank(address(deputyGuardian)); vm.expectRevert( - abi.encodeWithSelector(ExecutionFailed.selector, "OptimismPortal2: blacklistDisputeGame reverted") + abi.encodeWithSelector( + IDeputyGuardianModule.DeputyGuardianModule_ExecutionFailed.selector, + "AnchorStateRegistry: blacklistDisputeGame reverted" + ) ); - deputyGuardianModule.blacklistDisputeGame(optimismPortal2, game); + deputyGuardianModule.blacklistDisputeGame(anchorStateRegistry, game); } } @@ -240,11 +217,6 @@ contract DeputyGuardianModule_setRespectedGameType_Test is DeputyGuardianModule_ /// @dev Tests that `setRespectedGameType` successfully updates the respected game type when called by the deputy /// guardian. function testFuzz_setRespectedGameType_succeeds(GameType _gameType) external { - // Game type(uint32).max is reserved for setting the respectedGameTypeUpdatedAt timestamp. - // TODO(kelvin): Remove this once we've removed the hack. - uint32 boundedGameType = uint32(bound(_gameType.raw(), 0, type(uint32).max - 1)); - _gameType = GameType.wrap(boundedGameType); - vm.expectEmit(address(safeInstance.safe)); emit ExecutionFromModuleSuccess(address(deputyGuardianModule)); @@ -252,9 +224,8 @@ contract DeputyGuardianModule_setRespectedGameType_Test is DeputyGuardianModule_ emit RespectedGameTypeSet(_gameType, Timestamp.wrap(uint64(block.timestamp))); vm.prank(address(deputyGuardian)); - deputyGuardianModule.setRespectedGameType(optimismPortal2, _gameType); - assertEq(GameType.unwrap(optimismPortal2.respectedGameType()), GameType.unwrap(_gameType)); - assertEq(optimismPortal2.respectedGameTypeUpdatedAt(), uint64(block.timestamp)); + deputyGuardianModule.setRespectedGameType(anchorStateRegistry, _gameType); + assertEq(GameType.unwrap(anchorStateRegistry.respectedGameType()), GameType.unwrap(_gameType)); } } @@ -262,31 +233,81 @@ contract DeputyGuardianModule_setRespectedGameType_TestFail is DeputyGuardianMod /// @dev Tests that `setRespectedGameType` when called by a non deputy guardian. function testFuzz_setRespectedGameType_notDeputyGuardian_reverts(GameType _gameType) external { // Change the game type if it's the same to avoid test rejections. - if (GameType.unwrap(optimismPortal2.respectedGameType()) == GameType.unwrap(_gameType)) { + if (GameType.unwrap(anchorStateRegistry.respectedGameType()) == GameType.unwrap(_gameType)) { unchecked { _gameType = GameType.wrap(GameType.unwrap(_gameType) + 1); } } - vm.expectRevert(abi.encodeWithSelector(Unauthorized.selector)); - deputyGuardianModule.setRespectedGameType(optimismPortal2, _gameType); - assertNotEq(GameType.unwrap(optimismPortal2.respectedGameType()), GameType.unwrap(_gameType)); + vm.expectRevert(abi.encodeWithSelector(IDeputyGuardianModule.DeputyGuardianModule_Unauthorized.selector)); + deputyGuardianModule.setRespectedGameType(anchorStateRegistry, _gameType); + assertNotEq(GameType.unwrap(anchorStateRegistry.respectedGameType()), GameType.unwrap(_gameType)); } /// @dev Tests that when the call from the Safe reverts, the error message is returned. function test_setRespectedGameType_targetReverts_reverts() external { vm.mockCallRevert( - address(optimismPortal2), - abi.encodePacked(optimismPortal2.setRespectedGameType.selector), - "OptimismPortal2: setRespectedGameType reverted" + address(anchorStateRegistry), + abi.encodePacked(anchorStateRegistry.setRespectedGameType.selector), + "AnchorStateRegistry: setRespectedGameType reverted" ); GameType gameType = GameType.wrap(1); vm.prank(address(deputyGuardian)); vm.expectRevert( - abi.encodeWithSelector(ExecutionFailed.selector, "OptimismPortal2: setRespectedGameType reverted") + abi.encodeWithSelector( + IDeputyGuardianModule.DeputyGuardianModule_ExecutionFailed.selector, + "AnchorStateRegistry: setRespectedGameType reverted" + ) + ); + deputyGuardianModule.setRespectedGameType(anchorStateRegistry, gameType); + } +} + +contract DeputyGuardianModule_updateRetirementTimestamp_Test is DeputyGuardianModule_TestInit { + /// @notice Tests that updateRetirementTimestamp() successfully updates the retirement timestamp + /// when called by the deputy guardian. + function test_updateRetirementTimestamp_succeeds() external { + vm.expectEmit(address(safeInstance.safe)); + emit ExecutionFromModuleSuccess(address(deputyGuardianModule)); + + vm.expectEmit(address(deputyGuardianModule)); + emit RetirementTimestampUpdated(Timestamp.wrap(uint64(block.timestamp))); + + vm.prank(address(deputyGuardian)); + deputyGuardianModule.updateRetirementTimestamp(anchorStateRegistry); + assertEq(anchorStateRegistry.retirementTimestamp(), block.timestamp); + } +} + +contract DeputyGuardianModule_updateRetirementTimestamp_TestFail is DeputyGuardianModule_TestInit { + /// @notice Tests that updateRetirementTimestamp() reverts when called by an address other than + /// the deputy guardian. + function testFuzz_updateRetirementTimestamp_notDeputyGuardian_reverts(address _caller) external { + vm.assume(_caller != address(deputyGuardian)); + vm.prank(_caller); + vm.expectRevert(abi.encodeWithSelector(IDeputyGuardianModule.DeputyGuardianModule_Unauthorized.selector)); + deputyGuardianModule.updateRetirementTimestamp(anchorStateRegistry); + } + + /// @notice Tests that when the call from the Safe reverts, the error message is returned. + function test_updateRetirementTimestamp_targetReverts_reverts() external { + // Mock a revert from the ASR. + vm.mockCallRevert( + address(anchorStateRegistry), + abi.encodePacked(anchorStateRegistry.updateRetirementTimestamp.selector), + "AnchorStateRegistry: updateRetirementTimestamp reverted" + ); + + // Call the function and expect a revert. + vm.prank(address(deputyGuardian)); + vm.expectRevert( + abi.encodeWithSelector( + IDeputyGuardianModule.DeputyGuardianModule_ExecutionFailed.selector, + "AnchorStateRegistry: updateRetirementTimestamp reverted" + ) ); - deputyGuardianModule.setRespectedGameType(optimismPortal2, gameType); + deputyGuardianModule.updateRetirementTimestamp(anchorStateRegistry); } } @@ -296,16 +317,14 @@ contract DeputyGuardianModule_NoPortalCollisions_Test is DeputyGuardianModule_Te function test_noPortalCollisions_succeeds() external { string[] memory excludes = new string[](5); excludes[0] = "src/dispute/lib/*"; - excludes[1] = "src/L1/OptimismPortal2.sol"; - excludes[2] = "src/L1/OptimismPortalInterop.sol"; - excludes[3] = "interfaces/L1/IOptimismPortal2.sol"; - excludes[4] = "interfaces/L1/IOptimismPortalInterop.sol"; + excludes[1] = "src/dispute/AnchorStateRegistry.sol"; + excludes[2] = "interfaces/dispute/IAnchorStateRegistry.sol"; Abi[] memory abis = ForgeArtifacts.getContractFunctionAbis("src/{L1,dispute,universal}", excludes); for (uint256 i; i < abis.length; i++) { for (uint256 j; j < abis[i].entries.length; j++) { bytes4 sel = abis[i].entries[j].sel; - assertNotEq(sel, optimismPortal2.blacklistDisputeGame.selector); - assertNotEq(sel, optimismPortal2.setRespectedGameType.selector); + assertNotEq(sel, anchorStateRegistry.blacklistDisputeGame.selector); + assertNotEq(sel, anchorStateRegistry.setRespectedGameType.selector); } } } diff --git a/packages/contracts-bedrock/test/safe/DeputyPauseModule.t.sol b/packages/contracts-bedrock/test/safe/DeputyPauseModule.t.sol index 25c54a7d3d3a5..ca87a54662a53 100644 --- a/packages/contracts-bedrock/test/safe/DeputyPauseModule.t.sol +++ b/packages/contracts-bedrock/test/safe/DeputyPauseModule.t.sol @@ -568,7 +568,8 @@ contract DeputyPauseModule_Pause_TestFail is DeputyPauseModule_TestInit { IDeputyPauseModule.DeputyPauseModule_ExecutionFailed.selector, string( abi.encodeWithSelector( - IDeputyGuardianModule.ExecutionFailed.selector, "SuperchainConfig: pause() reverted" + IDeputyGuardianModule.DeputyGuardianModule_ExecutionFailed.selector, + "SuperchainConfig: pause() reverted" ) ) ) diff --git a/packages/contracts-bedrock/test/universal/Specs.t.sol b/packages/contracts-bedrock/test/universal/Specs.t.sol index abd2ebfc8d203..0d45a66402bf3 100644 --- a/packages/contracts-bedrock/test/universal/Specs.t.sol +++ b/packages/contracts-bedrock/test/universal/Specs.t.sol @@ -209,6 +209,8 @@ contract Specification_Test is CommonTest { _addSpec({ _name: "L1StandardBridge", _sel: _getSel("version()") }); // OptimismPortalInterop + _addSpec({ _name: "OptimismPortalInterop", _sel: _getSel("anchorStateRegistry()") }); + _addSpec({ _name: "OptimismPortalInterop", _sel: _getSel("checkWithdrawal(bytes32,address)") }); _addSpec({ _name: "OptimismPortalInterop", _sel: _getSel("depositTransaction(address,uint256,uint64,bool,bytes)") @@ -226,7 +228,7 @@ contract Specification_Test is CommonTest { }); _addSpec({ _name: "OptimismPortalInterop", _sel: _getSel("finalizedWithdrawals(bytes32)") }); _addSpec({ _name: "OptimismPortalInterop", _sel: _getSel("guardian()") }); - _addSpec({ _name: "OptimismPortalInterop", _sel: _getSel("initialize(address,address,address,uint32)") }); + _addSpec({ _name: "OptimismPortalInterop", _sel: _getSel("initialize(address,address,address)") }); _addSpec({ _name: "OptimismPortalInterop", _sel: _getSel("l2Sender()") }); _addSpec({ _name: "OptimismPortalInterop", _sel: _getSel("minimumGasLimit(uint64)") }); _addSpec({ _name: "OptimismPortalInterop", _sel: _getSel("params()") }); @@ -241,25 +243,13 @@ contract Specification_Test is CommonTest { _addSpec({ _name: "OptimismPortalInterop", _sel: _getSel("systemConfig()") }); _addSpec({ _name: "OptimismPortalInterop", _sel: _getSel("version()") }); _addSpec({ _name: "OptimismPortalInterop", _sel: _getSel("disputeGameFactory()") }); - _addSpec({ _name: "OptimismPortalInterop", _sel: _getSel("disputeGameBlacklist(address)") }); _addSpec({ _name: "OptimismPortalInterop", _sel: _getSel("respectedGameType()") }); - // Comment out the auth to not disturb the testDeputyGuardianAuth test. This code is not meant to run in - // production, - // and will be merged into the OptimismPortal2 contract itself in the future. - _addSpec({ - _name: "OptimismPortalInterop", - _sel: _getSel("blacklistDisputeGame(address)") /*, _auth: Role.GUARDIAN*/ - }); - _addSpec({ - _name: "OptimismPortalInterop", - _sel: _getSel("setRespectedGameType(uint32)") /*, _auth: Role.GUARDIAN*/ - }); - _addSpec({ _name: "OptimismPortalInterop", _sel: _getSel("checkWithdrawal(bytes32,address)") }); _addSpec({ _name: "OptimismPortalInterop", _sel: _getSel("proofMaturityDelaySeconds()") }); _addSpec({ _name: "OptimismPortalInterop", _sel: _getSel("disputeGameFinalityDelaySeconds()") }); _addSpec({ _name: "OptimismPortalInterop", _sel: _getSel("respectedGameTypeUpdatedAt()") }); _addSpec({ _name: "OptimismPortalInterop", _sel: _getSel("proofSubmitters(bytes32,uint256)") }); _addSpec({ _name: "OptimismPortalInterop", _sel: _getSel("numProofSubmitters(bytes32)") }); + _addSpec({ _name: "OptimismPortalInterop", _sel: _getSel("upgrade(address)") }); _addSpec({ _name: "OptimismPortalInterop", _sel: IOptimismPortalInterop.setConfig.selector, @@ -267,6 +257,8 @@ contract Specification_Test is CommonTest { }); // OptimismPortal2 + _addSpec({ _name: "OptimismPortal2", _sel: _getSel("anchorStateRegistry()") }); + _addSpec({ _name: "OptimismPortal2", _sel: _getSel("checkWithdrawal(bytes32,address)") }); _addSpec({ _name: "OptimismPortal2", _sel: _getSel("depositTransaction(address,uint256,uint64,bool,bytes)") }); _addSpec({ _name: "OptimismPortal2", _sel: _getSel("donateETH()") }); _addSpec({ @@ -281,7 +273,7 @@ contract Specification_Test is CommonTest { }); _addSpec({ _name: "OptimismPortal2", _sel: _getSel("finalizedWithdrawals(bytes32)") }); _addSpec({ _name: "OptimismPortal2", _sel: _getSel("guardian()") }); - _addSpec({ _name: "OptimismPortal2", _sel: _getSel("initialize(address,address,address,uint32)") }); + _addSpec({ _name: "OptimismPortal2", _sel: _getSel("initialize(address,address,address)") }); _addSpec({ _name: "OptimismPortal2", _sel: _getSel("l2Sender()") }); _addSpec({ _name: "OptimismPortal2", _sel: _getSel("minimumGasLimit(uint64)") }); _addSpec({ _name: "OptimismPortal2", _sel: _getSel("params()") }); @@ -296,16 +288,13 @@ contract Specification_Test is CommonTest { _addSpec({ _name: "OptimismPortal2", _sel: _getSel("systemConfig()") }); _addSpec({ _name: "OptimismPortal2", _sel: _getSel("version()") }); _addSpec({ _name: "OptimismPortal2", _sel: _getSel("disputeGameFactory()") }); - _addSpec({ _name: "OptimismPortal2", _sel: _getSel("disputeGameBlacklist(address)") }); _addSpec({ _name: "OptimismPortal2", _sel: _getSel("respectedGameType()") }); - _addSpec({ _name: "OptimismPortal2", _sel: _getSel("blacklistDisputeGame(address)"), _auth: Role.GUARDIAN }); - _addSpec({ _name: "OptimismPortal2", _sel: _getSel("setRespectedGameType(uint32)"), _auth: Role.GUARDIAN }); - _addSpec({ _name: "OptimismPortal2", _sel: _getSel("checkWithdrawal(bytes32,address)") }); _addSpec({ _name: "OptimismPortal2", _sel: _getSel("proofMaturityDelaySeconds()") }); _addSpec({ _name: "OptimismPortal2", _sel: _getSel("disputeGameFinalityDelaySeconds()") }); _addSpec({ _name: "OptimismPortal2", _sel: _getSel("respectedGameTypeUpdatedAt()") }); _addSpec({ _name: "OptimismPortal2", _sel: _getSel("proofSubmitters(bytes32,uint256)") }); _addSpec({ _name: "OptimismPortal2", _sel: _getSel("numProofSubmitters(bytes32)") }); + _addSpec({ _name: "OptimismPortal2", _sel: _getSel("upgrade(address)") }); // ProtocolVersions _addSpec({ _name: "ProtocolVersions", _sel: _getSel("RECOMMENDED_SLOT()") }); @@ -572,7 +561,7 @@ contract Specification_Test is CommonTest { _addSpec({ _name: "AnchorStateRegistry", _sel: _getSel("anchors(uint32)") }); _addSpec({ _name: "AnchorStateRegistry", _sel: _getSel("getAnchorRoot()") }); _addSpec({ _name: "AnchorStateRegistry", _sel: _getSel("disputeGameFactory()") }); - _addSpec({ _name: "AnchorStateRegistry", _sel: _getSel("initialize(address,address,address,(bytes32,uint256))") }); + _addSpec({ _name: "AnchorStateRegistry", _sel: _getSel("initialize(address,address,(bytes32,uint256),uint32)") }); _addSpec({ _name: "AnchorStateRegistry", _sel: _getSel("isGameBlacklisted(address)") }); _addSpec({ _name: "AnchorStateRegistry", _sel: _getSel("isGameClaimValid(address)") }); _addSpec({ _name: "AnchorStateRegistry", _sel: _getSel("isGameFinalized(address)") }); @@ -581,11 +570,17 @@ contract Specification_Test is CommonTest { _addSpec({ _name: "AnchorStateRegistry", _sel: _getSel("isGameResolved(address)") }); _addSpec({ _name: "AnchorStateRegistry", _sel: _getSel("isGameRespected(address)") }); _addSpec({ _name: "AnchorStateRegistry", _sel: _getSel("isGameRetired(address)") }); - _addSpec({ _name: "AnchorStateRegistry", _sel: _getSel("portal()") }); + _addSpec({ _name: "AnchorStateRegistry", _sel: _getSel("paused()") }); _addSpec({ _name: "AnchorStateRegistry", _sel: _getSel("respectedGameType()") }); _addSpec({ _name: "AnchorStateRegistry", _sel: _getSel("setAnchorState(address)") }); _addSpec({ _name: "AnchorStateRegistry", _sel: _getSel("superchainConfig()") }); _addSpec({ _name: "AnchorStateRegistry", _sel: _getSel("version()") }); + _addSpec({ _name: "AnchorStateRegistry", _sel: _getSel("disputeGameFinalityDelaySeconds()") }); + _addSpec({ _name: "AnchorStateRegistry", _sel: _getSel("disputeGameBlacklist(address)") }); + _addSpec({ _name: "AnchorStateRegistry", _sel: _getSel("retirementTimestamp()") }); + _addSpec({ _name: "AnchorStateRegistry", _sel: _getSel("blacklistDisputeGame(address)"), _auth: Role.GUARDIAN }); + _addSpec({ _name: "AnchorStateRegistry", _sel: _getSel("setRespectedGameType(uint32)"), _auth: Role.GUARDIAN }); + _addSpec({ _name: "AnchorStateRegistry", _sel: _getSel("updateRetirementTimestamp()"), _auth: Role.GUARDIAN }); // PermissionedDisputeGame _addSpec({ _name: "PermissionedDisputeGame", _sel: _getSel("absolutePrestate()") }); @@ -939,7 +934,7 @@ contract Specification_Test is CommonTest { }); _addSpec({ _name: "DeputyGuardianModule", - _sel: _getSel("setAnchorState(address,address)"), + _sel: _getSel("updateRetirementTimestamp(address)"), _auth: Role.DEPUTYGUARDIAN }); _addSpec({ _name: "DeputyGuardianModule", _sel: _getSel("pause()"), _auth: Role.DEPUTYGUARDIAN }); @@ -1104,13 +1099,12 @@ 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 - // Additional role for `setAnchorState` which is in DGM but no longer role-restricted. - assertEq(specsByRole[Role.GUARDIAN].length, 4); - assertEq(specsByRole[Role.DEPUTYGUARDIAN].length, specsByRole[Role.GUARDIAN].length + 3); + assertEq(specsByRole[Role.GUARDIAN].length, 5); + assertEq(specsByRole[Role.DEPUTYGUARDIAN].length, specsByRole[Role.GUARDIAN].length + 2); mapping(bytes4 => Spec) storage dgmFuncSpecs = specs["DeputyGuardianModule"]; mapping(bytes4 => Spec) storage superchainConfigFuncSpecs = specs["SuperchainConfig"]; - mapping(bytes4 => Spec) storage portal2FuncSpecs = specs["OptimismPortal2"]; + mapping(bytes4 => Spec) storage asrSpecs = specs["AnchorStateRegistry"]; // Ensure that for each of the DeputyGuardianModule's methods there is a corresponding method on another // system contract authed to the Guardian role. @@ -1121,11 +1115,12 @@ contract Specification_Test is CommonTest { _assertRolesEq(superchainConfigFuncSpecs[_getSel("unpause()")].auth, Role.GUARDIAN); _assertRolesEq(dgmFuncSpecs[_getSel("blacklistDisputeGame(address,address)")].auth, Role.DEPUTYGUARDIAN); - _assertRolesEq(portal2FuncSpecs[_getSel("blacklistDisputeGame(address)")].auth, Role.GUARDIAN); + _assertRolesEq(asrSpecs[_getSel("blacklistDisputeGame(address)")].auth, Role.GUARDIAN); _assertRolesEq(dgmFuncSpecs[_getSel("setRespectedGameType(address,uint32)")].auth, Role.DEPUTYGUARDIAN); - _assertRolesEq(portal2FuncSpecs[_getSel("setRespectedGameType(uint32)")].auth, Role.GUARDIAN); + _assertRolesEq(asrSpecs[_getSel("setRespectedGameType(uint32)")].auth, Role.GUARDIAN); - _assertRolesEq(dgmFuncSpecs[_getSel("setAnchorState(address,address)")].auth, Role.DEPUTYGUARDIAN); + _assertRolesEq(dgmFuncSpecs[_getSel("updateRetirementTimestamp(address)")].auth, Role.DEPUTYGUARDIAN); + _assertRolesEq(asrSpecs[_getSel("updateRetirementTimestamp()")].auth, Role.GUARDIAN); } } diff --git a/packages/contracts-bedrock/test/vendor/Initializable.t.sol b/packages/contracts-bedrock/test/vendor/Initializable.t.sol index 9835e4e73a0af..8464682c88c45 100644 --- a/packages/contracts-bedrock/test/vendor/Initializable.t.sol +++ b/packages/contracts-bedrock/test/vendor/Initializable.t.sol @@ -18,7 +18,6 @@ import { ISystemConfig } from "interfaces/L1/ISystemConfig.sol"; import { IResourceMetering } from "interfaces/L1/IResourceMetering.sol"; import { ISuperchainConfig } from "interfaces/L1/ISuperchainConfig.sol"; import { IDisputeGameFactory } from "interfaces/dispute/IDisputeGameFactory.sol"; -import { IOptimismPortal2 } from "interfaces/L1/IOptimismPortal2.sol"; import { ProtocolVersion } from "interfaces/L1/IProtocolVersions.sol"; /// @title Initializer_Test @@ -124,13 +123,7 @@ contract Initializer_Test is CommonTest { name: "OptimismPortal2Impl", target: EIP1967Helper.getImplementation(address(optimismPortal2)), initCalldata: abi.encodeCall( - optimismPortal2.initialize, - ( - disputeGameFactory, - systemConfig, - superchainConfig, - GameType.wrap(uint32(deploy.cfg().respectedGameType())) - ) + optimismPortal2.initialize, (systemConfig, superchainConfig, anchorStateRegistry) ) }) ); @@ -140,13 +133,7 @@ contract Initializer_Test is CommonTest { name: "OptimismPortal2Proxy", target: address(optimismPortal2), initCalldata: abi.encodeCall( - optimismPortal2.initialize, - ( - disputeGameFactory, - systemConfig, - superchainConfig, - GameType.wrap(uint32(deploy.cfg().respectedGameType())) - ) + optimismPortal2.initialize, (systemConfig, superchainConfig, anchorStateRegistry) ) }) ); @@ -314,8 +301,8 @@ contract Initializer_Test is CommonTest { ( ISuperchainConfig(address(0)), IDisputeGameFactory(address(0)), - IOptimismPortal2(payable(0)), - OutputRoot({ root: Hash.wrap(bytes32(0)), l2BlockNumber: 0 }) + OutputRoot({ root: Hash.wrap(bytes32(0)), l2BlockNumber: 0 }), + GameType.wrap(uint32(deploy.cfg().respectedGameType())) ) ) }) @@ -330,8 +317,8 @@ contract Initializer_Test is CommonTest { ( ISuperchainConfig(address(0)), IDisputeGameFactory(address(0)), - IOptimismPortal2(payable(0)), - OutputRoot({ root: Hash.wrap(bytes32(0)), l2BlockNumber: 0 }) + OutputRoot({ root: Hash.wrap(bytes32(0)), l2BlockNumber: 0 }), + GameType.wrap(uint32(deploy.cfg().respectedGameType())) ) ) }) From 21ce88534770fa124caf6951c8cc072be11d8437 Mon Sep 17 00:00:00 2001 From: Kelvin Fichter Date: Tue, 25 Feb 2025 15:52:29 -0600 Subject: [PATCH 16/50] feat: add Super Root specific method to OptimismPortal Updates the OptimismPortal to include a method that allows users to prove against Super Roots for interop. --- op-chain-ops/interopgen/configs.go | 19 +- op-chain-ops/interopgen/deploy.go | 37 +-- op-chain-ops/interopgen/recipe.go | 19 +- op-chain-ops/solc/types.go | 2 + .../deployer/integration_test/apply_test.go | 1 + op-deployer/pkg/deployer/opcm/opchain.go | 1 + op-deployer/pkg/deployer/pipeline/opchain.go | 14 +- op-deployer/pkg/deployer/standard/standard.go | 1 + .../pkg/deployer/state/chain_intent.go | 1 + .../deployer/upgrade/v2_0_0/upgrade_test.go | 2 + op-e2e/config/init.go | 39 +-- .../interfaces/L1/IOPContractsManager.sol | 21 +- .../interfaces/L1/IOPPrestateUpdater.sol | 19 +- .../interfaces/L1/IOptimismPortal2.sol | 23 +- .../interfaces/L1/IOptimismPortalInterop.sol | 23 +- .../interfaces/L1/ISystemConfig.sol | 8 +- .../interfaces/L1/ISystemConfigInterop.sol | 7 +- .../universal/IReinitializableBase.sol | 11 + packages/contracts-bedrock/justfile | 1 + .../scripts/checks/reinitializer/main.go | 25 +- .../scripts/checks/reinitializer/main_test.go | 112 +++++++ .../scripts/deploy/Deploy.s.sol | 4 +- .../scripts/deploy/DeployOPChain.s.sol | 9 + .../scripts/go-ffi/differential-testing.go | 61 +++- .../contracts-bedrock/scripts/go-ffi/utils.go | 85 +++++ .../scripts/libraries/DeployUtils.sol | 15 +- .../snapshots/abi/OPContractsManager.json | 45 ++- .../abi/OPContractsManagerDeployer.json | 7 +- .../OPContractsManagerDeployerInterop.json | 7 +- .../snapshots/abi/OptimismPortal2.json | 192 +++++++++++ .../snapshots/abi/OptimismPortalInterop.json | 192 +++++++++++ .../snapshots/abi/SystemConfig.json | 49 +++ .../snapshots/abi/SystemConfigInterop.json | 54 ++++ .../snapshots/semver-lock.json | 28 +- .../storageLayout/OptimismPortal2.json | 7 + .../storageLayout/OptimismPortalInterop.json | 7 + .../snapshots/storageLayout/SystemConfig.json | 2 +- .../storageLayout/SystemConfigInterop.json | 2 +- .../src/L1/OPContractsManager.sol | 22 +- .../src/L1/OPContractsManagerInterop.sol | 3 +- .../src/L1/OptimismPortal2.sol | 153 +++++++-- .../contracts-bedrock/src/L1/SystemConfig.sol | 22 +- .../src/L1/SystemConfigInterop.sol | 6 +- .../src/libraries/Encoding.sol | 32 ++ .../src/libraries/Hashing.sol | 7 + .../contracts-bedrock/src/libraries/Types.sol | 18 ++ .../src/universal/ReinitializableBase.sol | 25 ++ .../test/L1/OPContractsManager.t.sol | 5 +- .../test/L1/OPPrestateUpdater.t.sol | 18 +- .../test/L1/OptimismPortal2.t.sol | 304 +++++++++++++++++- .../test/L1/SystemConfig.t.sol | 76 ++++- .../test/invariants/SystemConfig.t.sol | 3 +- .../test/libraries/Encoding.t.sol | 198 +++++++++++- .../test/libraries/Hashing.t.sol | 54 ++++ .../test/opcm/UpgradeOPChain.t.sol | 12 +- .../test/setup/FFIInterface.sol | 22 ++ .../test/setup/ForkLive.s.sol | 3 +- .../test/universal/ReinitializableBase.t.sol | 36 +++ .../test/universal/Specs.t.sol | 41 ++- .../test/vendor/Initializable.t.sol | 10 +- 60 files changed, 2055 insertions(+), 167 deletions(-) create mode 100644 packages/contracts-bedrock/interfaces/universal/IReinitializableBase.sol create mode 100644 packages/contracts-bedrock/src/universal/ReinitializableBase.sol create mode 100644 packages/contracts-bedrock/test/universal/ReinitializableBase.t.sol diff --git a/op-chain-ops/interopgen/configs.go b/op-chain-ops/interopgen/configs.go index c3113d240e10c..a365fd6cb78ee 100644 --- a/op-chain-ops/interopgen/configs.go +++ b/op-chain-ops/interopgen/configs.go @@ -74,15 +74,16 @@ type L2Config struct { Challenger common.Address SystemConfigOwner common.Address genesis.L2InitializationConfig - Prefund map[common.Address]*big.Int - SaltMixer string - GasLimit uint64 - DisputeGameType uint32 - DisputeAbsolutePrestate common.Hash - DisputeMaxGameDepth uint64 - DisputeSplitDepth uint64 - DisputeClockExtension uint64 - DisputeMaxClockDuration uint64 + Prefund map[common.Address]*big.Int + SaltMixer string + GasLimit uint64 + DisputeGameUsesSuperRoots bool + DisputeGameType uint32 + DisputeAbsolutePrestate common.Hash + DisputeMaxGameDepth uint64 + DisputeSplitDepth uint64 + DisputeClockExtension uint64 + DisputeMaxClockDuration uint64 } func (c *L2Config) Check(log log.Logger) error { diff --git a/op-chain-ops/interopgen/deploy.go b/op-chain-ops/interopgen/deploy.go index 4ef1d32b658df..84a06245132ae 100644 --- a/op-chain-ops/interopgen/deploy.go +++ b/op-chain-ops/interopgen/deploy.go @@ -198,24 +198,25 @@ func DeployL2ToL1(l1Host *script.Host, superCfg *SuperchainConfig, superDeployme l1Host.SetTxOrigin(cfg.Deployer) output, err := opcm.DeployOPChain(l1Host, opcm.DeployOPChainInput{ - OpChainProxyAdminOwner: cfg.ProxyAdminOwner, - SystemConfigOwner: cfg.SystemConfigOwner, - Batcher: cfg.BatchSenderAddress, - UnsafeBlockSigner: cfg.P2PSequencerAddress, - Proposer: cfg.Proposer, - Challenger: cfg.Challenger, - BasefeeScalar: cfg.GasPriceOracleBaseFeeScalar, - BlobBaseFeeScalar: cfg.GasPriceOracleBlobBaseFeeScalar, - L2ChainId: new(big.Int).SetUint64(cfg.L2ChainID), - Opcm: superDeployment.Opcm, - SaltMixer: cfg.SaltMixer, - GasLimit: cfg.GasLimit, - DisputeGameType: cfg.DisputeGameType, - DisputeAbsolutePrestate: cfg.DisputeAbsolutePrestate, - DisputeMaxGameDepth: cfg.DisputeMaxGameDepth, - DisputeSplitDepth: cfg.DisputeSplitDepth, - DisputeClockExtension: cfg.DisputeClockExtension, - DisputeMaxClockDuration: cfg.DisputeMaxClockDuration, + OpChainProxyAdminOwner: cfg.ProxyAdminOwner, + SystemConfigOwner: cfg.SystemConfigOwner, + Batcher: cfg.BatchSenderAddress, + UnsafeBlockSigner: cfg.P2PSequencerAddress, + Proposer: cfg.Proposer, + Challenger: cfg.Challenger, + BasefeeScalar: cfg.GasPriceOracleBaseFeeScalar, + BlobBaseFeeScalar: cfg.GasPriceOracleBlobBaseFeeScalar, + L2ChainId: new(big.Int).SetUint64(cfg.L2ChainID), + Opcm: superDeployment.Opcm, + SaltMixer: cfg.SaltMixer, + GasLimit: cfg.GasLimit, + DisputeGameUsesSuperRoots: cfg.DisputeGameUsesSuperRoots, + DisputeGameType: cfg.DisputeGameType, + DisputeAbsolutePrestate: cfg.DisputeAbsolutePrestate, + DisputeMaxGameDepth: cfg.DisputeMaxGameDepth, + DisputeSplitDepth: cfg.DisputeSplitDepth, + DisputeClockExtension: cfg.DisputeClockExtension, + DisputeMaxClockDuration: cfg.DisputeMaxClockDuration, }) if err != nil { return nil, fmt.Errorf("failed to deploy L2 OP chain: %w", err) diff --git a/op-chain-ops/interopgen/recipe.go b/op-chain-ops/interopgen/recipe.go index 5c1d4ffac940a..75d64e002811b 100644 --- a/op-chain-ops/interopgen/recipe.go +++ b/op-chain-ops/interopgen/recipe.go @@ -251,15 +251,16 @@ func InteropL2DevConfig(l1ChainID, l2ChainID uint64, addrs devkeys.Addresses) (* UseAltDA: false, }, }, - Prefund: make(map[common.Address]*big.Int), - SaltMixer: "", - GasLimit: 60_000_000, - DisputeGameType: 1, // PERMISSIONED_CANNON Game Type - DisputeAbsolutePrestate: common.HexToHash("0x038512e02c4c3f7bdaec27d00edf55b7155e0905301e1a88083e4e0a6764d54c"), - DisputeMaxGameDepth: 73, - DisputeSplitDepth: 30, - DisputeClockExtension: 10800, // 3 hours (input in seconds) - DisputeMaxClockDuration: 302400, // 3.5 days (input in seconds) + Prefund: make(map[common.Address]*big.Int), + SaltMixer: "", + GasLimit: 60_000_000, + DisputeGameUsesSuperRoots: true, + DisputeGameType: 1, // PERMISSIONED_CANNON Game Type + DisputeAbsolutePrestate: common.HexToHash("0x038512e02c4c3f7bdaec27d00edf55b7155e0905301e1a88083e4e0a6764d54c"), + DisputeMaxGameDepth: 73, + DisputeSplitDepth: 30, + DisputeClockExtension: 10800, // 3 hours (input in seconds) + DisputeMaxClockDuration: 302400, // 3.5 days (input in seconds) } l2Users := devkeys.ChainUserKeys(new(big.Int).SetUint64(l2ChainID)) diff --git a/op-chain-ops/solc/types.go b/op-chain-ops/solc/types.go index d109231efff72..24f6a43a89f8e 100644 --- a/op-chain-ops/solc/types.go +++ b/op-chain-ops/solc/types.go @@ -262,6 +262,8 @@ type Expression struct { ReferencedDeclaration int `json:"referencedDeclaration,omitempty"` ArgumentTypes []AstTypeDescriptions `json:"argumentTypes,omitempty"` Value interface{} `json:"value,omitempty"` + Kind string `json:"kind,omitempty"` + Expression *Expression `json:"expression,omitempty"` } type ForgeArtifact struct { diff --git a/op-deployer/pkg/deployer/integration_test/apply_test.go b/op-deployer/pkg/deployer/integration_test/apply_test.go index c8a7d624f87c3..4b7494855b49a 100644 --- a/op-deployer/pkg/deployer/integration_test/apply_test.go +++ b/op-deployer/pkg/deployer/integration_test/apply_test.go @@ -462,6 +462,7 @@ func TestAdditionalDisputeGames(t *testing.T) { intent.Chains[0].AdditionalDisputeGames = []state.AdditionalDisputeGame{ { ChainProofParams: state.ChainProofParams{ + DisputeGameUsesSuperRoots: false, DisputeGameType: 255, DisputeAbsolutePrestate: standard.DisputeAbsolutePrestate, DisputeMaxGameDepth: 50, diff --git a/op-deployer/pkg/deployer/opcm/opchain.go b/op-deployer/pkg/deployer/opcm/opchain.go index a9015044b9e7b..e6cce9203433c 100644 --- a/op-deployer/pkg/deployer/opcm/opchain.go +++ b/op-deployer/pkg/deployer/opcm/opchain.go @@ -33,6 +33,7 @@ type DeployOPChainInput struct { SaltMixer string GasLimit uint64 + DisputeGameUsesSuperRoots bool DisputeGameType uint32 DisputeAbsolutePrestate common.Hash DisputeMaxGameDepth uint64 diff --git a/op-deployer/pkg/deployer/pipeline/opchain.go b/op-deployer/pkg/deployer/pipeline/opchain.go index 03d5b375671d3..e4ad67977b073 100644 --- a/op-deployer/pkg/deployer/pipeline/opchain.go +++ b/op-deployer/pkg/deployer/pipeline/opchain.go @@ -73,12 +73,13 @@ func DeployOPChain(env *Env, intent *state.Intent, st *state.State, chainID comm func makeDCI(intent *state.Intent, thisIntent *state.ChainIntent, chainID common.Hash, st *state.State) (opcm.DeployOPChainInput, error) { proofParams, err := jsonutil.MergeJSON( state.ChainProofParams{ - DisputeGameType: standard.DisputeGameType, - DisputeAbsolutePrestate: standard.DisputeAbsolutePrestate, - DisputeMaxGameDepth: standard.DisputeMaxGameDepth, - DisputeSplitDepth: standard.DisputeSplitDepth, - DisputeClockExtension: standard.DisputeClockExtension, - DisputeMaxClockDuration: standard.DisputeMaxClockDuration, + DisputeGameUsesSuperRoots: standard.DisputeGameUsesSuperRoots, + DisputeGameType: standard.DisputeGameType, + DisputeAbsolutePrestate: standard.DisputeAbsolutePrestate, + DisputeMaxGameDepth: standard.DisputeMaxGameDepth, + DisputeSplitDepth: standard.DisputeSplitDepth, + DisputeClockExtension: standard.DisputeClockExtension, + DisputeMaxClockDuration: standard.DisputeMaxClockDuration, }, intent.GlobalDeployOverrides, thisIntent.DeployOverrides, @@ -100,6 +101,7 @@ func makeDCI(intent *state.Intent, thisIntent *state.ChainIntent, chainID common Opcm: st.ImplementationsDeployment.OpcmAddress, SaltMixer: st.Create2Salt.String(), // passing through salt generated at state initialization GasLimit: standard.GasLimit, + DisputeGameUsesSuperRoots: proofParams.DisputeGameUsesSuperRoots, DisputeGameType: proofParams.DisputeGameType, DisputeAbsolutePrestate: proofParams.DisputeAbsolutePrestate, DisputeMaxGameDepth: proofParams.DisputeMaxGameDepth, diff --git a/op-deployer/pkg/deployer/standard/standard.go b/op-deployer/pkg/deployer/standard/standard.go index 1f43b33fe1137..9478f328d3971 100644 --- a/op-deployer/pkg/deployer/standard/standard.go +++ b/op-deployer/pkg/deployer/standard/standard.go @@ -26,6 +26,7 @@ const ( ProofMaturityDelaySeconds uint64 = 604800 DisputeGameFinalityDelaySeconds uint64 = 302400 MIPSVersion uint64 = 1 + DisputeGameUsesSuperRoots bool = false DisputeGameType uint32 = 1 // PERMISSIONED game type DisputeMaxGameDepth uint64 = 73 DisputeSplitDepth uint64 = 30 diff --git a/op-deployer/pkg/deployer/state/chain_intent.go b/op-deployer/pkg/deployer/state/chain_intent.go index f374466ea41ca..b53a8d0a8bc1e 100644 --- a/op-deployer/pkg/deployer/state/chain_intent.go +++ b/op-deployer/pkg/deployer/state/chain_intent.go @@ -17,6 +17,7 @@ const ( ) type ChainProofParams struct { + DisputeGameUsesSuperRoots bool `json:"disputeGameUsesSuperRoots" toml:"disputeGameUsesSuperRoots"` DisputeGameType uint32 `json:"respectedGameType" toml:"respectedGameType"` DisputeAbsolutePrestate common.Hash `json:"faultGameAbsolutePrestate" toml:"faultGameAbsolutePrestate"` DisputeMaxGameDepth uint64 `json:"faultGameMaxDepth" toml:"faultGameMaxDepth"` diff --git a/op-deployer/pkg/deployer/upgrade/v2_0_0/upgrade_test.go b/op-deployer/pkg/deployer/upgrade/v2_0_0/upgrade_test.go index 95585401b1182..b025855751b11 100644 --- a/op-deployer/pkg/deployer/upgrade/v2_0_0/upgrade_test.go +++ b/op-deployer/pkg/deployer/upgrade/v2_0_0/upgrade_test.go @@ -21,6 +21,7 @@ import ( ) func TestUpgradeOPChainInput_OpChainConfigs(t *testing.T) { + t.Skip("skipping for upgrade 15") input := &UpgradeOPChainInput{ Prank: common.Address{0xaa}, Opcm: common.Address{0xbb}, @@ -54,6 +55,7 @@ func TestUpgradeOPChainInput_OpChainConfigs(t *testing.T) { } func TestUpgrader_Upgrade(t *testing.T) { + t.Skip("skipping for upgrade 15") lgr := testlog.Logger(t, slog.LevelDebug) forkedL1, stopL1, err := devnet.NewForkedSepolia(lgr) diff --git a/op-e2e/config/init.go b/op-e2e/config/init.go index b13a60023426d..74c54093a2e0d 100644 --- a/op-e2e/config/init.go +++ b/op-e2e/config/init.go @@ -436,12 +436,13 @@ func defaultIntent(root string, loc *artifacts.Locator, deployer common.Address, { ChainProofParams: state.ChainProofParams{ // Fast game - DisputeGameType: 254, - DisputeAbsolutePrestate: defaultPrestate, - DisputeMaxGameDepth: 14 + 3 + 1, - DisputeSplitDepth: 14, - DisputeClockExtension: 0, - DisputeMaxClockDuration: 0, + DisputeGameUsesSuperRoots: false, + DisputeGameType: 254, + DisputeAbsolutePrestate: defaultPrestate, + DisputeMaxGameDepth: 14 + 3 + 1, + DisputeSplitDepth: 14, + DisputeClockExtension: 0, + DisputeMaxClockDuration: 0, }, VMType: state.VMTypeAlphabet, UseCustomOracle: true, @@ -452,23 +453,25 @@ func defaultIntent(root string, loc *artifacts.Locator, deployer common.Address, { ChainProofParams: state.ChainProofParams{ // Alphabet game - DisputeGameType: 255, - DisputeAbsolutePrestate: defaultPrestate, - DisputeMaxGameDepth: 14 + 3 + 1, - DisputeSplitDepth: 14, - DisputeClockExtension: 0, - DisputeMaxClockDuration: 1200, + DisputeGameUsesSuperRoots: false, + DisputeGameType: 255, + DisputeAbsolutePrestate: defaultPrestate, + DisputeMaxGameDepth: 14 + 3 + 1, + DisputeSplitDepth: 14, + DisputeClockExtension: 0, + DisputeMaxClockDuration: 1200, }, VMType: state.VMTypeAlphabet, }, { ChainProofParams: state.ChainProofParams{ - DisputeGameType: 0, - DisputeAbsolutePrestate: cannonPrestate(root, allocType), - DisputeMaxGameDepth: 50, - DisputeSplitDepth: 14, - DisputeClockExtension: 0, - DisputeMaxClockDuration: 1200, + DisputeGameUsesSuperRoots: false, + DisputeGameType: 0, + DisputeAbsolutePrestate: cannonPrestate(root, allocType), + DisputeMaxGameDepth: 50, + DisputeSplitDepth: 14, + DisputeClockExtension: 0, + DisputeMaxClockDuration: 1200, }, VMType: cannonVMType(allocType), }, diff --git a/packages/contracts-bedrock/interfaces/L1/IOPContractsManager.sol b/packages/contracts-bedrock/interfaces/L1/IOPContractsManager.sol index 37701010a88b1..67f3e2a5e9bdb 100644 --- a/packages/contracts-bedrock/interfaces/L1/IOPContractsManager.sol +++ b/packages/contracts-bedrock/interfaces/L1/IOPContractsManager.sol @@ -106,6 +106,7 @@ interface IOPContractsManager { string saltMixer; uint64 gasLimit; // Configurable dispute game parameters. + bool disputeGameUsesSuperRoots; GameType disputeGameType; Claim disputeAbsolutePrestate; uint256 disputeMaxGameDepth; @@ -171,6 +172,7 @@ interface IOPContractsManager { ISystemConfig systemConfigProxy; IProxyAdmin proxyAdmin; Claim absolutePrestate; + bool disputeGameUsesSuperRoots; } struct AddGameInput { @@ -227,8 +229,23 @@ interface IOPContractsManager { /// @notice Thrown when an invalid `l2ChainId` is provided to `deploy`. error InvalidChainId(); - /// @notice Thrown when a role's address is not valid. - error InvalidRoleAddress(string role); + /// @notice Thrown when a role's address is not valid (opChainProxyAdminOwner). + error InvalidRoleAddressPAO(); + + /// @notice Thrown when a role's address is not valid (systemConfigOwner). + error InvalidRoleAddressSCO(); + + /// @notice Thrown when a role's address is not valid (batcher). + error InvalidRoleAddressBatcher(); + + /// @notice Thrown when a role's address is not valid (unsafeBlockSigner). + error InvalidRoleAddressUBS(); + + /// @notice Thrown when a role's address is not valid (proposer). + error InvalidRoleAddressProposer(); + + /// @notice Thrown when a role's address is not valid (challenger). + error InvalidRoleAddressChallenger(); /// @notice Thrown when the latest release is not set upon initialization. error LatestReleaseNotSet(); diff --git a/packages/contracts-bedrock/interfaces/L1/IOPPrestateUpdater.sol b/packages/contracts-bedrock/interfaces/L1/IOPPrestateUpdater.sol index 7456fed5231d5..33533cf96a975 100644 --- a/packages/contracts-bedrock/interfaces/L1/IOPPrestateUpdater.sol +++ b/packages/contracts-bedrock/interfaces/L1/IOPPrestateUpdater.sol @@ -75,8 +75,23 @@ interface IOPPrestateUpdater { /// @notice Thrown when an invalid `l2ChainId` is provided to `deploy`. error InvalidChainId(); - /// @notice Thrown when a role's address is not valid. - error InvalidRoleAddress(string role); + /// @notice Thrown when a role's address is not valid (opChainProxyAdminOwner). + error InvalidRoleAddressPAO(); + + /// @notice Thrown when a role's address is not valid (systemConfigOwner). + error InvalidRoleAddressSCO(); + + /// @notice Thrown when a role's address is not valid (batcher). + error InvalidRoleAddressBatcher(); + + /// @notice Thrown when a role's address is not valid (unsafeBlockSigner). + error InvalidRoleAddressUBS(); + + /// @notice Thrown when a role's address is not valid (proposer). + error InvalidRoleAddressProposer(); + + /// @notice Thrown when a role's address is not valid (challenger). + error InvalidRoleAddressChallenger(); /// @notice Thrown when the latest release is not set upon initialization. error LatestReleaseNotSet(); diff --git a/packages/contracts-bedrock/interfaces/L1/IOptimismPortal2.sol b/packages/contracts-bedrock/interfaces/L1/IOptimismPortal2.sol index 76b0111f171e2..a4e05d87cb81f 100644 --- a/packages/contracts-bedrock/interfaces/L1/IOptimismPortal2.sol +++ b/packages/contracts-bedrock/interfaces/L1/IOptimismPortal2.sol @@ -14,6 +14,7 @@ interface IOptimismPortal2 { error EmptyItem(); error InvalidDataRemainder(); error InvalidHeader(); + error ReinitializableBase_ZeroInitVersion(); error OptimismPortal_AlreadyFinalized(); error OptimismPortal_BadTarget(); error OptimismPortal_CallPaused(); @@ -29,6 +30,12 @@ interface IOptimismPortal2 { error OptimismPortal_NoReentrancy(); error OptimismPortal_ProofNotOldEnough(); error OptimismPortal_Unproven(); + error OptimismPortal_InvalidOutputRootIndex(); + error OptimismPortal_InvalidSuperRootProof(); + error OptimismPortal_InvalidOutputRootChainId(); + error OptimismPortal_WrongProofMethod(); + error Encoding_EmptySuperRoot(); + error Encoding_InvalidSuperRootVersion(); error OutOfGas(); error UnexpectedList(); error UnexpectedString(); @@ -66,9 +73,11 @@ interface IOptimismPortal2 { function initialize( ISystemConfig _systemConfig, ISuperchainConfig _superchainConfig, - IAnchorStateRegistry _anchorStateRegistry + IAnchorStateRegistry _anchorStateRegistry, + bool _superRootsActive ) external; + function initVersion() external view returns (uint8); function l2Sender() external view returns (address); function minimumGasLimit(uint64 _byteCount) external pure returns (uint64); function numProofSubmitters(bytes32 _withdrawalHash) external view returns (uint256); @@ -83,6 +92,15 @@ interface IOptimismPortal2 { bytes[] memory _withdrawalProof ) external; + function proveWithdrawalTransaction( + Types.WithdrawalTransaction memory _tx, + IDisputeGame _disputeGameProxy, + uint256 _outputRootIndex, + Types.SuperRootProof memory _superRootProof, + Types.OutputRootProof memory _outputRootProof, + bytes[] memory _withdrawalProof + ) + external; function provenWithdrawals( bytes32, address @@ -93,8 +111,9 @@ interface IOptimismPortal2 { function respectedGameType() external view returns (GameType); function respectedGameTypeUpdatedAt() external view returns (uint64); function superchainConfig() external view returns (ISuperchainConfig); + function superRootsActive() external view returns (bool); function systemConfig() external view returns (ISystemConfig); - function upgrade(IAnchorStateRegistry _anchorStateRegistry) external; + function upgrade(IAnchorStateRegistry _anchorStateRegistry, bool _superRootsActive) external; function version() external pure returns (string memory); function __constructor__(uint256 _proofMaturityDelaySeconds) external; diff --git a/packages/contracts-bedrock/interfaces/L1/IOptimismPortalInterop.sol b/packages/contracts-bedrock/interfaces/L1/IOptimismPortalInterop.sol index 716b42f41985b..ff4df3b07357c 100644 --- a/packages/contracts-bedrock/interfaces/L1/IOptimismPortalInterop.sol +++ b/packages/contracts-bedrock/interfaces/L1/IOptimismPortalInterop.sol @@ -15,6 +15,7 @@ interface IOptimismPortalInterop { error EmptyItem(); error InvalidDataRemainder(); error InvalidHeader(); + error ReinitializableBase_ZeroInitVersion(); error OptimismPortal_AlreadyFinalized(); error OptimismPortal_BadTarget(); error OptimismPortal_CallPaused(); @@ -31,6 +32,12 @@ interface IOptimismPortalInterop { error OptimismPortal_ProofNotOldEnough(); error OptimismPortal_Unauthorized(); error OptimismPortal_Unproven(); + error OptimismPortal_InvalidOutputRootIndex(); + error OptimismPortal_InvalidSuperRootProof(); + error OptimismPortal_InvalidOutputRootChainId(); + error OptimismPortal_WrongProofMethod(); + error Encoding_EmptySuperRoot(); + error Encoding_InvalidSuperRootVersion(); error OutOfGas(); error UnexpectedList(); error UnexpectedString(); @@ -68,9 +75,11 @@ interface IOptimismPortalInterop { function initialize( ISystemConfig _systemConfig, ISuperchainConfig _superchainConfig, - IAnchorStateRegistry _anchorStateRegistry + IAnchorStateRegistry _anchorStateRegistry, + bool _superRootsActive ) external; + function initVersion() external view returns (uint8); function l2Sender() external view returns (address); function minimumGasLimit(uint64 _byteCount) external pure returns (uint64); function numProofSubmitters(bytes32 _withdrawalHash) external view returns (uint256); @@ -85,6 +94,15 @@ interface IOptimismPortalInterop { bytes[] memory _withdrawalProof ) external; + function proveWithdrawalTransaction( + Types.WithdrawalTransaction memory _tx, + IDisputeGame _disputeGameProxy, + uint256 _outputRootIndex, + Types.SuperRootProof memory _superRootProof, + Types.OutputRootProof memory _outputRootProof, + bytes[] memory _withdrawalProof + ) + external; function provenWithdrawals( bytes32, address @@ -96,8 +114,9 @@ interface IOptimismPortalInterop { function respectedGameTypeUpdatedAt() external view returns (uint64); function setConfig(ConfigType _type, bytes memory _value) external; function superchainConfig() external view returns (ISuperchainConfig); + function superRootsActive() external view returns (bool); function systemConfig() external view returns (ISystemConfig); - function upgrade(IAnchorStateRegistry _anchorStateRegistry) external; + function upgrade(IAnchorStateRegistry _anchorStateRegistry, bool _superRootsActive) external; function version() external pure returns (string memory); function __constructor__(uint256 _proofMaturityDelaySeconds) external; diff --git a/packages/contracts-bedrock/interfaces/L1/ISystemConfig.sol b/packages/contracts-bedrock/interfaces/L1/ISystemConfig.sol index e5677516fbf54..151c47808015e 100644 --- a/packages/contracts-bedrock/interfaces/L1/ISystemConfig.sol +++ b/packages/contracts-bedrock/interfaces/L1/ISystemConfig.sol @@ -22,6 +22,8 @@ interface ISystemConfig { address optimismMintableERC20Factory; } + error ReinitializableBase_ZeroInitVersion(); + event ConfigUpdate(uint256 indexed version, UpdateType indexed updateType, bytes data); event Initialized(uint8 version); event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); @@ -54,12 +56,15 @@ interface ISystemConfig { address _unsafeBlockSigner, IResourceMetering.ResourceConfig memory _config, address _batchInbox, - Addresses memory _addresses + Addresses memory _addresses, + uint256 _l2ChainId ) external; + function initVersion() external view returns (uint8); function l1CrossDomainMessenger() external view returns (address addr_); function l1ERC721Bridge() external view returns (address addr_); function l1StandardBridge() external view returns (address addr_); + function l2ChainId() external view returns (uint256); function maximumGasLimit() external pure returns (uint64); function minimumGasLimit() external view returns (uint64); function operatorFeeConstant() external view returns (uint64); @@ -81,6 +86,7 @@ interface ISystemConfig { function startBlock() external view returns (uint256 startBlock_); function transferOwnership(address newOwner) external; // nosemgrep function unsafeBlockSigner() external view returns (address addr_); + function upgrade(uint256 _l2ChainId) external; function version() external pure returns (string memory); function __constructor__() external; diff --git a/packages/contracts-bedrock/interfaces/L1/ISystemConfigInterop.sol b/packages/contracts-bedrock/interfaces/L1/ISystemConfigInterop.sol index 19cac2b41543a..6842e0f701392 100644 --- a/packages/contracts-bedrock/interfaces/L1/ISystemConfigInterop.sol +++ b/packages/contracts-bedrock/interfaces/L1/ISystemConfigInterop.sol @@ -5,6 +5,8 @@ import { ISystemConfig } from "interfaces/L1/ISystemConfig.sol"; import { IResourceMetering } from "interfaces/L1/IResourceMetering.sol"; interface ISystemConfigInterop { + error ReinitializableBase_ZeroInitVersion(); + event ConfigUpdate(uint256 indexed version, ISystemConfig.UpdateType indexed updateType, bytes data); event Initialized(uint8 version); event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); @@ -30,6 +32,7 @@ interface ISystemConfigInterop { function l1CrossDomainMessenger() external view returns (address addr_); function l1ERC721Bridge() external view returns (address addr_); function l1StandardBridge() external view returns (address addr_); + function l2ChainId() external view returns (uint256); function maximumGasLimit() external pure returns (uint64); function minimumGasLimit() external view returns (uint64); function operatorFeeConstant() external view returns (uint64); @@ -65,9 +68,11 @@ interface ISystemConfigInterop { IResourceMetering.ResourceConfig memory _config, address _batchInbox, ISystemConfig.Addresses memory _addresses, - address _dependencyManager + address _dependencyManager, + uint256 _l2ChainId ) external; + function initVersion() external view returns (uint8); function version() external pure returns (string memory); function __constructor__() external; diff --git a/packages/contracts-bedrock/interfaces/universal/IReinitializableBase.sol b/packages/contracts-bedrock/interfaces/universal/IReinitializableBase.sol new file mode 100644 index 0000000000000..d6d096a743bbe --- /dev/null +++ b/packages/contracts-bedrock/interfaces/universal/IReinitializableBase.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +interface IReinitializableBase { + error ReinitializableBase_ZeroInitVersion(); + + function initVersion() external view returns (uint8); + + // ReinitializerBase is abstract, so it has no constructor in its interface. + function __constructor__() external; +} diff --git a/packages/contracts-bedrock/justfile b/packages/contracts-bedrock/justfile index 09c0f1f8a23b5..5f6c8a97a745c 100644 --- a/packages/contracts-bedrock/justfile +++ b/packages/contracts-bedrock/justfile @@ -303,6 +303,7 @@ check: semver-diff-check-no-build \ validate-deploy-configs \ validate-spacers-no-build \ + reinitializer-check-no-build \ interfaces-check-no-build \ lint-forge-tests-check-no-build diff --git a/packages/contracts-bedrock/scripts/checks/reinitializer/main.go b/packages/contracts-bedrock/scripts/checks/reinitializer/main.go index c41e403b98562..e44c955d038bd 100644 --- a/packages/contracts-bedrock/scripts/checks/reinitializer/main.go +++ b/packages/contracts-bedrock/scripts/checks/reinitializer/main.go @@ -2,6 +2,7 @@ package main import ( "fmt" + "math" "os" "strconv" "strings" @@ -103,15 +104,23 @@ func getReinitializerValue(node *solc.AstNode) (uint64, error) { for _, modifier := range node.Modifiers { if modifier.ModifierName.Name == "reinitializer" { - valStr, ok := modifier.Arguments[0].Value.(string) - if !ok { - return 0, fmt.Errorf("reinitializer value is not a string") + if modifier.Arguments[0].Kind == "functionCall" { + if modifier.Arguments[0].Expression.Name == "initVersion" { + return math.MaxUint64, nil // uint64 max representing initVersion call + } else { + return 0, fmt.Errorf("reinitializer value is not a call to initVersion") + } + } else { + valStr, ok := modifier.Arguments[0].Value.(string) + if !ok { + return 0, fmt.Errorf("reinitializer value is not a string") + } + val, err := strconv.Atoi(valStr) + if err != nil { + return 0, fmt.Errorf("reinitializer value is not an integer") + } + return uint64(val), nil } - val, err := strconv.Atoi(valStr) - if err != nil { - return 0, fmt.Errorf("reinitializer value is not an integer") - } - return uint64(val), nil } } diff --git a/packages/contracts-bedrock/scripts/checks/reinitializer/main_test.go b/packages/contracts-bedrock/scripts/checks/reinitializer/main_test.go index 93ca35173b63c..0f9fc382e2def 100644 --- a/packages/contracts-bedrock/scripts/checks/reinitializer/main_test.go +++ b/packages/contracts-bedrock/scripts/checks/reinitializer/main_test.go @@ -1,6 +1,7 @@ package main import ( + "math" "testing" "github.com/ethereum-optimism/optimism/op-chain-ops/solc" @@ -155,6 +156,38 @@ func TestGetReinitializerValue(t *testing.T) { want: 0, wantErr: true, }, + { + name: "Valid reinitializer with initVersion call", + node: &solc.AstNode{ + Modifiers: []solc.AstNode{ + { + ModifierName: &solc.Expression{Name: "reinitializer"}, + Arguments: []solc.Expression{{ + Kind: "functionCall", + Expression: &solc.Expression{Name: "initVersion"}, + }}, + }, + }, + }, + want: math.MaxUint64, + wantErr: false, + }, + { + name: "Invalid function call - not initVersion", + node: &solc.AstNode{ + Modifiers: []solc.AstNode{ + { + ModifierName: &solc.Expression{Name: "reinitializer"}, + Arguments: []solc.Expression{{ + Kind: "functionCall", + Expression: &solc.Expression{Name: "someOtherFunction"}, + }}, + }, + }, + }, + want: 0, + wantErr: true, + }, } for _, tt := range tests { @@ -484,6 +517,85 @@ func TestCheckArtifact(t *testing.T) { }, wantErr: false, // Should return nil without error }, + { + name: "Matching reinitializer values with initVersion", + artifact: &solc.ForgeArtifact{ + Ast: solc.Ast{ + Nodes: []solc.AstNode{ + { + NodeType: "ContractDefinition", + Nodes: []solc.AstNode{ + { + NodeType: "FunctionDefinition", + Name: "initialize", + Modifiers: []solc.AstNode{ + { + ModifierName: &solc.Expression{Name: "reinitializer"}, + Arguments: []solc.Expression{{ + Kind: "functionCall", + Expression: &solc.Expression{Name: "initVersion"}, + }}, + }, + }, + }, + { + NodeType: "FunctionDefinition", + Name: "upgrade", + Modifiers: []solc.AstNode{ + { + ModifierName: &solc.Expression{Name: "reinitializer"}, + Arguments: []solc.Expression{{ + Kind: "functionCall", + Expression: &solc.Expression{Name: "initVersion"}, + }}, + }, + }, + }, + }, + }, + }, + }, + }, + wantErr: false, + }, + { + name: "Mismatched reinitializer values - one with initVersion, one with constant", + artifact: &solc.ForgeArtifact{ + Ast: solc.Ast{ + Nodes: []solc.AstNode{ + { + NodeType: "ContractDefinition", + Nodes: []solc.AstNode{ + { + NodeType: "FunctionDefinition", + Name: "initialize", + Modifiers: []solc.AstNode{ + { + ModifierName: &solc.Expression{Name: "reinitializer"}, + Arguments: []solc.Expression{{ + Kind: "functionCall", + Expression: &solc.Expression{Name: "initVersion"}, + }}, + }, + }, + }, + { + NodeType: "FunctionDefinition", + Name: "upgrade", + Modifiers: []solc.AstNode{ + { + ModifierName: &solc.Expression{Name: "reinitializer"}, + Arguments: []solc.Expression{{Value: "2"}}, + }, + }, + }, + }, + }, + }, + }, + }, + wantErr: true, + }, } for _, tt := range tests { diff --git a/packages/contracts-bedrock/scripts/deploy/Deploy.s.sol b/packages/contracts-bedrock/scripts/deploy/Deploy.s.sol index 006d91b5b072a..1208fbe13cdbc 100644 --- a/packages/contracts-bedrock/scripts/deploy/Deploy.s.sol +++ b/packages/contracts-bedrock/scripts/deploy/Deploy.s.sol @@ -523,7 +523,8 @@ contract Deploy is Deployer { disputeGameFactory: artifacts.mustGetAddress("DisputeGameFactoryProxy"), optimismPortal: artifacts.mustGetAddress("OptimismPortalProxy"), optimismMintableERC20Factory: artifacts.mustGetAddress("OptimismMintableERC20FactoryProxy") - }) + }), + cfg.l2ChainID() ) ) }); @@ -964,6 +965,7 @@ contract Deploy is Deployer { ), saltMixer: saltMixer, gasLimit: uint64(cfg.l2GenesisBlockGasLimit()), + disputeGameUsesSuperRoots: false, disputeGameType: GameTypes.PERMISSIONED_CANNON, disputeAbsolutePrestate: Claim.wrap(bytes32(cfg.faultGameAbsolutePrestate())), disputeMaxGameDepth: cfg.faultGameMaxDepth(), diff --git a/packages/contracts-bedrock/scripts/deploy/DeployOPChain.s.sol b/packages/contracts-bedrock/scripts/deploy/DeployOPChain.s.sol index 8af54e4224123..50744aac45768 100644 --- a/packages/contracts-bedrock/scripts/deploy/DeployOPChain.s.sol +++ b/packages/contracts-bedrock/scripts/deploy/DeployOPChain.s.sol @@ -51,6 +51,7 @@ contract DeployOPChainInput is BaseDeployIO { uint64 internal _gasLimit; // Configurable dispute game inputs + bool internal _disputeGameUsesSuperRoots; GameType internal _disputeGameType; Claim internal _disputeAbsolutePrestate; uint256 internal _disputeMaxGameDepth; @@ -98,6 +99,9 @@ contract DeployOPChainInput is BaseDeployIO { _operatorFeeScalar = SafeCast.toUint32(_value); } else if (_sel == this.operatorFeeConstant.selector) { _operatorFeeConstant = SafeCast.toUint64(_value); + } else if (_sel == this.disputeGameUsesSuperRoots.selector) { + require(_value == 0 || _value == 1, "DeployOPChainInput: invalid disputeGameUsesSuperRoots"); + _disputeGameUsesSuperRoots = _value == 1; } else { revert("DeployOPChainInput: unknown selector"); } @@ -194,6 +198,10 @@ contract DeployOPChainInput is BaseDeployIO { return _gasLimit; } + function disputeGameUsesSuperRoots() public view returns (bool) { + return _disputeGameUsesSuperRoots; + } + function disputeGameType() public view returns (GameType) { return _disputeGameType; } @@ -371,6 +379,7 @@ contract DeployOPChain is Script { startingAnchorRoot: _doi.startingAnchorRoot(), saltMixer: _doi.saltMixer(), gasLimit: _doi.gasLimit(), + disputeGameUsesSuperRoots: _doi.disputeGameUsesSuperRoots(), disputeGameType: _doi.disputeGameType(), disputeAbsolutePrestate: _doi.disputeAbsolutePrestate(), disputeMaxGameDepth: _doi.disputeMaxGameDepth(), diff --git a/packages/contracts-bedrock/scripts/go-ffi/differential-testing.go b/packages/contracts-bedrock/scripts/go-ffi/differential-testing.go index e35d2a82c3036..7c328d23f2686 100644 --- a/packages/contracts-bedrock/scripts/go-ffi/differential-testing.go +++ b/packages/contracts-bedrock/scripts/go-ffi/differential-testing.go @@ -38,14 +38,15 @@ var ( {Type: fixedBytes}, } - uint32Type, _ = abi.NewType("uint32", "", nil) - // Plain address type addressType, _ = abi.NewType("address", "", nil) // Plain uint8 type uint8Type, _ = abi.NewType("uint8", "", nil) + // Plain uint32 type + uint32Type, _ = abi.NewType("uint32", "", nil) + // Plain uint256 type uint256Type, _ = abi.NewType("uint256", "", nil) @@ -102,6 +103,19 @@ var ( {Name: "symbol", Type: fixedBytes}, } + // Super root proof tuple (uint8, uint64, OutputRootWithChainId[]) + superRootProof, _ = abi.NewType("tuple", "SuperRootProof", []abi.ArgumentMarshaling{ + {Name: "version", Type: "bytes1"}, + {Name: "timestamp", Type: "uint64"}, + {Name: "outputRoots", Type: "tuple[]", Components: []abi.ArgumentMarshaling{ + {Name: "chainId", Type: "uint256"}, + {Name: "root", Type: "bytes32"}, + }}, + }) + superRootProofArgs = abi.Arguments{ + {Type: superRootProof}, + } + // Dependency tuple (uint256) dependencyArgs = abi.Arguments{{Name: "chainId", Type: uint256Type}} ) @@ -520,6 +534,49 @@ func DiffTestUtils() { packed, err := bytesArgs.Pack(&encoded) checkErr(err, "Error encoding output") + fmt.Print(hexutil.Encode(packed)) + case "encodeSuperRootProof": + // Parse input argument as abi encoded super root proof + if len(args) < 2 { + panic("Error: encodeSuperRoot requires at least 1 argument") + } + + // Parse the input as hex data + superRootProofData := common.FromHex(args[1]) + proof, err := parseSuperRootProof(superRootProofData) + checkErr(err, "Error parsing super root proof") + + // Encode super root proof + encoded, err := encodeSuperRootProof(proof) + checkErr(err, "Error encoding super root") + + // Pack encoded super root + packed, err := bytesArgs.Pack(&encoded) + checkErr(err, "Error encoding output") + + fmt.Print(hexutil.Encode(packed)) + case "hashSuperRootProof": + // Parse input argument as abi encoded super root proof + if len(args) < 2 { + panic("Error: hashSuperRootProof requires at least 1 argument") + } + + // Parse the input as hex data + superRootProofData := common.FromHex(args[1]) + proof, err := parseSuperRootProof(superRootProofData) + checkErr(err, "Error parsing super root proof") + + // Encode super root proof + encoded, err := encodeSuperRootProof(proof) + checkErr(err, "Error encoding super root proof") + + // Hash super root proof + hash := crypto.Keccak256Hash(encoded) + + // Pack hash + packed, err := fixedBytesArgs.Pack(&hash) + checkErr(err, "Error encoding output") + fmt.Print(hexutil.Encode(packed)) default: panic(fmt.Errorf("Unknown command: %s", args[0])) diff --git a/packages/contracts-bedrock/scripts/go-ffi/utils.go b/packages/contracts-bedrock/scripts/go-ffi/utils.go index 0c535c03feef1..157dd4d85c547 100644 --- a/packages/contracts-bedrock/scripts/go-ffi/utils.go +++ b/packages/contracts-bedrock/scripts/go-ffi/utils.go @@ -1,6 +1,7 @@ package main import ( + "encoding/binary" "errors" "fmt" "math/big" @@ -13,6 +14,18 @@ import ( "github.com/ethereum/go-ethereum/core/types" ) +type OutputRootWithChainId struct { + ChainId *big.Int + Root common.Hash +} + +// Define a proper type for SuperRootProof +type SuperRootProof struct { + Version uint8 + Timestamp uint64 + OutputRoots []OutputRootWithChainId +} + var UnknownNonceVersion = errors.New("Unknown nonce version") // checkOk checks if ok is false, and panics if so. @@ -50,6 +63,78 @@ func encodeCrossDomainMessage(nonce *big.Int, sender common.Address, target comm return encoded, err } +// parseSuperRootProof parses an abi encoded super root proof into a SuperRootProof struct. +func parseSuperRootProof(abiEncodedProof []byte) (*SuperRootProof, error) { + // Parse the input as hex data + unpacked, err := superRootProofArgs.Unpack(abiEncodedProof) + if err != nil { + return nil, err + } + + // The Unpack method returns a slice of interface{}, so we need to get the first element + if len(unpacked) != 1 { + return nil, errors.New("unexpected number of values after unpacking super root proof") + } + + // Use an anonymous struct matching the tuple’s layout. + tmp := unpacked[0].(struct { + Version [1]uint8 `json:"version"` + Timestamp uint64 `json:"timestamp"` + OutputRoots []struct { + ChainId *big.Int `json:"chainId"` + Root [32]byte `json:"root"` + } `json:"outputRoots"` + }) + + // Convert into our desired SuperRootProof type. + proof := SuperRootProof{ + Version: tmp.Version[0], + Timestamp: tmp.Timestamp, + } + for _, o := range tmp.OutputRoots { + proof.OutputRoots = append(proof.OutputRoots, OutputRootWithChainId{ + ChainId: o.ChainId, + Root: common.BytesToHash(o.Root[:]), + }) + } + + return &proof, nil +} + +// encodeSuperRootProof encodes a super root proof into a byte array. +func encodeSuperRootProof(superRootProof *SuperRootProof) ([]byte, error) { + // Version must match the expected version (0x01) + if superRootProof.Version != 0x01 { + return nil, errors.New("invalid super root version") + } + + // Output roots must not be empty + if len(superRootProof.OutputRoots) == 0 { + return nil, errors.New("empty super root") + } + + // Start with version byte and timestamp + encoded := []byte{superRootProof.Version} + + // Add timestamp as bytes8 (uint64) + timestampBytes := make([]byte, 8) + binary.BigEndian.PutUint64(timestampBytes, superRootProof.Timestamp) + encoded = append(encoded, timestampBytes...) + + // Add each output root (chainId + root) + for _, outputRoot := range superRootProof.OutputRoots { + // Append chainId bytes (padded to 32 bytes) + chainIdBytes := make([]byte, 32) + outputRoot.ChainId.FillBytes(chainIdBytes) + encoded = append(encoded, chainIdBytes...) + + // Append root hash (already 32 bytes) + encoded = append(encoded, outputRoot.Root.Bytes()...) + } + + return encoded, nil +} + // hashWithdrawal hashes a withdrawal transaction. func hashWithdrawal(nonce *big.Int, sender common.Address, target common.Address, value *big.Int, gasLimit *big.Int, data []byte) (common.Hash, error) { wd := crossdomain.Withdrawal{ diff --git a/packages/contracts-bedrock/scripts/libraries/DeployUtils.sol b/packages/contracts-bedrock/scripts/libraries/DeployUtils.sol index 1cec8d04778b1..b4c5bd7b141ed 100644 --- a/packages/contracts-bedrock/scripts/libraries/DeployUtils.sol +++ b/packages/contracts-bedrock/scripts/libraries/DeployUtils.sol @@ -17,6 +17,7 @@ import { IProxy } from "interfaces/universal/IProxy.sol"; import { IAddressManager } from "interfaces/legacy/IAddressManager.sol"; import { IL1ChugSplashProxy, IStaticL1ChugSplashProxy } from "interfaces/legacy/IL1ChugSplashProxy.sol"; import { IResolvedDelegateProxy } from "interfaces/legacy/IResolvedDelegateProxy.sol"; +import { IReinitializableBase } from "interfaces/universal/IReinitializableBase.sol"; library DeployUtils { Vm internal constant vm = Vm(address(uint160(uint256(keccak256("hevm cheat code"))))); @@ -360,7 +361,19 @@ library DeployUtils { bytes32 slotVal = vm.load(_contractAddress, bytes32(_slot)); uint8 val = uint8((uint256(slotVal) >> (_offset * 8)) & 0xFF); if (_isProxy) { - require(val == 1, "DeployUtils: storage value is not 1 at the given slot and offset"); + // Using a try/catch here to check if the contract has an initVersion() defined. + // EIP-150 safe because we require that we have at least 200k gas before the call which + // is more than enough to avoid running out of gas when 63/64 of the gas is provided to + // the initVersion() call (which simply reads an immutable variable). Since this is + // only ever triggered as part of a script, we can safely assume we'll have the gas. + require(gasleft() > 200_000, "DeployUtils: insufficient gas for initVersion() call"); + + // eip150-safe + try IReinitializableBase(_contractAddress).initVersion() returns (uint8 initVersion_) { + require(val == initVersion_, "DeployUtils: storage value is incorrect at the given slot and offset"); + } catch { + require(val == 1, "DeployUtils: storage value is not set at the given slot and offset"); + } } else { require(val == type(uint8).max, "DeployUtils: storage value is not 0xff at the given slot and offset"); } diff --git a/packages/contracts-bedrock/snapshots/abi/OPContractsManager.json b/packages/contracts-bedrock/snapshots/abi/OPContractsManager.json index a12ea8e94117b..78f613a279507 100644 --- a/packages/contracts-bedrock/snapshots/abi/OPContractsManager.json +++ b/packages/contracts-bedrock/snapshots/abi/OPContractsManager.json @@ -293,6 +293,11 @@ "name": "gasLimit", "type": "uint64" }, + { + "internalType": "bool", + "name": "disputeGameUsesSuperRoots", + "type": "bool" + }, { "internalType": "GameType", "name": "disputeGameType", @@ -622,6 +627,11 @@ "internalType": "Claim", "name": "absolutePrestate", "type": "bytes32" + }, + { + "internalType": "bool", + "name": "disputeGameUsesSuperRoots", + "type": "bool" } ], "internalType": "struct OPContractsManager.OpChainConfig[]", @@ -728,14 +738,33 @@ "type": "error" }, { - "inputs": [ - { - "internalType": "string", - "name": "role", - "type": "string" - } - ], - "name": "InvalidRoleAddress", + "inputs": [], + "name": "InvalidRoleAddressBatcher", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidRoleAddressChallenger", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidRoleAddressPAO", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidRoleAddressProposer", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidRoleAddressSCO", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidRoleAddressUBS", "type": "error" }, { diff --git a/packages/contracts-bedrock/snapshots/abi/OPContractsManagerDeployer.json b/packages/contracts-bedrock/snapshots/abi/OPContractsManagerDeployer.json index d543175c82822..2b0073e0e1296 100644 --- a/packages/contracts-bedrock/snapshots/abi/OPContractsManagerDeployer.json +++ b/packages/contracts-bedrock/snapshots/abi/OPContractsManagerDeployer.json @@ -186,6 +186,11 @@ "name": "gasLimit", "type": "uint64" }, + { + "internalType": "bool", + "name": "disputeGameUsesSuperRoots", + "type": "bool" + }, { "internalType": "GameType", "name": "disputeGameType", @@ -499,4 +504,4 @@ "name": "UnsupportedERCVersion", "type": "error" } -] \ No newline at end of file +] diff --git a/packages/contracts-bedrock/snapshots/abi/OPContractsManagerDeployerInterop.json b/packages/contracts-bedrock/snapshots/abi/OPContractsManagerDeployerInterop.json index d543175c82822..2b0073e0e1296 100644 --- a/packages/contracts-bedrock/snapshots/abi/OPContractsManagerDeployerInterop.json +++ b/packages/contracts-bedrock/snapshots/abi/OPContractsManagerDeployerInterop.json @@ -186,6 +186,11 @@ "name": "gasLimit", "type": "uint64" }, + { + "internalType": "bool", + "name": "disputeGameUsesSuperRoots", + "type": "bool" + }, { "internalType": "GameType", "name": "disputeGameType", @@ -499,4 +504,4 @@ "name": "UnsupportedERCVersion", "type": "error" } -] \ 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 ebc1b9132de40..e3d34b6617568 100644 --- a/packages/contracts-bedrock/snapshots/abi/OptimismPortal2.json +++ b/packages/contracts-bedrock/snapshots/abi/OptimismPortal2.json @@ -238,6 +238,19 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [], + "name": "initVersion", + "outputs": [ + { + "internalType": "uint8", + "name": "", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [ { @@ -254,6 +267,11 @@ "internalType": "contract IAnchorStateRegistry", "name": "_anchorStateRegistry", "type": "address" + }, + { + "internalType": "bool", + "name": "_superRootsActive", + "type": "bool" } ], "name": "initialize", @@ -467,6 +485,127 @@ "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [ + { + "components": [ + { + "internalType": "uint256", + "name": "nonce", + "type": "uint256" + }, + { + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "internalType": "address", + "name": "target", + "type": "address" + }, + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "gasLimit", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "internalType": "struct Types.WithdrawalTransaction", + "name": "_tx", + "type": "tuple" + }, + { + "internalType": "contract IDisputeGame", + "name": "_disputeGameProxy", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_outputRootIndex", + "type": "uint256" + }, + { + "components": [ + { + "internalType": "bytes1", + "name": "version", + "type": "bytes1" + }, + { + "internalType": "uint64", + "name": "timestamp", + "type": "uint64" + }, + { + "components": [ + { + "internalType": "uint256", + "name": "chainId", + "type": "uint256" + }, + { + "internalType": "bytes32", + "name": "root", + "type": "bytes32" + } + ], + "internalType": "struct Types.OutputRootWithChainId[]", + "name": "outputRoots", + "type": "tuple[]" + } + ], + "internalType": "struct Types.SuperRootProof", + "name": "_superRootProof", + "type": "tuple" + }, + { + "components": [ + { + "internalType": "bytes32", + "name": "version", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "stateRoot", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "messagePasserStorageRoot", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "latestBlockhash", + "type": "bytes32" + } + ], + "internalType": "struct Types.OutputRootProof", + "name": "_outputRootProof", + "type": "tuple" + }, + { + "internalType": "bytes[]", + "name": "_withdrawalProof", + "type": "bytes[]" + } + ], + "name": "proveWithdrawalTransaction", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [ { @@ -522,6 +661,19 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [], + "name": "superRootsActive", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [], "name": "superchainConfig", @@ -554,6 +706,11 @@ "internalType": "contract IAnchorStateRegistry", "name": "_anchorStateRegistry", "type": "address" + }, + { + "internalType": "bool", + "name": "_superRootsActive", + "type": "bool" } ], "name": "upgrade", @@ -691,6 +848,16 @@ "name": "EmptyItem", "type": "error" }, + { + "inputs": [], + "name": "Encoding_EmptySuperRoot", + "type": "error" + }, + { + "inputs": [], + "name": "Encoding_InvalidSuperRootVersion", + "type": "error" + }, { "inputs": [], "name": "InvalidDataRemainder", @@ -746,6 +913,16 @@ "name": "OptimismPortal_InvalidMerkleProof", "type": "error" }, + { + "inputs": [], + "name": "OptimismPortal_InvalidOutputRootChainId", + "type": "error" + }, + { + "inputs": [], + "name": "OptimismPortal_InvalidOutputRootIndex", + "type": "error" + }, { "inputs": [], "name": "OptimismPortal_InvalidOutputRootProof", @@ -761,6 +938,11 @@ "name": "OptimismPortal_InvalidRootClaim", "type": "error" }, + { + "inputs": [], + "name": "OptimismPortal_InvalidSuperRootProof", + "type": "error" + }, { "inputs": [], "name": "OptimismPortal_NoReentrancy", @@ -776,11 +958,21 @@ "name": "OptimismPortal_Unproven", "type": "error" }, + { + "inputs": [], + "name": "OptimismPortal_WrongProofMethod", + "type": "error" + }, { "inputs": [], "name": "OutOfGas", "type": "error" }, + { + "inputs": [], + "name": "ReinitializableBase_ZeroInitVersion", + "type": "error" + }, { "inputs": [], "name": "UnexpectedList", diff --git a/packages/contracts-bedrock/snapshots/abi/OptimismPortalInterop.json b/packages/contracts-bedrock/snapshots/abi/OptimismPortalInterop.json index 4a6c342105f2c..f5c13f585fb90 100644 --- a/packages/contracts-bedrock/snapshots/abi/OptimismPortalInterop.json +++ b/packages/contracts-bedrock/snapshots/abi/OptimismPortalInterop.json @@ -238,6 +238,19 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [], + "name": "initVersion", + "outputs": [ + { + "internalType": "uint8", + "name": "", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [ { @@ -254,6 +267,11 @@ "internalType": "contract IAnchorStateRegistry", "name": "_anchorStateRegistry", "type": "address" + }, + { + "internalType": "bool", + "name": "_superRootsActive", + "type": "bool" } ], "name": "initialize", @@ -467,6 +485,127 @@ "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [ + { + "components": [ + { + "internalType": "uint256", + "name": "nonce", + "type": "uint256" + }, + { + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "internalType": "address", + "name": "target", + "type": "address" + }, + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "gasLimit", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "internalType": "struct Types.WithdrawalTransaction", + "name": "_tx", + "type": "tuple" + }, + { + "internalType": "contract IDisputeGame", + "name": "_disputeGameProxy", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_outputRootIndex", + "type": "uint256" + }, + { + "components": [ + { + "internalType": "bytes1", + "name": "version", + "type": "bytes1" + }, + { + "internalType": "uint64", + "name": "timestamp", + "type": "uint64" + }, + { + "components": [ + { + "internalType": "uint256", + "name": "chainId", + "type": "uint256" + }, + { + "internalType": "bytes32", + "name": "root", + "type": "bytes32" + } + ], + "internalType": "struct Types.OutputRootWithChainId[]", + "name": "outputRoots", + "type": "tuple[]" + } + ], + "internalType": "struct Types.SuperRootProof", + "name": "_superRootProof", + "type": "tuple" + }, + { + "components": [ + { + "internalType": "bytes32", + "name": "version", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "stateRoot", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "messagePasserStorageRoot", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "latestBlockhash", + "type": "bytes32" + } + ], + "internalType": "struct Types.OutputRootProof", + "name": "_outputRootProof", + "type": "tuple" + }, + { + "internalType": "bytes[]", + "name": "_withdrawalProof", + "type": "bytes[]" + } + ], + "name": "proveWithdrawalTransaction", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [ { @@ -540,6 +679,19 @@ "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [], + "name": "superRootsActive", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [], "name": "superchainConfig", @@ -572,6 +724,11 @@ "internalType": "contract IAnchorStateRegistry", "name": "_anchorStateRegistry", "type": "address" + }, + { + "internalType": "bool", + "name": "_superRootsActive", + "type": "bool" } ], "name": "upgrade", @@ -709,6 +866,16 @@ "name": "EmptyItem", "type": "error" }, + { + "inputs": [], + "name": "Encoding_EmptySuperRoot", + "type": "error" + }, + { + "inputs": [], + "name": "Encoding_InvalidSuperRootVersion", + "type": "error" + }, { "inputs": [], "name": "InvalidDataRemainder", @@ -764,6 +931,16 @@ "name": "OptimismPortal_InvalidMerkleProof", "type": "error" }, + { + "inputs": [], + "name": "OptimismPortal_InvalidOutputRootChainId", + "type": "error" + }, + { + "inputs": [], + "name": "OptimismPortal_InvalidOutputRootIndex", + "type": "error" + }, { "inputs": [], "name": "OptimismPortal_InvalidOutputRootProof", @@ -779,6 +956,11 @@ "name": "OptimismPortal_InvalidRootClaim", "type": "error" }, + { + "inputs": [], + "name": "OptimismPortal_InvalidSuperRootProof", + "type": "error" + }, { "inputs": [], "name": "OptimismPortal_NoReentrancy", @@ -799,11 +981,21 @@ "name": "OptimismPortal_Unproven", "type": "error" }, + { + "inputs": [], + "name": "OptimismPortal_WrongProofMethod", + "type": "error" + }, { "inputs": [], "name": "OutOfGas", "type": "error" }, + { + "inputs": [], + "name": "ReinitializableBase_ZeroInitVersion", + "type": "error" + }, { "inputs": [], "name": "UnexpectedList", diff --git a/packages/contracts-bedrock/snapshots/abi/SystemConfig.json b/packages/contracts-bedrock/snapshots/abi/SystemConfig.json index 5009ab31ca872..8f11882ac1b33 100644 --- a/packages/contracts-bedrock/snapshots/abi/SystemConfig.json +++ b/packages/contracts-bedrock/snapshots/abi/SystemConfig.json @@ -283,6 +283,19 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [], + "name": "initVersion", + "outputs": [ + { + "internalType": "uint8", + "name": "", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [ { @@ -393,6 +406,11 @@ "internalType": "struct SystemConfig.Addresses", "name": "_addresses", "type": "tuple" + }, + { + "internalType": "uint256", + "name": "_l2ChainId", + "type": "uint256" } ], "name": "initialize", @@ -439,6 +457,19 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [], + "name": "l2ChainId", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [], "name": "maximumGasLimit", @@ -758,6 +789,19 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_l2ChainId", + "type": "uint256" + } + ], + "name": "upgrade", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [], "name": "version", @@ -827,5 +871,10 @@ ], "name": "OwnershipTransferred", "type": "event" + }, + { + "inputs": [], + "name": "ReinitializableBase_ZeroInitVersion", + "type": "error" } ] \ No newline at end of file diff --git a/packages/contracts-bedrock/snapshots/abi/SystemConfigInterop.json b/packages/contracts-bedrock/snapshots/abi/SystemConfigInterop.json index 8bcac1f13ccc9..c11d382aa528f 100644 --- a/packages/contracts-bedrock/snapshots/abi/SystemConfigInterop.json +++ b/packages/contracts-bedrock/snapshots/abi/SystemConfigInterop.json @@ -304,6 +304,19 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [], + "name": "initVersion", + "outputs": [ + { + "internalType": "uint8", + "name": "", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [ { @@ -414,6 +427,11 @@ "internalType": "struct SystemConfig.Addresses", "name": "_addresses", "type": "tuple" + }, + { + "internalType": "uint256", + "name": "_l2ChainId", + "type": "uint256" } ], "name": "initialize", @@ -536,6 +554,11 @@ "internalType": "address", "name": "_dependencyManager", "type": "address" + }, + { + "internalType": "uint256", + "name": "_l2ChainId", + "type": "uint256" } ], "name": "initialize", @@ -582,6 +605,19 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [], + "name": "l2ChainId", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [], "name": "maximumGasLimit", @@ -914,6 +950,19 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_l2ChainId", + "type": "uint256" + } + ], + "name": "upgrade", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [], "name": "version", @@ -983,5 +1032,10 @@ ], "name": "OwnershipTransferred", "type": "event" + }, + { + "inputs": [], + "name": "ReinitializableBase_ZeroInitVersion", + "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 f56df9990471d..0818d19fa94d9 100644 --- a/packages/contracts-bedrock/snapshots/semver-lock.json +++ b/packages/contracts-bedrock/snapshots/semver-lock.json @@ -16,16 +16,24 @@ "sourceCodeHash": "0x44797707aea8c63dec049a02d69ea056662a06e5cf320028ab8b388634bf1c67" }, "src/L1/OPContractsManager.sol": { - "initCodeHash": "0x216fe3ceef5d3e839d18a00383793bd326f20944fc3b55ef099a26a22141de18", - "sourceCodeHash": "0xd1de3414a3db731447cb6172df23a00cb1664783de56a93fb29f87d6bc8b8bb0" + "initCodeHash": "0xfc5622ed6411ec01e7bcfb3c9250b130b8ca25e74ee40e70e4017fefa34cddf7", + "sourceCodeHash": "0x1f53a1e174479aa244ad78fc171d4cbeb8529505a57af59f89435837e2803bcc" + }, + "src/L1/OPContractsManagerInterop.sol": { + "initCodeHash": "0x6855bfe2f22b4ce53c62fca0d32029cc39f0a2ab2ded43808bb6c493acadc781", + "sourceCodeHash": "0x79b3ae68d83f021d9118f8a1943c1d0773e7e01a19f580f6b90ad4c00370cff5" + }, + "src/L1/OPPrestateUpdater.sol": { + "initCodeHash": "0x840b42954a8921e8216a93ace6d2a047e28ef8df8f426181b1b229d2d5818fc1", + "sourceCodeHash": "0x985c33cc1933d68de38d6c13afc1ac42ef7e455cb06bb255c1fed22b06b97a62" }, "src/L1/OptimismPortal2.sol": { - "initCodeHash": "0xd1651b8a6f4d25611a0105d5cc7c1da3921417bd44da870ec63bf5ccd1bc7c63", - "sourceCodeHash": "0xce7373d8c7df47caa8b090f3afb3d2539677f12cb3eff7fc0ab1fd85638f05c1" + "initCodeHash": "0xac11984d6c3e58fd02e33509540ee329c4f5caedb94e422f8ab3a7a95ce7a546", + "sourceCodeHash": "0x79652f4f6aebc11dd4ad201dcf9a436d94db76e2edef07b12486c3de9cd804d2" }, "src/L1/OptimismPortalInterop.sol": { - "initCodeHash": "0xd59854648bf205dfbea96b483b2937441c32e9ef66b002468c2c14c0d6661728", - "sourceCodeHash": "0xd00b267dcf125e77c10b28c088be4378ec779927e3bcfeb6aa9a7f3d51370490" + "initCodeHash": "0x8d76300f7a79686e522d4cac0582f4f7ec77b798f881747d0403762fa56b3a71", + "sourceCodeHash": "0x1b2ce1f2e3125bd449501b9d228b207d31f2bc39c7f23262c674e0d1f47c0d0b" }, "src/L1/ProtocolVersions.sol": { "initCodeHash": "0x5a76c8530cb24cf23d3baacc6eefaac226382af13f1e2a35535d2ec2b0573b29", @@ -36,12 +44,12 @@ "sourceCodeHash": "0xfd56e63e76b1f203cceeb9bbb14396ae803cbbbf7e80ca0ee11fb586321812af" }, "src/L1/SystemConfig.sol": { - "initCodeHash": "0xd5b8b8eb47763556d9953019d1f81b1d790f15433aa9696b159a3fc45ecee148", - "sourceCodeHash": "0x6bfbc78b0fef2f65beff11a81f924728a7bd439a56986997621099551805aff9" + "initCodeHash": "0x93e732c31e59dc78d6414ad12fcc0cbe4537d3a69a2ca34ff8713b0b51679b19", + "sourceCodeHash": "0x538518cf61bda80bc3f0b94e4fe236b65e53fff397fd43958e695705c57e02d9" }, "src/L1/SystemConfigInterop.sol": { - "initCodeHash": "0xbef4696b2dcb6d43c3b3c438338bfb2224a1ea5002ed0612ec36a7821d7e3da2", - "sourceCodeHash": "0x1653aaa4d2b44d34ca1f9f2b4971eeb7594c8c2d27771b1f68b8d38cb79f2368" + "initCodeHash": "0xe3651f84f0fecf6c4b60801d8168da55bfe3889912548fcd6b935c9f6dff91b3", + "sourceCodeHash": "0xe3031507a78ed71352c0c1523272881a48c8d6bd98d5322ce3a7cb8061eeade3" }, "src/L2/BaseFeeVault.sol": { "initCodeHash": "0xc403d4c555d8e69a2699e01d192ae7327136701fa02da10a6d75a584b3c364c9", diff --git a/packages/contracts-bedrock/snapshots/storageLayout/OptimismPortal2.json b/packages/contracts-bedrock/snapshots/storageLayout/OptimismPortal2.json index ccb5d970d342e..4a9abd3d2dc2c 100644 --- a/packages/contracts-bedrock/snapshots/storageLayout/OptimismPortal2.json +++ b/packages/contracts-bedrock/snapshots/storageLayout/OptimismPortal2.json @@ -131,5 +131,12 @@ "offset": 0, "slot": "62", "type": "contract IAnchorStateRegistry" + }, + { + "bytes": "1", + "label": "superRootsActive", + "offset": 20, + "slot": "62", + "type": "bool" } ] \ No newline at end of file diff --git a/packages/contracts-bedrock/snapshots/storageLayout/OptimismPortalInterop.json b/packages/contracts-bedrock/snapshots/storageLayout/OptimismPortalInterop.json index ccb5d970d342e..4a9abd3d2dc2c 100644 --- a/packages/contracts-bedrock/snapshots/storageLayout/OptimismPortalInterop.json +++ b/packages/contracts-bedrock/snapshots/storageLayout/OptimismPortalInterop.json @@ -131,5 +131,12 @@ "offset": 0, "slot": "62", "type": "contract IAnchorStateRegistry" + }, + { + "bytes": "1", + "label": "superRootsActive", + "offset": 20, + "slot": "62", + "type": "bool" } ] \ No newline at end of file diff --git a/packages/contracts-bedrock/snapshots/storageLayout/SystemConfig.json b/packages/contracts-bedrock/snapshots/storageLayout/SystemConfig.json index ea0d05feb90d5..8332aa216cb4e 100644 --- a/packages/contracts-bedrock/snapshots/storageLayout/SystemConfig.json +++ b/packages/contracts-bedrock/snapshots/storageLayout/SystemConfig.json @@ -111,4 +111,4 @@ "slot": "106", "type": "uint64" } -] \ No newline at end of file +] diff --git a/packages/contracts-bedrock/snapshots/storageLayout/SystemConfigInterop.json b/packages/contracts-bedrock/snapshots/storageLayout/SystemConfigInterop.json index ea0d05feb90d5..8332aa216cb4e 100644 --- a/packages/contracts-bedrock/snapshots/storageLayout/SystemConfigInterop.json +++ b/packages/contracts-bedrock/snapshots/storageLayout/SystemConfigInterop.json @@ -111,4 +111,4 @@ "slot": "106", "type": "uint64" } -] \ No newline at end of file +] diff --git a/packages/contracts-bedrock/src/L1/OPContractsManager.sol b/packages/contracts-bedrock/src/L1/OPContractsManager.sol index 8aaabee8ef251..9853e1aaf50ab 100644 --- a/packages/contracts-bedrock/src/L1/OPContractsManager.sol +++ b/packages/contracts-bedrock/src/L1/OPContractsManager.sol @@ -17,7 +17,6 @@ import { IAnchorStateRegistry } from "interfaces/dispute/IAnchorStateRegistry.so import { IDisputeGame } from "interfaces/dispute/IDisputeGame.sol"; import { IAddressManager } from "interfaces/legacy/IAddressManager.sol"; import { IProxyAdmin } from "interfaces/universal/IProxyAdmin.sol"; -import { IDelayedWETH } from "interfaces/dispute/IDelayedWETH.sol"; import { IDisputeGameFactory } from "interfaces/dispute/IDisputeGameFactory.sol"; import { IFaultDisputeGame } from "interfaces/dispute/IFaultDisputeGame.sol"; import { IPermissionedDisputeGame } from "interfaces/dispute/IPermissionedDisputeGame.sol"; @@ -1145,6 +1144,7 @@ contract OPContractsManager is ISemver { string saltMixer; uint64 gasLimit; // Configurable dispute game parameters. + bool disputeGameUsesSuperRoots; GameType disputeGameType; Claim disputeAbsolutePrestate; uint256 disputeMaxGameDepth; @@ -1210,6 +1210,7 @@ contract OPContractsManager is ISemver { ISystemConfig systemConfigProxy; IProxyAdmin proxyAdmin; Claim absolutePrestate; + bool disputeGameUsesSuperRoots; } struct AddGameInput { @@ -1291,8 +1292,23 @@ contract OPContractsManager is ISemver { /// @notice Thrown when an invalid `l2ChainId` is provided to `deploy`. error InvalidChainId(); - /// @notice Thrown when a role's address is not valid. - error InvalidRoleAddress(string role); + /// @notice Thrown when a role's address is not valid (opChainProxyAdminOwner). + error InvalidRoleAddressPAO(); + + /// @notice Thrown when a role's address is not valid (systemConfigOwner). + error InvalidRoleAddressSCO(); + + /// @notice Thrown when a role's address is not valid (batcher). + error InvalidRoleAddressBatcher(); + + /// @notice Thrown when a role's address is not valid (unsafeBlockSigner). + error InvalidRoleAddressUBS(); + + /// @notice Thrown when a role's address is not valid (proposer). + error InvalidRoleAddressProposer(); + + /// @notice Thrown when a role's address is not valid (challenger). + error InvalidRoleAddressChallenger(); /// @notice Thrown when the latest release is not set upon initialization. error LatestReleaseNotSet(); diff --git a/packages/contracts-bedrock/src/L1/OPContractsManagerInterop.sol b/packages/contracts-bedrock/src/L1/OPContractsManagerInterop.sol index dcd98a3ed3d01..6358bc741d715 100644 --- a/packages/contracts-bedrock/src/L1/OPContractsManagerInterop.sol +++ b/packages/contracts-bedrock/src/L1/OPContractsManagerInterop.sol @@ -72,7 +72,8 @@ contract OPContractsManagerInterop is OPContractsManager { referenceResourceConfig, chainIdToBatchInboxAddress(_input.l2ChainId), opChainAddrs, - dependencyManager + dependencyManager, + _input.l2ChainId ) ); } diff --git a/packages/contracts-bedrock/src/L1/OptimismPortal2.sol b/packages/contracts-bedrock/src/L1/OptimismPortal2.sol index 3125d92e4a054..7450cf97e3be4 100644 --- a/packages/contracts-bedrock/src/L1/OptimismPortal2.sol +++ b/packages/contracts-bedrock/src/L1/OptimismPortal2.sol @@ -4,6 +4,7 @@ pragma solidity 0.8.15; // Contracts import { Initializable } from "@openzeppelin/contracts/proxy/utils/Initializable.sol"; import { ResourceMetering } from "src/L1/ResourceMetering.sol"; +import { ReinitializableBase } from "src/universal/ReinitializableBase.sol"; // Libraries import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; @@ -31,7 +32,7 @@ import { IAnchorStateRegistry } from "interfaces/dispute/IAnchorStateRegistry.so /// @notice The OptimismPortal is a low-level contract responsible for passing messages between L1 /// 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 OptimismPortal2 is Initializable, ResourceMetering, ISemver { +contract OptimismPortal2 is Initializable, ResourceMetering, ReinitializableBase, ISemver { /// @notice Allows for interactions with non standard ERC20 tokens. using SafeERC20 for IERC20; @@ -122,6 +123,9 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ISemver { /// @notice Address of the AnchorStateRegistry contract. IAnchorStateRegistry public anchorStateRegistry; + /// @notice Whether the OptimismPortal is using Super Roots or Output Roots. + bool public superRootsActive; + /// @notice Emitted when a transaction is deposited from L1 to L2. The parameters of this event /// are read by the rollup node and used to derive deposit transactions on L2. /// @param from Address that triggered the deposit transaction. @@ -193,6 +197,18 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ISemver { /// @notice Thrown when a withdrawal has not been proven. error OptimismPortal_Unproven(); + /// @notice Thrown when the wrong proof method is used. + error OptimismPortal_WrongProofMethod(); + + /// @notice Thrown when a super root proof is invalid. + error OptimismPortal_InvalidSuperRootProof(); + + /// @notice Thrown when an output root index is invalid. + error OptimismPortal_InvalidOutputRootIndex(); + + /// @notice Thrown when an output root chain id is invalid. + error OptimismPortal_InvalidOutputRootChainId(); + /// @notice Reverts when paused. modifier whenNotPaused() { if (paused()) revert OptimismPortal_CallPaused(); @@ -206,7 +222,7 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ISemver { } /// @param _proofMaturityDelaySeconds The proof maturity delay in seconds. - constructor(uint256 _proofMaturityDelaySeconds) { + constructor(uint256 _proofMaturityDelaySeconds) ReinitializableBase(2) { PROOF_MATURITY_DELAY_SECONDS = _proofMaturityDelaySeconds; _disableInitializers(); } @@ -215,17 +231,20 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ISemver { /// @param _systemConfig Address of the SystemConfig. /// @param _superchainConfig Address of the SuperchainConfig. /// @param _anchorStateRegistry Address of the AnchorStateRegistry. + /// @param _superRootsActive Whether the OptimismPortal is using Super Roots or Output Roots. function initialize( ISystemConfig _systemConfig, ISuperchainConfig _superchainConfig, - IAnchorStateRegistry _anchorStateRegistry + IAnchorStateRegistry _anchorStateRegistry, + bool _superRootsActive ) external - reinitializer(2) + reinitializer(initVersion()) { systemConfig = _systemConfig; superchainConfig = _superchainConfig; anchorStateRegistry = _anchorStateRegistry; + superRootsActive = _superRootsActive; // Set the l2Sender slot, only if it is currently empty. This signals the first // initialization of the contract. @@ -238,8 +257,16 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ISemver { /// @notice Upgrades the OptimismPortal contract to have a reference to the AnchorStateRegistry. /// @param _anchorStateRegistry AnchorStateRegistry contract. - function upgrade(IAnchorStateRegistry _anchorStateRegistry) external reinitializer(2) { + /// @param _superRootsActive Whether the OptimismPortal is using Super Roots or Output Roots. + function upgrade( + IAnchorStateRegistry _anchorStateRegistry, + bool _superRootsActive + ) + external + reinitializer(initVersion()) + { anchorStateRegistry = _anchorStateRegistry; + superRootsActive = _superRootsActive; } /// @notice Getter for the current paused status. @@ -307,7 +334,38 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ISemver { // Intentionally empty. } - /// @notice Proves a withdrawal transaction. + /// @notice Proves a withdrawal transaction using a Super Root proof. Only callable when the + /// OptimismPortal is using Super Roots (superRootsActive flag is true). + /// @param _tx Withdrawal transaction to finalize. + /// @param _disputeGameProxy Address of the dispute game to prove the withdrawal against. + /// @param _outputRootIndex Index of the target Output Root within the Super Root. + /// @param _superRootProof Inclusion proof of the Output Root within the Super Root. + /// @param _outputRootProof Inclusion proof of the L2ToL1MessagePasser storage root. + /// @param _withdrawalProof Inclusion proof of the withdrawal within the L2ToL1MessagePasser. + function proveWithdrawalTransaction( + Types.WithdrawalTransaction memory _tx, + IDisputeGame _disputeGameProxy, + uint256 _outputRootIndex, + Types.SuperRootProof calldata _superRootProof, + Types.OutputRootProof calldata _outputRootProof, + bytes[] calldata _withdrawalProof + ) + external + whenNotPaused + { + // Make sure that the OptimismPortal is using Super Roots. + if (!superRootsActive) { + revert OptimismPortal_WrongProofMethod(); + } + + // Prove the transaction. + _proveWithdrawalTransaction( + _tx, _disputeGameProxy, _outputRootIndex, _superRootProof, _outputRootProof, _withdrawalProof + ); + } + + /// @notice Proves a withdrawal transaction using an Output Root proof. Only callable when the + /// OptimismPortal is using Output Roots (superRootsActive flag is false). /// @param _tx Withdrawal transaction to finalize. /// @param _disputeGameIndex Index of the dispute game to prove the withdrawal against. /// @param _outputRootProof Inclusion proof of the L2ToL1MessagePasser storage root. @@ -320,43 +378,100 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ISemver { ) external whenNotPaused + { + // Make sure that the OptimismPortal is using Output Roots. + if (superRootsActive) { + revert OptimismPortal_WrongProofMethod(); + } + + // Fetch the dispute game proxy from the `DisputeGameFactory` contract. + (,, IDisputeGame disputeGameProxy) = disputeGameFactory().gameAtIndex(_disputeGameIndex); + + // Create a dummy super root proof to pass into the internal function. Note that this is + // not a valid Super Root proof but it isn't used anywhere in the internal function when + // using Output Roots. + Types.SuperRootProof memory superRootProof; + + // Prove the transaction. + _proveWithdrawalTransaction(_tx, disputeGameProxy, 0, superRootProof, _outputRootProof, _withdrawalProof); + } + + /// @notice Internal function for proving a withdrawal transaction, used by both the Super Root + /// and Output Root proof functions. Will eventually be replaced with a single function + /// when the Output Root proof method is deprecated. + /// @param _tx Withdrawal transaction to prove. + /// @param _disputeGameProxy Address of the dispute game to prove the withdrawal against. + /// @param _outputRootIndex Index of the target Output Root within the Super Root. + /// @param _superRootProof Inclusion proof of the Output Root within the Super Root. + /// @param _outputRootProof Inclusion proof of the L2ToL1MessagePasser storage root. + /// @param _withdrawalProof Inclusion proof of the withdrawal within the L2ToL1MessagePasser. + function _proveWithdrawalTransaction( + Types.WithdrawalTransaction memory _tx, + IDisputeGame _disputeGameProxy, + uint256 _outputRootIndex, + Types.SuperRootProof memory _superRootProof, + Types.OutputRootProof memory _outputRootProof, + bytes[] memory _withdrawalProof + ) + internal { // Make sure that the target address is safe. if (_isUnsafeTarget(_tx.target)) { revert OptimismPortal_BadTarget(); } - // Fetch the dispute game proxy from the `DisputeGameFactory` contract. - (,, IDisputeGame gameProxy) = disputeGameFactory().gameAtIndex(_disputeGameIndex); - // Game must be a Proper Game. - if (!anchorStateRegistry.isGameProper(gameProxy)) { + if (!anchorStateRegistry.isGameProper(_disputeGameProxy)) { revert OptimismPortal_ImproperDisputeGame(); } // Game must have been respected game type when created. - if (!anchorStateRegistry.isGameRespected(gameProxy)) { + if (!anchorStateRegistry.isGameRespected(_disputeGameProxy)) { revert OptimismPortal_InvalidDisputeGame(); } // Game must not have resolved in favor of the Challenger (invalid root claim). - if (gameProxy.status() == GameStatus.CHALLENGER_WINS) { + if (_disputeGameProxy.status() == GameStatus.CHALLENGER_WINS) { revert OptimismPortal_InvalidDisputeGame(); } - // Verify that the output root can be generated with the elements in the proof. - if (gameProxy.rootClaim().raw() != Hashing.hashOutputRootProof(_outputRootProof)) { - revert OptimismPortal_InvalidOutputRootProof(); - } - // As a sanity check, we make sure that the current timestamp is not less than or equal to // the dispute game's creation timestamp. Not strictly necessary but extra layer of // safety against weird bugs. Note that this blocks withdrawals from being proven in the // same block that a dispute game is created. - if (block.timestamp <= gameProxy.createdAt().raw()) { + if (block.timestamp <= _disputeGameProxy.createdAt().raw()) { revert OptimismPortal_InvalidProofTimestamp(); } + // Validate the provided Output Root and/or Super Root proof depending on proof method. + if (superRootsActive) { + // Verify that the super root can be generated with the elements in the proof. + if (_disputeGameProxy.rootClaim().raw() != Hashing.hashSuperRootProof(_superRootProof)) { + revert OptimismPortal_InvalidSuperRootProof(); + } + + // Check that the index exists in the super root proof. + if (_outputRootIndex >= _superRootProof.outputRoots.length) { + revert OptimismPortal_InvalidOutputRootIndex(); + } + + // Check that the output root has the correct chain id. + Types.OutputRootWithChainId memory outputRoot = _superRootProof.outputRoots[_outputRootIndex]; + if (outputRoot.chainId != systemConfig.l2ChainId()) { + revert OptimismPortal_InvalidOutputRootChainId(); + } + + // Verify that the output root can be generated with the elements in the proof. + if (outputRoot.root != Hashing.hashOutputRootProof(_outputRootProof)) { + revert OptimismPortal_InvalidOutputRootProof(); + } + } else { + // Verify that the output root can be generated with the elements in the proof. + if (_disputeGameProxy.rootClaim().raw() != Hashing.hashOutputRootProof(_outputRootProof)) { + revert OptimismPortal_InvalidOutputRootProof(); + } + } + // Load the ProvenWithdrawal into memory, using the withdrawal hash as a unique identifier. bytes32 withdrawalHash = Hashing.hashWithdrawal(_tx); @@ -389,7 +504,7 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ISemver { // the provenWithdrawals mapping. A given user may re-prove a withdrawalHash multiple // times, but each proof will reset the proof timer. provenWithdrawals[withdrawalHash][msg.sender] = - ProvenWithdrawal({ disputeGameProxy: gameProxy, timestamp: uint64(block.timestamp) }); + ProvenWithdrawal({ disputeGameProxy: _disputeGameProxy, timestamp: uint64(block.timestamp) }); // Add the proof submitter to the list of proof submitters for this withdrawal hash. proofSubmitters[withdrawalHash].push(msg.sender); diff --git a/packages/contracts-bedrock/src/L1/SystemConfig.sol b/packages/contracts-bedrock/src/L1/SystemConfig.sol index e767bc64a50ba..1828f3fb23291 100644 --- a/packages/contracts-bedrock/src/L1/SystemConfig.sol +++ b/packages/contracts-bedrock/src/L1/SystemConfig.sol @@ -3,6 +3,7 @@ pragma solidity 0.8.15; // Contracts import { OwnableUpgradeable } from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; +import { ReinitializableBase } from "src/universal/ReinitializableBase.sol"; // Libraries import { Storage } from "src/libraries/Storage.sol"; @@ -16,7 +17,7 @@ import { IResourceMetering } from "interfaces/L1/IResourceMetering.sol"; /// @notice The SystemConfig contract is used to manage configuration of an Optimism network. /// All configuration is stored on L1 and picked up by L2 as part of the derviation of /// the L2 chain. -contract SystemConfig is OwnableUpgradeable, ISemver { +contract SystemConfig is OwnableUpgradeable, ReinitializableBase, ISemver { /// @notice Enum representing different types of updates. /// @custom:value BATCHER Represents an update to the batcher hash. /// @custom:value FEE_SCALARS Represents an update to l1 data fee scalars. @@ -129,6 +130,9 @@ contract SystemConfig is OwnableUpgradeable, ISemver { /// @notice The operator fee constant. uint64 public operatorFeeConstant; + /// @notice The L2 chain ID that this SystemConfig configures. + uint256 public l2ChainId; + /// @notice Emitted when configuration is updated. /// @param version SystemConfig version. /// @param updateType Type of update. @@ -144,7 +148,7 @@ contract SystemConfig is OwnableUpgradeable, ISemver { /// @notice Constructs the SystemConfig contract. /// @dev START_BLOCK_SLOT is set to type(uint256).max here so that it will be a dead value /// in the singleton. - constructor() { + constructor() ReinitializableBase(2) { Storage.setUint(START_BLOCK_SLOT, type(uint256).max); _disableInitializers(); } @@ -161,6 +165,7 @@ contract SystemConfig is OwnableUpgradeable, ISemver { /// @param _batchInbox Batch inbox address. An identifier for the op-node to find /// canonical data. /// @param _addresses Set of L1 contract addresses. These should be the proxies. + /// @param _l2ChainId The L2 chain ID that this SystemConfig configures. function initialize( address _owner, uint32 _basefeeScalar, @@ -170,10 +175,11 @@ contract SystemConfig is OwnableUpgradeable, ISemver { address _unsafeBlockSigner, IResourceMetering.ResourceConfig memory _config, address _batchInbox, - SystemConfig.Addresses memory _addresses + SystemConfig.Addresses memory _addresses, + uint256 _l2ChainId ) public - initializer + reinitializer(initVersion()) { __Ownable_init(); transferOwnership(_owner); @@ -195,6 +201,14 @@ contract SystemConfig is OwnableUpgradeable, ISemver { _setStartBlock(); _setResourceConfig(_config); + + l2ChainId = _l2ChainId; + } + + /// @notice Upgrades the SystemConfig by setting the L2 chain ID variable. + /// @param _l2ChainId The L2 chain ID that this SystemConfig configures. + function upgrade(uint256 _l2ChainId) external reinitializer(initVersion()) { + l2ChainId = _l2ChainId; } /// @notice Returns the minimum L2 gas limit that can be safely set for the system to diff --git a/packages/contracts-bedrock/src/L1/SystemConfigInterop.sol b/packages/contracts-bedrock/src/L1/SystemConfigInterop.sol index 1d7bd8794517f..dbb9a3dbb2562 100644 --- a/packages/contracts-bedrock/src/L1/SystemConfigInterop.sol +++ b/packages/contracts-bedrock/src/L1/SystemConfigInterop.sol @@ -46,7 +46,8 @@ contract SystemConfigInterop is SystemConfig { IResourceMetering.ResourceConfig memory _config, address _batchInbox, SystemConfig.Addresses memory _addresses, - address _dependencyManager + address _dependencyManager, + uint256 _l2ChainId ) external { @@ -60,7 +61,8 @@ contract SystemConfigInterop is SystemConfig { _unsafeBlockSigner: _unsafeBlockSigner, _config: _config, _batchInbox: _batchInbox, - _addresses: _addresses + _addresses: _addresses, + _l2ChainId: _l2ChainId }); Storage.setAddress(DEPENDENCY_MANAGER_SLOT, _dependencyManager); } diff --git a/packages/contracts-bedrock/src/libraries/Encoding.sol b/packages/contracts-bedrock/src/libraries/Encoding.sol index 00c20ea459bb4..6140b62c9e655 100644 --- a/packages/contracts-bedrock/src/libraries/Encoding.sol +++ b/packages/contracts-bedrock/src/libraries/Encoding.sol @@ -9,6 +9,12 @@ import { RLPWriter } from "src/libraries/rlp/RLPWriter.sol"; /// @title Encoding /// @notice Encoding handles Optimism's various different encoding schemes. library Encoding { + /// @notice Thrown when a provided Super Root proof has an invalid version. + error Encoding_InvalidSuperRootVersion(); + + /// @notice Thrown when a provided Super Root proof has no Output Roots. + error Encoding_EmptySuperRoot(); + /// @notice RLP encodes the L2 transaction that would be generated when a given deposit is sent /// to the L2 system. Useful for searching for a deposit in the L2 system. The /// transaction is prefixed with 0x7e to identify its EIP-2718 type. @@ -262,4 +268,30 @@ library Encoding { _batcherHash ); } + + /// @notice Encodes a super root proof into the preimage of a Super Root. + /// @param _superRootProof Super root proof to encode. + /// @return Encoded super root proof. + function encodeSuperRootProof(Types.SuperRootProof memory _superRootProof) internal pure returns (bytes memory) { + // Version must match the expected version. + if (_superRootProof.version != 0x01) { + revert Encoding_InvalidSuperRootVersion(); + } + + // Output roots must not be empty. + if (_superRootProof.outputRoots.length == 0) { + revert Encoding_EmptySuperRoot(); + } + + // Start with version byte and timestamp. + bytes memory encoded = bytes.concat(bytes1(0x01), bytes8(_superRootProof.timestamp)); + + // Add each output root (chainId + root) + for (uint256 i = 0; i < _superRootProof.outputRoots.length; i++) { + Types.OutputRootWithChainId memory outputRoot = _superRootProof.outputRoots[i]; + encoded = bytes.concat(encoded, bytes32(outputRoot.chainId), outputRoot.root); + } + + return encoded; + } } diff --git a/packages/contracts-bedrock/src/libraries/Hashing.sol b/packages/contracts-bedrock/src/libraries/Hashing.sol index b736ad9e4b7e7..782bbbe4f9960 100644 --- a/packages/contracts-bedrock/src/libraries/Hashing.sol +++ b/packages/contracts-bedrock/src/libraries/Hashing.sol @@ -146,4 +146,11 @@ library Hashing { { return keccak256(abi.encode(_destination, _source, _nonce, _sender, _target, _message)); } + + /// @notice Hashes a Super Root proof into a Super Root. + /// @param _superRootProof Super Root proof to hash. + /// @return Hashed super root proof. + function hashSuperRootProof(Types.SuperRootProof memory _superRootProof) internal pure returns (bytes32) { + return keccak256(Encoding.encodeSuperRootProof(_superRootProof)); + } } diff --git a/packages/contracts-bedrock/src/libraries/Types.sol b/packages/contracts-bedrock/src/libraries/Types.sol index 7e9a65654bc19..3c18c4e1bc151 100644 --- a/packages/contracts-bedrock/src/libraries/Types.sol +++ b/packages/contracts-bedrock/src/libraries/Types.sol @@ -29,6 +29,24 @@ library Types { bytes32 latestBlockhash; } + /// @notice Struct representing an output root with a chain id. + /// @custom:field chainId The chain ID of the L2 chain that the output root commits to. + /// @custom:field root The output root. + struct OutputRootWithChainId { + uint256 chainId; + bytes32 root; + } + + /// @notice Struct representing a super root proof. + /// @custom:field version The version of the super root proof. + /// @custom:field timestamp The timestamp of the super root proof. + /// @custom:field outputRoots The output roots that are included in the super root proof. + struct SuperRootProof { + bytes1 version; + uint64 timestamp; + OutputRootWithChainId[] outputRoots; + } + /// @notice Struct representing a deposit transaction (L1 => L2 transaction) created by an end /// user (as opposed to a system deposit transaction generated by the system). /// @custom:field from Address of the sender of the transaction. diff --git a/packages/contracts-bedrock/src/universal/ReinitializableBase.sol b/packages/contracts-bedrock/src/universal/ReinitializableBase.sol new file mode 100644 index 0000000000000..056a15986e02a --- /dev/null +++ b/packages/contracts-bedrock/src/universal/ReinitializableBase.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.15; + +/// @title ReinitializableBase +/// @notice A base contract for reinitializable contracts that exposes a version number. +abstract contract ReinitializableBase { + /// @notice Thrown when the initialization version is zero. + error ReinitializableBase_ZeroInitVersion(); + + /// @notice Current initialization version. + uint8 internal immutable INIT_VERSION; + + /// @param _initVersion Current initialization version. + constructor(uint8 _initVersion) { + // Sanity check, we should never have a zero init version. + if (_initVersion == 0) revert ReinitializableBase_ZeroInitVersion(); + INIT_VERSION = _initVersion; + } + + /// @notice Getter for the current initialization version. + /// @return The current initialization version. + function initVersion() public view returns (uint8) { + return INIT_VERSION; + } +} diff --git a/packages/contracts-bedrock/test/L1/OPContractsManager.t.sol b/packages/contracts-bedrock/test/L1/OPContractsManager.t.sol index 492b608da6d6e..26119404693fe 100644 --- a/packages/contracts-bedrock/test/L1/OPContractsManager.t.sol +++ b/packages/contracts-bedrock/test/L1/OPContractsManager.t.sol @@ -144,6 +144,7 @@ contract OPContractsManager_Deploy_Test is DeployOPChain_TestBase { startingAnchorRoot: _doi.startingAnchorRoot(), saltMixer: _doi.saltMixer(), gasLimit: _doi.gasLimit(), + disputeGameUsesSuperRoots: _doi.disputeGameUsesSuperRoots(), disputeGameType: _doi.disputeGameType(), disputeAbsolutePrestate: _doi.disputeAbsolutePrestate(), disputeMaxGameDepth: _doi.disputeMaxGameDepth(), @@ -268,7 +269,8 @@ contract OPContractsManager_Upgrade_Harness is CommonTest { IOPContractsManager.OpChainConfig({ systemConfigProxy: systemConfig, proxyAdmin: proxyAdmin, - absolutePrestate: absolutePrestate + absolutePrestate: absolutePrestate, + disputeGameUsesSuperRoots: false }) ); @@ -773,6 +775,7 @@ contract OPContractsManager_AddGameType_Test is Test { l2ChainId: 100, saltMixer: "hello", gasLimit: 30_000_000, + disputeGameUsesSuperRoots: false, disputeGameType: GameType.wrap(1), disputeAbsolutePrestate: Claim.wrap( bytes32(hex"038512e02c4c3f7bdaec27d00edf55b7155e0905301e1a88083e4e0a6764d54c") diff --git a/packages/contracts-bedrock/test/L1/OPPrestateUpdater.t.sol b/packages/contracts-bedrock/test/L1/OPPrestateUpdater.t.sol index 56edac1f8fa2b..563e21d16be6c 100644 --- a/packages/contracts-bedrock/test/L1/OPPrestateUpdater.t.sol +++ b/packages/contracts-bedrock/test/L1/OPPrestateUpdater.t.sol @@ -167,6 +167,7 @@ contract OPPrestateUpdater_Test is Test { l2ChainId: 100, saltMixer: "hello", gasLimit: 30_000_000, + disputeGameUsesSuperRoots: false, disputeGameType: GameType.wrap(1), disputeAbsolutePrestate: Claim.wrap( bytes32(hex"038512e02c4c3f7bdaec27d00edf55b7155e0905301e1a88083e4e0a6764d54c") @@ -199,7 +200,10 @@ contract OPPrestateUpdater_Test is Test { function test_updatePrestate_pdgOnlyWithValidInput_succeeds() public { OPContractsManager.OpChainConfig[] memory inputs = new OPContractsManager.OpChainConfig[](1); inputs[0] = OPContractsManager.OpChainConfig( - chainDeployOutput.systemConfigProxy, chainDeployOutput.opChainProxyAdmin, Claim.wrap(bytes32(hex"ABBA")) + chainDeployOutput.systemConfigProxy, + chainDeployOutput.opChainProxyAdmin, + Claim.wrap(bytes32(hex"ABBA")), + false ); address proxyAdminOwner = chainDeployOutput.opChainProxyAdmin.owner(); @@ -230,7 +234,10 @@ contract OPPrestateUpdater_Test is Test { OPContractsManager.OpChainConfig[] memory inputs = new OPContractsManager.OpChainConfig[](1); inputs[0] = OPContractsManager.OpChainConfig( - chainDeployOutput.systemConfigProxy, chainDeployOutput.opChainProxyAdmin, Claim.wrap(bytes32(hex"ABBA")) + chainDeployOutput.systemConfigProxy, + chainDeployOutput.opChainProxyAdmin, + Claim.wrap(bytes32(hex"ABBA")), + false ); address proxyAdminOwner = chainDeployOutput.opChainProxyAdmin.owner(); @@ -267,7 +274,8 @@ contract OPPrestateUpdater_Test is Test { inputs[0] = OPContractsManager.OpChainConfig({ systemConfigProxy: chainDeployOutput.systemConfigProxy, proxyAdmin: chainDeployOutput.opChainProxyAdmin, - absolutePrestate: Claim.wrap(bytes32(0)) + absolutePrestate: Claim.wrap(bytes32(0)), + disputeGameUsesSuperRoots: false }); address proxyAdminOwner = chainDeployOutput.opChainProxyAdmin.owner(); @@ -295,6 +303,7 @@ contract OPPrestateUpdater_Test is Test { startingAnchorRoot: bytes(abi.encode(0)), saltMixer: "", gasLimit: 0, + disputeGameUsesSuperRoots: false, disputeGameType: GameType.wrap(0), disputeAbsolutePrestate: Claim.wrap(0), disputeMaxGameDepth: 0, @@ -312,7 +321,8 @@ contract OPPrestateUpdater_Test is Test { OPContractsManager.OpChainConfig({ systemConfigProxy: ISystemConfig(address(0)), proxyAdmin: IProxyAdmin(address(0)), - absolutePrestate: Claim.wrap(0) + absolutePrestate: Claim.wrap(0), + disputeGameUsesSuperRoots: false }) ); diff --git a/packages/contracts-bedrock/test/L1/OptimismPortal2.t.sol b/packages/contracts-bedrock/test/L1/OptimismPortal2.t.sol index b1d1882e84a3b..c6f02f3766856 100644 --- a/packages/contracts-bedrock/test/L1/OptimismPortal2.t.sol +++ b/packages/contracts-bedrock/test/L1/OptimismPortal2.t.sol @@ -1,12 +1,18 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.15; -// Testing +// Forge import { VmSafe } from "forge-std/Vm.sol"; +import { console2 as console } from "forge-std/console2.sol"; + +// Testing import { CommonTest } from "test/setup/CommonTest.sol"; import { NextImpl } from "test/mocks/NextImpl.sol"; import { EIP1967Helper } from "test/mocks/EIP1967Helper.sol"; +// Scripts +import { ForgeArtifacts, StorageSlot } from "scripts/libraries/ForgeArtifacts.sol"; + // Contracts import { SuperchainConfig } from "src/L1/SuperchainConfig.sol"; @@ -24,6 +30,7 @@ import { IOptimismPortal2 } from "interfaces/L1/IOptimismPortal2.sol"; import { IDisputeGame } from "interfaces/dispute/IDisputeGame.sol"; import { IFaultDisputeGame } from "interfaces/dispute/IFaultDisputeGame.sol"; import { IProxy } from "interfaces/universal/IProxy.sol"; +import { IAnchorStateRegistry } from "interfaces/dispute/IAnchorStateRegistry.sol"; contract OptimismPortal2_Test is CommonTest { address depositor; @@ -508,6 +515,24 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { assertFalse(optimismPortal2.finalizedWithdrawals(Hashing.hashWithdrawal(_defaultTx))); } + /// @notice Sets the supeRootsActive variable to the provided value. + /// @param _superRootsActive The value to set the superRootsActive variable to. + function setSuperRootsActive(bool _superRootsActive) public { + // Get the slot for superRootsActive. + StorageSlot memory slot = ForgeArtifacts.getSlot("OptimismPortal2", "superRootsActive"); + + // Load the existing storage slot value. + bytes32 existingValue = vm.load(address(optimismPortal2), bytes32(slot.slot)); + + // Inject the bool into the existing storage slot value with a bitwise OR. + // Shift the bool left by the offset of the storage slot and OR with existing value. + bytes32 newValue = + bytes32(uint256(uint8(_superRootsActive ? 1 : 0)) << slot.offset * 8 | uint256(existingValue)); + + // Store the new value at the correct slot/offset. + vm.store(address(optimismPortal2), bytes32(slot.slot), newValue); + } + /// @dev Tests that `proveWithdrawalTransaction` reverts when paused. function test_proveWithdrawalTransaction_paused_reverts() external { vm.prank(optimismPortal2.guardian()); @@ -749,6 +774,216 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { }); } + /// @dev Tests that `proveWithdrawalTransaction` reverts when using the Output Roots version of + /// `proveWithdrawalTransaction` when `superRootsActive` is true. + function test_proveWithdrawalTransaction_outputRootVersionWhenSuperRootsActive_reverts() external { + // Set superRootsActive to true. + setSuperRootsActive(true); + + // Should revert. + vm.expectRevert(IOptimismPortal2.OptimismPortal_WrongProofMethod.selector); + optimismPortal2.proveWithdrawalTransaction({ + _tx: _defaultTx, + _disputeGameIndex: _proposedGameIndex, + _outputRootProof: _outputRootProof, + _withdrawalProof: _withdrawalProof + }); + } + + /// @dev Tests that `proveWithdrawalTransaction` reverts when using the Super Roots version of + /// `proveWithdrawalTransaction` when `superRootsActive` is false. + function test_proveWithdrawalTransaction_superRootsVersionWhenSuperRootsInactive_reverts() external { + // Set up a dummy super root proof. + Types.OutputRootWithChainId[] memory outputRootWithChainIdArr = new Types.OutputRootWithChainId[](1); + outputRootWithChainIdArr[0] = + Types.OutputRootWithChainId({ root: _outputRoot, chainId: systemConfig.l2ChainId() }); + Types.SuperRootProof memory superRootProof = Types.SuperRootProof({ + version: 0x01, + timestamp: uint64(block.timestamp), + outputRoots: outputRootWithChainIdArr + }); + + // Should revert. + vm.expectRevert(IOptimismPortal2.OptimismPortal_WrongProofMethod.selector); + optimismPortal2.proveWithdrawalTransaction({ + _tx: _defaultTx, + _disputeGameProxy: game, + _outputRootIndex: 0, + _superRootProof: superRootProof, + _outputRootProof: _outputRootProof, + _withdrawalProof: _withdrawalProof + }); + } + + /// @dev Tests that `proveWithdrawalTransaction` reverts when using the Super Roots version of + /// `proveWithdrawalTransaction` when the provided proof is invalid. + function test_proveWithdrawalTransaction_superRootsVersionBadProof_reverts() external { + // Enable super roots. + setSuperRootsActive(true); + + // Set up a dummy super root proof. + Types.OutputRootWithChainId[] memory outputRootWithChainIdArr = new Types.OutputRootWithChainId[](1); + outputRootWithChainIdArr[0] = + Types.OutputRootWithChainId({ root: _outputRoot, chainId: systemConfig.l2ChainId() }); + Types.SuperRootProof memory superRootProof = Types.SuperRootProof({ + version: 0x01, + timestamp: uint64(block.timestamp), + outputRoots: outputRootWithChainIdArr + }); + + // Should revert because the proof is wrong. + vm.expectRevert(IOptimismPortal2.OptimismPortal_InvalidSuperRootProof.selector); + optimismPortal2.proveWithdrawalTransaction({ + _tx: _defaultTx, + _disputeGameProxy: game, + _outputRootIndex: 0, + _superRootProof: superRootProof, + _outputRootProof: _outputRootProof, + _withdrawalProof: _withdrawalProof + }); + } + + /// @dev Tests that `proveWithdrawalTransaction` reverts when using the Super Roots version of + /// `proveWithdrawalTransaction` when the provided proof is valid but the index is out of + /// bounds. + function test_proveWithdrawalTransaction_superRootsVersionBadIndex_reverts() external { + // Enable super roots. + setSuperRootsActive(true); + + // Set up a dummy super root proof. + Types.OutputRootWithChainId[] memory outputRootWithChainIdArr = new Types.OutputRootWithChainId[](1); + outputRootWithChainIdArr[0] = + Types.OutputRootWithChainId({ root: _outputRoot, chainId: systemConfig.l2ChainId() }); + Types.SuperRootProof memory superRootProof = Types.SuperRootProof({ + version: 0x01, + timestamp: uint64(block.timestamp), + outputRoots: outputRootWithChainIdArr + }); + + // Figure out what the right hash would be. + bytes32 expectedSuperRoot = Hashing.hashSuperRootProof(superRootProof); + + // Mock the game to return the expected super root. + vm.mockCall(address(game), abi.encodeCall(game.rootClaim, ()), abi.encode(expectedSuperRoot)); + + // Should revert because the proof is wrong. + vm.expectRevert(IOptimismPortal2.OptimismPortal_InvalidOutputRootIndex.selector); + optimismPortal2.proveWithdrawalTransaction({ + _tx: _defaultTx, + _disputeGameProxy: game, + _outputRootIndex: outputRootWithChainIdArr.length, // out of bounds + _superRootProof: superRootProof, + _outputRootProof: _outputRootProof, + _withdrawalProof: _withdrawalProof + }); + } + + /// @dev Tests that `proveWithdrawalTransaction` reverts when using the Super Roots version of + /// `proveWithdrawalTransaction` when the provided proof is valid, index is correct, but + /// the output root has the wrong chain id. + function test_proveWithdrawalTransaction_superRootsVersionBadChainId_reverts() external { + // Enable super roots. + setSuperRootsActive(true); + + // Set up a dummy super root proof. + Types.OutputRootWithChainId[] memory outputRootWithChainIdArr = new Types.OutputRootWithChainId[](1); + outputRootWithChainIdArr[0] = Types.OutputRootWithChainId({ + root: _outputRoot, + chainId: systemConfig.l2ChainId() + 1 // wrong chain id + }); + Types.SuperRootProof memory superRootProof = Types.SuperRootProof({ + version: 0x01, + timestamp: uint64(block.timestamp), + outputRoots: outputRootWithChainIdArr + }); + + // Figure out what the right hash would be. + bytes32 expectedSuperRoot = Hashing.hashSuperRootProof(superRootProof); + + // Mock the game to return the expected super root. + vm.mockCall(address(game), abi.encodeCall(game.rootClaim, ()), abi.encode(expectedSuperRoot)); + + // Should revert because the proof is wrong. + vm.expectRevert(IOptimismPortal2.OptimismPortal_InvalidOutputRootChainId.selector); + optimismPortal2.proveWithdrawalTransaction({ + _tx: _defaultTx, + _disputeGameProxy: game, + _outputRootIndex: 0, + _superRootProof: superRootProof, + _outputRootProof: _outputRootProof, + _withdrawalProof: _withdrawalProof + }); + } + + /// @dev Tests that `proveWithdrawalTransaction` reverts when using the Super Roots version of + /// `proveWithdrawalTransaction` when the provided proof is valid, index is correct, chain + /// id is correct, but the output root proof is invalid. + function test_proveWithdrawalTransaction_superRootsVersionBadOutputRootProof_reverts() external { + // Enable super roots. + setSuperRootsActive(true); + + // Set up a dummy super root proof. + Types.OutputRootWithChainId[] memory outputRootWithChainIdArr = new Types.OutputRootWithChainId[](1); + outputRootWithChainIdArr[0] = Types.OutputRootWithChainId({ + root: keccak256(abi.encode(_outputRoot)), // random root so the proof is wrong + chainId: systemConfig.l2ChainId() + }); + Types.SuperRootProof memory superRootProof = Types.SuperRootProof({ + version: 0x01, + timestamp: uint64(block.timestamp), + outputRoots: outputRootWithChainIdArr + }); + + // Figure out what the right hash would be. + bytes32 expectedSuperRoot = Hashing.hashSuperRootProof(superRootProof); + + // Mock the game to return the expected super root. + vm.mockCall(address(game), abi.encodeCall(game.rootClaim, ()), abi.encode(expectedSuperRoot)); + + // Should revert because the proof is wrong. + vm.expectRevert(IOptimismPortal2.OptimismPortal_InvalidOutputRootProof.selector); + optimismPortal2.proveWithdrawalTransaction({ + _tx: _defaultTx, + _disputeGameProxy: game, + _outputRootIndex: 0, + _superRootProof: superRootProof, + _outputRootProof: _outputRootProof, + _withdrawalProof: _withdrawalProof + }); + } + + /// @dev Tests that `proveWithdrawalTransaction` succeeds when all parameters are valid. + function test_proveWithdrawalTransaction_superRootsVersion_succeeds() external { + // Enable super roots. + setSuperRootsActive(true); + + // Set up a dummy super root proof. + Types.OutputRootWithChainId[] memory outputRootWithChainIdArr = new Types.OutputRootWithChainId[](1); + outputRootWithChainIdArr[0] = + Types.OutputRootWithChainId({ root: _outputRoot, chainId: systemConfig.l2ChainId() }); + Types.SuperRootProof memory superRootProof = Types.SuperRootProof({ + version: 0x01, + timestamp: uint64(block.timestamp), + outputRoots: outputRootWithChainIdArr + }); + + // Figure out what the right hash would be. + bytes32 expectedSuperRoot = Hashing.hashSuperRootProof(superRootProof); + + // Mock the game to return the expected super root. + vm.mockCall(address(game), abi.encodeCall(game.rootClaim, ()), abi.encode(expectedSuperRoot)); + + // Should succeed. + optimismPortal2.proveWithdrawalTransaction({ + _tx: _defaultTx, + _disputeGameProxy: game, + _outputRootIndex: 0, + _superRootProof: superRootProof, + _outputRootProof: _outputRootProof, + _withdrawalProof: _withdrawalProof + }); + } + /// @dev Tests that `proveWithdrawalTransaction` succeeds. function test_proveWithdrawalTransaction_validWithdrawalProof_succeeds() external { vm.expectEmit(true, true, true, true); @@ -1759,6 +1994,73 @@ contract OptimismPortal2_Upgradeable_Test is CommonTest { } } +/// @title OptimismPortal2_upgrade_Test +/// @notice Reusable test for the current upgrade() function in the OptimismPortal2 contract. If +/// the upgrade() function is changed, tests inside of this contract should be updated to +/// reflect the new function. If the upgrade() function is removed, remove the +/// corresponding tests but leave this contract in place so it's easy to add tests back +/// in the future. +contract OptimismPortal2_upgrade_Test is CommonTest { + function setUp() public override { + super.setUp(); + } + + /// @notice Tests that the upgrade() function succeeds. + function test_upgrade_succeeds() external { + // Get the slot for _initialized. + StorageSlot memory slot = ForgeArtifacts.getSlot("OptimismPortal2", "_initialized"); + + // Set the initialized slot to 0. + vm.store(address(optimismPortal2), bytes32(slot.slot), bytes32(0)); + + // Trigger upgrade(). + optimismPortal2.upgrade(IAnchorStateRegistry(address(0xdeadbeef)), true); + + // Verify that the initialized slot was updated. + bytes32 initializedSlotAfter = vm.load(address(optimismPortal2), bytes32(slot.slot)); + assertEq(initializedSlotAfter, bytes32(uint256(2))); + + // Verify that superRootsActive was set to true. + assertEq(optimismPortal2.superRootsActive(), true); + + // Verify that the AnchorStateRegistry was set. + assertEq(address(optimismPortal2.anchorStateRegistry()), address(0xdeadbeef)); + } + + /// @notice Tests that the upgrade() function reverts if called a second time. + function test_upgrade_upgradeTwice_reverts() external { + // Get the slot for _initialized. + StorageSlot memory slot = ForgeArtifacts.getSlot("OptimismPortal2", "_initialized"); + + // Set the initialized slot to 0. + vm.store(address(optimismPortal2), bytes32(slot.slot), bytes32(0)); + + // Trigger first upgrade. + optimismPortal2.upgrade(IAnchorStateRegistry(address(0xdeadbeef)), true); + + // Try to trigger second upgrade. + vm.expectRevert("Initializable: contract is already initialized"); + optimismPortal2.upgrade(IAnchorStateRegistry(address(0xdeadbeef)), true); + } + + /// @notice Tests that the upgrade() function reverts if called after initialization. + function test_upgrade_afterInitialization_reverts() external { + // Get the slot for _initialized. + StorageSlot memory slot = ForgeArtifacts.getSlot("OptimismPortal2", "_initialized"); + + // Slot value should be set to 2 (already initialized). + bytes32 initializedSlotBefore = vm.load(address(optimismPortal2), bytes32(slot.slot)); + assertEq(initializedSlotBefore, bytes32(uint256(2))); + + // AnchorStateRegistry address should be non-zero. + assertNotEq(address(optimismPortal2.anchorStateRegistry()), address(0)); + + // Try to trigger upgrade(). + vm.expectRevert("Initializable: contract is already initialized"); + optimismPortal2.upgrade(IAnchorStateRegistry(address(0xdeadbeef)), true); + } +} + /// @title OptimismPortal2_ResourceFuzz_Test /// @dev Test various values of the resource metering config to ensure that deposits cannot be /// broken by changing the config. diff --git a/packages/contracts-bedrock/test/L1/SystemConfig.t.sol b/packages/contracts-bedrock/test/L1/SystemConfig.t.sol index fbae6b41524b3..9fc8dfc974414 100644 --- a/packages/contracts-bedrock/test/L1/SystemConfig.t.sol +++ b/packages/contracts-bedrock/test/L1/SystemConfig.t.sol @@ -4,6 +4,9 @@ pragma solidity 0.8.15; // Testing import { CommonTest } from "test/setup/CommonTest.sol"; +// Scripts +import { ForgeArtifacts, StorageSlot } from "scripts/libraries/ForgeArtifacts.sol"; + // Libraries import { Constants } from "src/libraries/Constants.sol"; import { EIP1967Helper } from "test/mocks/EIP1967Helper.sol"; @@ -114,6 +117,7 @@ contract SystemConfig_Initialize_Test is SystemConfig_Init { assertEq(addrs.optimismPortal, address(optimismPortal2)); assertEq(address(systemConfig.optimismMintableERC20Factory()), address(optimismMintableERC20Factory)); assertEq(addrs.optimismMintableERC20Factory, address(optimismMintableERC20Factory)); + assertNotEq(systemConfig.l2ChainId(), 0); } } @@ -145,7 +149,8 @@ contract SystemConfig_Initialize_TestFail is SystemConfig_Initialize_Test { disputeGameFactory: address(0), optimismPortal: address(0), optimismMintableERC20Factory: address(0) - }) + }), + _l2ChainId: 1234 }); } @@ -174,7 +179,8 @@ contract SystemConfig_Initialize_TestFail is SystemConfig_Initialize_Test { disputeGameFactory: address(0), optimismPortal: address(0), optimismMintableERC20Factory: address(0) - }) + }), + _l2ChainId: 1234 }); assertEq(systemConfig.startBlock(), block.number); } @@ -204,7 +210,8 @@ contract SystemConfig_Initialize_TestFail is SystemConfig_Initialize_Test { disputeGameFactory: address(0), optimismPortal: address(0), optimismMintableERC20Factory: address(0) - }) + }), + _l2ChainId: 1234 }); assertEq(systemConfig.startBlock(), 1); } @@ -318,7 +325,8 @@ contract SystemConfig_Init_ResourceConfig is SystemConfig_Init { disputeGameFactory: address(0), optimismPortal: address(0), optimismMintableERC20Factory: address(0) - }) + }), + _l2ChainId: 1234 }); } } @@ -480,3 +488,63 @@ contract SystemConfig_Setters_Test is SystemConfig_Init { assertEq(systemConfig.eip1559Elasticity(), _elasticity); } } + +/// @title SystemConfig_upgrade_Test +/// @notice Reusable test for the current upgrade() function in the SystemConfig contract. If +/// the upgrade() function is changed, tests inside of this contract should be updated to +/// reflect the new function. If the upgrade() function is removed, remove the +/// corresponding tests but leave this contract in place so it's easy to add tests back +/// in the future. +contract SystemConfig_upgrade_Test is SystemConfig_Init { + /// @notice Tests that the upgrade() function succeeds. + function test_upgrade_succeeds() external { + // Get the slot for _initialized. + StorageSlot memory slot = ForgeArtifacts.getSlot("SystemConfig", "_initialized"); + + // Set the initialized slot to 0. + vm.store(address(systemConfig), bytes32(slot.slot), bytes32(0)); + + // Trigger upgrade(). + systemConfig.upgrade(1234); + + // Verify that the initialized slot was updated. + bytes32 initializedSlotAfter = vm.load(address(systemConfig), bytes32(slot.slot)); + assertEq(initializedSlotAfter, bytes32(uint256(2))); + + // Verify that the l2ChainId was updated. + assertEq(systemConfig.l2ChainId(), 1234); + } + + /// @notice Tests that the upgrade() function reverts if called a second time. + function test_upgrade_upgradeTwice_reverts() external { + // Get the slot for _initialized. + StorageSlot memory slot = ForgeArtifacts.getSlot("SystemConfig", "_initialized"); + + // Set the initialized slot to 0. + vm.store(address(systemConfig), bytes32(slot.slot), bytes32(0)); + + // Trigger first upgrade. + systemConfig.upgrade(1234); + + // Try to trigger second upgrade. + vm.expectRevert("Initializable: contract is already initialized"); + systemConfig.upgrade(1234); + } + + /// @notice Tests that the upgrade() function reverts if called after initialization. + function test_upgrade_afterInitialization_reverts() external { + // Get the slot for _initialized. + StorageSlot memory slot = ForgeArtifacts.getSlot("SystemConfig", "_initialized"); + + // Slot value should be set to 2 (already initialized). + bytes32 initializedSlotBefore = vm.load(address(systemConfig), bytes32(slot.slot)); + assertEq(initializedSlotBefore, bytes32(uint256(2))); + + // l2ChainId should be non-zero. + assertNotEq(systemConfig.l2ChainId(), 0); + + // Try to trigger upgrade(). + vm.expectRevert("Initializable: contract is already initialized"); + systemConfig.upgrade(1234); + } +} diff --git a/packages/contracts-bedrock/test/invariants/SystemConfig.t.sol b/packages/contracts-bedrock/test/invariants/SystemConfig.t.sol index 4ca84d96b0209..2107e15dc5021 100644 --- a/packages/contracts-bedrock/test/invariants/SystemConfig.t.sol +++ b/packages/contracts-bedrock/test/invariants/SystemConfig.t.sol @@ -45,7 +45,8 @@ contract SystemConfig_GasLimitBoundaries_Invariant is Test { disputeGameFactory: address(0), optimismPortal: address(0), optimismMintableERC20Factory: address(0) - }) + }), + 1234 // _l2ChainId ) ) ); diff --git a/packages/contracts-bedrock/test/libraries/Encoding.t.sol b/packages/contracts-bedrock/test/libraries/Encoding.t.sol index 277cce328dcdd..76ab3343d74c6 100644 --- a/packages/contracts-bedrock/test/libraries/Encoding.t.sol +++ b/packages/contracts-bedrock/test/libraries/Encoding.t.sol @@ -1,17 +1,24 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.15; -// Testing utilities +// Testing import { CommonTest } from "test/setup/CommonTest.sol"; // Libraries +import { Encoding } from "src/libraries/Encoding.sol"; import { Types } from "src/libraries/Types.sol"; import { LegacyCrossDomainUtils } from "src/libraries/LegacyCrossDomainUtils.sol"; -// Target contract -import { Encoding } from "src/libraries/Encoding.sol"; +contract Encoding_TestInit is CommonTest { + Encoding_Harness encoding; -contract Encoding_Test is CommonTest { + function setUp() public override { + super.setUp(); + encoding = new Encoding_Harness(); + } +} + +contract Encoding_Test is Encoding_TestInit { /// @dev Tests encoding and decoding a nonce and version. function testFuzz_nonceVersioning_succeeds(uint240 _nonce, uint16 _version) external pure { (uint240 nonce, uint16 version) = Encoding.decodeVersionedNonce(Encoding.encodeVersionedNonce(_nonce, _version)); @@ -77,8 +84,6 @@ contract Encoding_Test is CommonTest { uint256 minInvalidNonce = (uint256(type(uint240).max) + 1) * 2; nonce = bound(nonce, minInvalidNonce, type(uint256).max); - EncodingContract encoding = new EncodingContract(); - vm.expectRevert(bytes("Encoding: unknown cross domain message version")); encoding.encodeCrossDomainMessage(nonce, address(this), address(this), 1, 100, hex""); } @@ -107,7 +112,182 @@ contract Encoding_Test is CommonTest { } } -contract EncodingContract { +contract Encoding_encodeSuperRootProof_Test is Encoding_TestInit { + /// @notice Tests successful encoding of a valid super root proof + /// @param _timestamp The timestamp of the super root proof + /// @param _length The number of output roots in the super root proof + /// @param _seed The seed used to generate the output roots + function testFuzz_encodeSuperRootProof_succeeds(uint64 _timestamp, uint256 _length, uint256 _seed) external pure { + // Ensure at least 1 element and cap at a reasonable maximum to avoid gas issues + _length = uint256(bound(_length, 1, 50)); + + // Create output roots array + Types.OutputRootWithChainId[] memory outputRoots = new Types.OutputRootWithChainId[](_length); + + // Generate deterministic chain IDs and roots based on the seed + for (uint256 i = 0; i < _length; i++) { + // Use different derivations of the seed for each value + uint256 chainId = uint256(keccak256(abi.encode(_seed, "chainId", i))); + bytes32 root = keccak256(abi.encode(_seed, "root", i)); + + outputRoots[i] = Types.OutputRootWithChainId({ chainId: chainId, root: root }); + } + + // Create the super root proof + Types.SuperRootProof memory proof = + Types.SuperRootProof({ version: 0x01, timestamp: _timestamp, outputRoots: outputRoots }); + + // Encode the proof + bytes memory encoded = Encoding.encodeSuperRootProof(proof); + + // Verify encoding structure + assertEq(encoded[0], bytes1(0x01), "Version byte should be 0x01"); + + // Verify timestamp (bytes 1-8) + bytes8 encodedTimestamp; + for (uint256 i = 0; i < 8; i++) { + encodedTimestamp |= bytes8(encoded[i + 1]) >> (i * 8); + } + assertEq(uint64(encodedTimestamp), _timestamp, "Timestamp should match"); + + // Verify each chain ID and root is encoded correctly + uint256 offset = 9; // 1 byte version + 8 bytes timestamp + for (uint256 i = 0; i < _length; i++) { + // Extract chain ID (32 bytes) + uint256 encodedChainId; + assembly { + // Load 32 bytes from encoded at position offset + encodedChainId := mload(add(add(encoded, 32), offset)) + } + assertEq(encodedChainId, outputRoots[i].chainId, "Chain ID should match"); + offset += 32; + + // Extract root (32 bytes) + bytes32 encodedRoot; + assembly { + // Load 32 bytes from encoded at position offset + encodedRoot := mload(add(add(encoded, 32), offset)) + } + assertEq(encodedRoot, outputRoots[i].root, "Root should match"); + offset += 32; + } + + // Verify total length + assertEq(encoded.length, 9 + (_length * 64), "Encoded length should match expected"); + } + + /// @notice Tests encoding with a single output root + function test_encodeSuperRootProof_singleOutputRoot_succeeds() external pure { + // Create a single output root + Types.OutputRootWithChainId[] memory outputRoots = new Types.OutputRootWithChainId[](1); + outputRoots[0] = Types.OutputRootWithChainId({ chainId: 10, root: bytes32(uint256(0xdeadbeef)) }); + + // Create the super root proof + Types.SuperRootProof memory proof = + Types.SuperRootProof({ version: 0x01, timestamp: 1234567890, outputRoots: outputRoots }); + + // Encode the proof + bytes memory encoded = Encoding.encodeSuperRootProof(proof); + + // Expected: 1 byte version + 8 bytes timestamp + (32 bytes chainId + 32 bytes root) + assertEq(encoded.length, 1 + 8 + 64, "Encoded length should be 73 bytes"); + assertEq(encoded[0], bytes1(0x01), "First byte should be version 0x01"); + } + + /// @notice Tests encoding with multiple output roots + function test_encodeSuperRootProof_multipleOutputRoots_succeeds() external pure { + // Create multiple output roots + Types.OutputRootWithChainId[] memory outputRoots = new Types.OutputRootWithChainId[](3); + outputRoots[0] = Types.OutputRootWithChainId({ chainId: 10, root: bytes32(uint256(0xdeadbeef)) }); + outputRoots[1] = Types.OutputRootWithChainId({ chainId: 20, root: bytes32(uint256(0xbeefcafe)) }); + outputRoots[2] = Types.OutputRootWithChainId({ chainId: 30, root: bytes32(uint256(0xcafebabe)) }); + + // Create the super root proof + Types.SuperRootProof memory proof = + Types.SuperRootProof({ version: 0x01, timestamp: 1234567890, outputRoots: outputRoots }); + + // Encode the proof + bytes memory encoded = Encoding.encodeSuperRootProof(proof); + + // Expected: 1 byte version + 8 bytes timestamp + 3 * (32 bytes chainId + 32 bytes root) + assertEq(encoded.length, 1 + 8 + (3 * 64), "Encoded length should be 201 bytes"); + } + + /// @notice Tests that the Solidity impl of encodeSuperRootProof matches the FFI impl + /// @param _timestamp The timestamp of the super root proof + /// @param _length The number of output roots in the super root proof + /// @param _seed The seed used to generate the output roots + function testDiff_encodeSuperRootProof_succeeds(uint64 _timestamp, uint256 _length, uint256 _seed) external { + // Ensure at least 1 element and cap at a reasonable maximum to avoid gas issues + _length = uint256(bound(_length, 1, 50)); + + // Create output roots array + Types.OutputRootWithChainId[] memory outputRoots = new Types.OutputRootWithChainId[](_length); + + // Generate deterministic chain IDs and roots based on the seed + for (uint256 i = 0; i < _length; i++) { + // Use different derivations of the seed for each value + uint256 chainId = uint256(keccak256(abi.encode(_seed, "chainId", i))); + bytes32 root = keccak256(abi.encode(_seed, "root", i)); + + outputRoots[i] = Types.OutputRootWithChainId({ chainId: chainId, root: root }); + } + + // Create the super root proof + Types.SuperRootProof memory proof = + Types.SuperRootProof({ version: 0x01, timestamp: _timestamp, outputRoots: outputRoots }); + + // Encode using the Solidity implementation + bytes memory encoding1 = Encoding.encodeSuperRootProof(proof); + + // Encode using the FFI implementation + bytes memory encoding2 = ffi.encodeSuperRootProof(proof); + + // Compare the results + assertEq(encoding1, encoding2, "Solidity and FFI implementations should match"); + } +} + +contract Encoding_encodeSuperRootProof_TestFail is Encoding_TestInit { + /// @notice Tests that encoding fails when version is not 0x01 + /// @param _version The version to use for the super root proof + /// @param _timestamp The timestamp of the super root proof + function testFuzz_encodeSuperRootProof_invalidVersion_reverts(bytes1 _version, uint64 _timestamp) external { + // Ensure version is not 0x01 + if (_version == 0x01) { + _version = 0x02; + } + + // Create a minimal valid output roots array + Types.OutputRootWithChainId[] memory outputRoots = new Types.OutputRootWithChainId[](1); + outputRoots[0] = Types.OutputRootWithChainId({ chainId: 1, root: bytes32(uint256(1)) }); + + // Create the super root proof with invalid version + Types.SuperRootProof memory proof = + Types.SuperRootProof({ version: _version, timestamp: _timestamp, outputRoots: outputRoots }); + + // Expect revert when encoding + vm.expectRevert(Encoding.Encoding_InvalidSuperRootVersion.selector); + encoding.encodeSuperRootProof(proof); + } + + /// @notice Tests that encoding fails when output roots array is empty + /// @param _timestamp The timestamp of the super root proof + function testFuzz_encodeSuperRootProof_emptyOutputRoots_reverts(uint64 _timestamp) external { + // Create an empty output roots array + Types.OutputRootWithChainId[] memory outputRoots = new Types.OutputRootWithChainId[](0); + + // Create the super root proof with empty output roots + Types.SuperRootProof memory proof = + Types.SuperRootProof({ version: 0x01, timestamp: _timestamp, outputRoots: outputRoots }); + + // Expect revert when encoding + vm.expectRevert(Encoding.Encoding_EmptySuperRoot.selector); + encoding.encodeSuperRootProof(proof); + } +} + +contract Encoding_Harness { function encodeCrossDomainMessage( uint256 nonce, address sender, @@ -122,4 +302,8 @@ contract EncodingContract { { return Encoding.encodeCrossDomainMessage(nonce, sender, target, value, gasLimit, data); } + + function encodeSuperRootProof(Types.SuperRootProof memory proof) external pure returns (bytes memory) { + return Encoding.encodeSuperRootProof(proof); + } } diff --git a/packages/contracts-bedrock/test/libraries/Hashing.t.sol b/packages/contracts-bedrock/test/libraries/Hashing.t.sol index 45242acb088c6..73aef7cb22023 100644 --- a/packages/contracts-bedrock/test/libraries/Hashing.t.sol +++ b/packages/contracts-bedrock/test/libraries/Hashing.t.sol @@ -12,6 +12,12 @@ import { LegacyCrossDomainUtils } from "src/libraries/LegacyCrossDomainUtils.sol // Target contract import { Hashing } from "src/libraries/Hashing.sol"; +contract Hashing_Harness { + function hashSuperRootProof(Types.SuperRootProof memory _proof) external pure returns (bytes32) { + return Hashing.hashSuperRootProof(_proof); + } +} + contract Hashing_hashDepositSource_Test is CommonTest { /// @notice Tests that hashDepositSource returns the correct hash in a simple case. function test_hashDepositSource_succeeds() external pure { @@ -136,3 +142,51 @@ contract Hashing_hashDepositTransaction_Test is CommonTest { ); } } + +contract Hashing_hashSuperRootProof_Test is CommonTest { + Hashing_Harness internal harness; + + /// @notice Sets up the test. + function setUp() public override { + super.setUp(); + harness = new Hashing_Harness(); + } + + /// @notice Tests that the Solidity impl of hashSuperRootProof matches the FFI impl + /// @param _proof The super root proof to test. + function testDiff_hashSuperRootProof_succeeds(Types.SuperRootProof memory _proof) external { + // Make sure the proof has the right version. + _proof.version = 0x01; + + // Make sure the proof has at least one output root. + if (_proof.outputRoots.length == 0) { + _proof.outputRoots = new Types.OutputRootWithChainId[](1); + _proof.outputRoots[0] = Types.OutputRootWithChainId({ + chainId: vm.randomUint(0, type(uint64).max), + root: bytes32(vm.randomUint()) + }); + } + + // Encode using the Solidity implementation + bytes32 hash1 = harness.hashSuperRootProof(_proof); + + // Encode using the FFI implementation + bytes32 hash2 = ffi.hashSuperRootProof(_proof); + + // Compare the results + assertEq(hash1, hash2, "Solidity and FFI implementations should match"); + } + + /// @notice Tests that hashSuperRootProof reverts when the version is incorrect. + /// @param _proof The super root proof to test. + function testFuzz_hashSuperRootProof_wrongVersion_reverts(Types.SuperRootProof memory _proof) external { + // 0x01 is the correct version, so we need any other version. + if (_proof.version == 0x01) { + _proof.version = 0x00; + } + + // Should always revert when the version is incorrect. + vm.expectRevert(Encoding.Encoding_InvalidSuperRootVersion.selector); + harness.hashSuperRootProof(_proof); + } +} diff --git a/packages/contracts-bedrock/test/opcm/UpgradeOPChain.t.sol b/packages/contracts-bedrock/test/opcm/UpgradeOPChain.t.sol index 06e20c078c122..8b4944be7077a 100644 --- a/packages/contracts-bedrock/test/opcm/UpgradeOPChain.t.sol +++ b/packages/contracts-bedrock/test/opcm/UpgradeOPChain.t.sol @@ -55,7 +55,8 @@ contract UpgradeOPChainInput_Test is Test { configs[0] = OPContractsManager.OpChainConfig({ systemConfigProxy: ISystemConfig(systemConfig1), proxyAdmin: IProxyAdmin(proxyAdmin1), - absolutePrestate: Claim.wrap(bytes32(uint256(1))) + absolutePrestate: Claim.wrap(bytes32(uint256(1))), + disputeGameUsesSuperRoots: false }); // Setup mock addresses and contracts for second config @@ -67,7 +68,8 @@ contract UpgradeOPChainInput_Test is Test { configs[1] = OPContractsManager.OpChainConfig({ systemConfigProxy: ISystemConfig(systemConfig2), proxyAdmin: IProxyAdmin(proxyAdmin2), - absolutePrestate: Claim.wrap(bytes32(uint256(2))) + absolutePrestate: Claim.wrap(bytes32(uint256(2))), + disputeGameUsesSuperRoots: false }); input.set(input.opChainConfigs.selector, configs); @@ -111,7 +113,8 @@ contract UpgradeOPChainInput_Test is Test { configs[0] = OPContractsManager.OpChainConfig({ systemConfigProxy: ISystemConfig(mockSystemConfig), proxyAdmin: IProxyAdmin(mockProxyAdmin), - absolutePrestate: Claim.wrap(bytes32(uint256(1))) + absolutePrestate: Claim.wrap(bytes32(uint256(1))), + disputeGameUsesSuperRoots: false }); vm.expectRevert("UpgradeOPCMInput: unknown selector"); @@ -147,7 +150,8 @@ contract UpgradeOPChain_Test is Test { config = OPContractsManager.OpChainConfig({ systemConfigProxy: ISystemConfig(makeAddr("systemConfigProxy")), proxyAdmin: IProxyAdmin(makeAddr("proxyAdmin")), - absolutePrestate: Claim.wrap(keccak256("absolutePrestate")) + absolutePrestate: Claim.wrap(keccak256("absolutePrestate")), + disputeGameUsesSuperRoots: false }); OPContractsManager.OpChainConfig[] memory configs = new OPContractsManager.OpChainConfig[](1); configs[0] = config; diff --git a/packages/contracts-bedrock/test/setup/FFIInterface.sol b/packages/contracts-bedrock/test/setup/FFIInterface.sol index c1e1612da8e6b..97ed21cbc7fa6 100644 --- a/packages/contracts-bedrock/test/setup/FFIInterface.sol +++ b/packages/contracts-bedrock/test/setup/FFIInterface.sol @@ -188,6 +188,28 @@ contract FFIInterface { return abi.decode(result, (bytes)); } + function encodeSuperRootProof(Types.SuperRootProof calldata proof) external returns (bytes memory) { + string[] memory cmds = new string[](4); + cmds[0] = "scripts/go-ffi/go-ffi"; + cmds[1] = "diff"; + cmds[2] = "encodeSuperRootProof"; + cmds[3] = vm.toString(abi.encode(proof)); + + bytes memory result = Process.run(cmds); + return abi.decode(result, (bytes)); + } + + function hashSuperRootProof(Types.SuperRootProof calldata proof) external returns (bytes32) { + string[] memory cmds = new string[](4); + cmds[0] = "scripts/go-ffi/go-ffi"; + cmds[1] = "diff"; + cmds[2] = "hashSuperRootProof"; + cmds[3] = vm.toString(abi.encode(proof)); + + bytes memory result = Process.run(cmds); + return abi.decode(result, (bytes32)); + } + function decodeVersionedNonce(uint256 nonce) external returns (uint256, uint256) { string[] memory cmds = new string[](4); cmds[0] = "scripts/go-ffi/go-ffi"; diff --git a/packages/contracts-bedrock/test/setup/ForkLive.s.sol b/packages/contracts-bedrock/test/setup/ForkLive.s.sol index 3de0214c8ea2c..80b516f4ee09e 100644 --- a/packages/contracts-bedrock/test/setup/ForkLive.s.sol +++ b/packages/contracts-bedrock/test/setup/ForkLive.s.sol @@ -177,7 +177,8 @@ contract ForkLive is Deployer { opChains[0] = IOPContractsManager.OpChainConfig({ systemConfigProxy: systemConfig, proxyAdmin: proxyAdmin, - absolutePrestate: Claim.wrap(bytes32(keccak256("absolutePrestate"))) + absolutePrestate: Claim.wrap(bytes32(keccak256("absolutePrestate"))), + disputeGameUsesSuperRoots: false }); // Temporarily replace the upgrader with a DelegateCaller so we can test the upgrade, diff --git a/packages/contracts-bedrock/test/universal/ReinitializableBase.t.sol b/packages/contracts-bedrock/test/universal/ReinitializableBase.t.sol new file mode 100644 index 0000000000000..1c3f7d93eb052 --- /dev/null +++ b/packages/contracts-bedrock/test/universal/ReinitializableBase.t.sol @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.15; + +// Testing +import { Test } from "forge-std/Test.sol"; + +// Contracts +import { ReinitializableBase } from "src/universal/ReinitializableBase.sol"; + +contract ReinitializableBase_Test is Test { + /// @notice Tests that the contract is created correctly and initVersion returns the right + /// value when the provided init version is non-zero. + /// @param _initVersion The init version to use when creating the contract. + function testFuzz_initVersion_validVersion_succeeds(uint8 _initVersion) public { + // Zero version not allowed. + _initVersion = uint8(bound(_initVersion, 1, type(uint8).max)); + + // Deploy the reinitializable contract. + ReinitializableBase_Harness harness = new ReinitializableBase_Harness(_initVersion); + + // Check the init version. + assertEq(harness.initVersion(), _initVersion); + } +} + +contract ReinitializableBase_TestFail is Test { + /// @notice Tests that the contract creation reverts when the init version is zero. + function test_initVersion_zeroVersion_reverts() public { + vm.expectRevert(ReinitializableBase.ReinitializableBase_ZeroInitVersion.selector); + new ReinitializableBase_Harness(0); + } +} + +contract ReinitializableBase_Harness is ReinitializableBase { + constructor(uint8 _initVersion) ReinitializableBase(_initVersion) { } +} diff --git a/packages/contracts-bedrock/test/universal/Specs.t.sol b/packages/contracts-bedrock/test/universal/Specs.t.sol index 0d45a66402bf3..ad8cdc4ab70a4 100644 --- a/packages/contracts-bedrock/test/universal/Specs.t.sol +++ b/packages/contracts-bedrock/test/universal/Specs.t.sol @@ -228,14 +228,25 @@ contract Specification_Test is CommonTest { }); _addSpec({ _name: "OptimismPortalInterop", _sel: _getSel("finalizedWithdrawals(bytes32)") }); _addSpec({ _name: "OptimismPortalInterop", _sel: _getSel("guardian()") }); - _addSpec({ _name: "OptimismPortalInterop", _sel: _getSel("initialize(address,address,address)") }); + _addSpec({ _name: "OptimismPortalInterop", _sel: _getSel("initialize(address,address,address,bool)") }); _addSpec({ _name: "OptimismPortalInterop", _sel: _getSel("l2Sender()") }); _addSpec({ _name: "OptimismPortalInterop", _sel: _getSel("minimumGasLimit(uint64)") }); _addSpec({ _name: "OptimismPortalInterop", _sel: _getSel("params()") }); _addSpec({ _name: "OptimismPortalInterop", _sel: _getSel("paused()") }); + _addSpec({ _name: "OptimismPortalInterop", _sel: _getSel("initVersion()") }); + _addSpec({ _name: "OptimismPortalInterop", _sel: _getSel("superRootsActive()") }); _addSpec({ _name: "OptimismPortalInterop", - _sel: IOptimismPortal2.proveWithdrawalTransaction.selector, + _sel: _getSel( + "proveWithdrawalTransaction((uint256,address,address,uint256,uint256,bytes),uint256,(bytes32,bytes32,bytes32,bytes32),bytes[])" + ), + _pausable: true + }); + _addSpec({ + _name: "OptimismPortalInterop", + _sel: _getSel( + "proveWithdrawalTransaction((uint256,address,address,uint256,uint256,bytes),address,uint256,(bytes1,uint64,(uint256,bytes32)[]),(bytes32,bytes32,bytes32,bytes32),bytes[])" + ), _pausable: true }); _addSpec({ _name: "OptimismPortalInterop", _sel: _getSel("provenWithdrawals(bytes32,address)") }); @@ -249,7 +260,7 @@ contract Specification_Test is CommonTest { _addSpec({ _name: "OptimismPortalInterop", _sel: _getSel("respectedGameTypeUpdatedAt()") }); _addSpec({ _name: "OptimismPortalInterop", _sel: _getSel("proofSubmitters(bytes32,uint256)") }); _addSpec({ _name: "OptimismPortalInterop", _sel: _getSel("numProofSubmitters(bytes32)") }); - _addSpec({ _name: "OptimismPortalInterop", _sel: _getSel("upgrade(address)") }); + _addSpec({ _name: "OptimismPortalInterop", _sel: _getSel("upgrade(address,bool)") }); _addSpec({ _name: "OptimismPortalInterop", _sel: IOptimismPortalInterop.setConfig.selector, @@ -273,14 +284,25 @@ contract Specification_Test is CommonTest { }); _addSpec({ _name: "OptimismPortal2", _sel: _getSel("finalizedWithdrawals(bytes32)") }); _addSpec({ _name: "OptimismPortal2", _sel: _getSel("guardian()") }); - _addSpec({ _name: "OptimismPortal2", _sel: _getSel("initialize(address,address,address)") }); + _addSpec({ _name: "OptimismPortal2", _sel: _getSel("initialize(address,address,address,bool)") }); _addSpec({ _name: "OptimismPortal2", _sel: _getSel("l2Sender()") }); _addSpec({ _name: "OptimismPortal2", _sel: _getSel("minimumGasLimit(uint64)") }); _addSpec({ _name: "OptimismPortal2", _sel: _getSel("params()") }); _addSpec({ _name: "OptimismPortal2", _sel: _getSel("paused()") }); + _addSpec({ _name: "OptimismPortal2", _sel: _getSel("initVersion()") }); + _addSpec({ _name: "OptimismPortal2", _sel: _getSel("superRootsActive()") }); _addSpec({ _name: "OptimismPortal2", - _sel: IOptimismPortal2.proveWithdrawalTransaction.selector, + _sel: _getSel( + "proveWithdrawalTransaction((uint256,address,address,uint256,uint256,bytes),uint256,(bytes32,bytes32,bytes32,bytes32),bytes[])" + ), + _pausable: true + }); + _addSpec({ + _name: "OptimismPortal2", + _sel: _getSel( + "proveWithdrawalTransaction((uint256,address,address,uint256,uint256,bytes),address,uint256,(bytes1,uint64,(uint256,bytes32)[]),(bytes32,bytes32,bytes32,bytes32),bytes[])" + ), _pausable: true }); _addSpec({ _name: "OptimismPortal2", _sel: _getSel("provenWithdrawals(bytes32,address)") }); @@ -294,7 +316,7 @@ contract Specification_Test is CommonTest { _addSpec({ _name: "OptimismPortal2", _sel: _getSel("respectedGameTypeUpdatedAt()") }); _addSpec({ _name: "OptimismPortal2", _sel: _getSel("proofSubmitters(bytes32,uint256)") }); _addSpec({ _name: "OptimismPortal2", _sel: _getSel("numProofSubmitters(bytes32)") }); - _addSpec({ _name: "OptimismPortal2", _sel: _getSel("upgrade(address)") }); + _addSpec({ _name: "OptimismPortal2", _sel: _getSel("upgrade(address,bool)") }); // ProtocolVersions _addSpec({ _name: "ProtocolVersions", _sel: _getSel("RECOMMENDED_SLOT()") }); @@ -389,6 +411,9 @@ contract Specification_Test is CommonTest { _addSpec({ _name: "SystemConfig", _sel: _getSel("blobbasefeeScalar()") }); _addSpec({ _name: "SystemConfig", _sel: _getSel("maximumGasLimit()") }); _addSpec({ _name: "SystemConfig", _sel: _getSel("getAddresses()") }); + _addSpec({ _name: "SystemConfig", _sel: _getSel("l2ChainId()") }); + _addSpec({ _name: "SystemConfig", _sel: _getSel("upgrade(uint256)") }); + _addSpec({ _name: "SystemConfig", _sel: _getSel("initVersion()") }); // SystemConfigInterop _addSpec({ _name: "SystemConfigInterop", _sel: _getSel("UNSAFE_BLOCK_SIGNER_SLOT()") }); @@ -476,7 +501,9 @@ contract Specification_Test is CommonTest { }); _addSpec({ _name: "SystemConfigInterop", _sel: _getSel("dependencyManager()") }); _addSpec({ _name: "SystemConfigInterop", _sel: _getSel("getAddresses()") }); - + _addSpec({ _name: "SystemConfigInterop", _sel: _getSel("l2ChainId()") }); + _addSpec({ _name: "SystemConfigInterop", _sel: _getSel("upgrade(uint256)") }); + _addSpec({ _name: "SystemConfigInterop", _sel: _getSel("initVersion()") }); // ProxyAdmin _addSpec({ _name: "ProxyAdmin", _sel: _getSel("addressManager()") }); _addSpec({ diff --git a/packages/contracts-bedrock/test/vendor/Initializable.t.sol b/packages/contracts-bedrock/test/vendor/Initializable.t.sol index 8464682c88c45..fa48af8acf2ca 100644 --- a/packages/contracts-bedrock/test/vendor/Initializable.t.sol +++ b/packages/contracts-bedrock/test/vendor/Initializable.t.sol @@ -123,7 +123,7 @@ contract Initializer_Test is CommonTest { name: "OptimismPortal2Impl", target: EIP1967Helper.getImplementation(address(optimismPortal2)), initCalldata: abi.encodeCall( - optimismPortal2.initialize, (systemConfig, superchainConfig, anchorStateRegistry) + optimismPortal2.initialize, (systemConfig, superchainConfig, anchorStateRegistry, false) ) }) ); @@ -133,7 +133,7 @@ contract Initializer_Test is CommonTest { name: "OptimismPortal2Proxy", target: address(optimismPortal2), initCalldata: abi.encodeCall( - optimismPortal2.initialize, (systemConfig, superchainConfig, anchorStateRegistry) + optimismPortal2.initialize, (systemConfig, superchainConfig, anchorStateRegistry, false) ) }) ); @@ -167,7 +167,8 @@ contract Initializer_Test is CommonTest { disputeGameFactory: address(0), optimismPortal: address(0), optimismMintableERC20Factory: address(0) - }) + }), + 0 ) ) }) @@ -202,7 +203,8 @@ contract Initializer_Test is CommonTest { disputeGameFactory: address(0), optimismPortal: address(0), optimismMintableERC20Factory: address(0) - }) + }), + 0 ) ) }) From ffa4d6e6ac7cf498e35831f3046c2e2ee1e7e01c Mon Sep 17 00:00:00 2001 From: Kelvin Fichter Date: Wed, 5 Mar 2025 14:31:27 -0600 Subject: [PATCH 17/50] feat: interop portal OPCM updates --- .../interfaces/L1/IOPContractsManager.sol | 19 +- .../snapshots/abi/OPContractsManager.json | 40 +- .../abi/OPContractsManagerDeployer.json | 2 +- .../OPContractsManagerDeployerInterop.json | 2 +- .../abi/OPContractsManagerGameTypeAdder.json | 5 + .../abi/OPContractsManagerUpgrader.json | 10 +- .../snapshots/semver-lock.json | 26 +- .../src/L1/OPContractsManager.sol | 141 +++---- .../src/L1/OPContractsManagerInterop.sol | 80 ---- .../src/L1/OPPrestateUpdater.sol | 172 -------- .../src/L1/OptimismPortalInterop.sol | 4 +- .../contracts-bedrock/src/L1/SystemConfig.sol | 4 +- .../src/L1/SystemConfigInterop.sol | 4 +- .../test/L1/OPContractsManager.t.sol | 18 +- .../test/L1/OPPrestateUpdater.t.sol | 395 ------------------ 15 files changed, 124 insertions(+), 798 deletions(-) delete mode 100644 packages/contracts-bedrock/src/L1/OPContractsManagerInterop.sol delete mode 100644 packages/contracts-bedrock/src/L1/OPPrestateUpdater.sol delete mode 100644 packages/contracts-bedrock/test/L1/OPPrestateUpdater.t.sol diff --git a/packages/contracts-bedrock/interfaces/L1/IOPContractsManager.sol b/packages/contracts-bedrock/interfaces/L1/IOPContractsManager.sol index 67f3e2a5e9bdb..6b7bac8297129 100644 --- a/packages/contracts-bedrock/interfaces/L1/IOPContractsManager.sol +++ b/packages/contracts-bedrock/interfaces/L1/IOPContractsManager.sol @@ -229,23 +229,8 @@ interface IOPContractsManager { /// @notice Thrown when an invalid `l2ChainId` is provided to `deploy`. error InvalidChainId(); - /// @notice Thrown when a role's address is not valid (opChainProxyAdminOwner). - error InvalidRoleAddressPAO(); - - /// @notice Thrown when a role's address is not valid (systemConfigOwner). - error InvalidRoleAddressSCO(); - - /// @notice Thrown when a role's address is not valid (batcher). - error InvalidRoleAddressBatcher(); - - /// @notice Thrown when a role's address is not valid (unsafeBlockSigner). - error InvalidRoleAddressUBS(); - - /// @notice Thrown when a role's address is not valid (proposer). - error InvalidRoleAddressProposer(); - - /// @notice Thrown when a role's address is not valid (challenger). - error InvalidRoleAddressChallenger(); + /// @notice Thrown when a role's address is not valid. + error InvalidRoleAddress(string role); /// @notice Thrown when the latest release is not set upon initialization. error LatestReleaseNotSet(); diff --git a/packages/contracts-bedrock/snapshots/abi/OPContractsManager.json b/packages/contracts-bedrock/snapshots/abi/OPContractsManager.json index 78f613a279507..92ae15a241750 100644 --- a/packages/contracts-bedrock/snapshots/abi/OPContractsManager.json +++ b/packages/contracts-bedrock/snapshots/abi/OPContractsManager.json @@ -662,6 +662,11 @@ "internalType": "Claim", "name": "absolutePrestate", "type": "bytes32" + }, + { + "internalType": "bool", + "name": "disputeGameUsesSuperRoots", + "type": "bool" } ], "internalType": "struct OPContractsManager.OpChainConfig[]", @@ -738,33 +743,14 @@ "type": "error" }, { - "inputs": [], - "name": "InvalidRoleAddressBatcher", - "type": "error" - }, - { - "inputs": [], - "name": "InvalidRoleAddressChallenger", - "type": "error" - }, - { - "inputs": [], - "name": "InvalidRoleAddressPAO", - "type": "error" - }, - { - "inputs": [], - "name": "InvalidRoleAddressProposer", - "type": "error" - }, - { - "inputs": [], - "name": "InvalidRoleAddressSCO", - "type": "error" - }, - { - "inputs": [], - "name": "InvalidRoleAddressUBS", + "inputs": [ + { + "internalType": "string", + "name": "role", + "type": "string" + } + ], + "name": "InvalidRoleAddress", "type": "error" }, { diff --git a/packages/contracts-bedrock/snapshots/abi/OPContractsManagerDeployer.json b/packages/contracts-bedrock/snapshots/abi/OPContractsManagerDeployer.json index 2b0073e0e1296..600ddfba513b5 100644 --- a/packages/contracts-bedrock/snapshots/abi/OPContractsManagerDeployer.json +++ b/packages/contracts-bedrock/snapshots/abi/OPContractsManagerDeployer.json @@ -504,4 +504,4 @@ "name": "UnsupportedERCVersion", "type": "error" } -] +] \ No newline at end of file diff --git a/packages/contracts-bedrock/snapshots/abi/OPContractsManagerDeployerInterop.json b/packages/contracts-bedrock/snapshots/abi/OPContractsManagerDeployerInterop.json index 2b0073e0e1296..600ddfba513b5 100644 --- a/packages/contracts-bedrock/snapshots/abi/OPContractsManagerDeployerInterop.json +++ b/packages/contracts-bedrock/snapshots/abi/OPContractsManagerDeployerInterop.json @@ -504,4 +504,4 @@ "name": "UnsupportedERCVersion", "type": "error" } -] +] \ No newline at end of file diff --git a/packages/contracts-bedrock/snapshots/abi/OPContractsManagerGameTypeAdder.json b/packages/contracts-bedrock/snapshots/abi/OPContractsManagerGameTypeAdder.json index a2088238caf93..663ec41ae920a 100644 --- a/packages/contracts-bedrock/snapshots/abi/OPContractsManagerGameTypeAdder.json +++ b/packages/contracts-bedrock/snapshots/abi/OPContractsManagerGameTypeAdder.json @@ -311,6 +311,11 @@ "internalType": "Claim", "name": "absolutePrestate", "type": "bytes32" + }, + { + "internalType": "bool", + "name": "disputeGameUsesSuperRoots", + "type": "bool" } ], "internalType": "struct OPContractsManager.OpChainConfig[]", diff --git a/packages/contracts-bedrock/snapshots/abi/OPContractsManagerUpgrader.json b/packages/contracts-bedrock/snapshots/abi/OPContractsManagerUpgrader.json index 8efb6ad77e6d9..40b1e1f952673 100644 --- a/packages/contracts-bedrock/snapshots/abi/OPContractsManagerUpgrader.json +++ b/packages/contracts-bedrock/snapshots/abi/OPContractsManagerUpgrader.json @@ -208,6 +208,11 @@ "internalType": "Claim", "name": "absolutePrestate", "type": "bytes32" + }, + { + "internalType": "bool", + "name": "disputeGameUsesSuperRoots", + "type": "bool" } ], "internalType": "struct OPContractsManager.OpChainConfig[]", @@ -281,11 +286,6 @@ "name": "NotABlueprint", "type": "error" }, - { - "inputs": [], - "name": "PrestateNotSet", - "type": "error" - }, { "inputs": [], "name": "ReservedBitsSet", diff --git a/packages/contracts-bedrock/snapshots/semver-lock.json b/packages/contracts-bedrock/snapshots/semver-lock.json index 0818d19fa94d9..36188da6a7ad2 100644 --- a/packages/contracts-bedrock/snapshots/semver-lock.json +++ b/packages/contracts-bedrock/snapshots/semver-lock.json @@ -16,24 +16,16 @@ "sourceCodeHash": "0x44797707aea8c63dec049a02d69ea056662a06e5cf320028ab8b388634bf1c67" }, "src/L1/OPContractsManager.sol": { - "initCodeHash": "0xfc5622ed6411ec01e7bcfb3c9250b130b8ca25e74ee40e70e4017fefa34cddf7", - "sourceCodeHash": "0x1f53a1e174479aa244ad78fc171d4cbeb8529505a57af59f89435837e2803bcc" - }, - "src/L1/OPContractsManagerInterop.sol": { - "initCodeHash": "0x6855bfe2f22b4ce53c62fca0d32029cc39f0a2ab2ded43808bb6c493acadc781", - "sourceCodeHash": "0x79b3ae68d83f021d9118f8a1943c1d0773e7e01a19f580f6b90ad4c00370cff5" - }, - "src/L1/OPPrestateUpdater.sol": { - "initCodeHash": "0x840b42954a8921e8216a93ace6d2a047e28ef8df8f426181b1b229d2d5818fc1", - "sourceCodeHash": "0x985c33cc1933d68de38d6c13afc1ac42ef7e455cb06bb255c1fed22b06b97a62" + "initCodeHash": "0x3ba214bc6333b9d8b913b39fbecf24c008f15f8e02dd2d8cc4446f6adee56a35", + "sourceCodeHash": "0x4efca6044e969f2d5cf03baf2515baef8e05886c2a12de9fa4d5c438704fa882" }, "src/L1/OptimismPortal2.sol": { "initCodeHash": "0xac11984d6c3e58fd02e33509540ee329c4f5caedb94e422f8ab3a7a95ce7a546", "sourceCodeHash": "0x79652f4f6aebc11dd4ad201dcf9a436d94db76e2edef07b12486c3de9cd804d2" }, "src/L1/OptimismPortalInterop.sol": { - "initCodeHash": "0x8d76300f7a79686e522d4cac0582f4f7ec77b798f881747d0403762fa56b3a71", - "sourceCodeHash": "0x1b2ce1f2e3125bd449501b9d228b207d31f2bc39c7f23262c674e0d1f47c0d0b" + "initCodeHash": "0xa0fd54587f16bc9f024d842211ab7c7b9c07ca72e08204b0b3cf1c578b6f1872", + "sourceCodeHash": "0xcd94e7641d43620f4169b083b2e597c2db2eb679b44ccaf5c7eeae6f63d2774a" }, "src/L1/ProtocolVersions.sol": { "initCodeHash": "0x5a76c8530cb24cf23d3baacc6eefaac226382af13f1e2a35535d2ec2b0573b29", @@ -44,12 +36,12 @@ "sourceCodeHash": "0xfd56e63e76b1f203cceeb9bbb14396ae803cbbbf7e80ca0ee11fb586321812af" }, "src/L1/SystemConfig.sol": { - "initCodeHash": "0x93e732c31e59dc78d6414ad12fcc0cbe4537d3a69a2ca34ff8713b0b51679b19", - "sourceCodeHash": "0x538518cf61bda80bc3f0b94e4fe236b65e53fff397fd43958e695705c57e02d9" + "initCodeHash": "0x35ec072ff4ced7a22217bc30e62513d1db5916d2a5e3271a6a46e23d2eec14da", + "sourceCodeHash": "0x94a28da7dae79c0a1dcc91031ce00b34cc66d32a6f9d934bb990c1a7279e8a61" }, "src/L1/SystemConfigInterop.sol": { - "initCodeHash": "0xe3651f84f0fecf6c4b60801d8168da55bfe3889912548fcd6b935c9f6dff91b3", - "sourceCodeHash": "0xe3031507a78ed71352c0c1523272881a48c8d6bd98d5322ce3a7cb8061eeade3" + "initCodeHash": "0xfcb5a5d6dd83cca57b7508276467ed8558460940ad26270e29c1525aba62c6c8", + "sourceCodeHash": "0x71914fa1408befaef3a06a67404e0ab580d9d945e068ba23063ea6588f0f68a6" }, "src/L2/BaseFeeVault.sol": { "initCodeHash": "0xc403d4c555d8e69a2699e01d192ae7327136701fa02da10a6d75a584b3c364c9", @@ -243,4 +235,4 @@ "initCodeHash": "0x2bfce526f82622288333d53ca3f43a0a94306ba1bab99241daa845f8f4b18bd4", "sourceCodeHash": "0xf49d7b0187912a6bb67926a3222ae51121e9239495213c975b3b4b217ee57a1b" } -} +} \ No newline at end of file diff --git a/packages/contracts-bedrock/src/L1/OPContractsManager.sol b/packages/contracts-bedrock/src/L1/OPContractsManager.sol index 9853e1aaf50ab..4534354557761 100644 --- a/packages/contracts-bedrock/src/L1/OPContractsManager.sol +++ b/packages/contracts-bedrock/src/L1/OPContractsManager.sol @@ -5,7 +5,7 @@ pragma solidity 0.8.15; import { Blueprint } from "src/libraries/Blueprint.sol"; import { Constants } from "src/libraries/Constants.sol"; import { Bytes } from "src/libraries/Bytes.sol"; -import { Claim, Duration, GameType, GameTypes, OutputRoot } from "src/dispute/lib/Types.sol"; +import { Claim, Duration, GameType, GameTypes, OutputRoot, Hash } from "src/dispute/lib/Types.sol"; import { Strings } from "@openzeppelin/contracts/utils/Strings.sol"; // Interfaces @@ -525,34 +525,12 @@ contract OPContractsManagerUpgrader is OPContractsManagerBase { /// @dev This function is intended to be called via DELEGATECALL from the Upgrade Controller Safe function upgrade(OPContractsManager.OpChainConfig[] memory _opChainConfigs) external virtual { OPContractsManager.Implementations memory impls = getImplementations(); - OPContractsManager.Blueprints memory bps = getBlueprints(); for (uint256 i = 0; i < _opChainConfigs.length; i++) { assertValidOpChainConfig(_opChainConfigs[i]); ISystemConfig.Addresses memory opChainAddrs = _opChainConfigs[i].systemConfigProxy.getAddresses(); - // -------- Upgrade SystemConfig to Isthmus implementation -------- - upgradeTo( - _opChainConfigs[i].proxyAdmin, address(_opChainConfigs[i].systemConfigProxy), impls.systemConfigImpl - ); - - // -------- Upgrade Contracts Stored in SystemConfig -------- - - // OptimismPortal and L1CrossDomainMessenger are being upgraded to include the fixes - // for EIP-7623 (minimum gas limits for L1 -> L2 messages). - upgradeTo(_opChainConfigs[i].proxyAdmin, opChainAddrs.optimismPortal, impls.optimismPortalImpl); - upgradeTo( - _opChainConfigs[i].proxyAdmin, opChainAddrs.l1CrossDomainMessenger, impls.l1CrossDomainMessengerImpl - ); - - // L1ERC721Bridge and L1StandardBridge are being upgraded to include the tweaks to the - // EOA checking code for EIP-7702 (code length == 23). - upgradeTo(_opChainConfigs[i].proxyAdmin, opChainAddrs.l1ERC721Bridge, impls.l1ERC721BridgeImpl); - upgradeTo(_opChainConfigs[i].proxyAdmin, opChainAddrs.l1StandardBridge, impls.l1StandardBridgeImpl); - - // -------- Discover and Upgrade Proofs Contracts -------- - - // All chains have the Permissioned Dispute Game. + // All chains have the PermissionedDisputeGame, grab that. IPermissionedDisputeGame permissionedDisputeGame = IPermissionedDisputeGame( address( getGameImplementation( @@ -561,35 +539,66 @@ contract OPContractsManagerUpgrader is OPContractsManagerBase { ) ); - // We're also going to need the l2ChainId below, so we cache it in the outer scope. + // Grab the L2 chain ID from the PermissionedDisputeGame. uint256 l2ChainId = getL2ChainId(IFaultDisputeGame(address(permissionedDisputeGame))); - deployAndSetNewGameImpl({ - _l2ChainId: l2ChainId, - _disputeGame: IDisputeGame(address(permissionedDisputeGame)), - _gameType: GameTypes.PERMISSIONED_CANNON, - _opChainConfig: _opChainConfigs[i], - _implementations: impls, - _blueprints: bps, - _opChainAddrs: opChainAddrs - }); + // Grab the current respectedGameType from the OptimismPortal contract before the upgrade. + GameType respectedGameType = IOptimismPortal2(payable(opChainAddrs.optimismPortal)).respectedGameType(); + + // Grab the current SuperchainConfig from the OptimismPortal contract before the upgrade. + ISuperchainConfig superchainConfig = + IOptimismPortal2(payable(opChainAddrs.optimismPortal)).superchainConfig(); + + IAnchorStateRegistry newAnchorStateRegistryProxy; + { + // Deploy a new AnchorStateRegistry contract. + newAnchorStateRegistryProxy = IAnchorStateRegistry( + deployProxy({ + _l2ChainId: l2ChainId, + _proxyAdmin: _opChainConfigs[i].proxyAdmin, + _saltMixer: reusableSaltMixer(_opChainConfigs[i]), + _contractName: "AnchorStateRegistry" + }) + ); - // Now retrieve the permissionless game. If it exists, replace its implementation. - IFaultDisputeGame permissionlessDisputeGame = IFaultDisputeGame( - address(getGameImplementation(IDisputeGameFactory(opChainAddrs.disputeGameFactory), GameTypes.CANNON)) - ); + { + // Get the existing anchor root from the old AnchorStateRegistry contract. + // Get the AnchorStateRegistry from the PermissionedDisputeGame. + (Hash root, uint256 l2BlockNumber) = getAnchorStateRegistry( + IFaultDisputeGame(address(permissionedDisputeGame)) + ).anchors(respectedGameType); + + // Upgrade and initialize the AnchorStateRegistry contract. + // Since this is a net-new contract, we need to initialize it. + upgradeToAndCall( + _opChainConfigs[i].proxyAdmin, + address(newAnchorStateRegistryProxy), + impls.anchorStateRegistryImpl, + abi.encodeCall( + IAnchorStateRegistry.initialize, + ( + superchainConfig, + IDisputeGameFactory(opChainAddrs.disputeGameFactory), + OutputRoot({ root: root, l2BlockNumber: l2BlockNumber }), + respectedGameType + ) + ) + ); + } + } - if (address(permissionlessDisputeGame) != address(0)) { - // Deploy and set a new permissionless game to update its prestate - deployAndSetNewGameImpl({ - _l2ChainId: l2ChainId, - _disputeGame: IDisputeGame(address(permissionlessDisputeGame)), - _gameType: GameTypes.CANNON, - _opChainConfig: _opChainConfigs[i], - _implementations: impls, - _blueprints: bps, - _opChainAddrs: opChainAddrs - }); + // Separate context to avoid stack too deep. + { + // Upgrade the OptimismPortal contract. + upgradeToAndCall( + _opChainConfigs[i].proxyAdmin, + opChainAddrs.optimismPortal, + impls.optimismPortalImpl, + abi.encodeCall( + IOptimismPortal2.upgrade, + (newAnchorStateRegistryProxy, _opChainConfigs[i].disputeGameUsesSuperRoots) + ) + ); } // Emit the upgraded event with the address of the caller. Since this will be a delegatecall, @@ -805,7 +814,7 @@ contract OPContractsManagerDeployer is OPContractsManagerBase { output.opChainProxyAdmin, address(output.l1ERC721BridgeProxy), implementation.l1ERC721BridgeImpl, data ); - data = encodeOptimismPortalInitializer(output, _superchainConfig); + data = encodeOptimismPortalInitializer(_input, output, _superchainConfig); upgradeToAndCall( output.opChainProxyAdmin, address(output.optimismPortalProxy), implementation.optimismPortalImpl, data ); @@ -954,6 +963,7 @@ contract OPContractsManagerDeployer is OPContractsManagerBase { /// @notice Helper method for encoding the OptimismPortal initializer data. function encodeOptimismPortalInitializer( + OPContractsManager.DeployInput memory _input, OPContractsManager.DeployOutput memory _output, ISuperchainConfig _superchainConfig ) @@ -965,10 +975,10 @@ contract OPContractsManagerDeployer is OPContractsManagerBase { return abi.encodeCall( IOptimismPortal2.initialize, ( - _output.disputeGameFactoryProxy, _output.systemConfigProxy, _superchainConfig, - GameTypes.PERMISSIONED_CANNON + _output.anchorStateRegistryProxy, + _input.disputeGameUsesSuperRoots ) ); } @@ -997,7 +1007,8 @@ contract OPContractsManagerDeployer is OPContractsManagerBase { _input.roles.unsafeBlockSigner, referenceResourceConfig, chainIdToBatchInboxAddress(_input.l2ChainId), - opChainAddrs + opChainAddrs, + _input.l2ChainId ) ); } @@ -1057,7 +1068,7 @@ contract OPContractsManagerDeployer is OPContractsManagerBase { OutputRoot memory startingAnchorRoot = abi.decode(_input.startingAnchorRoot, (OutputRoot)); return abi.encodeCall( IAnchorStateRegistry.initialize, - (_superchainConfig, _output.disputeGameFactoryProxy, _output.optimismPortalProxy, startingAnchorRoot) + (_superchainConfig, _output.disputeGameFactoryProxy, startingAnchorRoot, GameTypes.PERMISSIONED_CANNON) ); } @@ -1113,7 +1124,8 @@ contract OPContractsManagerDeployerInterop is OPContractsManagerDeployer { referenceResourceConfig, chainIdToBatchInboxAddress(_input.l2ChainId), opChainAddrs, - dependencyManager + dependencyManager, + _input.l2ChainId ) ); } @@ -1292,23 +1304,8 @@ contract OPContractsManager is ISemver { /// @notice Thrown when an invalid `l2ChainId` is provided to `deploy`. error InvalidChainId(); - /// @notice Thrown when a role's address is not valid (opChainProxyAdminOwner). - error InvalidRoleAddressPAO(); - - /// @notice Thrown when a role's address is not valid (systemConfigOwner). - error InvalidRoleAddressSCO(); - - /// @notice Thrown when a role's address is not valid (batcher). - error InvalidRoleAddressBatcher(); - - /// @notice Thrown when a role's address is not valid (unsafeBlockSigner). - error InvalidRoleAddressUBS(); - - /// @notice Thrown when a role's address is not valid (proposer). - error InvalidRoleAddressProposer(); - - /// @notice Thrown when a role's address is not valid (challenger). - error InvalidRoleAddressChallenger(); + /// @notice Thrown when a role's address is not valid. + error InvalidRoleAddress(string role); /// @notice Thrown when the latest release is not set upon initialization. error LatestReleaseNotSet(); diff --git a/packages/contracts-bedrock/src/L1/OPContractsManagerInterop.sol b/packages/contracts-bedrock/src/L1/OPContractsManagerInterop.sol deleted file mode 100644 index 6358bc741d715..0000000000000 --- a/packages/contracts-bedrock/src/L1/OPContractsManagerInterop.sol +++ /dev/null @@ -1,80 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.15; - -// Contracts -import { OPContractsManager } from "src/L1/OPContractsManager.sol"; - -// Interfaces -import { ISuperchainConfig } from "interfaces/L1/ISuperchainConfig.sol"; -import { IProtocolVersions } from "interfaces/L1/IProtocolVersions.sol"; -import { IResourceMetering } from "interfaces/L1/IResourceMetering.sol"; -import { ISystemConfig } from "interfaces/L1/ISystemConfig.sol"; -import { ISystemConfigInterop } from "interfaces/L1/ISystemConfigInterop.sol"; -import { IProxyAdmin } from "interfaces/universal/IProxyAdmin.sol"; - -contract OPContractsManagerInterop is OPContractsManager { - /// @custom:semver +interop.11 - function version() public pure override returns (string memory) { - return string.concat(super.version(), "+interop.11"); - } - - constructor( - ISuperchainConfig _superchainConfig, - IProtocolVersions _protocolVersions, - IProxyAdmin _superchainProxyAdmin, - string memory _l1ContractsRelease, - Blueprints memory _blueprints, - Implementations memory _implementations, - address _upgradeController - ) - OPContractsManager( - _superchainConfig, - _protocolVersions, - _superchainProxyAdmin, - _l1ContractsRelease, - _blueprints, - _implementations, - _upgradeController - ) - { } - - // The `SystemConfigInterop` contract has an extra `address _dependencyManager` argument - // that we must account for. - function encodeSystemConfigInitializer( - DeployInput memory _input, - DeployOutput memory _output - ) - internal - view - virtual - override - returns (bytes memory) - { - (IResourceMetering.ResourceConfig memory referenceResourceConfig, ISystemConfig.Addresses memory opChainAddrs) = - defaultSystemConfigParams(_input, _output); - - // TODO For now we assume that the dependency manager is the same as system config owner. - // This is currently undefined since it's not part of the standard config, so we may need - // to update where this value is pulled from in the future. To support a different dependency - // manager in this contract without an invasive change of redefining the `Roles` struct, - // we will make the change described in https://github.com/ethereum-optimism/optimism/issues/11783. - address dependencyManager = address(_input.roles.systemConfigOwner); - - return abi.encodeCall( - ISystemConfigInterop.initialize, - ( - _input.roles.systemConfigOwner, - _input.basefeeScalar, - _input.blobBasefeeScalar, - bytes32(uint256(uint160(_input.roles.batcher))), // batcherHash - _input.gasLimit, - _input.roles.unsafeBlockSigner, - referenceResourceConfig, - chainIdToBatchInboxAddress(_input.l2ChainId), - opChainAddrs, - dependencyManager, - _input.l2ChainId - ) - ); - } -} diff --git a/packages/contracts-bedrock/src/L1/OPPrestateUpdater.sol b/packages/contracts-bedrock/src/L1/OPPrestateUpdater.sol deleted file mode 100644 index dd6f50989fec0..0000000000000 --- a/packages/contracts-bedrock/src/L1/OPPrestateUpdater.sol +++ /dev/null @@ -1,172 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.15; - -// Contracts -import { OPContractsManager } from "src/L1/OPContractsManager.sol"; - -// Interfaces -import { ISuperchainConfig } from "interfaces/L1/ISuperchainConfig.sol"; -import { IProtocolVersions } from "interfaces/L1/IProtocolVersions.sol"; -import { IPermissionedDisputeGame } from "interfaces/dispute/IPermissionedDisputeGame.sol"; -import { IDisputeGameFactory } from "interfaces/dispute/IDisputeGameFactory.sol"; -import { IFaultDisputeGame } from "interfaces/dispute/IFaultDisputeGame.sol"; -import { IDelayedWETH } from "interfaces/dispute/IDelayedWETH.sol"; -import { IProxyAdmin } from "interfaces/universal/IProxyAdmin.sol"; - -// Libraries -import { Claim, GameTypes } from "src/dispute/lib/Types.sol"; - -/// @title OPPrestateUpdater -/// @notice A custom implementation of OPContractsManager that enables updating the prestate hash -/// for the permissioned and fault dispute games on a set of chains. -contract OPPrestateUpdater is OPContractsManager { - /// @notice Thrown when a function from the parent (OPCM) is not implemented. - error NotImplemented(); - - /// @notice Thrown when the prestate of a permissioned disputed game is 0. - error PrestateRequired(); - - // @return Version string - /// @custom:semver 1.7.1 - function version() public pure override returns (string memory) { - return "1.7.1"; - } - - // @notice Constructs the OPPrestateUpdater contract - // @param _superchainConfig Address of the SuperchainConfig contract - // @param _protocolVersions Address of the ProtocolVersions contract - // @param _blueprints Addresses of Blueprint contracts - constructor( - ISuperchainConfig _superchainConfig, - IProtocolVersions _protocolVersions, - Blueprints memory _blueprints - ) - OPContractsManager( - _superchainConfig, - _protocolVersions, - IProxyAdmin(address(0)), - "", - _blueprints, - Implementations( - address(0), //superchainConfigImpl - address(0), //protocolVersionsImpl - address(0), //l1ERC721BridgeImpl - address(0), //optimismPortalImpl - address(0), //systemConfigImpl - address(0), //optimismMintableERC20FactoryImpl - address(0), //l1CrossDomainMessengerImpl - address(0), //l1StandardBridgeImpl - address(0), //disputeGameFactoryImpl - address(0), //anchorStateRegistryImpl - address(0), //delayedWETHImpl - address(0) // mipsImpl - ), - address(0) - ) - { } - - /// @notice Overrides the l1ContractsRelease function to return "none", as this OPCM - /// is not releasing new contracts. - function l1ContractsRelease() external pure override returns (string memory) { - return "none"; - } - - function deploy(DeployInput memory _input) external pure override returns (DeployOutput memory) { - _input; // Silence warning - revert NotImplemented(); - } - - function upgrade(OpChainConfig[] memory _opChainConfigs) external pure override { - _opChainConfigs; // Silence warning - revert NotImplemented(); - } - - function addGameType(AddGameInput[] memory _gameConfigs) public pure override returns (AddGameOutput[] memory) { - _gameConfigs; // Silence warning - revert NotImplemented(); - } - - /// @notice Updates the prestate hash for a new game type while keeping all other parameters the same - /// @param _prestateUpdateInputs The new prestate hash to use - function updatePrestate(OpChainConfig[] memory _prestateUpdateInputs) external { - // Loop through each chain and prestate hash - for (uint256 i = 0; i < _prestateUpdateInputs.length; i++) { - if (Claim.unwrap(_prestateUpdateInputs[i].absolutePrestate) == bytes32(0)) { - revert PrestateRequired(); - } - - // Get the DisputeGameFactory and existing game implementations - IDisputeGameFactory dgf = - IDisputeGameFactory(_prestateUpdateInputs[i].systemConfigProxy.disputeGameFactory()); - IFaultDisputeGame fdg = IFaultDisputeGame(address(getGameImplementation(dgf, GameTypes.CANNON))); - IPermissionedDisputeGame pdg = - IPermissionedDisputeGame(address(getGameImplementation(dgf, GameTypes.PERMISSIONED_CANNON))); - - // All chains must have a permissioned game, but not all chains must have a fault dispute game. - // Whether a chain has a fault dispute game determines how many AddGameInput objects are needed. - bool hasFDG = address(fdg) != address(0); - - AddGameInput[] memory inputs = new AddGameInput[](hasFDG ? 2 : 1); - AddGameInput memory pdgInput; - AddGameInput memory fdgInput; - - // Get the existing game parameters and init bond for the permissioned game - IFaultDisputeGame.GameConstructorParams memory pdgParams = - getGameConstructorParams(IFaultDisputeGame(address(pdg))); - uint256 initBond = dgf.initBonds(GameTypes.PERMISSIONED_CANNON); - - string memory saltMixer = reusableSaltMixer(_prestateUpdateInputs[i]); - // Create game input with updated prestate but same other params - pdgInput = AddGameInput({ - disputeAbsolutePrestate: _prestateUpdateInputs[i].absolutePrestate, - saltMixer: saltMixer, - systemConfig: _prestateUpdateInputs[i].systemConfigProxy, - proxyAdmin: _prestateUpdateInputs[i].proxyAdmin, - delayedWETH: IDelayedWETH(payable(address(pdgParams.weth))), - disputeGameType: pdgParams.gameType, - disputeMaxGameDepth: pdgParams.maxGameDepth, - disputeSplitDepth: pdgParams.splitDepth, - disputeClockExtension: pdgParams.clockExtension, - disputeMaxClockDuration: pdgParams.maxClockDuration, - initialBond: initBond, - vm: pdgParams.vm, - permissioned: true - }); - - // If a fault dispute game exists, create a new game with the same parameters but updated prestate. - if (hasFDG) { - // Get the existing game parameters and init bond for the fault dispute game - IFaultDisputeGame.GameConstructorParams memory fdgParams = - getGameConstructorParams(IFaultDisputeGame(address(fdg))); - initBond = dgf.initBonds(GameTypes.CANNON); - - // Create game input with updated prestate but same other params - fdgInput = AddGameInput({ - disputeAbsolutePrestate: _prestateUpdateInputs[i].absolutePrestate, - saltMixer: saltMixer, - systemConfig: _prestateUpdateInputs[i].systemConfigProxy, - proxyAdmin: _prestateUpdateInputs[i].proxyAdmin, - delayedWETH: IDelayedWETH(payable(address(fdgParams.weth))), - disputeGameType: fdgParams.gameType, - disputeMaxGameDepth: fdgParams.maxGameDepth, - disputeSplitDepth: fdgParams.splitDepth, - disputeClockExtension: fdgParams.clockExtension, - disputeMaxClockDuration: fdgParams.maxClockDuration, - initialBond: initBond, - vm: fdgParams.vm, - permissioned: false - }); - } - - // Game inputs must be ordered with increasing game type values. So FDG is first if it exists. - if (hasFDG) { - inputs[0] = fdgInput; - inputs[1] = pdgInput; - } else { - inputs[0] = pdgInput; - } - // Add the new game type with updated prestate - super.addGameType(inputs); - } - } -} diff --git a/packages/contracts-bedrock/src/L1/OptimismPortalInterop.sol b/packages/contracts-bedrock/src/L1/OptimismPortalInterop.sol index 532f67ac0be5a..7a56632256fe3 100644 --- a/packages/contracts-bedrock/src/L1/OptimismPortalInterop.sol +++ b/packages/contracts-bedrock/src/L1/OptimismPortalInterop.sol @@ -23,9 +23,9 @@ contract OptimismPortalInterop is OptimismPortal2 { /// @param _proofMaturityDelaySeconds The proof maturity delay in seconds. constructor(uint256 _proofMaturityDelaySeconds) OptimismPortal2(_proofMaturityDelaySeconds) { } - /// @custom:semver +interop.3 + /// @custom:semver +interop.4 function version() public pure override returns (string memory) { - return string.concat(super.version(), "+interop.3"); + return string.concat(super.version(), "+interop.4"); } /// @notice Sets static configuration options for the L2 system. diff --git a/packages/contracts-bedrock/src/L1/SystemConfig.sol b/packages/contracts-bedrock/src/L1/SystemConfig.sol index 1828f3fb23291..092767efb54bd 100644 --- a/packages/contracts-bedrock/src/L1/SystemConfig.sol +++ b/packages/contracts-bedrock/src/L1/SystemConfig.sol @@ -140,9 +140,9 @@ contract SystemConfig is OwnableUpgradeable, ReinitializableBase, ISemver { event ConfigUpdate(uint256 indexed version, UpdateType indexed updateType, bytes data); /// @notice Semantic version. - /// @custom:semver 2.5.0 + /// @custom:semver 2.6.0 function version() public pure virtual returns (string memory) { - return "2.5.0"; + return "2.6.0"; } /// @notice Constructs the SystemConfig contract. diff --git a/packages/contracts-bedrock/src/L1/SystemConfigInterop.sol b/packages/contracts-bedrock/src/L1/SystemConfigInterop.sol index dbb9a3dbb2562..fc4135c088326 100644 --- a/packages/contracts-bedrock/src/L1/SystemConfigInterop.sol +++ b/packages/contracts-bedrock/src/L1/SystemConfigInterop.sol @@ -67,9 +67,9 @@ contract SystemConfigInterop is SystemConfig { Storage.setAddress(DEPENDENCY_MANAGER_SLOT, _dependencyManager); } - /// @custom:semver +interop.1 + /// @custom:semver +interop.2 function version() public pure override returns (string memory) { - return string.concat(super.version(), "+interop.1"); + return string.concat(super.version(), "+interop.2"); } /// @notice Adds a chain to the interop dependency set. Can only be called by the dependency manager. diff --git a/packages/contracts-bedrock/test/L1/OPContractsManager.t.sol b/packages/contracts-bedrock/test/L1/OPContractsManager.t.sol index 26119404693fe..5b317f9b89399 100644 --- a/packages/contracts-bedrock/test/L1/OPContractsManager.t.sol +++ b/packages/contracts-bedrock/test/L1/OPContractsManager.t.sol @@ -1012,7 +1012,7 @@ contract OPContractsManager_UpdatePrestate_Test is Test { }), optimismPortalImpl: DeployUtils.create1({ _name: "OptimismPortal2", - _args: DeployUtils.encodeConstructor(abi.encodeCall(IOptimismPortal2.__constructor__, (1, 1))) + _args: DeployUtils.encodeConstructor(abi.encodeCall(IOptimismPortal2.__constructor__, (1))) }), systemConfigImpl: DeployUtils.create1({ _name: "SystemConfig", @@ -1036,7 +1036,7 @@ contract OPContractsManager_UpdatePrestate_Test is Test { }), anchorStateRegistryImpl: DeployUtils.create1({ _name: "AnchorStateRegistry", - _args: DeployUtils.encodeConstructor(abi.encodeCall(IAnchorStateRegistry.__constructor__, ())) + _args: DeployUtils.encodeConstructor(abi.encodeCall(IAnchorStateRegistry.__constructor__, (1))) }), delayedWETHImpl: DeployUtils.create1({ _name: "DelayedWETH", @@ -1128,6 +1128,7 @@ contract OPContractsManager_UpdatePrestate_Test is Test { l2ChainId: 100, saltMixer: "hello", gasLimit: 30_000_000, + disputeGameUsesSuperRoots: false, disputeGameType: GameType.wrap(1), disputeAbsolutePrestate: Claim.wrap( bytes32(hex"038512e02c4c3f7bdaec27d00edf55b7155e0905301e1a88083e4e0a6764d54c") @@ -1149,7 +1150,10 @@ contract OPContractsManager_UpdatePrestate_Test is Test { function test_updatePrestate_pdgOnlyWithValidInput_succeeds() public { IOPContractsManager.OpChainConfig[] memory inputs = new IOPContractsManager.OpChainConfig[](1); inputs[0] = IOPContractsManager.OpChainConfig( - chainDeployOutput.systemConfigProxy, chainDeployOutput.opChainProxyAdmin, Claim.wrap(bytes32(hex"ABBA")) + chainDeployOutput.systemConfigProxy, + chainDeployOutput.opChainProxyAdmin, + Claim.wrap(bytes32(hex"ABBA")), + false ); address proxyAdminOwner = chainDeployOutput.opChainProxyAdmin.owner(); @@ -1180,7 +1184,10 @@ contract OPContractsManager_UpdatePrestate_Test is Test { IOPContractsManager.OpChainConfig[] memory inputs = new IOPContractsManager.OpChainConfig[](1); inputs[0] = IOPContractsManager.OpChainConfig( - chainDeployOutput.systemConfigProxy, chainDeployOutput.opChainProxyAdmin, Claim.wrap(bytes32(hex"ABBA")) + chainDeployOutput.systemConfigProxy, + chainDeployOutput.opChainProxyAdmin, + Claim.wrap(bytes32(hex"ABBA")), + false ); address proxyAdminOwner = chainDeployOutput.opChainProxyAdmin.owner(); @@ -1217,7 +1224,8 @@ contract OPContractsManager_UpdatePrestate_Test is Test { inputs[0] = IOPContractsManager.OpChainConfig({ systemConfigProxy: chainDeployOutput.systemConfigProxy, proxyAdmin: chainDeployOutput.opChainProxyAdmin, - absolutePrestate: Claim.wrap(bytes32(0)) + absolutePrestate: Claim.wrap(bytes32(0)), + disputeGameUsesSuperRoots: false }); address proxyAdminOwner = chainDeployOutput.opChainProxyAdmin.owner(); diff --git a/packages/contracts-bedrock/test/L1/OPPrestateUpdater.t.sol b/packages/contracts-bedrock/test/L1/OPPrestateUpdater.t.sol deleted file mode 100644 index 563e21d16be6c..0000000000000 --- a/packages/contracts-bedrock/test/L1/OPPrestateUpdater.t.sol +++ /dev/null @@ -1,395 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.15; - -// Testing -import { Test } from "forge-std/Test.sol"; -import { DelegateCaller } from "test/mocks/Callers.sol"; - -// Scripts -import { DeployUtils } from "scripts/libraries/DeployUtils.sol"; - -// Libraries -import { Blueprint } from "src/libraries/Blueprint.sol"; - -// Interfaces -import { IL1ERC721Bridge } from "interfaces/L1/IL1ERC721Bridge.sol"; -import { IL1StandardBridge } from "interfaces/L1/IL1StandardBridge.sol"; -import { IL1CrossDomainMessenger } from "interfaces/L1/IL1CrossDomainMessenger.sol"; -import { IOptimismMintableERC20Factory } from "interfaces/universal/IOptimismMintableERC20Factory.sol"; -import { IAnchorStateRegistry } from "interfaces/dispute/IAnchorStateRegistry.sol"; -import { IMIPS } from "interfaces/cannon/IMIPS.sol"; -import { IOptimismPortal2 } from "interfaces/L1/IOptimismPortal2.sol"; -import { IProxyAdmin } from "interfaces/universal/IProxyAdmin.sol"; -import { ISuperchainConfig } from "interfaces/L1/ISuperchainConfig.sol"; -import { IProtocolVersions } from "interfaces/L1/IProtocolVersions.sol"; -import { IPreimageOracle } from "interfaces/cannon/IPreimageOracle.sol"; -import { IPermissionedDisputeGame } from "interfaces/dispute/IPermissionedDisputeGame.sol"; -import { IDelayedWETH } from "interfaces/dispute/IDelayedWETH.sol"; -import { IOPContractsManager } from "interfaces/L1/IOPContractsManager.sol"; -import { IDisputeGameFactory } from "interfaces/dispute/IDisputeGameFactory.sol"; -import { ISystemConfig } from "interfaces/L1/ISystemConfig.sol"; -import { IOPPrestateUpdater } from "interfaces/L1/IOPPrestateUpdater.sol"; - -// Contracts -import { OPContractsManager } from "src/L1/OPContractsManager.sol"; -import { OPPrestateUpdater } from "src/L1/OPPrestateUpdater.sol"; -import { Blueprint } from "src/libraries/Blueprint.sol"; -import { IBigStepper } from "interfaces/dispute/IBigStepper.sol"; -import { GameType, Duration, Hash, Claim } from "src/dispute/lib/LibUDT.sol"; -import { OutputRoot, GameTypes } from "src/dispute/lib/Types.sol"; - -contract OPPrestateUpdater_Test is Test { - IOPContractsManager internal opcm; - OPPrestateUpdater internal prestateUpdater; - - OPContractsManager.OpChainConfig[] internal opChainConfigs; - OPContractsManager.AddGameInput[] internal gameInput; - - IOPContractsManager.DeployOutput internal chainDeployOutput; - - function setUp() public { - IProxyAdmin superchainProxyAdmin = IProxyAdmin(makeAddr("superchainProxyAdmin")); - ISuperchainConfig superchainConfigProxy = ISuperchainConfig(makeAddr("superchainConfig")); - IProtocolVersions protocolVersionsProxy = IProtocolVersions(makeAddr("protocolVersions")); - bytes32 salt = hex"01"; - IOPContractsManager.Blueprints memory blueprints; - - (blueprints.addressManager,) = Blueprint.create(vm.getCode("AddressManager"), salt); - (blueprints.proxy,) = Blueprint.create(vm.getCode("Proxy"), salt); - (blueprints.proxyAdmin,) = Blueprint.create(vm.getCode("ProxyAdmin"), salt); - (blueprints.l1ChugSplashProxy,) = Blueprint.create(vm.getCode("L1ChugSplashProxy"), salt); - (blueprints.resolvedDelegateProxy,) = Blueprint.create(vm.getCode("ResolvedDelegateProxy"), salt); - (blueprints.permissionedDisputeGame1, blueprints.permissionedDisputeGame2) = - Blueprint.create(vm.getCode("PermissionedDisputeGame"), salt); - (blueprints.permissionlessDisputeGame1, blueprints.permissionlessDisputeGame2) = - Blueprint.create(vm.getCode("FaultDisputeGame"), salt); - - IPreimageOracle oracle = IPreimageOracle( - DeployUtils.create1({ - _name: "PreimageOracle", - _args: DeployUtils.encodeConstructor(abi.encodeCall(IPreimageOracle.__constructor__, (126000, 86400))) - }) - ); - - IOPContractsManager.Implementations memory impls = IOPContractsManager.Implementations({ - superchainConfigImpl: DeployUtils.create1({ - _name: "SuperchainConfig", - _args: DeployUtils.encodeConstructor(abi.encodeCall(ISuperchainConfig.__constructor__, ())) - }), - protocolVersionsImpl: DeployUtils.create1({ - _name: "ProtocolVersions", - _args: DeployUtils.encodeConstructor(abi.encodeCall(IProtocolVersions.__constructor__, ())) - }), - l1ERC721BridgeImpl: DeployUtils.create1({ - _name: "L1ERC721Bridge", - _args: DeployUtils.encodeConstructor(abi.encodeCall(IL1ERC721Bridge.__constructor__, ())) - }), - optimismPortalImpl: DeployUtils.create1({ - _name: "OptimismPortal2", - _args: DeployUtils.encodeConstructor(abi.encodeCall(IOptimismPortal2.__constructor__, (1))) - }), - systemConfigImpl: DeployUtils.create1({ - _name: "SystemConfig", - _args: DeployUtils.encodeConstructor(abi.encodeCall(ISystemConfig.__constructor__, ())) - }), - optimismMintableERC20FactoryImpl: DeployUtils.create1({ - _name: "OptimismMintableERC20Factory", - _args: DeployUtils.encodeConstructor(abi.encodeCall(IOptimismMintableERC20Factory.__constructor__, ())) - }), - l1CrossDomainMessengerImpl: DeployUtils.create1({ - _name: "L1CrossDomainMessenger", - _args: DeployUtils.encodeConstructor(abi.encodeCall(IL1CrossDomainMessenger.__constructor__, ())) - }), - l1StandardBridgeImpl: DeployUtils.create1({ - _name: "L1StandardBridge", - _args: DeployUtils.encodeConstructor(abi.encodeCall(IL1StandardBridge.__constructor__, ())) - }), - disputeGameFactoryImpl: DeployUtils.create1({ - _name: "DisputeGameFactory", - _args: DeployUtils.encodeConstructor(abi.encodeCall(IDisputeGameFactory.__constructor__, ())) - }), - anchorStateRegistryImpl: DeployUtils.create1({ - _name: "AnchorStateRegistry", - _args: DeployUtils.encodeConstructor(abi.encodeCall(IAnchorStateRegistry.__constructor__, (1))) - }), - delayedWETHImpl: DeployUtils.create1({ - _name: "DelayedWETH", - _args: DeployUtils.encodeConstructor(abi.encodeCall(IDelayedWETH.__constructor__, (3))) - }), - mipsImpl: DeployUtils.create1({ - _name: "MIPS", - _args: DeployUtils.encodeConstructor(abi.encodeCall(IMIPS.__constructor__, (oracle))) - }) - }); - - vm.etch(address(superchainConfigProxy), hex"01"); - vm.etch(address(protocolVersionsProxy), hex"01"); - - opcm = IOPContractsManager( - DeployUtils.createDeterministic({ - _name: "OPContractsManager", - _args: DeployUtils.encodeConstructor( - abi.encodeCall( - IOPContractsManager.__constructor__, - ( - superchainConfigProxy, - protocolVersionsProxy, - superchainProxyAdmin, - "dev", - blueprints, - impls, - address(this) - ) - ) - ), - _salt: DeployUtils.DEFAULT_SALT - }) - ); - - chainDeployOutput = opcm.deploy( - IOPContractsManager.DeployInput({ - roles: IOPContractsManager.Roles({ - opChainProxyAdminOwner: address(this), - systemConfigOwner: address(this), - batcher: address(this), - unsafeBlockSigner: address(this), - proposer: address(this), - challenger: address(this) - }), - basefeeScalar: 1, - blobBasefeeScalar: 1, - startingAnchorRoot: abi.encode( - OutputRoot({ - root: Hash.wrap(0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef), - l2BlockNumber: 0 - }) - ), - l2ChainId: 100, - saltMixer: "hello", - gasLimit: 30_000_000, - disputeGameUsesSuperRoots: false, - disputeGameType: GameType.wrap(1), - disputeAbsolutePrestate: Claim.wrap( - bytes32(hex"038512e02c4c3f7bdaec27d00edf55b7155e0905301e1a88083e4e0a6764d54c") - ), - disputeMaxGameDepth: 73, - disputeSplitDepth: 30, - disputeClockExtension: Duration.wrap(10800), - disputeMaxClockDuration: Duration.wrap(302400) - }) - ); - - prestateUpdater = OPPrestateUpdater( - DeployUtils.createDeterministic({ - _name: "OPPrestateUpdater", - _args: DeployUtils.encodeConstructor( - abi.encodeCall( - IOPPrestateUpdater.__constructor__, - (ISuperchainConfig(address(this)), IProtocolVersions(address(this)), blueprints) - ) - ), - _salt: DeployUtils.DEFAULT_SALT - }) - ); - } - - function test_semver_works() public view { - assertNotEq(abi.encode(prestateUpdater.version()), abi.encode(0)); - } - - function test_updatePrestate_pdgOnlyWithValidInput_succeeds() public { - OPContractsManager.OpChainConfig[] memory inputs = new OPContractsManager.OpChainConfig[](1); - inputs[0] = OPContractsManager.OpChainConfig( - chainDeployOutput.systemConfigProxy, - chainDeployOutput.opChainProxyAdmin, - Claim.wrap(bytes32(hex"ABBA")), - false - ); - address proxyAdminOwner = chainDeployOutput.opChainProxyAdmin.owner(); - - vm.etch(address(proxyAdminOwner), vm.getDeployedCode("test/mocks/Callers.sol:DelegateCaller")); - DelegateCaller(proxyAdminOwner).dcForward( - address(prestateUpdater), abi.encodeCall(OPPrestateUpdater.updatePrestate, (inputs)) - ); - - IPermissionedDisputeGame pdg = IPermissionedDisputeGame( - address( - IDisputeGameFactory(chainDeployOutput.systemConfigProxy.disputeGameFactory()).gameImpls( - GameTypes.PERMISSIONED_CANNON - ) - ) - ); - - assertEq(pdg.absolutePrestate().raw(), inputs[0].absolutePrestate.raw(), "pdg prestate mismatch"); - - // Ensure that the WETH contract is not reverting - pdg.weth().balanceOf(address(0)); - } - - function test_updatePrestate_bothGamesWithValidInput_succeeds() public { - // Also add a permissionless game - IOPContractsManager.AddGameInput memory input = newGameInputFactory({ permissioned: false }); - input.disputeGameType = GameTypes.CANNON; - addGameType(input); - - OPContractsManager.OpChainConfig[] memory inputs = new OPContractsManager.OpChainConfig[](1); - inputs[0] = OPContractsManager.OpChainConfig( - chainDeployOutput.systemConfigProxy, - chainDeployOutput.opChainProxyAdmin, - Claim.wrap(bytes32(hex"ABBA")), - false - ); - address proxyAdminOwner = chainDeployOutput.opChainProxyAdmin.owner(); - - vm.etch(address(proxyAdminOwner), vm.getDeployedCode("test/mocks/Callers.sol:DelegateCaller")); - DelegateCaller(proxyAdminOwner).dcForward( - address(prestateUpdater), abi.encodeCall(OPPrestateUpdater.updatePrestate, (inputs)) - ); - - IPermissionedDisputeGame pdg = IPermissionedDisputeGame( - address( - IDisputeGameFactory(chainDeployOutput.systemConfigProxy.disputeGameFactory()).gameImpls( - GameTypes.PERMISSIONED_CANNON - ) - ) - ); - IPermissionedDisputeGame fdg = IPermissionedDisputeGame( - address( - IDisputeGameFactory(chainDeployOutput.systemConfigProxy.disputeGameFactory()).gameImpls( - GameTypes.CANNON - ) - ) - ); - - assertEq(pdg.absolutePrestate().raw(), inputs[0].absolutePrestate.raw(), "pdg prestate mismatch"); - assertEq(fdg.absolutePrestate().raw(), inputs[0].absolutePrestate.raw(), "fdg prestate mismatch"); - - // Ensure that the WETH contracts are not reverting - pdg.weth().balanceOf(address(0)); - fdg.weth().balanceOf(address(0)); - } - - function test_updatePrestate_whenPDGPrestateIsZero_reverts() public { - OPPrestateUpdater.OpChainConfig[] memory inputs = new OPPrestateUpdater.OpChainConfig[](1); - inputs[0] = OPContractsManager.OpChainConfig({ - systemConfigProxy: chainDeployOutput.systemConfigProxy, - proxyAdmin: chainDeployOutput.opChainProxyAdmin, - absolutePrestate: Claim.wrap(bytes32(0)), - disputeGameUsesSuperRoots: false - }); - - address proxyAdminOwner = chainDeployOutput.opChainProxyAdmin.owner(); - vm.etch(address(proxyAdminOwner), vm.getDeployedCode("test/mocks/Callers.sol:DelegateCaller")); - - vm.expectRevert(OPPrestateUpdater.PrestateRequired.selector); - DelegateCaller(proxyAdminOwner).dcForward( - address(prestateUpdater), abi.encodeCall(OPPrestateUpdater.updatePrestate, (inputs)) - ); - } - - function test_deploy_notImplemented_reverts() public { - OPContractsManager.DeployInput memory input = OPContractsManager.DeployInput({ - roles: OPContractsManager.Roles({ - opChainProxyAdminOwner: address(0), - systemConfigOwner: address(0), - batcher: address(0), - unsafeBlockSigner: address(0), - proposer: address(0), - challenger: address(0) - }), - basefeeScalar: 0, - blobBasefeeScalar: 0, - l2ChainId: 0, - startingAnchorRoot: bytes(abi.encode(0)), - saltMixer: "", - gasLimit: 0, - disputeGameUsesSuperRoots: false, - disputeGameType: GameType.wrap(0), - disputeAbsolutePrestate: Claim.wrap(0), - disputeMaxGameDepth: 0, - disputeSplitDepth: 0, - disputeClockExtension: Duration.wrap(0), - disputeMaxClockDuration: Duration.wrap(0) - }); - - vm.expectRevert(OPPrestateUpdater.NotImplemented.selector); - prestateUpdater.deploy(input); - } - - function test_upgrade_notImplemented_reverts() public { - opChainConfigs.push( - OPContractsManager.OpChainConfig({ - systemConfigProxy: ISystemConfig(address(0)), - proxyAdmin: IProxyAdmin(address(0)), - absolutePrestate: Claim.wrap(0), - disputeGameUsesSuperRoots: false - }) - ); - - vm.expectRevert(OPPrestateUpdater.NotImplemented.selector); - prestateUpdater.upgrade(opChainConfigs); - } - - function test_addGameType_notImplemented_reverts() public { - gameInput.push( - OPContractsManager.AddGameInput({ - saltMixer: "hello", - systemConfig: ISystemConfig(address(0)), - proxyAdmin: IProxyAdmin(address(0)), - delayedWETH: IDelayedWETH(payable(address(0))), - disputeGameType: GameType.wrap(2000), - disputeAbsolutePrestate: Claim.wrap(bytes32(hex"deadbeef1234")), - disputeMaxGameDepth: 73, - disputeSplitDepth: 30, - disputeClockExtension: Duration.wrap(10800), - disputeMaxClockDuration: Duration.wrap(302400), - initialBond: 1 ether, - vm: IBigStepper(address(0)), - permissioned: true - }) - ); - - vm.expectRevert(OPPrestateUpdater.NotImplemented.selector); - prestateUpdater.addGameType(gameInput); - } - - function test_l1ContractsRelease_works() public view { - string memory result = "none"; - - assertEq(result, prestateUpdater.l1ContractsRelease()); - } - - function addGameType(IOPContractsManager.AddGameInput memory input) - internal - returns (IOPContractsManager.AddGameOutput memory) - { - IOPContractsManager.AddGameInput[] memory inputs = new IOPContractsManager.AddGameInput[](1); - inputs[0] = input; - - (bool success, bytes memory rawGameOut) = - address(opcm).delegatecall(abi.encodeCall(IOPContractsManager.addGameType, (inputs))); - assertTrue(success, "addGameType failed"); - - IOPContractsManager.AddGameOutput[] memory addGameOutAll = - abi.decode(rawGameOut, (IOPContractsManager.AddGameOutput[])); - return addGameOutAll[0]; - } - - function newGameInputFactory(bool permissioned) internal view returns (IOPContractsManager.AddGameInput memory) { - return IOPContractsManager.AddGameInput({ - saltMixer: "hello", - systemConfig: chainDeployOutput.systemConfigProxy, - proxyAdmin: chainDeployOutput.opChainProxyAdmin, - delayedWETH: IDelayedWETH(payable(address(0))), - disputeGameType: GameType.wrap(2000), - disputeAbsolutePrestate: Claim.wrap(bytes32(hex"deadbeef1234")), - disputeMaxGameDepth: 73, - disputeSplitDepth: 30, - disputeClockExtension: Duration.wrap(10800), - disputeMaxClockDuration: Duration.wrap(302400), - initialBond: 1 ether, - vm: IBigStepper(address(opcm.implementations().mipsImpl)), - permissioned: permissioned - }); - } -} From 2f0052b1af0cbbf7ff18c7217dd9577bcaf2d365 Mon Sep 17 00:00:00 2001 From: Kelvin Fichter Date: Wed, 5 Mar 2025 15:04:52 -0600 Subject: [PATCH 18/50] fix: remove old respectedGameType check from portal test --- packages/contracts-bedrock/test/L1/OptimismPortal2.t.sol | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/contracts-bedrock/test/L1/OptimismPortal2.t.sol b/packages/contracts-bedrock/test/L1/OptimismPortal2.t.sol index c6f02f3766856..e5fc247994fd5 100644 --- a/packages/contracts-bedrock/test/L1/OptimismPortal2.t.sol +++ b/packages/contracts-bedrock/test/L1/OptimismPortal2.t.sol @@ -54,7 +54,6 @@ contract OptimismPortal2_Test is CommonTest { assertEq(address(opImpl.anchorStateRegistry()), address(0)); assertEq(address(opImpl.systemConfig()), address(0)); assertEq(address(opImpl.superchainConfig()), address(0)); - assertEq(opImpl.respectedGameType().raw(), deploy.cfg().respectedGameType()); // TODO(opcm upgrades): remove skip once upgrade path is implemented returnIfForkTest("OptimismPortal2_Test: l2Sender is nonzero on OP mainnet"); From a983bbf61e4d41261e4b51ebf1b1466de233297c Mon Sep 17 00:00:00 2001 From: Kelvin Fichter Date: Wed, 5 Mar 2025 16:28:35 -0600 Subject: [PATCH 19/50] fix: deploy new dispute games in OPCM --- .../snapshots/semver-lock.json | 2 +- .../src/L1/OPContractsManager.sol | 59 ++++++++++++++++--- 2 files changed, 51 insertions(+), 10 deletions(-) diff --git a/packages/contracts-bedrock/snapshots/semver-lock.json b/packages/contracts-bedrock/snapshots/semver-lock.json index 36188da6a7ad2..6d10e87ba49c8 100644 --- a/packages/contracts-bedrock/snapshots/semver-lock.json +++ b/packages/contracts-bedrock/snapshots/semver-lock.json @@ -17,7 +17,7 @@ }, "src/L1/OPContractsManager.sol": { "initCodeHash": "0x3ba214bc6333b9d8b913b39fbecf24c008f15f8e02dd2d8cc4446f6adee56a35", - "sourceCodeHash": "0x4efca6044e969f2d5cf03baf2515baef8e05886c2a12de9fa4d5c438704fa882" + "sourceCodeHash": "0x148ac3ef4c934e4348d4d7c2a06289e5bc456024ff9c245df5d8eb49aa1d3d9e" }, "src/L1/OptimismPortal2.sol": { "initCodeHash": "0xac11984d6c3e58fd02e33509540ee329c4f5caedb94e422f8ab3a7a95ce7a546", diff --git a/packages/contracts-bedrock/src/L1/OPContractsManager.sol b/packages/contracts-bedrock/src/L1/OPContractsManager.sol index 4534354557761..5d15a891d5675 100644 --- a/packages/contracts-bedrock/src/L1/OPContractsManager.sol +++ b/packages/contracts-bedrock/src/L1/OPContractsManager.sol @@ -549,6 +549,7 @@ contract OPContractsManagerUpgrader is OPContractsManagerBase { ISuperchainConfig superchainConfig = IOptimismPortal2(payable(opChainAddrs.optimismPortal)).superchainConfig(); + // Separate context to avoid stack too deep. IAnchorStateRegistry newAnchorStateRegistryProxy; { // Deploy a new AnchorStateRegistry contract. @@ -561,6 +562,7 @@ contract OPContractsManagerUpgrader is OPContractsManagerBase { }) ); + // Separate context to avoid stack too deep. { // Get the existing anchor root from the old AnchorStateRegistry contract. // Get the AnchorStateRegistry from the PermissionedDisputeGame. @@ -601,6 +603,43 @@ contract OPContractsManagerUpgrader is OPContractsManagerBase { ); } + // We also need to redeploy the dispute games because the AnchorStateRegistry is new. + // Separate context to avoid stack too deep. + { + // Deploy and set a new permissioned game to update its prestate. + deployAndSetNewGameImpl({ + _l2ChainId: l2ChainId, + _disputeGame: IDisputeGame(address(permissionedDisputeGame)), + _newAnchorStateRegistryProxy: newAnchorStateRegistryProxy, + _gameType: GameTypes.PERMISSIONED_CANNON, + _opChainConfig: _opChainConfigs[i], + _opChainAddrs: opChainAddrs + }); + } + + // Separate context to avoid stack too deep. + { + // Now retrieve the permissionless game. + IFaultDisputeGame permissionlessDisputeGame = IFaultDisputeGame( + address( + getGameImplementation(IDisputeGameFactory(opChainAddrs.disputeGameFactory), GameTypes.CANNON) + ) + ); + + // If it exists, replace its implementation. + if (address(permissionlessDisputeGame) != address(0)) { + // Deploy and set a new permissionless game to update its prestate + deployAndSetNewGameImpl({ + _l2ChainId: l2ChainId, + _disputeGame: IDisputeGame(address(permissionlessDisputeGame)), + _newAnchorStateRegistryProxy: newAnchorStateRegistryProxy, + _gameType: GameTypes.CANNON, + _opChainConfig: _opChainConfigs[i], + _opChainAddrs: opChainAddrs + }); + } + } + // Emit the upgraded event with the address of the caller. Since this will be a delegatecall, // the caller will be the value of the ADDRESS opcode. emit Upgraded(l2ChainId, _opChainConfigs[i].systemConfigProxy, address(this)); @@ -629,28 +668,30 @@ contract OPContractsManagerUpgrader is OPContractsManagerBase { /// @notice Deploys and sets a new dispute game implementation /// @param _l2ChainId The L2 chain ID /// @param _disputeGame The current dispute game implementation + /// @param _newAnchorStateRegistryProxy The new anchor state registry proxy /// @param _gameType The type of game to deploy /// @param _opChainConfig The OP chain configuration - /// @param _blueprints The blueprint addresses - /// @param _implementations The implementation addresses /// @param _opChainAddrs The OP chain addresses function deployAndSetNewGameImpl( uint256 _l2ChainId, IDisputeGame _disputeGame, + IAnchorStateRegistry _newAnchorStateRegistryProxy, GameType _gameType, OPContractsManager.OpChainConfig memory _opChainConfig, - OPContractsManager.Blueprints memory _blueprints, - OPContractsManager.Implementations memory _implementations, ISystemConfig.Addresses memory _opChainAddrs ) internal { + OPContractsManager.Blueprints memory bps = getBlueprints(); + OPContractsManager.Implementations memory impls = getImplementations(); + // Get the constructor params for the game IFaultDisputeGame.GameConstructorParams memory params = getGameConstructorParams(IFaultDisputeGame(address(_disputeGame))); // Modify the params with the new vm values. - params.vm = IBigStepper(_implementations.mipsImpl); + params.anchorStateRegistry = IAnchorStateRegistry(address(_newAnchorStateRegistryProxy)); + params.vm = IBigStepper(impls.mipsImpl); if (Claim.unwrap(_opChainConfig.absolutePrestate) == bytes32(0)) { revert OPContractsManager.PrestateNotSet(); } @@ -662,8 +703,8 @@ contract OPContractsManagerUpgrader is OPContractsManagerBase { address challenger = getChallenger(IPermissionedDisputeGame(address(_disputeGame))); newGame = IDisputeGame( Blueprint.deployFrom( - _blueprints.permissionedDisputeGame1, - _blueprints.permissionedDisputeGame2, + bps.permissionedDisputeGame1, + bps.permissionedDisputeGame2, computeSalt(_l2ChainId, reusableSaltMixer(_opChainConfig), "PermissionedDisputeGame"), encodePermissionedFDGConstructor(params, proposer, challenger) ) @@ -671,8 +712,8 @@ contract OPContractsManagerUpgrader is OPContractsManagerBase { } else { newGame = IDisputeGame( Blueprint.deployFrom( - _blueprints.permissionlessDisputeGame1, - _blueprints.permissionlessDisputeGame2, + bps.permissionlessDisputeGame1, + bps.permissionlessDisputeGame2, computeSalt(_l2ChainId, reusableSaltMixer(_opChainConfig), "PermissionlessDisputeGame"), encodePermissionlessFDGConstructor(params) ) From 61b7fc72a2b3fc7f6faf55002d74068854eb46cf Mon Sep 17 00:00:00 2001 From: Kelvin Fichter Date: Wed, 5 Mar 2025 17:20:09 -0600 Subject: [PATCH 20/50] feat: add upgrade 15 test path --- .../scripts/checks/interfaces/main.go | 3 + .../abi/OPContractsManagerUpgrader.json | 5 + .../snapshots/semver-lock.json | 2 +- .../src/L1/OPContractsManager.sol | 8 + .../test/L1/OPContractsManager.t.sol | 164 +++++++++++++++++- 5 files changed, 178 insertions(+), 4 deletions(-) diff --git a/packages/contracts-bedrock/scripts/checks/interfaces/main.go b/packages/contracts-bedrock/scripts/checks/interfaces/main.go index 211730d3adc1f..0ecc45e19034b 100644 --- a/packages/contracts-bedrock/scripts/checks/interfaces/main.go +++ b/packages/contracts-bedrock/scripts/checks/interfaces/main.go @@ -26,6 +26,9 @@ var excludeContracts = []string{ // EAS "IEAS", "ISchemaResolver", "ISchemaRegistry", + // Misc stuff that can be ignored + "IOPContractsManagerLegacyUpgrade", + // TODO: Interfaces that need to be fixed "IInitializable", "IOptimismMintableERC20", "ILegacyMintableERC20", "KontrolCheatsBase", "ISystemConfigInterop", "IResolvedDelegateProxy", diff --git a/packages/contracts-bedrock/snapshots/abi/OPContractsManagerUpgrader.json b/packages/contracts-bedrock/snapshots/abi/OPContractsManagerUpgrader.json index 40b1e1f952673..195f8d2acbc87 100644 --- a/packages/contracts-bedrock/snapshots/abi/OPContractsManagerUpgrader.json +++ b/packages/contracts-bedrock/snapshots/abi/OPContractsManagerUpgrader.json @@ -286,6 +286,11 @@ "name": "NotABlueprint", "type": "error" }, + { + "inputs": [], + "name": "PrestateNotSet", + "type": "error" + }, { "inputs": [], "name": "ReservedBitsSet", diff --git a/packages/contracts-bedrock/snapshots/semver-lock.json b/packages/contracts-bedrock/snapshots/semver-lock.json index 6d10e87ba49c8..0a4f051b4ea9f 100644 --- a/packages/contracts-bedrock/snapshots/semver-lock.json +++ b/packages/contracts-bedrock/snapshots/semver-lock.json @@ -17,7 +17,7 @@ }, "src/L1/OPContractsManager.sol": { "initCodeHash": "0x3ba214bc6333b9d8b913b39fbecf24c008f15f8e02dd2d8cc4446f6adee56a35", - "sourceCodeHash": "0x148ac3ef4c934e4348d4d7c2a06289e5bc456024ff9c245df5d8eb49aa1d3d9e" + "sourceCodeHash": "0x42a09d457fff63d8657313b000b0fd30afb29b61c09edc5e647ea84c27810b37" }, "src/L1/OptimismPortal2.sol": { "initCodeHash": "0xac11984d6c3e58fd02e33509540ee329c4f5caedb94e422f8ab3a7a95ce7a546", diff --git a/packages/contracts-bedrock/src/L1/OPContractsManager.sol b/packages/contracts-bedrock/src/L1/OPContractsManager.sol index 5d15a891d5675..f7893955cfffa 100644 --- a/packages/contracts-bedrock/src/L1/OPContractsManager.sol +++ b/packages/contracts-bedrock/src/L1/OPContractsManager.sol @@ -549,6 +549,14 @@ contract OPContractsManagerUpgrader is OPContractsManagerBase { ISuperchainConfig superchainConfig = IOptimismPortal2(payable(opChainAddrs.optimismPortal)).superchainConfig(); + // Start by upgrading the SystemConfig contract to have the l2ChainId. + upgradeToAndCall( + _opChainConfigs[i].proxyAdmin, + address(_opChainConfigs[i].systemConfigProxy), + impls.systemConfigImpl, + abi.encodeCall(ISystemConfig.upgrade, (l2ChainId)) + ); + // Separate context to avoid stack too deep. IAnchorStateRegistry newAnchorStateRegistryProxy; { diff --git a/packages/contracts-bedrock/test/L1/OPContractsManager.t.sol b/packages/contracts-bedrock/test/L1/OPContractsManager.t.sol index 5b317f9b89399..0b345af557ca6 100644 --- a/packages/contracts-bedrock/test/L1/OPContractsManager.t.sol +++ b/packages/contracts-bedrock/test/L1/OPContractsManager.t.sol @@ -59,6 +59,21 @@ import { IBigStepper } from "interfaces/dispute/IBigStepper.sol"; import { GameType, Duration, Hash, Claim } from "src/dispute/lib/LibUDT.sol"; import { OutputRoot, GameTypes } from "src/dispute/lib/Types.sol"; +/// @title IOPContractsManagerLegacyUpgrade +/// @notice Interface for the legacy OPContractsManager upgrade function. +/// This interface is used to test Upgrade 13 and 14 paths and can be safely removed +/// after those upgrades are completed. Only difference in the new struct is the added +/// disputeGameUsesSuperRoots boolean. +interface IOPContractsManagerLegacyUpgrade { + struct OpChainConfig { + ISystemConfig systemConfigProxy; + IProxyAdmin proxyAdmin; + Claim absolutePrestate; + } + + function upgrade(OpChainConfig[] memory _opChainConfigs) external; +} + // Exposes internal functions for testing. contract OPContractsManager_Harness is OPContractsManager { constructor( @@ -285,6 +300,29 @@ contract OPContractsManager_Upgrade_Harness is CommonTest { faultDisputeGame = IFaultDisputeGame(address(artifacts.mustGetAddress("FaultDisputeGame"))); } + /// @notice Converts the new OpChainConfig struct to the legacy OpChainConfig struct format. + /// This function is used to test Upgrade 13 and 14 paths and can be safely removed + /// after those upgrades are completed. Only difference in the new struct is the added + /// disputeGameUsesSuperRoots boolean. + /// @param _opChainConfigs The new OpChainConfig structs to convert. + /// @return The legacy OpChainConfig structs. + function convertToLegacyConfigs(IOPContractsManager.OpChainConfig[] memory _opChainConfigs) + public + pure + returns (IOPContractsManagerLegacyUpgrade.OpChainConfig[] memory) + { + IOPContractsManagerLegacyUpgrade.OpChainConfig[] memory legacyConfigs = + new IOPContractsManagerLegacyUpgrade.OpChainConfig[](_opChainConfigs.length); + for (uint256 i = 0; i < _opChainConfigs.length; i++) { + legacyConfigs[i] = IOPContractsManagerLegacyUpgrade.OpChainConfig({ + systemConfigProxy: _opChainConfigs[i].systemConfigProxy, + proxyAdmin: _opChainConfigs[i].proxyAdmin, + absolutePrestate: _opChainConfigs[i].absolutePrestate + }); + } + return legacyConfigs; + } + function expectEmitUpgraded(address impl, address proxy) public { vm.expectEmit(proxy); emit Upgraded(impl); @@ -351,7 +389,8 @@ contract OPContractsManager_Upgrade_Harness is CommonTest { vm.etch(_delegateCaller, vm.getDeployedCode("test/mocks/Callers.sol:DelegateCaller")); DelegateCaller(_delegateCaller).dcForward( - address(deployedOPCM), abi.encodeCall(IOPContractsManager.upgrade, (opChainConfigs)) + address(deployedOPCM), + abi.encodeCall(IOPContractsManagerLegacyUpgrade.upgrade, (convertToLegacyConfigs(opChainConfigs))) ); VmSafe.Gas memory gas = vm.lastCallGas(); @@ -398,7 +437,9 @@ contract OPContractsManager_Upgrade_Harness is CommonTest { } function runUpgrade14UpgradeAndChecks(address _delegateCaller) public { - IOPContractsManager.Implementations memory impls = opcm.implementations(); + // TODO: Change this address! + IOPContractsManager deployedOPCM = IOPContractsManager(address(0x026b2F158255Beac46c1E7c6b8BbF29A4b6A7B76)); + IOPContractsManager.Implementations memory impls = deployedOPCM.implementations(); // sanity check IPermissionedDisputeGame oldPDG = @@ -429,7 +470,8 @@ contract OPContractsManager_Upgrade_Harness is CommonTest { vm.etch(_delegateCaller, vm.getDeployedCode("test/mocks/Callers.sol:DelegateCaller")); DelegateCaller(_delegateCaller).dcForward( - address(opcm), abi.encodeCall(IOPContractsManager.upgrade, (opChainConfigs)) + address(deployedOPCM), + abi.encodeCall(IOPContractsManagerLegacyUpgrade.upgrade, (convertToLegacyConfigs(opChainConfigs))) ); VmSafe.Gas memory gas = vm.lastCallGas(); @@ -460,6 +502,122 @@ contract OPContractsManager_Upgrade_Harness is CommonTest { } } + function runUpgrade15UpgradeAndChecks(address _delegateCaller) public { + IOPContractsManager.Implementations memory impls = opcm.implementations(); + + // Predict the address of the new AnchorStateRegistry proxy. + // Subcontext to avoid stack too deep. + address newAsrProxy; + { + // Compute the salt using the system config address. + bytes32 salt = keccak256( + abi.encode( + l2ChainId, + string.concat(string(bytes.concat(bytes32(uint256(uint160(address(systemConfig))))))), + "AnchorStateRegistry" + ) + ); + + // Use the actual proxy instead of the local code so we can reuse this test. + address proxyBp = opcm.blueprints().proxy; + Blueprint.Preamble memory preamble = Blueprint.parseBlueprintPreamble(proxyBp.code); + bytes memory initCode = bytes.concat(preamble.initcode, abi.encode(proxyAdmin)); + newAsrProxy = vm.computeCreate2Address(salt, keccak256(initCode), _delegateCaller); + vm.label(newAsrProxy, "NewAnchorStateRegistryProxy"); + } + + // Grab the PermissionedDisputeGame and FaultDisputeGame implementations before upgrade. + address oldPDGImpl = address(disputeGameFactory.gameImpls(GameTypes.PERMISSIONED_CANNON)); + address oldFDGImpl = address(disputeGameFactory.gameImpls(GameTypes.CANNON)); + IPermissionedDisputeGame oldPDG = IPermissionedDisputeGame(oldPDGImpl); + IFaultDisputeGame oldFDG = IFaultDisputeGame(oldFDGImpl); + + // Expect the SystemConfig and OptimismPortal to be upgraded. + expectEmitUpgraded(impls.systemConfigImpl, address(systemConfig)); + expectEmitUpgraded(impls.optimismPortalImpl, address(optimismPortal2)); + + // Expect the new AnchorStateRegistry to be initialized. + vm.expectEmit(address(newAsrProxy)); + emit AdminChanged(address(0), address(proxyAdmin)); + + // We always expect the PermissionedDisputeGame to be deployed. We don't yet know the + // address of the new permissionedGame which will be deployed by the + // OPContractsManager.upgrade() call, so ignore the first topic. + vm.expectEmit(false, true, true, true, address(disputeGameFactory)); + emit ImplementationSet(address(0), GameTypes.PERMISSIONED_CANNON); + + // If the old FaultDisputeGame exists, we expect it to be upgraded. + if (address(oldFDG) != address(0)) { + // Ignore the first topic for the same reason as the previous comment. + vm.expectEmit(false, true, true, true, address(disputeGameFactory)); + emit ImplementationSet(address(0), GameTypes.CANNON); + } + + vm.expectEmit(address(_delegateCaller)); + emit Upgraded(l2ChainId, systemConfig, address(_delegateCaller)); + + // Temporarily replace the upgrader with a DelegateCaller so we can test the upgrade, + // then reset its code to the original code. + bytes memory delegateCallerCode = address(_delegateCaller).code; + vm.etch(_delegateCaller, vm.getDeployedCode("test/mocks/Callers.sol:DelegateCaller")); + + // Execute the upgrade. + // We use the new format here, not the legacy one. + DelegateCaller(_delegateCaller).dcForward( + address(opcm), abi.encodeCall(IOPContractsManager.upgrade, (opChainConfigs)) + ); + + // Less than 90% of the gas target of 20M to account for the gas used by using Safe. + VmSafe.Gas memory gas = vm.lastCallGas(); + assertLt(gas.gasTotalUsed, 0.9 * 20_000_000, "Upgrade exceeds gas target of 15M"); + + // Reset the upgrader's code to the original code. + vm.etch(_delegateCaller, delegateCallerCode); + + // Grab the new implementations. + address newPDGImpl = address(disputeGameFactory.gameImpls(GameTypes.PERMISSIONED_CANNON)); + IPermissionedDisputeGame pdg = IPermissionedDisputeGame(newPDGImpl); + address newFDGImpl = address(disputeGameFactory.gameImpls(GameTypes.CANNON)); + IFaultDisputeGame fdg = IFaultDisputeGame(newFDGImpl); + + // Check that the PermissionedDisputeGame is upgraded to the expected version, references + // the correct anchor state and has the mipsImpl. Although Upgrade 15 doesn't actually + // change any of this, we might as well check it again. + assertEq(ISemver(address(pdg)).version(), "1.4.1"); + assertEq(address(pdg.vm()), impls.mipsImpl); + assertEq(pdg.l2ChainId(), oldPDG.l2ChainId()); + + // If the old FaultDisputeGame exists, we expect it to be upgraded. Check same as above. + if (address(oldFDG) != address(0)) { + assertEq(ISemver(address(fdg)).version(), "1.4.1"); + assertEq(address(fdg.vm()), impls.mipsImpl); + assertEq(fdg.l2ChainId(), oldFDG.l2ChainId()); + } + + // Make sure that the SystemConfig is upgraded to the right version. It must also have the + // right l2ChainId and must be properly initialized. + assertEq(ISemver(address(systemConfig)).version(), "2.6.0"); + assertEq(impls.systemConfigImpl, EIP1967Helper.getImplementation(address(systemConfig))); + assertEq(systemConfig.l2ChainId(), l2ChainId); + DeployUtils.assertInitialized({ _contractAddress: address(systemConfig), _isProxy: true, _slot: 0, _offset: 0 }); + + // Make sure that the OptimismPortal is upgraded to the right version. It must also have a + // reference to the new AnchorStateRegistry. + assertEq(ISemver(address(optimismPortal2)).version(), "4.0.0"); + assertEq(impls.optimismPortalImpl, EIP1967Helper.getImplementation(address(optimismPortal2))); + assertEq(address(optimismPortal2.anchorStateRegistry()), address(newAsrProxy)); + DeployUtils.assertInitialized({ + _contractAddress: address(optimismPortal2), + _isProxy: true, + _slot: 0, + _offset: 0 + }); + + // Make sure the new AnchorStateRegistry has the right version and is initialized. + assertEq(ISemver(address(newAsrProxy)).version(), "3.0.0"); + DeployUtils.assertInitialized({ _contractAddress: address(newAsrProxy), _isProxy: false, _slot: 0, _offset: 0 }); + } + function runUpgradeTestAndChecks(address _delegateCaller) public { runV200UpgradeAndChecks(_delegateCaller); runUpgrade14UpgradeAndChecks(_delegateCaller); From 4fb777e0739b088405a68d0c3b6170181a23677c Mon Sep 17 00:00:00 2001 From: Kelvin Fichter Date: Wed, 5 Mar 2025 17:52:23 -0600 Subject: [PATCH 21/50] feat: integrate into opcm fork tests --- .../L1/IOPContractsManagerLegacyUpgrade.sol | 21 +++ .../interfaces/L1/IOPPrestateUpdater.sol | 157 ------------------ packages/contracts-bedrock/justfile | 2 +- .../snapshots/semver-lock.json | 2 +- .../src/L1/OPContractsManager.sol | 3 +- .../test/L1/OPContractsManager.t.sol | 21 +-- .../test/setup/ForkLive.s.sol | 29 +++- 7 files changed, 56 insertions(+), 179 deletions(-) create mode 100644 packages/contracts-bedrock/interfaces/L1/IOPContractsManagerLegacyUpgrade.sol delete mode 100644 packages/contracts-bedrock/interfaces/L1/IOPPrestateUpdater.sol diff --git a/packages/contracts-bedrock/interfaces/L1/IOPContractsManagerLegacyUpgrade.sol b/packages/contracts-bedrock/interfaces/L1/IOPContractsManagerLegacyUpgrade.sol new file mode 100644 index 0000000000000..057139005a96a --- /dev/null +++ b/packages/contracts-bedrock/interfaces/L1/IOPContractsManagerLegacyUpgrade.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import { ISystemConfig } from "interfaces/L1/ISystemConfig.sol"; +import { IProxyAdmin } from "interfaces/universal/IProxyAdmin.sol"; +import { Claim } from "src/dispute/lib/Types.sol"; + +/// @title IOPContractsManagerLegacyUpgrade +/// @notice Interface for the legacy OPContractsManager upgrade function. +/// This interface is used to test Upgrade 13 and 14 paths and can be safely removed +/// after those upgrades are completed. Only difference in the new struct is the added +/// disputeGameUsesSuperRoots boolean. +interface IOPContractsManagerLegacyUpgrade { + struct OpChainConfig { + ISystemConfig systemConfigProxy; + IProxyAdmin proxyAdmin; + Claim absolutePrestate; + } + + function upgrade(OpChainConfig[] memory _opChainConfigs) external; +} diff --git a/packages/contracts-bedrock/interfaces/L1/IOPPrestateUpdater.sol b/packages/contracts-bedrock/interfaces/L1/IOPPrestateUpdater.sol deleted file mode 100644 index 33533cf96a975..0000000000000 --- a/packages/contracts-bedrock/interfaces/L1/IOPPrestateUpdater.sol +++ /dev/null @@ -1,157 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; - -// Libraries -import { GameType } from "src/dispute/lib/Types.sol"; - -// Interfaces -import { IProxyAdmin } from "interfaces/universal/IProxyAdmin.sol"; -import { ISuperchainConfig } from "interfaces/L1/ISuperchainConfig.sol"; -import { IProtocolVersions } from "interfaces/L1/IProtocolVersions.sol"; -import { ISystemConfig } from "interfaces/L1/ISystemConfig.sol"; -import { IOPContractsManager } from "interfaces/L1/IOPContractsManager.sol"; -import { IDisputeGame } from "interfaces/dispute/IDisputeGame.sol"; - -interface IOPPrestateUpdater { - // -------- Constants and Variables -------- - - function version() external pure returns (string memory); - - /// @notice Address of the SuperchainConfig contract shared by all chains. - function superchainConfig() external view returns (ISuperchainConfig); - - /// @notice Address of the ProtocolVersions contract shared by all chains. - function protocolVersions() external view returns (IProtocolVersions); - - /// @notice Address of the ProxyAdmin contract shared by all chains. - function superchainProxyAdmin() external view returns (IProxyAdmin); - - /// @notice L1 smart contracts release deployed by this version of OPCM. This is used in opcm to signal which - /// version of the L1 smart contracts is deployed. It takes the format of `op-contracts/vX.Y.Z`. - function l1ContractsRelease() external view returns (string memory); - - // -------- Events -------- - - /// @notice Emitted when a new OP Stack chain is deployed. - /// @param l2ChainId Chain ID of the new chain. - /// @param deployer Address that deployed the chain. - /// @param deployOutput ABI-encoded output of the deployment. - event Deployed(uint256 indexed l2ChainId, address indexed deployer, bytes deployOutput); - - /// @notice Emitted when a chain is upgraded - /// @param systemConfig Address of the chain's SystemConfig contract - /// @param upgrader Address that initiated the upgrade - event Upgraded(uint256 indexed l2ChainId, ISystemConfig indexed systemConfig, address indexed upgrader); - - /// @notice Emitted when a new game type is added to a chain - /// @param l2ChainId Chain ID of the chain - /// @param gameType Type of the game being added - /// @param newDisputeGame Address of the deployed dispute game - /// @param oldDisputeGame Address of the old dispute game - event GameTypeAdded(uint256 indexed l2ChainId, GameType indexed gameType, IDisputeGame newDisputeGame, IDisputeGame oldDisputeGame); - - // -------- Errors -------- - - error BytesArrayTooLong(); - error DeploymentFailed(); - error EmptyInitcode(); - error IdentityPrecompileCallFailed(); - error NotABlueprint(); - error ReservedBitsSet(); - error UnexpectedPreambleData(bytes data); - error UnsupportedERCVersion(uint8 version); - error OnlyUpgradeController(); - error PrestateNotSet(); - - /// @notice Thrown when an address is the zero address. - error AddressNotFound(address who); - - /// @notice Throw when a contract address has no code. - error AddressHasNoCode(address who); - - /// @notice Thrown when a release version is already set. - error AlreadyReleased(); - - /// @notice Thrown when an invalid `l2ChainId` is provided to `deploy`. - error InvalidChainId(); - - /// @notice Thrown when a role's address is not valid (opChainProxyAdminOwner). - error InvalidRoleAddressPAO(); - - /// @notice Thrown when a role's address is not valid (systemConfigOwner). - error InvalidRoleAddressSCO(); - - /// @notice Thrown when a role's address is not valid (batcher). - error InvalidRoleAddressBatcher(); - - /// @notice Thrown when a role's address is not valid (unsafeBlockSigner). - error InvalidRoleAddressUBS(); - - /// @notice Thrown when a role's address is not valid (proposer). - error InvalidRoleAddressProposer(); - - /// @notice Thrown when a role's address is not valid (challenger). - error InvalidRoleAddressChallenger(); - - /// @notice Thrown when the latest release is not set upon initialization. - error LatestReleaseNotSet(); - - /// @notice Thrown when the starting anchor root is not provided. - error InvalidStartingAnchorRoot(); - - /// @notice Thrown when certain methods are called outside of a DELEGATECALL. - error OnlyDelegatecall(); - - /// @notice Thrown when game configs passed to addGameType are invalid. - error InvalidGameConfigs(); - - /// @notice Thrown when the SuperchainConfig of the chain does not match the SuperchainConfig of this OPCM. - error SuperchainConfigMismatch(ISystemConfig systemConfig); - - error SuperchainProxyAdminMismatch(); - - /// @notice Thrown when a function from the parent (OPCM) is not implemented. - error NotImplemented(); - - /// @notice Thrown when the prestate of a permissioned disputed game is 0. - error PrestateRequired(); - - // -------- Methods -------- - - function __constructor__( - ISuperchainConfig _superchainConfig, - IProtocolVersions _protocolVersions, - IOPContractsManager.Blueprints memory _blueprints - ) - external; - - function deploy(IOPContractsManager.DeployInput calldata _input) external returns (IOPContractsManager.DeployOutput memory); - - /// @notice Upgrades the implementation of all proxies in the specified chains - /// @param _opChainConfigs The chains to upgrade - function upgrade(IOPContractsManager.OpChainConfig[] memory _opChainConfigs) external; - - /// @notice addGameType deploys a new dispute game and links it to the DisputeGameFactory. The inputted _gameConfigs - /// must be added in ascending GameType order. - function addGameType(IOPContractsManager.AddGameInput[] memory _gameConfigs) external returns (IOPContractsManager.AddGameOutput[] memory); - - /// @notice Maps an L2 chain ID to an L1 batch inbox address as defined by the standard - /// configuration's convention. This convention is `versionByte || keccak256(bytes32(chainId))[:19]`, - /// where || denotes concatenation`, versionByte is 0x00, and chainId is a uint256. - /// https://specs.optimism.io/protocol/configurability.html#consensus-parameters - function chainIdToBatchInboxAddress(uint256 _l2ChainId) external pure returns (address); - - /// @notice Returns the blueprint contract addresses. - function blueprints() external view returns (IOPContractsManager.Blueprints memory); - - /// @notice Returns the implementation contract addresses. - function implementations() external view returns (IOPContractsManager.Implementations memory); - - function upgradeController() external view returns (address); - - function isRC() external view returns (bool); - - function setRC(bool _isRC) external; - - function updatePrestate(IOPContractsManager.OpChainConfig[] memory _prestateUpdateInputs) external; -} diff --git a/packages/contracts-bedrock/justfile b/packages/contracts-bedrock/justfile index 5f6c8a97a745c..39bc17d459ec9 100644 --- a/packages/contracts-bedrock/justfile +++ b/packages/contracts-bedrock/justfile @@ -65,7 +65,7 @@ test-dev *ARGS: build-go-ffi # Default block number for the forked upgrade path. export sepoliaBlockNumber := "7701807" -export mainnetBlockNumber := "21971446" +export mainnetBlockNumber := "21983964" export pinnedBlockNumber := if env_var_or_default("FORK_BASE_CHAIN", "") == "mainnet" { mainnetBlockNumber diff --git a/packages/contracts-bedrock/snapshots/semver-lock.json b/packages/contracts-bedrock/snapshots/semver-lock.json index 0a4f051b4ea9f..ba18d64d1897b 100644 --- a/packages/contracts-bedrock/snapshots/semver-lock.json +++ b/packages/contracts-bedrock/snapshots/semver-lock.json @@ -17,7 +17,7 @@ }, "src/L1/OPContractsManager.sol": { "initCodeHash": "0x3ba214bc6333b9d8b913b39fbecf24c008f15f8e02dd2d8cc4446f6adee56a35", - "sourceCodeHash": "0x42a09d457fff63d8657313b000b0fd30afb29b61c09edc5e647ea84c27810b37" + "sourceCodeHash": "0x9f017a7b33f4be8e3054a330355334d439163ed63003e401de54fb1c70cfa413" }, "src/L1/OptimismPortal2.sol": { "initCodeHash": "0xac11984d6c3e58fd02e33509540ee329c4f5caedb94e422f8ab3a7a95ce7a546", diff --git a/packages/contracts-bedrock/src/L1/OPContractsManager.sol b/packages/contracts-bedrock/src/L1/OPContractsManager.sol index f7893955cfffa..0e3d29e520dd6 100644 --- a/packages/contracts-bedrock/src/L1/OPContractsManager.sol +++ b/packages/contracts-bedrock/src/L1/OPContractsManager.sol @@ -561,12 +561,13 @@ contract OPContractsManagerUpgrader is OPContractsManagerBase { IAnchorStateRegistry newAnchorStateRegistryProxy; { // Deploy a new AnchorStateRegistry contract. + // We use the SOT suffix to avoid CREATE2 conflicts with the existing ASR. newAnchorStateRegistryProxy = IAnchorStateRegistry( deployProxy({ _l2ChainId: l2ChainId, _proxyAdmin: _opChainConfigs[i].proxyAdmin, _saltMixer: reusableSaltMixer(_opChainConfigs[i]), - _contractName: "AnchorStateRegistry" + _contractName: "AnchorStateRegistry-SOT" }) ); diff --git a/packages/contracts-bedrock/test/L1/OPContractsManager.t.sol b/packages/contracts-bedrock/test/L1/OPContractsManager.t.sol index 0b345af557ca6..e596852f5598f 100644 --- a/packages/contracts-bedrock/test/L1/OPContractsManager.t.sol +++ b/packages/contracts-bedrock/test/L1/OPContractsManager.t.sol @@ -44,6 +44,7 @@ import { IOPContractsManagerUpgrader, IOPContractsManagerContractsContainer } from "interfaces/L1/IOPContractsManager.sol"; +import { IOPContractsManagerLegacyUpgrade } from "interfaces/L1/IOPContractsManagerLegacyUpgrade.sol"; import { ISemver } from "interfaces/universal/ISemver.sol"; // Contracts @@ -59,21 +60,6 @@ import { IBigStepper } from "interfaces/dispute/IBigStepper.sol"; import { GameType, Duration, Hash, Claim } from "src/dispute/lib/LibUDT.sol"; import { OutputRoot, GameTypes } from "src/dispute/lib/Types.sol"; -/// @title IOPContractsManagerLegacyUpgrade -/// @notice Interface for the legacy OPContractsManager upgrade function. -/// This interface is used to test Upgrade 13 and 14 paths and can be safely removed -/// after those upgrades are completed. Only difference in the new struct is the added -/// disputeGameUsesSuperRoots boolean. -interface IOPContractsManagerLegacyUpgrade { - struct OpChainConfig { - ISystemConfig systemConfigProxy; - IProxyAdmin proxyAdmin; - Claim absolutePrestate; - } - - function upgrade(OpChainConfig[] memory _opChainConfigs) external; -} - // Exposes internal functions for testing. contract OPContractsManager_Harness is OPContractsManager { constructor( @@ -437,8 +423,8 @@ contract OPContractsManager_Upgrade_Harness is CommonTest { } function runUpgrade14UpgradeAndChecks(address _delegateCaller) public { - // TODO: Change this address! - IOPContractsManager deployedOPCM = IOPContractsManager(address(0x026b2F158255Beac46c1E7c6b8BbF29A4b6A7B76)); + // TODO(#14665): Replace this address with once the final OPCM is deployed for Upgrade 14. + IOPContractsManager deployedOPCM = IOPContractsManager(address(0x3A1f523a4bc09cd344A2745a108Bb0398288094F)); IOPContractsManager.Implementations memory impls = deployedOPCM.implementations(); // sanity check @@ -621,6 +607,7 @@ contract OPContractsManager_Upgrade_Harness is CommonTest { function runUpgradeTestAndChecks(address _delegateCaller) public { runV200UpgradeAndChecks(_delegateCaller); runUpgrade14UpgradeAndChecks(_delegateCaller); + runUpgrade15UpgradeAndChecks(_delegateCaller); } } diff --git a/packages/contracts-bedrock/test/setup/ForkLive.s.sol b/packages/contracts-bedrock/test/setup/ForkLive.s.sol index 80b516f4ee09e..2fcee28ee5532 100644 --- a/packages/contracts-bedrock/test/setup/ForkLive.s.sol +++ b/packages/contracts-bedrock/test/setup/ForkLive.s.sol @@ -24,7 +24,7 @@ import { ISystemConfig } from "interfaces/L1/ISystemConfig.sol"; import { IProxyAdmin } from "interfaces/universal/IProxyAdmin.sol"; import { IOPContractsManager } from "interfaces/L1/IOPContractsManager.sol"; import { IAnchorStateRegistry } from "interfaces/dispute/IAnchorStateRegistry.sol"; - +import { IOPContractsManagerLegacyUpgrade } from "interfaces/L1/IOPContractsManagerLegacyUpgrade.sol"; /// @title ForkLive /// @notice This script is called by Setup.sol as a preparation step for the foundry test suite, and is run as an /// alternative to Deploy.s.sol, when `FORK_TEST=true` is set in the env. @@ -34,6 +34,7 @@ import { IAnchorStateRegistry } from "interfaces/dispute/IAnchorStateRegistry.so /// Therefore this script can only be run against a fork of a production network which is listed in the /// superchain-registry. /// This contract must not have constructor logic because it is set into state using `etch`. + contract ForkLive is Deployer { using stdToml for string; @@ -185,10 +186,34 @@ contract ForkLive is Deployer { // then reset its code to the original code. bytes memory upgraderCode = address(upgrader).code; vm.etch(upgrader, vm.getDeployedCode("test/mocks/Callers.sol:DelegateCaller")); + + // Some upgrades require the legacy format. + IOPContractsManagerLegacyUpgrade.OpChainConfig[] memory legacyConfigs = + new IOPContractsManagerLegacyUpgrade.OpChainConfig[](opChains.length); + for (uint256 i = 0; i < opChains.length; i++) { + legacyConfigs[i] = IOPContractsManagerLegacyUpgrade.OpChainConfig({ + systemConfigProxy: opChains[i].systemConfigProxy, + proxyAdmin: opChains[i].proxyAdmin, + absolutePrestate: opChains[i].absolutePrestate + }); + } + + // Start by doing Upgrade 13. DelegateCaller(upgrader).dcForward( - address(0x026b2F158255Beac46c1E7c6b8BbF29A4b6A7B76), abi.encodeCall(IOPContractsManager.upgrade, (opChains)) + address(0x026b2F158255Beac46c1E7c6b8BbF29A4b6A7B76), + abi.encodeCall(IOPContractsManagerLegacyUpgrade.upgrade, (legacyConfigs)) ); + + // Then do Upgrade 14. + DelegateCaller(upgrader).dcForward( + address(0x3A1f523a4bc09cd344A2745a108Bb0398288094F), + abi.encodeCall(IOPContractsManagerLegacyUpgrade.upgrade, (legacyConfigs)) + ); + + // Then do the final upgrade. DelegateCaller(upgrader).dcForward(address(opcm), abi.encodeCall(IOPContractsManager.upgrade, (opChains))); + + // Reset the upgrader to the original code. vm.etch(upgrader, upgraderCode); console.log("ForkLive: Saving newly deployed contracts"); From 43bc99d68986af17b6010ffa245d87a396b3f817 Mon Sep 17 00:00:00 2001 From: Kelvin Fichter Date: Wed, 5 Mar 2025 18:32:43 -0600 Subject: [PATCH 22/50] fix: corrected checks for ASR --- packages/contracts-bedrock/justfile | 2 +- .../test/L1/OPContractsManager.t.sol | 11 +++++------ 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/packages/contracts-bedrock/justfile b/packages/contracts-bedrock/justfile index 39bc17d459ec9..1908267920fe3 100644 --- a/packages/contracts-bedrock/justfile +++ b/packages/contracts-bedrock/justfile @@ -65,7 +65,7 @@ test-dev *ARGS: build-go-ffi # Default block number for the forked upgrade path. export sepoliaBlockNumber := "7701807" -export mainnetBlockNumber := "21983964" +export mainnetBlockNumber := "21983965" export pinnedBlockNumber := if env_var_or_default("FORK_BASE_CHAIN", "") == "mainnet" { mainnetBlockNumber diff --git a/packages/contracts-bedrock/test/L1/OPContractsManager.t.sol b/packages/contracts-bedrock/test/L1/OPContractsManager.t.sol index e596852f5598f..82c90379fd553 100644 --- a/packages/contracts-bedrock/test/L1/OPContractsManager.t.sol +++ b/packages/contracts-bedrock/test/L1/OPContractsManager.t.sol @@ -27,6 +27,7 @@ import { IOptimismMintableERC20Factory } from "interfaces/universal/IOptimismMin import { IL1CrossDomainMessenger } from "interfaces/L1/IL1CrossDomainMessenger.sol"; import { IMIPS } from "interfaces/cannon/IMIPS.sol"; import { IOptimismPortal2 } from "interfaces/L1/IOptimismPortal2.sol"; +import { IProxy } from "interfaces/universal/IProxy.sol"; import { IProxyAdmin } from "interfaces/universal/IProxyAdmin.sol"; import { ISuperchainConfig } from "interfaces/L1/ISuperchainConfig.sol"; import { IProtocolVersions } from "interfaces/L1/IProtocolVersions.sol"; @@ -500,7 +501,7 @@ contract OPContractsManager_Upgrade_Harness is CommonTest { abi.encode( l2ChainId, string.concat(string(bytes.concat(bytes32(uint256(uint160(address(systemConfig))))))), - "AnchorStateRegistry" + "AnchorStateRegistry-SOT" ) ); @@ -522,10 +523,6 @@ contract OPContractsManager_Upgrade_Harness is CommonTest { expectEmitUpgraded(impls.systemConfigImpl, address(systemConfig)); expectEmitUpgraded(impls.optimismPortalImpl, address(optimismPortal2)); - // Expect the new AnchorStateRegistry to be initialized. - vm.expectEmit(address(newAsrProxy)); - emit AdminChanged(address(0), address(proxyAdmin)); - // We always expect the PermissionedDisputeGame to be deployed. We don't yet know the // address of the new permissionedGame which will be deployed by the // OPContractsManager.upgrade() call, so ignore the first topic. @@ -601,7 +598,9 @@ contract OPContractsManager_Upgrade_Harness is CommonTest { // Make sure the new AnchorStateRegistry has the right version and is initialized. assertEq(ISemver(address(newAsrProxy)).version(), "3.0.0"); - DeployUtils.assertInitialized({ _contractAddress: address(newAsrProxy), _isProxy: false, _slot: 0, _offset: 0 }); + vm.prank(address(proxyAdmin)); + assertEq(IProxy(payable(newAsrProxy)).admin(), address(proxyAdmin)); + DeployUtils.assertInitialized({ _contractAddress: address(newAsrProxy), _isProxy: true, _slot: 0, _offset: 0 }); } function runUpgradeTestAndChecks(address _delegateCaller) public { From cfeb68f24dee0aeed127316ef55c97e1f390648d Mon Sep 17 00:00:00 2001 From: Kelvin Fichter Date: Wed, 5 Mar 2025 19:00:42 -0600 Subject: [PATCH 23/50] fix: rebase tweaks --- .../contracts-bedrock/snapshots/semver-lock.json | 16 ++++++++-------- .../snapshots/storageLayout/SystemConfig.json | 9 ++++++++- .../storageLayout/SystemConfigInterop.json | 9 ++++++++- .../src/L1/OPContractsManager.sol | 4 ++-- 4 files changed, 26 insertions(+), 12 deletions(-) diff --git a/packages/contracts-bedrock/snapshots/semver-lock.json b/packages/contracts-bedrock/snapshots/semver-lock.json index ba18d64d1897b..9d1dd675cc2ea 100644 --- a/packages/contracts-bedrock/snapshots/semver-lock.json +++ b/packages/contracts-bedrock/snapshots/semver-lock.json @@ -16,15 +16,15 @@ "sourceCodeHash": "0x44797707aea8c63dec049a02d69ea056662a06e5cf320028ab8b388634bf1c67" }, "src/L1/OPContractsManager.sol": { - "initCodeHash": "0x3ba214bc6333b9d8b913b39fbecf24c008f15f8e02dd2d8cc4446f6adee56a35", - "sourceCodeHash": "0x9f017a7b33f4be8e3054a330355334d439163ed63003e401de54fb1c70cfa413" + "initCodeHash": "0x5cbd9da550ffc2fdd17434145798cbfd5cf8140c474319671bf339f43cbb994c", + "sourceCodeHash": "0x1af10fb0f17db1dfc8b02844f36577bffe54c942f6f8f09a0b75fa9ba38654d9" }, "src/L1/OptimismPortal2.sol": { - "initCodeHash": "0xac11984d6c3e58fd02e33509540ee329c4f5caedb94e422f8ab3a7a95ce7a546", - "sourceCodeHash": "0x79652f4f6aebc11dd4ad201dcf9a436d94db76e2edef07b12486c3de9cd804d2" + "initCodeHash": "0x22eddc89e6acc58ab5c22927d303a1be8d05a21220e6d314883cba2bca4ffb56", + "sourceCodeHash": "0x03da70e6c4b48c9e58722a0ea41c9cb7ca6bf12ce26f8118316a4168b8715b98" }, "src/L1/OptimismPortalInterop.sol": { - "initCodeHash": "0xa0fd54587f16bc9f024d842211ab7c7b9c07ca72e08204b0b3cf1c578b6f1872", + "initCodeHash": "0xd77aedf895785318db654a0a7f72f80f0a83a70d32e8b479f3fa3ffa1e50265f", "sourceCodeHash": "0xcd94e7641d43620f4169b083b2e597c2db2eb679b44ccaf5c7eeae6f63d2774a" }, "src/L1/ProtocolVersions.sol": { @@ -36,11 +36,11 @@ "sourceCodeHash": "0xfd56e63e76b1f203cceeb9bbb14396ae803cbbbf7e80ca0ee11fb586321812af" }, "src/L1/SystemConfig.sol": { - "initCodeHash": "0x35ec072ff4ced7a22217bc30e62513d1db5916d2a5e3271a6a46e23d2eec14da", - "sourceCodeHash": "0x94a28da7dae79c0a1dcc91031ce00b34cc66d32a6f9d934bb990c1a7279e8a61" + "initCodeHash": "0x471ac69544fdee81d0734b87151297ad4cf91ca1d43a2c95e9e644c0424eed65", + "sourceCodeHash": "0x8c3ccb8f3718c00c3f9bf7c866a22d87ea18a87a6e9454c31d1c97638107434c" }, "src/L1/SystemConfigInterop.sol": { - "initCodeHash": "0xfcb5a5d6dd83cca57b7508276467ed8558460940ad26270e29c1525aba62c6c8", + "initCodeHash": "0xcdca84b074ddfd6b4e4df1a57d3500c1d48ecf88852af47f6351e9ae24b6fc2a", "sourceCodeHash": "0x71914fa1408befaef3a06a67404e0ab580d9d945e068ba23063ea6588f0f68a6" }, "src/L2/BaseFeeVault.sol": { diff --git a/packages/contracts-bedrock/snapshots/storageLayout/SystemConfig.json b/packages/contracts-bedrock/snapshots/storageLayout/SystemConfig.json index 8332aa216cb4e..3d1796e5e4063 100644 --- a/packages/contracts-bedrock/snapshots/storageLayout/SystemConfig.json +++ b/packages/contracts-bedrock/snapshots/storageLayout/SystemConfig.json @@ -110,5 +110,12 @@ "offset": 12, "slot": "106", "type": "uint64" + }, + { + "bytes": "32", + "label": "l2ChainId", + "offset": 0, + "slot": "107", + "type": "uint256" } -] +] \ No newline at end of file diff --git a/packages/contracts-bedrock/snapshots/storageLayout/SystemConfigInterop.json b/packages/contracts-bedrock/snapshots/storageLayout/SystemConfigInterop.json index 8332aa216cb4e..3d1796e5e4063 100644 --- a/packages/contracts-bedrock/snapshots/storageLayout/SystemConfigInterop.json +++ b/packages/contracts-bedrock/snapshots/storageLayout/SystemConfigInterop.json @@ -110,5 +110,12 @@ "offset": 12, "slot": "106", "type": "uint64" + }, + { + "bytes": "32", + "label": "l2ChainId", + "offset": 0, + "slot": "107", + "type": "uint256" } -] +] \ No newline at end of file diff --git a/packages/contracts-bedrock/src/L1/OPContractsManager.sol b/packages/contracts-bedrock/src/L1/OPContractsManager.sol index 0e3d29e520dd6..903e026afd1ae 100644 --- a/packages/contracts-bedrock/src/L1/OPContractsManager.sol +++ b/packages/contracts-bedrock/src/L1/OPContractsManager.sol @@ -1298,9 +1298,9 @@ contract OPContractsManager is ISemver { // -------- Constants and Variables -------- - /// @custom:semver 1.9.0 + /// @custom:semver 1.10.0 function version() public pure virtual returns (string memory) { - return "1.9.0"; + return "1.10.0"; } OPContractsManagerGameTypeAdder public immutable opcmGameTypeAdder; From 27928548eba3529998e93d92bd418034096e091b Mon Sep 17 00:00:00 2001 From: agusduha Date: Thu, 6 Mar 2025 11:34:45 -0300 Subject: [PATCH 24/50] fix: OPCM integration --- .../src/L1/OPContractsManager.sol | 59 ++++++++++++++++--- .../test/L1/OPContractsManager.t.sol | 4 ++ .../test/L1/OptimismPortal2.t.sol | 8 ++- 3 files changed, 63 insertions(+), 8 deletions(-) diff --git a/packages/contracts-bedrock/src/L1/OPContractsManager.sol b/packages/contracts-bedrock/src/L1/OPContractsManager.sol index e66b2dbc94515..8050fa63ce6e8 100644 --- a/packages/contracts-bedrock/src/L1/OPContractsManager.sol +++ b/packages/contracts-bedrock/src/L1/OPContractsManager.sol @@ -30,6 +30,7 @@ import { IL1StandardBridge } from "interfaces/L1/IL1StandardBridge.sol"; import { IOptimismMintableERC20Factory } from "interfaces/universal/IOptimismMintableERC20Factory.sol"; import { IHasSuperchainConfig } from "interfaces/L1/IHasSuperchainConfig.sol"; import { ISystemConfigInterop } from "interfaces/L1/ISystemConfigInterop.sol"; +import { IETHLockbox } from "interfaces/L1/IETHLockbox.sol"; contract OPContractsManagerContractsContainer { /// @notice Addresses of the Blueprint contracts. @@ -546,11 +547,11 @@ contract OPContractsManagerUpgrader is OPContractsManagerBase { uint256 l2ChainId = getL2ChainId(IFaultDisputeGame(address(permissionedDisputeGame))); // Grab the current respectedGameType from the OptimismPortal contract before the upgrade. - GameType respectedGameType = IOptimismPortal2(payable(opChainAddrs.optimismPortal)).respectedGameType(); + GameType respectedGameType = IOptimismPortal(payable(opChainAddrs.optimismPortal)).respectedGameType(); // Grab the current SuperchainConfig from the OptimismPortal contract before the upgrade. ISuperchainConfig superchainConfig = - IOptimismPortal2(payable(opChainAddrs.optimismPortal)).superchainConfig(); + IOptimismPortal(payable(opChainAddrs.optimismPortal)).superchainConfig(); // Start by upgrading the SystemConfig contract to have the l2ChainId. upgradeToAndCall( @@ -603,14 +604,34 @@ contract OPContractsManagerUpgrader is OPContractsManagerBase { // Separate context to avoid stack too deep. { + // Deploy the ETHLockbox proxy. + IETHLockbox ethLockbox = IETHLockbox( + deployProxy({ + _l2ChainId: l2ChainId, + _proxyAdmin: _opChainConfigs[i].proxyAdmin, + _saltMixer: reusableSaltMixer(_opChainConfigs[i]), + _contractName: "ETHLockbox" + }) + ); + + // Initialize the ETHLockbox setting the OptimismPortal as an authorized portal. + IOptimismPortal[] memory portals = new IOptimismPortal[](1); + portals[0] = IOptimismPortal(payable(opChainAddrs.optimismPortal)); + upgradeToAndCall( + _opChainConfigs[i].proxyAdmin, + address(ethLockbox), + impls.ethLockboxImpl, + abi.encodeCall(IETHLockbox.initialize, (superchainConfig, portals)) + ); + // Upgrade the OptimismPortal contract. upgradeToAndCall( _opChainConfigs[i].proxyAdmin, opChainAddrs.optimismPortal, impls.optimismPortalImpl, abi.encodeCall( - IOptimismPortal2.upgrade, - (newAnchorStateRegistryProxy, _opChainConfigs[i].disputeGameUsesSuperRoots) + IOptimismPortal.upgrade, + (newAnchorStateRegistryProxy, ethLockbox, _opChainConfigs[i].disputeGameUsesSuperRoots) ) ); } @@ -788,9 +809,11 @@ contract OPContractsManagerDeployer is OPContractsManagerBase { // Deploy ERC-1967 proxied contracts. output.l1ERC721BridgeProxy = IL1ERC721Bridge(deployProxy(_input.l2ChainId, output.opChainProxyAdmin, _input.saltMixer, "L1ERC721Bridge")); - output.optimismPortalProxy = IOptimismPortal2( + output.optimismPortalProxy = IOptimismPortal( payable(deployProxy(_input.l2ChainId, output.opChainProxyAdmin, _input.saltMixer, "OptimismPortal")) ); + output.ethLockboxProxy = + IETHLockbox(deployProxy(_input.l2ChainId, output.opChainProxyAdmin, _input.saltMixer, "ETHLockbox")); output.systemConfigProxy = ISystemConfig(deployProxy(_input.l2ChainId, output.opChainProxyAdmin, _input.saltMixer, "SystemConfig")); output.optimismMintableERC20FactoryProxy = IOptimismMintableERC20Factory( @@ -872,6 +895,12 @@ contract OPContractsManagerDeployer is OPContractsManagerBase { output.opChainProxyAdmin, address(output.optimismPortalProxy), implementation.optimismPortalImpl, data ); + // Initialize the ETHLockbox. + IOptimismPortal[] memory portals = new IOptimismPortal[](1); + portals[0] = output.optimismPortalProxy; + data = encodeETHLockboxInitializer(_superchainConfig, portals); + upgradeToAndCall(output.opChainProxyAdmin, address(output.ethLockboxProxy), implementation.ethLockboxImpl, data); + data = encodeSystemConfigInitializer(_input, output); upgradeToAndCall( output.opChainProxyAdmin, address(output.systemConfigProxy), implementation.systemConfigImpl, data @@ -1026,16 +1055,30 @@ contract OPContractsManagerDeployer is OPContractsManagerBase { returns (bytes memory) { return abi.encodeCall( - IOptimismPortal2.initialize, + IOptimismPortal.initialize, ( _output.systemConfigProxy, _superchainConfig, _output.anchorStateRegistryProxy, + _output.ethLockboxProxy, _input.disputeGameUsesSuperRoots ) ); } + /// @notice Helper method for encoding the ETHLockbox initializer data. + function encodeETHLockboxInitializer( + ISuperchainConfig _superchainConfig, + IOptimismPortal[] memory _portals + ) + internal + view + virtual + returns (bytes memory) + { + return abi.encodeCall(IETHLockbox.initialize, (_superchainConfig, _portals)); + } + /// @notice Helper method for encoding the SystemConfig initializer data. function encodeSystemConfigInitializer( OPContractsManager.DeployInput memory _input, @@ -1227,8 +1270,9 @@ contract OPContractsManager is ISemver { IOptimismMintableERC20Factory optimismMintableERC20FactoryProxy; IL1StandardBridge l1StandardBridgeProxy; IL1CrossDomainMessenger l1CrossDomainMessengerProxy; + IETHLockbox ethLockboxProxy; // Fault proof contracts below. - IOptimismPortal2 optimismPortalProxy; + IOptimismPortal optimismPortalProxy; IDisputeGameFactory disputeGameFactoryProxy; IAnchorStateRegistry anchorStateRegistryProxy; IFaultDisputeGame faultDisputeGame; @@ -1260,6 +1304,7 @@ contract OPContractsManager is ISemver { address protocolVersionsImpl; address l1ERC721BridgeImpl; address optimismPortalImpl; + address ethLockboxImpl; address systemConfigImpl; address optimismMintableERC20FactoryImpl; address l1CrossDomainMessengerImpl; diff --git a/packages/contracts-bedrock/test/L1/OPContractsManager.t.sol b/packages/contracts-bedrock/test/L1/OPContractsManager.t.sol index c2842957d9269..0099aa6040649 100644 --- a/packages/contracts-bedrock/test/L1/OPContractsManager.t.sol +++ b/packages/contracts-bedrock/test/L1/OPContractsManager.t.sol @@ -1169,6 +1169,10 @@ contract OPContractsManager_UpdatePrestate_Test is Test { _name: "OptimismPortal2", _args: DeployUtils.encodeConstructor(abi.encodeCall(IOptimismPortal2.__constructor__, (1))) }), + ethLockboxImpl: DeployUtils.create1({ + _name: "ETHLockbox", + _args: DeployUtils.encodeConstructor(abi.encodeCall(IETHLockbox.__constructor__, ())) + }), systemConfigImpl: DeployUtils.create1({ _name: "SystemConfig", _args: DeployUtils.encodeConstructor(abi.encodeCall(ISystemConfig.__constructor__, ())) diff --git a/packages/contracts-bedrock/test/L1/OptimismPortal2.t.sol b/packages/contracts-bedrock/test/L1/OptimismPortal2.t.sol index c1a6e534cab32..30c2111305f96 100644 --- a/packages/contracts-bedrock/test/L1/OptimismPortal2.t.sol +++ b/packages/contracts-bedrock/test/L1/OptimismPortal2.t.sol @@ -645,7 +645,13 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { /// @dev Tests that `proveWithdrawalTransaction` reverts when the target is the portal contract. function test_proveWithdrawalTransaction_onSelfCall_reverts() external { _defaultTx.target = address(optimismPortal2); - optimismPortal2.proveWithdrawalTransaction({ _withdrawalProof: _withdrawalProof }); + vm.expectRevert(IOptimismPortal.OptimismPortal_BadTarget.selector); + optimismPortal2.proveWithdrawalTransaction({ + _tx: _defaultTx, + _disputeGameIndex: _proposedGameIndex, + _outputRootProof: _outputRootProof, + _withdrawalProof: _withdrawalProof + }); _defaultTx.target = address(ethLockbox); vm.expectRevert(IOptimismPortal.OptimismPortal_BadTarget.selector); From d4348e58cb80b0a5a68b85d19063b88dd48a3f19 Mon Sep 17 00:00:00 2001 From: agusduha Date: Thu, 6 Mar 2025 11:53:58 -0300 Subject: [PATCH 25/50] fix: OPCM stack too deep --- .../src/L1/OPContractsManager.sol | 37 ++++++++++--------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/packages/contracts-bedrock/src/L1/OPContractsManager.sol b/packages/contracts-bedrock/src/L1/OPContractsManager.sol index 8050fa63ce6e8..6e19a578ff609 100644 --- a/packages/contracts-bedrock/src/L1/OPContractsManager.sol +++ b/packages/contracts-bedrock/src/L1/OPContractsManager.sol @@ -605,24 +605,27 @@ contract OPContractsManagerUpgrader is OPContractsManagerBase { // Separate context to avoid stack too deep. { // Deploy the ETHLockbox proxy. - IETHLockbox ethLockbox = IETHLockbox( - deployProxy({ - _l2ChainId: l2ChainId, - _proxyAdmin: _opChainConfigs[i].proxyAdmin, - _saltMixer: reusableSaltMixer(_opChainConfigs[i]), - _contractName: "ETHLockbox" - }) - ); + IETHLockbox ethLockbox; + { + ethLockbox = IETHLockbox( + deployProxy({ + _l2ChainId: l2ChainId, + _proxyAdmin: _opChainConfigs[i].proxyAdmin, + _saltMixer: reusableSaltMixer(_opChainConfigs[i]), + _contractName: "ETHLockbox" + }) + ); - // Initialize the ETHLockbox setting the OptimismPortal as an authorized portal. - IOptimismPortal[] memory portals = new IOptimismPortal[](1); - portals[0] = IOptimismPortal(payable(opChainAddrs.optimismPortal)); - upgradeToAndCall( - _opChainConfigs[i].proxyAdmin, - address(ethLockbox), - impls.ethLockboxImpl, - abi.encodeCall(IETHLockbox.initialize, (superchainConfig, portals)) - ); + // Initialize the ETHLockbox setting the OptimismPortal as an authorized portal. + IOptimismPortal[] memory portals = new IOptimismPortal[](1); + portals[0] = IOptimismPortal(payable(opChainAddrs.optimismPortal)); + upgradeToAndCall( + _opChainConfigs[i].proxyAdmin, + address(ethLockbox), + impls.ethLockboxImpl, + abi.encodeCall(IETHLockbox.initialize, (superchainConfig, portals)) + ); + } // Upgrade the OptimismPortal contract. upgradeToAndCall( From 08ec7e22c0de1b36277ab7b46e5c90507805c1f7 Mon Sep 17 00:00:00 2001 From: agusduha Date: Thu, 6 Mar 2025 12:00:14 -0300 Subject: [PATCH 26/50] fix: pre pr --- .../interfaces/L1/IOPContractsManager.sol | 19 +--------- .../snapshots/abi/OPContractsManager.json | 37 +++++-------------- .../OPContractsManagerContractsContainer.json | 10 +++++ .../abi/OPContractsManagerDeployer.json | 2 +- .../OPContractsManagerDeployerInterop.json | 2 +- .../abi/OPContractsManagerGameTypeAdder.json | 5 +++ .../abi/OPContractsManagerUpgrader.json | 5 +++ .../snapshots/abi/OptimismPortal2.json | 2 +- .../snapshots/abi/OptimismPortalInterop.json | 2 +- .../snapshots/semver-lock.json | 30 ++++++--------- .../storageLayout/OPContractsManager.json | 2 +- .../OPContractsManagerContractsContainer.json | 2 +- .../storageLayout/OptimismPortal2.json | 2 +- .../storageLayout/OptimismPortalInterop.json | 2 +- .../snapshots/storageLayout/SystemConfig.json | 2 +- .../storageLayout/SystemConfigInterop.json | 2 +- .../src/L1/OptimismPortalInterop.sol | 4 +- 17 files changed, 54 insertions(+), 76 deletions(-) diff --git a/packages/contracts-bedrock/interfaces/L1/IOPContractsManager.sol b/packages/contracts-bedrock/interfaces/L1/IOPContractsManager.sol index afc1c8a22542b..a1f1ff1906f0b 100644 --- a/packages/contracts-bedrock/interfaces/L1/IOPContractsManager.sol +++ b/packages/contracts-bedrock/interfaces/L1/IOPContractsManager.sol @@ -233,23 +233,8 @@ interface IOPContractsManager { /// @notice Thrown when an invalid `l2ChainId` is provided to `deploy`. error InvalidChainId(); - /// @notice Thrown when a role's address is not valid (opChainProxyAdminOwner). - error InvalidRoleAddressPAO(); - - /// @notice Thrown when a role's address is not valid (systemConfigOwner). - error InvalidRoleAddressSCO(); - - /// @notice Thrown when a role's address is not valid (batcher). - error InvalidRoleAddressBatcher(); - - /// @notice Thrown when a role's address is not valid (unsafeBlockSigner). - error InvalidRoleAddressUBS(); - - /// @notice Thrown when a role's address is not valid (proposer). - error InvalidRoleAddressProposer(); - - /// @notice Thrown when a role's address is not valid (challenger). - error InvalidRoleAddressChallenger(); + /// @notice Thrown when a role's address is not valid. + error InvalidRoleAddress(string role); /// @notice Thrown when the latest release is not set upon initialization. error LatestReleaseNotSet(); diff --git a/packages/contracts-bedrock/snapshots/abi/OPContractsManager.json b/packages/contracts-bedrock/snapshots/abi/OPContractsManager.json index 1f1b3332ceca1..381040d946efc 100644 --- a/packages/contracts-bedrock/snapshots/abi/OPContractsManager.json +++ b/packages/contracts-bedrock/snapshots/abi/OPContractsManager.json @@ -753,33 +753,14 @@ "type": "error" }, { - "inputs": [], - "name": "InvalidRoleAddressBatcher", - "type": "error" - }, - { - "inputs": [], - "name": "InvalidRoleAddressChallenger", - "type": "error" - }, - { - "inputs": [], - "name": "InvalidRoleAddressPAO", - "type": "error" - }, - { - "inputs": [], - "name": "InvalidRoleAddressProposer", - "type": "error" - }, - { - "inputs": [], - "name": "InvalidRoleAddressSCO", - "type": "error" - }, - { - "inputs": [], - "name": "InvalidRoleAddressUBS", + "inputs": [ + { + "internalType": "string", + "name": "role", + "type": "string" + } + ], + "name": "InvalidRoleAddress", "type": "error" }, { @@ -828,4 +809,4 @@ "name": "SuperchainProxyAdminMismatch", "type": "error" } -] +] \ No newline at end of file diff --git a/packages/contracts-bedrock/snapshots/abi/OPContractsManagerContractsContainer.json b/packages/contracts-bedrock/snapshots/abi/OPContractsManagerContractsContainer.json index 45af4462a5637..9e09b81cb68b2 100644 --- a/packages/contracts-bedrock/snapshots/abi/OPContractsManagerContractsContainer.json +++ b/packages/contracts-bedrock/snapshots/abi/OPContractsManagerContractsContainer.json @@ -75,6 +75,11 @@ "name": "optimismPortalImpl", "type": "address" }, + { + "internalType": "address", + "name": "ethLockboxImpl", + "type": "address" + }, { "internalType": "address", "name": "systemConfigImpl", @@ -210,6 +215,11 @@ "name": "optimismPortalImpl", "type": "address" }, + { + "internalType": "address", + "name": "ethLockboxImpl", + "type": "address" + }, { "internalType": "address", "name": "systemConfigImpl", diff --git a/packages/contracts-bedrock/snapshots/abi/OPContractsManagerDeployer.json b/packages/contracts-bedrock/snapshots/abi/OPContractsManagerDeployer.json index 82391e2555e59..a3e0783fd0d34 100644 --- a/packages/contracts-bedrock/snapshots/abi/OPContractsManagerDeployer.json +++ b/packages/contracts-bedrock/snapshots/abi/OPContractsManagerDeployer.json @@ -514,4 +514,4 @@ "name": "UnsupportedERCVersion", "type": "error" } -] +] \ No newline at end of file diff --git a/packages/contracts-bedrock/snapshots/abi/OPContractsManagerDeployerInterop.json b/packages/contracts-bedrock/snapshots/abi/OPContractsManagerDeployerInterop.json index 82391e2555e59..a3e0783fd0d34 100644 --- a/packages/contracts-bedrock/snapshots/abi/OPContractsManagerDeployerInterop.json +++ b/packages/contracts-bedrock/snapshots/abi/OPContractsManagerDeployerInterop.json @@ -514,4 +514,4 @@ "name": "UnsupportedERCVersion", "type": "error" } -] +] \ No newline at end of file diff --git a/packages/contracts-bedrock/snapshots/abi/OPContractsManagerGameTypeAdder.json b/packages/contracts-bedrock/snapshots/abi/OPContractsManagerGameTypeAdder.json index 663ec41ae920a..75a4caf40467b 100644 --- a/packages/contracts-bedrock/snapshots/abi/OPContractsManagerGameTypeAdder.json +++ b/packages/contracts-bedrock/snapshots/abi/OPContractsManagerGameTypeAdder.json @@ -244,6 +244,11 @@ "name": "optimismPortalImpl", "type": "address" }, + { + "internalType": "address", + "name": "ethLockboxImpl", + "type": "address" + }, { "internalType": "address", "name": "systemConfigImpl", diff --git a/packages/contracts-bedrock/snapshots/abi/OPContractsManagerUpgrader.json b/packages/contracts-bedrock/snapshots/abi/OPContractsManagerUpgrader.json index 195f8d2acbc87..d9ac40df7ca94 100644 --- a/packages/contracts-bedrock/snapshots/abi/OPContractsManagerUpgrader.json +++ b/packages/contracts-bedrock/snapshots/abi/OPContractsManagerUpgrader.json @@ -141,6 +141,11 @@ "name": "optimismPortalImpl", "type": "address" }, + { + "internalType": "address", + "name": "ethLockboxImpl", + "type": "address" + }, { "internalType": "address", "name": "systemConfigImpl", diff --git a/packages/contracts-bedrock/snapshots/abi/OptimismPortal2.json b/packages/contracts-bedrock/snapshots/abi/OptimismPortal2.json index 7d2c1ab2c467d..ff90cc3ca3701 100644 --- a/packages/contracts-bedrock/snapshots/abi/OptimismPortal2.json +++ b/packages/contracts-bedrock/snapshots/abi/OptimismPortal2.json @@ -1076,4 +1076,4 @@ "name": "UnexpectedString", "type": "error" } -] +] \ No newline at end of file diff --git a/packages/contracts-bedrock/snapshots/abi/OptimismPortalInterop.json b/packages/contracts-bedrock/snapshots/abi/OptimismPortalInterop.json index 7468c39b5f2d2..b94c51d314cd7 100644 --- a/packages/contracts-bedrock/snapshots/abi/OptimismPortalInterop.json +++ b/packages/contracts-bedrock/snapshots/abi/OptimismPortalInterop.json @@ -1094,4 +1094,4 @@ "name": "UnexpectedString", "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 ff9858e7873f6..41d514f3d3ebe 100644 --- a/packages/contracts-bedrock/snapshots/semver-lock.json +++ b/packages/contracts-bedrock/snapshots/semver-lock.json @@ -20,24 +20,16 @@ "sourceCodeHash": "0x44797707aea8c63dec049a02d69ea056662a06e5cf320028ab8b388634bf1c67" }, "src/L1/OPContractsManager.sol": { - "initCodeHash": "0xb68b46bb78e111809dae6e2a0404056691144796c006f988a9f17dfcafdd44e8", - "sourceCodeHash": "0x7c5ad88de15e2f2910558ccddb58b6a5ba523fb70087c4683e8418034b2dc11d" - }, - "src/L1/OPContractsManagerInterop.sol": { - "initCodeHash": "0x47188fbb81c947426602099104046ce5ccd008292a233585493ce67e905a6082", - "sourceCodeHash": "0x79b3ae68d83f021d9118f8a1943c1d0773e7e01a19f580f6b90ad4c00370cff5" - }, - "src/L1/OPPrestateUpdater.sol": { - "initCodeHash": "0x0b7626d038f385df1a51f862f6188292ce34aa624eaa5aca0df1d1452d8d5387", - "sourceCodeHash": "0xcdeb0f532ff90288ee13694a51a15aaee99392b7dfe2d46fffacf2af82cdb38f" + "initCodeHash": "0x9e7496b9720bae99e5db18dc18c99b5c06cd755c6d4079e0d38c56d625fe7b4e", + "sourceCodeHash": "0x3d74123edd7f474779817d10baf8726832b304865614ddddd02c681a8a997857" }, "src/L1/OptimismPortal2.sol": { - "initCodeHash": "0xa66af9c53c197dad8a662a77573e371b55a3c23dac84a8febdd1d07084475b99", - "sourceCodeHash": "0xf735d2c64c3675b178fc2ea334aa0e3689622a5e33a5f9aeba6844682b2be05b" + "initCodeHash": "0xbfb7f71d76023a02e04e758399bf7db73f83063e989ade28b0432e47a2b86f1a", + "sourceCodeHash": "0xddc6fb2d5e6a6864f9ea29e89ea57d03ca7e527477c4862d4b262164003dbc58" }, "src/L1/OptimismPortalInterop.sol": { - "initCodeHash": "0xb2b094a8a39f198cd947855dcdd470039059901f53253d867732ca04adb3ba52", - "sourceCodeHash": "0xd3db51a961120a97f8300a2587edda6311c862b48ba4178c4380f62368b07c09" + "initCodeHash": "0x333b0db068574b43e3efb7a5d2e756a2ec01291407d07c6b216e42bcdbdfd4a5", + "sourceCodeHash": "0xa1ecf8a19638dd24d80f62e2faef132afa15c6945c14133fe58709015fcb14a6" }, "src/L1/ProtocolVersions.sol": { "initCodeHash": "0x5a76c8530cb24cf23d3baacc6eefaac226382af13f1e2a35535d2ec2b0573b29", @@ -48,12 +40,12 @@ "sourceCodeHash": "0xfd56e63e76b1f203cceeb9bbb14396ae803cbbbf7e80ca0ee11fb586321812af" }, "src/L1/SystemConfig.sol": { - "initCodeHash": "0x93e732c31e59dc78d6414ad12fcc0cbe4537d3a69a2ca34ff8713b0b51679b19", - "sourceCodeHash": "0x538518cf61bda80bc3f0b94e4fe236b65e53fff397fd43958e695705c57e02d9" + "initCodeHash": "0x471ac69544fdee81d0734b87151297ad4cf91ca1d43a2c95e9e644c0424eed65", + "sourceCodeHash": "0x8c3ccb8f3718c00c3f9bf7c866a22d87ea18a87a6e9454c31d1c97638107434c" }, "src/L1/SystemConfigInterop.sol": { - "initCodeHash": "0xe3651f84f0fecf6c4b60801d8168da55bfe3889912548fcd6b935c9f6dff91b3", - "sourceCodeHash": "0xe3031507a78ed71352c0c1523272881a48c8d6bd98d5322ce3a7cb8061eeade3" + "initCodeHash": "0xcdca84b074ddfd6b4e4df1a57d3500c1d48ecf88852af47f6351e9ae24b6fc2a", + "sourceCodeHash": "0x71914fa1408befaef3a06a67404e0ab580d9d945e068ba23063ea6588f0f68a6" }, "src/L2/BaseFeeVault.sol": { "initCodeHash": "0xc403d4c555d8e69a2699e01d192ae7327136701fa02da10a6d75a584b3c364c9", @@ -247,4 +239,4 @@ "initCodeHash": "0x2bfce526f82622288333d53ca3f43a0a94306ba1bab99241daa845f8f4b18bd4", "sourceCodeHash": "0xf49d7b0187912a6bb67926a3222ae51121e9239495213c975b3b4b217ee57a1b" } -} +} \ No newline at end of file diff --git a/packages/contracts-bedrock/snapshots/storageLayout/OPContractsManager.json b/packages/contracts-bedrock/snapshots/storageLayout/OPContractsManager.json index b35530bbad18c..a22b6b8e38333 100644 --- a/packages/contracts-bedrock/snapshots/storageLayout/OPContractsManager.json +++ b/packages/contracts-bedrock/snapshots/storageLayout/OPContractsManager.json @@ -13,4 +13,4 @@ "slot": "1", "type": "bool" } -] +] \ No newline at end of file diff --git a/packages/contracts-bedrock/snapshots/storageLayout/OPContractsManagerContractsContainer.json b/packages/contracts-bedrock/snapshots/storageLayout/OPContractsManagerContractsContainer.json index 6937c925be6ae..a8ce2f3c54c12 100644 --- a/packages/contracts-bedrock/snapshots/storageLayout/OPContractsManagerContractsContainer.json +++ b/packages/contracts-bedrock/snapshots/storageLayout/OPContractsManagerContractsContainer.json @@ -13,4 +13,4 @@ "slot": "9", "type": "struct OPContractsManager.Implementations" } -] +] \ No newline at end of file diff --git a/packages/contracts-bedrock/snapshots/storageLayout/OptimismPortal2.json b/packages/contracts-bedrock/snapshots/storageLayout/OptimismPortal2.json index 03fb5aaa34a49..a66c4b71b4d16 100644 --- a/packages/contracts-bedrock/snapshots/storageLayout/OptimismPortal2.json +++ b/packages/contracts-bedrock/snapshots/storageLayout/OptimismPortal2.json @@ -146,4 +146,4 @@ "slot": "63", "type": "bool" } -] +] \ No newline at end of file diff --git a/packages/contracts-bedrock/snapshots/storageLayout/OptimismPortalInterop.json b/packages/contracts-bedrock/snapshots/storageLayout/OptimismPortalInterop.json index 03fb5aaa34a49..a66c4b71b4d16 100644 --- a/packages/contracts-bedrock/snapshots/storageLayout/OptimismPortalInterop.json +++ b/packages/contracts-bedrock/snapshots/storageLayout/OptimismPortalInterop.json @@ -146,4 +146,4 @@ "slot": "63", "type": "bool" } -] +] \ No newline at end of file diff --git a/packages/contracts-bedrock/snapshots/storageLayout/SystemConfig.json b/packages/contracts-bedrock/snapshots/storageLayout/SystemConfig.json index a18bd5703f6e4..3d1796e5e4063 100644 --- a/packages/contracts-bedrock/snapshots/storageLayout/SystemConfig.json +++ b/packages/contracts-bedrock/snapshots/storageLayout/SystemConfig.json @@ -118,4 +118,4 @@ "slot": "107", "type": "uint256" } -] +] \ No newline at end of file diff --git a/packages/contracts-bedrock/snapshots/storageLayout/SystemConfigInterop.json b/packages/contracts-bedrock/snapshots/storageLayout/SystemConfigInterop.json index a18bd5703f6e4..3d1796e5e4063 100644 --- a/packages/contracts-bedrock/snapshots/storageLayout/SystemConfigInterop.json +++ b/packages/contracts-bedrock/snapshots/storageLayout/SystemConfigInterop.json @@ -118,4 +118,4 @@ "slot": "107", "type": "uint256" } -] +] \ No newline at end of file diff --git a/packages/contracts-bedrock/src/L1/OptimismPortalInterop.sol b/packages/contracts-bedrock/src/L1/OptimismPortalInterop.sol index 4627ab1ee9c9c..da2e9559e645b 100644 --- a/packages/contracts-bedrock/src/L1/OptimismPortalInterop.sol +++ b/packages/contracts-bedrock/src/L1/OptimismPortalInterop.sol @@ -20,9 +20,9 @@ contract OptimismPortalInterop is OptimismPortal2 { /// @param _proofMaturityDelaySeconds The proof maturity delay in seconds. constructor(uint256 _proofMaturityDelaySeconds) OptimismPortal2(_proofMaturityDelaySeconds) { } - /// @custom:semver +interop.3 + /// @custom:semver +interop.4 function version() public pure override returns (string memory) { - return string.concat(super.version(), "+interop.3"); + return string.concat(super.version(), "+interop.4"); } /// @notice Sets static configuration options for the L2 system. From 2f7f09474e741ef57401c1f8ffad83cd23438e1a Mon Sep 17 00:00:00 2001 From: 0xDiscotech <131301107+0xDiscotech@users.noreply.github.com> Date: Thu, 6 Mar 2025 12:55:42 -0300 Subject: [PATCH 27/50] fix: update portal implementation on opcm upgrade --- packages/contracts-bedrock/snapshots/semver-lock.json | 2 +- packages/contracts-bedrock/src/L1/OPContractsManager.sol | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/contracts-bedrock/snapshots/semver-lock.json b/packages/contracts-bedrock/snapshots/semver-lock.json index 41d514f3d3ebe..d3ae280f3df9d 100644 --- a/packages/contracts-bedrock/snapshots/semver-lock.json +++ b/packages/contracts-bedrock/snapshots/semver-lock.json @@ -21,7 +21,7 @@ }, "src/L1/OPContractsManager.sol": { "initCodeHash": "0x9e7496b9720bae99e5db18dc18c99b5c06cd755c6d4079e0d38c56d625fe7b4e", - "sourceCodeHash": "0x3d74123edd7f474779817d10baf8726832b304865614ddddd02c681a8a997857" + "sourceCodeHash": "0x45212da23179f7ccab461ba2a1847ed3a8fe4473b3ebe8e809bafae506426f0f" }, "src/L1/OptimismPortal2.sol": { "initCodeHash": "0xbfb7f71d76023a02e04e758399bf7db73f83063e989ade28b0432e47a2b86f1a", diff --git a/packages/contracts-bedrock/src/L1/OPContractsManager.sol b/packages/contracts-bedrock/src/L1/OPContractsManager.sol index 6e19a578ff609..1b6ab1316580e 100644 --- a/packages/contracts-bedrock/src/L1/OPContractsManager.sol +++ b/packages/contracts-bedrock/src/L1/OPContractsManager.sol @@ -602,6 +602,9 @@ contract OPContractsManagerUpgrader is OPContractsManagerBase { } } + // Upgrade the OptimismPortal contract implementation. + upgradeTo(_opChainConfigs[i].proxyAdmin, opChainAddrs.optimismPortal, impls.optimismPortalImpl); + // Separate context to avoid stack too deep. { // Deploy the ETHLockbox proxy. From 982a13aecdf393c6d61070ddc8fed84cb6e07a22 Mon Sep 17 00:00:00 2001 From: 0xDiscotech <131301107+0xDiscotech@users.noreply.github.com> Date: Thu, 6 Mar 2025 13:30:20 -0300 Subject: [PATCH 28/50] fix: call upgrade on portal without upgrading any impl --- .../contracts-bedrock/src/L1/OPContractsManager.sol | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/packages/contracts-bedrock/src/L1/OPContractsManager.sol b/packages/contracts-bedrock/src/L1/OPContractsManager.sol index 1b6ab1316580e..ac92222873d1c 100644 --- a/packages/contracts-bedrock/src/L1/OPContractsManager.sol +++ b/packages/contracts-bedrock/src/L1/OPContractsManager.sol @@ -630,15 +630,9 @@ contract OPContractsManagerUpgrader is OPContractsManagerBase { ); } - // Upgrade the OptimismPortal contract. - upgradeToAndCall( - _opChainConfigs[i].proxyAdmin, - opChainAddrs.optimismPortal, - impls.optimismPortalImpl, - abi.encodeCall( - IOptimismPortal.upgrade, - (newAnchorStateRegistryProxy, ethLockbox, _opChainConfigs[i].disputeGameUsesSuperRoots) - ) + // Call `upgrade` on the OptimismPortal contract. + IOptimismPortal(payable(opChainAddrs.optimismPortal)).upgrade( + newAnchorStateRegistryProxy, ethLockbox, _opChainConfigs[i].disputeGameUsesSuperRoots ); } From c3a588d4ab066031381d4100fc0eda8d834ab9c1 Mon Sep 17 00:00:00 2001 From: 0xDiscotech <131301107+0xDiscotech@users.noreply.github.com> Date: Thu, 6 Mar 2025 13:44:50 -0300 Subject: [PATCH 29/50] fix: iopcm interface for older versions already deployed --- .../interfaces/L1/IOPContractsManager.sol | 225 ++++++++++++++++++ .../test/L1/OPContractsManager.t.sol | 20 +- 2 files changed, 235 insertions(+), 10 deletions(-) diff --git a/packages/contracts-bedrock/interfaces/L1/IOPContractsManager.sol b/packages/contracts-bedrock/interfaces/L1/IOPContractsManager.sol index a1f1ff1906f0b..702a60dd70909 100644 --- a/packages/contracts-bedrock/interfaces/L1/IOPContractsManager.sol +++ b/packages/contracts-bedrock/interfaces/L1/IOPContractsManager.sol @@ -309,3 +309,228 @@ interface IOPContractsManager { function setRC(bool _isRC) external; } + +interface IOPContractsManagerOld { + // -------- Structs -------- + + /// @notice Represents the roles that can be set when deploying a standard OP Stack chain. + struct Roles { + address opChainProxyAdminOwner; + address systemConfigOwner; + address batcher; + address unsafeBlockSigner; + address proposer; + address challenger; + } + + /// @notice The full set of inputs to deploy a new OP Stack chain. + struct DeployInput { + Roles roles; + uint32 basefeeScalar; + uint32 blobBasefeeScalar; + uint256 l2ChainId; + // The correct type is OutputRoot memory but OP Deployer does not yet support structs. + bytes startingAnchorRoot; + // The salt mixer is used as part of making the resulting salt unique. + string saltMixer; + uint64 gasLimit; + // Configurable dispute game parameters. + bool disputeGameUsesSuperRoots; + GameType disputeGameType; + Claim disputeAbsolutePrestate; + uint256 disputeMaxGameDepth; + uint256 disputeSplitDepth; + Duration disputeClockExtension; + Duration disputeMaxClockDuration; + } + + /// @notice The full set of outputs from deploying a new OP Stack chain. + struct DeployOutput { + IProxyAdmin opChainProxyAdmin; + IAddressManager addressManager; + IL1ERC721Bridge l1ERC721BridgeProxy; + ISystemConfig systemConfigProxy; + IOptimismMintableERC20Factory optimismMintableERC20FactoryProxy; + IL1StandardBridge l1StandardBridgeProxy; + IL1CrossDomainMessenger l1CrossDomainMessengerProxy; + // Fault proof contracts below. + IOptimismPortal2 optimismPortalProxy; + IDisputeGameFactory disputeGameFactoryProxy; + IAnchorStateRegistry anchorStateRegistryProxy; + IFaultDisputeGame faultDisputeGame; + IPermissionedDisputeGame permissionedDisputeGame; + IDelayedWETH delayedWETHPermissionedGameProxy; + IDelayedWETH delayedWETHPermissionlessGameProxy; + } + + /// @notice Addresses of ERC-5202 Blueprint contracts. There are used for deploying full size + /// contracts, to reduce the code size of this factory contract. If it deployed full contracts + /// using the `new Proxy()` syntax, the code size would get large fast, since this contract would + /// contain the bytecode of every contract it deploys. Therefore we instead use Blueprints to + /// reduce the code size of this contract. + struct Blueprints { + address addressManager; + address proxy; + address proxyAdmin; + address l1ChugSplashProxy; + address resolvedDelegateProxy; + address permissionedDisputeGame1; + address permissionedDisputeGame2; + address permissionlessDisputeGame1; + address permissionlessDisputeGame2; + } + + /// @notice The latest implementation contracts for the OP Stack. + struct Implementations { + address superchainConfigImpl; + address protocolVersionsImpl; + address l1ERC721BridgeImpl; + address optimismPortalImpl; + address systemConfigImpl; + address optimismMintableERC20FactoryImpl; + address l1CrossDomainMessengerImpl; + address l1StandardBridgeImpl; + address disputeGameFactoryImpl; + address anchorStateRegistryImpl; + address delayedWETHImpl; + address mipsImpl; + } + + /// @notice The input required to identify a chain for upgrading. + struct OpChainConfig { + ISystemConfig systemConfigProxy; + IProxyAdmin proxyAdmin; + Claim absolutePrestate; + bool disputeGameUsesSuperRoots; + } + + struct AddGameInput { + string saltMixer; + ISystemConfig systemConfig; + IProxyAdmin proxyAdmin; + IDelayedWETH delayedWETH; + GameType disputeGameType; + Claim disputeAbsolutePrestate; + uint256 disputeMaxGameDepth; + uint256 disputeSplitDepth; + Duration disputeClockExtension; + Duration disputeMaxClockDuration; + uint256 initialBond; + IBigStepper vm; + bool permissioned; + } + + struct AddGameOutput { + IDelayedWETH delayedWETH; + IFaultDisputeGame faultDisputeGame; + } + + // -------- Constants and Variables -------- + + function version() external pure returns (string memory); + + /// @notice Address of the SuperchainConfig contract shared by all chains. + function superchainConfig() external view returns (ISuperchainConfig); + + /// @notice Address of the ProtocolVersions contract shared by all chains. + function protocolVersions() external view returns (IProtocolVersions); + + /// @notice Address of the ProxyAdmin contract shared by all chains. + function superchainProxyAdmin() external view returns (IProxyAdmin); + + /// @notice L1 smart contracts release deployed by this version of OPCM. This is used in opcm to signal which + /// version of the L1 smart contracts is deployed. It takes the format of `op-contracts/vX.Y.Z`. + function l1ContractsRelease() external view returns (string memory); + + // -------- Errors -------- + + error OnlyUpgradeController(); + + /// @notice Thrown when an address is the zero address. + error AddressNotFound(address who); + + /// @notice Throw when a contract address has no code. + error AddressHasNoCode(address who); + + /// @notice Thrown when a release version is already set. + error AlreadyReleased(); + + /// @notice Thrown when an invalid `l2ChainId` is provided to `deploy`. + error InvalidChainId(); + + /// @notice Thrown when a role's address is not valid. + error InvalidRoleAddress(string role); + + /// @notice Thrown when the latest release is not set upon initialization. + error LatestReleaseNotSet(); + + /// @notice Thrown when the starting anchor root is not provided. + error InvalidStartingAnchorRoot(); + + /// @notice Thrown when certain methods are called outside of a DELEGATECALL. + error OnlyDelegatecall(); + + /// @notice Thrown when game configs passed to addGameType are invalid. + error InvalidGameConfigs(); + + /// @notice Thrown when the SuperchainConfig of the chain does not match the SuperchainConfig of this OPCM. + error SuperchainConfigMismatch(ISystemConfig systemConfig); + + error SuperchainProxyAdminMismatch(); + + error PrestateNotSet(); + + error PrestateRequired(); + + // -------- Methods -------- + + function __constructor__( + IOPContractsManagerGameTypeAdder _opcmGameTypeAdder, + IOPContractsManagerDeployer _opcmDeployer, + IOPContractsManagerUpgrader _opcmUpgrader, + ISuperchainConfig _superchainConfig, + IProtocolVersions _protocolVersions, + IProxyAdmin _superchainProxyAdmin, + string memory _l1ContractsRelease, + address _upgradeController + ) + external; + + function deploy(DeployInput calldata _input) external returns (DeployOutput memory); + + /// @notice Upgrades the implementation of all proxies in the specified chains + /// @param _opChainConfigs The chains to upgrade + function upgrade(OpChainConfig[] memory _opChainConfigs) external; + + /// @notice addGameType deploys a new dispute game and links it to the DisputeGameFactory. The inputted _gameConfigs + /// must be added in ascending GameType order. + function addGameType(AddGameInput[] memory _gameConfigs) external returns (AddGameOutput[] memory); + + /// @notice Updates the prestate hash for a new game type while keeping all other parameters the same + /// @param _prestateUpdateInputs The new prestate hash to use + function updatePrestate(OpChainConfig[] memory _prestateUpdateInputs) external; + + /// @notice Maps an L2 chain ID to an L1 batch inbox address as defined by the standard + /// configuration's convention. This convention is `versionByte || keccak256(bytes32(chainId))[:19]`, + /// where || denotes concatenation`, versionByte is 0x00, and chainId is a uint256. + /// https://specs.optimism.io/protocol/configurability.html#consensus-parameters + function chainIdToBatchInboxAddress(uint256 _l2ChainId) external pure returns (address); + + /// @notice Returns the blueprint contract addresses. + function blueprints() external view returns (Blueprints memory); + + function opcmDeployer() external view returns (IOPContractsManagerDeployer); + + function opcmUpgrader() external view returns (IOPContractsManagerUpgrader); + + function opcmGameTypeAdder() external view returns (IOPContractsManagerGameTypeAdder); + + /// @notice Returns the implementation contract addresses. + function implementations() external view returns (Implementations memory); + + function upgradeController() external view returns (address); + + function isRC() external view returns (bool); + + function setRC(bool _isRC) external; +} diff --git a/packages/contracts-bedrock/test/L1/OPContractsManager.t.sol b/packages/contracts-bedrock/test/L1/OPContractsManager.t.sol index 0099aa6040649..0d62ceb0ac817 100644 --- a/packages/contracts-bedrock/test/L1/OPContractsManager.t.sol +++ b/packages/contracts-bedrock/test/L1/OPContractsManager.t.sol @@ -40,6 +40,7 @@ import { IFaultDisputeGame } from "interfaces/dispute/IFaultDisputeGame.sol"; import { ISystemConfig } from "interfaces/L1/ISystemConfig.sol"; import { IOPContractsManager, + IOPContractsManagerOld, IOPContractsManagerGameTypeAdder, IOPContractsManagerDeployer, IOPContractsManagerUpgrader, @@ -318,8 +319,9 @@ contract OPContractsManager_Upgrade_Harness is CommonTest { function runV200UpgradeAndChecks(address _delegateCaller) public { // The address below corresponds with the address of the v2.0.0-rc.1 OPCM on mainnet. - IOPContractsManager deployedOPCM = IOPContractsManager(address(0x026b2F158255Beac46c1E7c6b8BbF29A4b6A7B76)); - IOPContractsManager.Implementations memory impls = deployedOPCM.implementations(); + IOPContractsManagerOld deployedOPCM = + IOPContractsManagerOld(address(0x026b2F158255Beac46c1E7c6b8BbF29A4b6A7B76)); + IOPContractsManagerOld.Implementations memory impls = deployedOPCM.implementations(); // Cache the old L1xDM address so we can look for it in the AddressManager's event address oldL1CrossDomainMessenger = addressManager.getAddress("OVM_L1CrossDomainMessenger"); @@ -348,9 +350,6 @@ contract OPContractsManager_Upgrade_Harness is CommonTest { expectEmitUpgraded(impls.l1ERC721BridgeImpl, address(l1ERC721Bridge)); expectEmitUpgraded(impls.disputeGameFactoryImpl, address(disputeGameFactory)); expectEmitUpgraded(impls.optimismPortalImpl, address(optimismPortal2)); - if (deploy.cfg().useUpgradedFork()) { - expectEmitUpgraded(impls.ethLockboxImpl, address(ethLockbox)); - } expectEmitUpgraded(impls.optimismMintableERC20FactoryImpl, address(l1OptimismMintableERC20Factory)); vm.expectEmit(address(newAnchorStateRegistryProxy)); emit AdminChanged(address(0), address(proxyAdmin)); @@ -396,9 +395,9 @@ contract OPContractsManager_Upgrade_Harness is CommonTest { assertEq(impls.l1ERC721BridgeImpl, EIP1967Helper.getImplementation(address(l1ERC721Bridge))); assertEq(impls.disputeGameFactoryImpl, EIP1967Helper.getImplementation(address(disputeGameFactory))); assertEq(impls.optimismPortalImpl, EIP1967Helper.getImplementation(address(optimismPortal2))); - if (deploy.cfg().useUpgradedFork()) { - assertEq(impls.ethLockboxImpl, EIP1967Helper.getImplementation(address(ethLockbox))); - } + // if (deploy.cfg().useUpgradedFork()) { + // assertEq(impls.ethLockboxImpl, EIP1967Helper.getImplementation(address(ethLockbox))); + // } assertEq( impls.optimismMintableERC20FactoryImpl, EIP1967Helper.getImplementation(address(l1OptimismMintableERC20Factory)) @@ -432,8 +431,9 @@ contract OPContractsManager_Upgrade_Harness is CommonTest { function runUpgrade14UpgradeAndChecks(address _delegateCaller) public { // TODO(#14665): Replace this address with once the final OPCM is deployed for Upgrade 14. - IOPContractsManager deployedOPCM = IOPContractsManager(address(0x3A1f523a4bc09cd344A2745a108Bb0398288094F)); - IOPContractsManager.Implementations memory impls = deployedOPCM.implementations(); + IOPContractsManagerOld deployedOPCM = + IOPContractsManagerOld(address(0x3A1f523a4bc09cd344A2745a108Bb0398288094F)); + IOPContractsManagerOld.Implementations memory impls = deployedOPCM.implementations(); // sanity check IPermissionedDisputeGame oldPDG = From f71a15de0b48c96dd1d03e9e7c2524bf1c2f3fb8 Mon Sep 17 00:00:00 2001 From: 0xDiscotech <131301107+0xDiscotech@users.noreply.github.com> Date: Thu, 6 Mar 2025 15:02:02 -0300 Subject: [PATCH 30/50] refactor: use minimal interface for opcm without lockbox --- .../interfaces/L1/IOPContractsManager.sol | 213 +----------------- .../snapshots/semver-lock.json | 2 +- .../test/L1/OPContractsManager.t.sol | 17 +- 3 files changed, 14 insertions(+), 218 deletions(-) diff --git a/packages/contracts-bedrock/interfaces/L1/IOPContractsManager.sol b/packages/contracts-bedrock/interfaces/L1/IOPContractsManager.sol index 702a60dd70909..d2f83d685d99e 100644 --- a/packages/contracts-bedrock/interfaces/L1/IOPContractsManager.sol +++ b/packages/contracts-bedrock/interfaces/L1/IOPContractsManager.sol @@ -310,77 +310,11 @@ interface IOPContractsManager { function setRC(bool _isRC) external; } -interface IOPContractsManagerOld { - // -------- Structs -------- - - /// @notice Represents the roles that can be set when deploying a standard OP Stack chain. - struct Roles { - address opChainProxyAdminOwner; - address systemConfigOwner; - address batcher; - address unsafeBlockSigner; - address proposer; - address challenger; - } - - /// @notice The full set of inputs to deploy a new OP Stack chain. - struct DeployInput { - Roles roles; - uint32 basefeeScalar; - uint32 blobBasefeeScalar; - uint256 l2ChainId; - // The correct type is OutputRoot memory but OP Deployer does not yet support structs. - bytes startingAnchorRoot; - // The salt mixer is used as part of making the resulting salt unique. - string saltMixer; - uint64 gasLimit; - // Configurable dispute game parameters. - bool disputeGameUsesSuperRoots; - GameType disputeGameType; - Claim disputeAbsolutePrestate; - uint256 disputeMaxGameDepth; - uint256 disputeSplitDepth; - Duration disputeClockExtension; - Duration disputeMaxClockDuration; - } - - /// @notice The full set of outputs from deploying a new OP Stack chain. - struct DeployOutput { - IProxyAdmin opChainProxyAdmin; - IAddressManager addressManager; - IL1ERC721Bridge l1ERC721BridgeProxy; - ISystemConfig systemConfigProxy; - IOptimismMintableERC20Factory optimismMintableERC20FactoryProxy; - IL1StandardBridge l1StandardBridgeProxy; - IL1CrossDomainMessenger l1CrossDomainMessengerProxy; - // Fault proof contracts below. - IOptimismPortal2 optimismPortalProxy; - IDisputeGameFactory disputeGameFactoryProxy; - IAnchorStateRegistry anchorStateRegistryProxy; - IFaultDisputeGame faultDisputeGame; - IPermissionedDisputeGame permissionedDisputeGame; - IDelayedWETH delayedWETHPermissionedGameProxy; - IDelayedWETH delayedWETHPermissionlessGameProxy; - } - - /// @notice Addresses of ERC-5202 Blueprint contracts. There are used for deploying full size - /// contracts, to reduce the code size of this factory contract. If it deployed full contracts - /// using the `new Proxy()` syntax, the code size would get large fast, since this contract would - /// contain the bytecode of every contract it deploys. Therefore we instead use Blueprints to - /// reduce the code size of this contract. - struct Blueprints { - address addressManager; - address proxy; - address proxyAdmin; - address l1ChugSplashProxy; - address resolvedDelegateProxy; - address permissionedDisputeGame1; - address permissionedDisputeGame2; - address permissionlessDisputeGame1; - address permissionlessDisputeGame2; - } - - /// @notice The latest implementation contracts for the OP Stack. +/// @notice Minimal interface only used for calling `implementations()` method but without retrieving the ETHLockbox +/// on it, since the OPCM contracts already deployed on mainnet don't have it. +/// @dev Only used for testing. +interface IOPCMImplementationsWithoutLockbox { + /// @notice The implementation contracts for the OP Stack, without the newly added ETHLockbox. struct Implementations { address superchainConfigImpl; address protocolVersionsImpl; @@ -396,141 +330,6 @@ interface IOPContractsManagerOld { address mipsImpl; } - /// @notice The input required to identify a chain for upgrading. - struct OpChainConfig { - ISystemConfig systemConfigProxy; - IProxyAdmin proxyAdmin; - Claim absolutePrestate; - bool disputeGameUsesSuperRoots; - } - - struct AddGameInput { - string saltMixer; - ISystemConfig systemConfig; - IProxyAdmin proxyAdmin; - IDelayedWETH delayedWETH; - GameType disputeGameType; - Claim disputeAbsolutePrestate; - uint256 disputeMaxGameDepth; - uint256 disputeSplitDepth; - Duration disputeClockExtension; - Duration disputeMaxClockDuration; - uint256 initialBond; - IBigStepper vm; - bool permissioned; - } - - struct AddGameOutput { - IDelayedWETH delayedWETH; - IFaultDisputeGame faultDisputeGame; - } - - // -------- Constants and Variables -------- - - function version() external pure returns (string memory); - - /// @notice Address of the SuperchainConfig contract shared by all chains. - function superchainConfig() external view returns (ISuperchainConfig); - - /// @notice Address of the ProtocolVersions contract shared by all chains. - function protocolVersions() external view returns (IProtocolVersions); - - /// @notice Address of the ProxyAdmin contract shared by all chains. - function superchainProxyAdmin() external view returns (IProxyAdmin); - - /// @notice L1 smart contracts release deployed by this version of OPCM. This is used in opcm to signal which - /// version of the L1 smart contracts is deployed. It takes the format of `op-contracts/vX.Y.Z`. - function l1ContractsRelease() external view returns (string memory); - - // -------- Errors -------- - - error OnlyUpgradeController(); - - /// @notice Thrown when an address is the zero address. - error AddressNotFound(address who); - - /// @notice Throw when a contract address has no code. - error AddressHasNoCode(address who); - - /// @notice Thrown when a release version is already set. - error AlreadyReleased(); - - /// @notice Thrown when an invalid `l2ChainId` is provided to `deploy`. - error InvalidChainId(); - - /// @notice Thrown when a role's address is not valid. - error InvalidRoleAddress(string role); - - /// @notice Thrown when the latest release is not set upon initialization. - error LatestReleaseNotSet(); - - /// @notice Thrown when the starting anchor root is not provided. - error InvalidStartingAnchorRoot(); - - /// @notice Thrown when certain methods are called outside of a DELEGATECALL. - error OnlyDelegatecall(); - - /// @notice Thrown when game configs passed to addGameType are invalid. - error InvalidGameConfigs(); - - /// @notice Thrown when the SuperchainConfig of the chain does not match the SuperchainConfig of this OPCM. - error SuperchainConfigMismatch(ISystemConfig systemConfig); - - error SuperchainProxyAdminMismatch(); - - error PrestateNotSet(); - - error PrestateRequired(); - - // -------- Methods -------- - - function __constructor__( - IOPContractsManagerGameTypeAdder _opcmGameTypeAdder, - IOPContractsManagerDeployer _opcmDeployer, - IOPContractsManagerUpgrader _opcmUpgrader, - ISuperchainConfig _superchainConfig, - IProtocolVersions _protocolVersions, - IProxyAdmin _superchainProxyAdmin, - string memory _l1ContractsRelease, - address _upgradeController - ) - external; - - function deploy(DeployInput calldata _input) external returns (DeployOutput memory); - - /// @notice Upgrades the implementation of all proxies in the specified chains - /// @param _opChainConfigs The chains to upgrade - function upgrade(OpChainConfig[] memory _opChainConfigs) external; - - /// @notice addGameType deploys a new dispute game and links it to the DisputeGameFactory. The inputted _gameConfigs - /// must be added in ascending GameType order. - function addGameType(AddGameInput[] memory _gameConfigs) external returns (AddGameOutput[] memory); - - /// @notice Updates the prestate hash for a new game type while keeping all other parameters the same - /// @param _prestateUpdateInputs The new prestate hash to use - function updatePrestate(OpChainConfig[] memory _prestateUpdateInputs) external; - - /// @notice Maps an L2 chain ID to an L1 batch inbox address as defined by the standard - /// configuration's convention. This convention is `versionByte || keccak256(bytes32(chainId))[:19]`, - /// where || denotes concatenation`, versionByte is 0x00, and chainId is a uint256. - /// https://specs.optimism.io/protocol/configurability.html#consensus-parameters - function chainIdToBatchInboxAddress(uint256 _l2ChainId) external pure returns (address); - - /// @notice Returns the blueprint contract addresses. - function blueprints() external view returns (Blueprints memory); - - function opcmDeployer() external view returns (IOPContractsManagerDeployer); - - function opcmUpgrader() external view returns (IOPContractsManagerUpgrader); - - function opcmGameTypeAdder() external view returns (IOPContractsManagerGameTypeAdder); - - /// @notice Returns the implementation contract addresses. + /// @notice Returns the implementation contracts without the ETHLockbox. function implementations() external view returns (Implementations memory); - - function upgradeController() external view returns (address); - - function isRC() external view returns (bool); - - function setRC(bool _isRC) external; } diff --git a/packages/contracts-bedrock/snapshots/semver-lock.json b/packages/contracts-bedrock/snapshots/semver-lock.json index d3ae280f3df9d..492ff26b26dcb 100644 --- a/packages/contracts-bedrock/snapshots/semver-lock.json +++ b/packages/contracts-bedrock/snapshots/semver-lock.json @@ -21,7 +21,7 @@ }, "src/L1/OPContractsManager.sol": { "initCodeHash": "0x9e7496b9720bae99e5db18dc18c99b5c06cd755c6d4079e0d38c56d625fe7b4e", - "sourceCodeHash": "0x45212da23179f7ccab461ba2a1847ed3a8fe4473b3ebe8e809bafae506426f0f" + "sourceCodeHash": "0x7aa17b5a06ffd0d8ac69be957d400a166708cc903361563f84a2c523f870a0cc" }, "src/L1/OptimismPortal2.sol": { "initCodeHash": "0xbfb7f71d76023a02e04e758399bf7db73f83063e989ade28b0432e47a2b86f1a", diff --git a/packages/contracts-bedrock/test/L1/OPContractsManager.t.sol b/packages/contracts-bedrock/test/L1/OPContractsManager.t.sol index 0d62ceb0ac817..2cad35fbb1bef 100644 --- a/packages/contracts-bedrock/test/L1/OPContractsManager.t.sol +++ b/packages/contracts-bedrock/test/L1/OPContractsManager.t.sol @@ -40,7 +40,7 @@ import { IFaultDisputeGame } from "interfaces/dispute/IFaultDisputeGame.sol"; import { ISystemConfig } from "interfaces/L1/ISystemConfig.sol"; import { IOPContractsManager, - IOPContractsManagerOld, + IOPContractsManager, IOPContractsManagerGameTypeAdder, IOPContractsManagerDeployer, IOPContractsManagerUpgrader, @@ -319,9 +319,9 @@ contract OPContractsManager_Upgrade_Harness is CommonTest { function runV200UpgradeAndChecks(address _delegateCaller) public { // The address below corresponds with the address of the v2.0.0-rc.1 OPCM on mainnet. - IOPContractsManagerOld deployedOPCM = - IOPContractsManagerOld(address(0x026b2F158255Beac46c1E7c6b8BbF29A4b6A7B76)); - IOPContractsManagerOld.Implementations memory impls = deployedOPCM.implementations(); + IOPContractsManager deployedOPCM = IOPContractsManager(address(0x026b2F158255Beac46c1E7c6b8BbF29A4b6A7B76)); + IOPCMImplementationsWithoutLockbox.Implementations memory impls = + IOPCMImplementationsWithoutLockbox(address(deployedOPCM)).implementations(); // Cache the old L1xDM address so we can look for it in the AddressManager's event address oldL1CrossDomainMessenger = addressManager.getAddress("OVM_L1CrossDomainMessenger"); @@ -395,9 +395,6 @@ contract OPContractsManager_Upgrade_Harness is CommonTest { assertEq(impls.l1ERC721BridgeImpl, EIP1967Helper.getImplementation(address(l1ERC721Bridge))); assertEq(impls.disputeGameFactoryImpl, EIP1967Helper.getImplementation(address(disputeGameFactory))); assertEq(impls.optimismPortalImpl, EIP1967Helper.getImplementation(address(optimismPortal2))); - // if (deploy.cfg().useUpgradedFork()) { - // assertEq(impls.ethLockboxImpl, EIP1967Helper.getImplementation(address(ethLockbox))); - // } assertEq( impls.optimismMintableERC20FactoryImpl, EIP1967Helper.getImplementation(address(l1OptimismMintableERC20Factory)) @@ -431,9 +428,9 @@ contract OPContractsManager_Upgrade_Harness is CommonTest { function runUpgrade14UpgradeAndChecks(address _delegateCaller) public { // TODO(#14665): Replace this address with once the final OPCM is deployed for Upgrade 14. - IOPContractsManagerOld deployedOPCM = - IOPContractsManagerOld(address(0x3A1f523a4bc09cd344A2745a108Bb0398288094F)); - IOPContractsManagerOld.Implementations memory impls = deployedOPCM.implementations(); + IOPContractsManager deployedOPCM = IOPContractsManager(address(0x3A1f523a4bc09cd344A2745a108Bb0398288094F)); + IOPCMImplementationsWithoutLockbox.Implementations memory impls = + IOPCMImplementationsWithoutLockbox(address(deployedOPCM)).implementations(); // sanity check IPermissionedDisputeGame oldPDG = From 34fa32a92982ae200ca4180ba0701317ee5284f7 Mon Sep 17 00:00:00 2001 From: 0xDiscotech <131301107+0xDiscotech@users.noreply.github.com> Date: Thu, 6 Mar 2025 15:05:40 -0300 Subject: [PATCH 31/50] fix: import --- packages/contracts-bedrock/test/L1/OPContractsManager.t.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/contracts-bedrock/test/L1/OPContractsManager.t.sol b/packages/contracts-bedrock/test/L1/OPContractsManager.t.sol index 2cad35fbb1bef..0110918bdb696 100644 --- a/packages/contracts-bedrock/test/L1/OPContractsManager.t.sol +++ b/packages/contracts-bedrock/test/L1/OPContractsManager.t.sol @@ -40,7 +40,7 @@ import { IFaultDisputeGame } from "interfaces/dispute/IFaultDisputeGame.sol"; import { ISystemConfig } from "interfaces/L1/ISystemConfig.sol"; import { IOPContractsManager, - IOPContractsManager, + IOPCMImplementationsWithoutLockbox, IOPContractsManagerGameTypeAdder, IOPContractsManagerDeployer, IOPContractsManagerUpgrader, From d7f97c8284f7a183a8f238d8cc6819a9d0a00c16 Mon Sep 17 00:00:00 2001 From: Disco <131301107+0xDiscotech@users.noreply.github.com> Date: Thu, 6 Mar 2025 16:28:55 -0300 Subject: [PATCH 32/50] chore: undo changes on opcm carried when resolving conflicts (#302) --- .../snapshots/semver-lock.json | 2 +- .../src/L1/OPContractsManager.sol | 37 +++++++++---------- 2 files changed, 19 insertions(+), 20 deletions(-) diff --git a/packages/contracts-bedrock/snapshots/semver-lock.json b/packages/contracts-bedrock/snapshots/semver-lock.json index 492ff26b26dcb..152b1dee29c76 100644 --- a/packages/contracts-bedrock/snapshots/semver-lock.json +++ b/packages/contracts-bedrock/snapshots/semver-lock.json @@ -21,7 +21,7 @@ }, "src/L1/OPContractsManager.sol": { "initCodeHash": "0x9e7496b9720bae99e5db18dc18c99b5c06cd755c6d4079e0d38c56d625fe7b4e", - "sourceCodeHash": "0x7aa17b5a06ffd0d8ac69be957d400a166708cc903361563f84a2c523f870a0cc" + "sourceCodeHash": "0x8302de0ccd551512f2430d545ea55c938b002701d2292fd7aad2b4d2b975727b" }, "src/L1/OptimismPortal2.sol": { "initCodeHash": "0xbfb7f71d76023a02e04e758399bf7db73f83063e989ade28b0432e47a2b86f1a", diff --git a/packages/contracts-bedrock/src/L1/OPContractsManager.sol b/packages/contracts-bedrock/src/L1/OPContractsManager.sol index ac92222873d1c..096b46c5bd477 100644 --- a/packages/contracts-bedrock/src/L1/OPContractsManager.sol +++ b/packages/contracts-bedrock/src/L1/OPContractsManager.sol @@ -317,20 +317,14 @@ contract OPContractsManagerGameTypeAdder is OPContractsManagerBase { // Deploy a new DelayedWETH proxy for this game if one hasn't already been specified. Leaving /// gameConfig.delayedWETH as the zero address will cause a new DelayedWETH to be deployed for this game. if (address(gameConfig.delayedWETH) == address(0)) { + string memory contractName = string.concat( + "DelayedWETH-", + // This is a safe cast because GameType is a uint256 under the hood and no operation has been done + // on it at this point + Strings.toString(uint256(gameTypeInt)) + ); outputs[i].delayedWETH = IDelayedWETH( - payable( - deployProxy( - l2ChainId, - gameConfig.proxyAdmin, - gameConfig.saltMixer, - string.concat( - "DelayedWETH-", - // This is a safe cast because GameType is a uint256 under the hood - // and no operation has been done on it at this point - Strings.toString(uint256(gameTypeInt)) - ) - ) - ) + payable(deployProxy(l2ChainId, gameConfig.proxyAdmin, gameConfig.saltMixer, contractName)) ); // Initialize the proxy. @@ -409,12 +403,17 @@ contract OPContractsManagerGameTypeAdder is OPContractsManagerBase { setDGFImplementation(dgf, gameConfig.disputeGameType, IDisputeGame(address(outputs[i].faultDisputeGame))); dgf.setInitBond(gameConfig.disputeGameType, gameConfig.initialBond); - emit GameTypeAdded( - l2ChainId, - gameConfig.disputeGameType, - outputs[i].faultDisputeGame, - IDisputeGame(gameConfig.permissioned ? address(pdg) : address(fdg)) - ); + if (gameConfig.permissioned) { + // Emit event for the newly added game type with the old permissioned dispute game + emit GameTypeAdded( + l2ChainId, gameConfig.disputeGameType, outputs[i].faultDisputeGame, IDisputeGame(address(pdg)) + ); + } else { + // Emit event for the newly added game type with the old fault dispute game + emit GameTypeAdded( + l2ChainId, gameConfig.disputeGameType, outputs[i].faultDisputeGame, IDisputeGame(address(fdg)) + ); + } } return outputs; From 859810a3b71a5e149fbd9293b098aa2a568b0526 Mon Sep 17 00:00:00 2001 From: Disco <131301107+0xDiscotech@users.noreply.github.com> Date: Thu, 6 Mar 2025 17:40:48 -0300 Subject: [PATCH 33/50] fix: include lockbox on reinitialization test and add todos with issue number (#303) * fix: include lockbox on reinitialization test * fix: polish nits * refactor: get eth lockbox on test condition * chore: add todos pointing to the issue --- packages/contracts-bedrock/test/L1/ETHLockbox.t.sol | 1 + .../contracts-bedrock/test/L1/OPContractsManager.t.sol | 2 ++ packages/contracts-bedrock/test/L1/OptimismPortal2.t.sol | 1 + .../test/opcm/DeployImplementations.t.sol | 1 + packages/contracts-bedrock/test/setup/ForkLive.s.sol | 1 + packages/contracts-bedrock/test/setup/Setup.sol | 8 ++++---- .../contracts-bedrock/test/vendor/Initializable.t.sol | 4 +--- 7 files changed, 11 insertions(+), 7 deletions(-) diff --git a/packages/contracts-bedrock/test/L1/ETHLockbox.t.sol b/packages/contracts-bedrock/test/L1/ETHLockbox.t.sol index 02c8fd2e1050b..3cf10cf7fc766 100644 --- a/packages/contracts-bedrock/test/L1/ETHLockbox.t.sol +++ b/packages/contracts-bedrock/test/L1/ETHLockbox.t.sol @@ -35,6 +35,7 @@ contract ETHLockboxTest is CommonTest { super.setUp(); // If not on the last upgrade network, we skip the test since the `ETHLockbox` won't be yet deployed + // TODO(#14691): Remove this check once Upgrade 15 is deployed on Mainnet. if (isForkTest() && !deploy.cfg().useUpgradedFork()) vm.skip(true); proxyAdmin = ProxyAdmin(artifacts.mustGetAddress("ProxyAdmin")); diff --git a/packages/contracts-bedrock/test/L1/OPContractsManager.t.sol b/packages/contracts-bedrock/test/L1/OPContractsManager.t.sol index 0110918bdb696..80880840d8bef 100644 --- a/packages/contracts-bedrock/test/L1/OPContractsManager.t.sol +++ b/packages/contracts-bedrock/test/L1/OPContractsManager.t.sol @@ -608,7 +608,9 @@ contract OPContractsManager_Upgrade_Harness is CommonTest { } function runUpgradeTestAndChecks(address _delegateCaller) public { + // TODO(#14691): Remove this function once Upgrade 15 is deployed on Mainnet. runV200UpgradeAndChecks(_delegateCaller); + // TODO(#14691): Remove this function once Upgrade 15 is deployed on Mainnet. runUpgrade14UpgradeAndChecks(_delegateCaller); runUpgrade15UpgradeAndChecks(_delegateCaller); } diff --git a/packages/contracts-bedrock/test/L1/OptimismPortal2.t.sol b/packages/contracts-bedrock/test/L1/OptimismPortal2.t.sol index 30c2111305f96..9c15c26a6a604 100644 --- a/packages/contracts-bedrock/test/L1/OptimismPortal2.t.sol +++ b/packages/contracts-bedrock/test/L1/OptimismPortal2.t.sol @@ -92,6 +92,7 @@ contract OptimismPortal2_Test is CommonTest { assertEq(optimismPortal2.respectedGameType().raw(), deploy.cfg().respectedGameType()); } + /// @dev Tests that the upgrade function succeeds. function testFuzz_upgrade_succeeds(address _newAnchorStateRegistry, uint256 _balance) external { // Prevent overflow on an upgrade context _balance = bound(_balance, 0, type(uint256).max - address(ethLockbox).balance); diff --git a/packages/contracts-bedrock/test/opcm/DeployImplementations.t.sol b/packages/contracts-bedrock/test/opcm/DeployImplementations.t.sol index 0a4d37d99b6eb..07d577e642239 100644 --- a/packages/contracts-bedrock/test/opcm/DeployImplementations.t.sol +++ b/packages/contracts-bedrock/test/opcm/DeployImplementations.t.sol @@ -154,6 +154,7 @@ contract DeployImplementationsOutput_Test is Test { vm.expectRevert(expectedErr); dio.ethLockboxImpl(); + vm.expectRevert(expectedErr); dio.delayedWETHImpl(); diff --git a/packages/contracts-bedrock/test/setup/ForkLive.s.sol b/packages/contracts-bedrock/test/setup/ForkLive.s.sol index 395a51517257b..6f3d663000ee6 100644 --- a/packages/contracts-bedrock/test/setup/ForkLive.s.sol +++ b/packages/contracts-bedrock/test/setup/ForkLive.s.sol @@ -121,6 +121,7 @@ contract ForkLive is Deployer { artifacts.save("OptimismPortal2Impl", EIP1967Helper.getImplementation(optimismPortal)); // Get the lockbox address from the portal, and save it + /// NOTE: Using try catch because this function could be called before or after the upgrade. try IOptimismPortal2(payable(optimismPortal)).ethLockbox() returns (IETHLockbox ethLockbox_) { console.log("ForkLive: ETHLockboxProxy found: %s", address(ethLockbox_)); artifacts.save("ETHLockboxProxy", address(ethLockbox_)); diff --git a/packages/contracts-bedrock/test/setup/Setup.sol b/packages/contracts-bedrock/test/setup/Setup.sol index 13cc4924706d4..a650d23ee3405 100644 --- a/packages/contracts-bedrock/test/setup/Setup.sol +++ b/packages/contracts-bedrock/test/setup/Setup.sol @@ -233,12 +233,12 @@ contract Setup { optimismPortal2 = IOptimismPortal(artifacts.mustGetAddress("OptimismPortalProxy")); - if (isForkTest() && deploy.cfg().useUpgradedFork()) { - // If we are on the last upgrade network, we can use the predeployed ETHLockbox for the fork test - ethLockbox = IETHLockbox(artifacts.mustGetAddress("ETHLockboxProxy")); - } else if (!isForkTest()) { + // Only skip ETHLockbox assignment if we're in a fork test with non-upgraded fork + // TODO(#14691): Remove this check once Upgrade 15 is deployed on Mainnet. + if (!isForkTest() || deploy.cfg().useUpgradedFork()) { ethLockbox = IETHLockbox(artifacts.mustGetAddress("ETHLockboxProxy")); } + systemConfig = ISystemConfig(artifacts.mustGetAddress("SystemConfigProxy")); l1StandardBridge = IL1StandardBridge(artifacts.mustGetAddress("L1StandardBridgeProxy")); l1CrossDomainMessenger = IL1CrossDomainMessenger(artifacts.mustGetAddress("L1CrossDomainMessengerProxy")); diff --git a/packages/contracts-bedrock/test/vendor/Initializable.t.sol b/packages/contracts-bedrock/test/vendor/Initializable.t.sol index cc05da2689d22..3eb79187e65ff 100644 --- a/packages/contracts-bedrock/test/vendor/Initializable.t.sol +++ b/packages/contracts-bedrock/test/vendor/Initializable.t.sol @@ -357,7 +357,7 @@ 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[](12); + 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. @@ -381,8 +381,6 @@ contract Initializer_Test is CommonTest { excludes[9] = "src/L1/OPContractsManagerInterop.sol"; // L2 contract initialization is tested in Predeploys.t.sol excludes[10] = "src/L2/*"; - // ETHLockbox uses OZ v5 initializer - excludes[11] = "src/L1/ETHLockbox.sol"; // Get all contract names in the src directory, minus the excluded contracts. string[] memory contractNames = ForgeArtifacts.getContractNames("src/*", excludes); From 23f539560b44611fad219c5ef455c6dd09ab755e Mon Sep 17 00:00:00 2001 From: Kelvin Fichter Date: Thu, 6 Mar 2025 15:22:56 -0600 Subject: [PATCH 34/50] fix: add ETHLockbox to op-deployer --- op-chain-ops/genesis/config.go | 2 ++ op-chain-ops/interopgen/deployments.go | 2 ++ op-deployer/pkg/deployer/bootstrap/validator.go | 1 + op-deployer/pkg/deployer/bootstrap/validator_test.go | 1 + op-deployer/pkg/deployer/inspect/l1.go | 5 +++++ op-deployer/pkg/deployer/integration_test/apply_test.go | 1 + op-deployer/pkg/deployer/opcm/implementations.go | 2 +- op-deployer/pkg/deployer/opcm/opchain.go | 2 ++ op-deployer/pkg/deployer/pipeline/implementations.go | 1 + op-deployer/pkg/deployer/pipeline/opchain.go | 1 + op-deployer/pkg/deployer/state/state.go | 1 + 11 files changed, 18 insertions(+), 1 deletion(-) diff --git a/op-chain-ops/genesis/config.go b/op-chain-ops/genesis/config.go index 3cd7f9d677e0c..8821e9b60fd28 100644 --- a/op-chain-ops/genesis/config.go +++ b/op-chain-ops/genesis/config.go @@ -1100,6 +1100,8 @@ type L1Deployments struct { OptimismMintableERC20FactoryProxy common.Address `json:"OptimismMintableERC20FactoryProxy"` OptimismPortal common.Address `json:"OptimismPortal"` OptimismPortalProxy common.Address `json:"OptimismPortalProxy"` + ETHLockbox common.Address `json:"ETHLockbox"` + ETHLockboxProxy common.Address `json:"ETHLockboxProxy"` ProxyAdmin common.Address `json:"ProxyAdmin"` SystemConfig common.Address `json:"SystemConfig"` SystemConfigProxy common.Address `json:"SystemConfigProxy"` diff --git a/op-chain-ops/interopgen/deployments.go b/op-chain-ops/interopgen/deployments.go index 6b9e34b7a871e..48a2b93b2245b 100644 --- a/op-chain-ops/interopgen/deployments.go +++ b/op-chain-ops/interopgen/deployments.go @@ -16,6 +16,7 @@ type Implementations struct { OpcmUpgrader common.Address `json:"OPCMUpgrader"` DelayedWETHImpl common.Address `json:"DelayedWETHImpl"` OptimismPortalImpl common.Address `json:"OptimismPortalImpl"` + ETHLockboxImpl common.Address `json:"ETHLockboxImpl"` PreimageOracleSingleton common.Address `json:"PreimageOracleSingleton"` MipsSingleton common.Address `json:"MipsSingleton"` SystemConfigImpl common.Address `json:"SystemConfigImpl"` @@ -51,6 +52,7 @@ type L2OpchainDeployment struct { L1CrossDomainMessengerProxy common.Address `json:"L1CrossDomainMessengerProxy"` // Fault proof contracts below. OptimismPortalProxy common.Address `json:"OptimismPortalProxy"` + ETHLockboxProxy common.Address `json:"ETHLockboxProxy"` DisputeGameFactoryProxy common.Address `json:"DisputeGameFactoryProxy"` AnchorStateRegistryProxy common.Address `json:"AnchorStateRegistryProxy"` FaultDisputeGame common.Address `json:"FaultDisputeGame"` diff --git a/op-deployer/pkg/deployer/bootstrap/validator.go b/op-deployer/pkg/deployer/bootstrap/validator.go index f32b00b13d4c4..25ff1668b9f4e 100644 --- a/op-deployer/pkg/deployer/bootstrap/validator.go +++ b/op-deployer/pkg/deployer/bootstrap/validator.go @@ -47,6 +47,7 @@ type ValidatorInput struct { ProtocolVersionsImpl common.Address `json:"protocolVersionsImpl"` L1ERC721BridgeImpl common.Address `json:"l1ERC721BridgeImpl"` OptimismPortalImpl common.Address `json:"optimismPortalImpl"` + ETHLockboxImpl common.Address `json:"ethLockboxImpl"` SystemConfigImpl common.Address `json:"systemConfigImpl"` OptimismMintableERC20FactoryImpl common.Address `json:"optimismMintableERC20FactoryImpl"` L1CrossDomainMessengerImpl common.Address `json:"l1CrossDomainMessengerImpl"` diff --git a/op-deployer/pkg/deployer/bootstrap/validator_test.go b/op-deployer/pkg/deployer/bootstrap/validator_test.go index ff58e714867a9..e2450b599bef4 100644 --- a/op-deployer/pkg/deployer/bootstrap/validator_test.go +++ b/op-deployer/pkg/deployer/bootstrap/validator_test.go @@ -62,6 +62,7 @@ func testValidator(t *testing.T, forkRPCURL string, loc *artifacts.Locator, rele ProtocolVersionsImpl: common.Address{'2'}, L1ERC721BridgeImpl: common.Address{'3'}, OptimismPortalImpl: common.Address{'4'}, + ETHLockboxImpl: common.Address{'5'}, SystemConfigImpl: common.Address{'5'}, OptimismMintableERC20FactoryImpl: common.Address{'6'}, L1CrossDomainMessengerImpl: common.Address{'7'}, diff --git a/op-deployer/pkg/deployer/inspect/l1.go b/op-deployer/pkg/deployer/inspect/l1.go index cabcc997c4377..d432f0cac7fca 100644 --- a/op-deployer/pkg/deployer/inspect/l1.go +++ b/op-deployer/pkg/deployer/inspect/l1.go @@ -37,6 +37,8 @@ func (l L1Contracts) AsL1Deployments() *genesis.L1Deployments { OptimismMintableERC20FactoryProxy: l.OpChainDeployment.OptimismMintableERC20FactoryProxyAddress, OptimismPortal: l.ImplementationsDeployment.OptimismPortalImplAddress, OptimismPortalProxy: l.OpChainDeployment.OptimismPortalProxyAddress, + ETHLockbox: l.ImplementationsDeployment.ETHLockboxImplAddress, + ETHLockboxProxy: l.OpChainDeployment.ETHLockboxProxyAddress, ProxyAdmin: l.OpChainDeployment.ProxyAdminAddress, SystemConfig: l.ImplementationsDeployment.SystemConfigImplAddress, SystemConfigProxy: l.OpChainDeployment.SystemConfigProxyAddress, @@ -64,6 +66,7 @@ type OpChainDeployment struct { L1StandardBridgeProxyAddress common.Address `json:"l1StandardBridgeProxyAddress"` L1CrossDomainMessengerProxyAddress common.Address `json:"l1CrossDomainMessengerProxyAddress"` OptimismPortalProxyAddress common.Address `json:"optimismPortalProxyAddress"` + ETHLockboxProxyAddress common.Address `json:"ethLockboxProxyAddress"` DisputeGameFactoryProxyAddress common.Address `json:"disputeGameFactoryProxyAddress"` AnchorStateRegistryProxyAddress common.Address `json:"anchorStateRegistryProxyAddress"` AnchorStateRegistryImplAddress common.Address `json:"anchorStateRegistryImplAddress"` @@ -79,6 +82,7 @@ type ImplementationsDeployment struct { OpcmAddress common.Address `json:"opcmAddress"` DelayedWETHImplAddress common.Address `json:"delayedWETHImplAddress"` OptimismPortalImplAddress common.Address `json:"optimismPortalImplAddress"` + ETHLockboxImplAddress common.Address `json:"ethLockboxImplAddress"` PreimageOracleSingletonAddress common.Address `json:"preimageOracleSingletonAddress"` MipsSingletonAddress common.Address `json:"mipsSingletonAddress"` SystemConfigImplAddress common.Address `json:"systemConfigImplAddress"` @@ -148,6 +152,7 @@ func L1(globalState *state.State, chainID common.Hash) (*L1Contracts, error) { OpcmAddress: globalState.ImplementationsDeployment.OpcmAddress, DelayedWETHImplAddress: globalState.ImplementationsDeployment.DelayedWETHImplAddress, OptimismPortalImplAddress: globalState.ImplementationsDeployment.OptimismPortalImplAddress, + ETHLockboxImplAddress: globalState.ImplementationsDeployment.ETHLockboxImplAddress, PreimageOracleSingletonAddress: globalState.ImplementationsDeployment.PreimageOracleSingletonAddress, MipsSingletonAddress: globalState.ImplementationsDeployment.MipsSingletonAddress, SystemConfigImplAddress: globalState.ImplementationsDeployment.SystemConfigImplAddress, diff --git a/op-deployer/pkg/deployer/integration_test/apply_test.go b/op-deployer/pkg/deployer/integration_test/apply_test.go index 4b7494855b49a..fda137d0a093a 100644 --- a/op-deployer/pkg/deployer/integration_test/apply_test.go +++ b/op-deployer/pkg/deployer/integration_test/apply_test.go @@ -682,6 +682,7 @@ func validateOPChainDeployment(t *testing.T, cg codeGetter, st *state.State, int }{ {"DelayedWETHImplAddress", st.ImplementationsDeployment.DelayedWETHImplAddress}, {"OptimismPortalImplAddress", st.ImplementationsDeployment.OptimismPortalImplAddress}, + {"ETHLockboxImplAddress", st.ImplementationsDeployment.ETHLockboxImplAddress}, {"SystemConfigImplAddress", st.ImplementationsDeployment.SystemConfigImplAddress}, {"L1CrossDomainMessengerImplAddress", st.ImplementationsDeployment.L1CrossDomainMessengerImplAddress}, {"L1ERC721BridgeImplAddress", st.ImplementationsDeployment.L1ERC721BridgeImplAddress}, diff --git a/op-deployer/pkg/deployer/opcm/implementations.go b/op-deployer/pkg/deployer/opcm/implementations.go index 19261d791aa88..3acc0395c5ce6 100644 --- a/op-deployer/pkg/deployer/opcm/implementations.go +++ b/op-deployer/pkg/deployer/opcm/implementations.go @@ -37,6 +37,7 @@ type DeployImplementationsOutput struct { OpcmUpgrader common.Address DelayedWETHImpl common.Address OptimismPortalImpl common.Address + ETHLockboxImpl common.Address PreimageOracleSingleton common.Address MipsSingleton common.Address SystemConfigImpl common.Address @@ -48,7 +49,6 @@ type DeployImplementationsOutput struct { AnchorStateRegistryImpl common.Address SuperchainConfigImpl common.Address ProtocolVersionsImpl common.Address - EthLockboxImpl common.Address } func (output *DeployImplementationsOutput) CheckOutput(input common.Address) error { diff --git a/op-deployer/pkg/deployer/opcm/opchain.go b/op-deployer/pkg/deployer/opcm/opchain.go index e6cce9203433c..96237782c8122 100644 --- a/op-deployer/pkg/deployer/opcm/opchain.go +++ b/op-deployer/pkg/deployer/opcm/opchain.go @@ -64,6 +64,7 @@ type DeployOPChainOutput struct { L1CrossDomainMessengerProxy common.Address // Fault proof contracts below. OptimismPortalProxy common.Address + ETHLockboxProxy common.Address DisputeGameFactoryProxy common.Address AnchorStateRegistryProxy common.Address FaultDisputeGame common.Address @@ -93,6 +94,7 @@ type ReadImplementationAddressesInput struct { type ReadImplementationAddressesOutput struct { DelayedWETH common.Address OptimismPortal common.Address + ETHLockbox common.Address SystemConfig common.Address L1CrossDomainMessenger common.Address L1ERC721Bridge common.Address diff --git a/op-deployer/pkg/deployer/pipeline/implementations.go b/op-deployer/pkg/deployer/pipeline/implementations.go index ef591210b6de7..a88bec43543d8 100644 --- a/op-deployer/pkg/deployer/pipeline/implementations.go +++ b/op-deployer/pkg/deployer/pipeline/implementations.go @@ -72,6 +72,7 @@ func DeployImplementations(env *Env, intent *state.Intent, st *state.State) erro OpcmUpgraderAddress: dio.OpcmUpgrader, DelayedWETHImplAddress: dio.DelayedWETHImpl, OptimismPortalImplAddress: dio.OptimismPortalImpl, + ETHLockboxImplAddress: dio.ETHLockboxImpl, PreimageOracleSingletonAddress: dio.PreimageOracleSingleton, MipsSingletonAddress: dio.MipsSingleton, SystemConfigImplAddress: dio.SystemConfigImpl, diff --git a/op-deployer/pkg/deployer/pipeline/opchain.go b/op-deployer/pkg/deployer/pipeline/opchain.go index e4ad67977b073..6110feda2fc72 100644 --- a/op-deployer/pkg/deployer/pipeline/opchain.go +++ b/op-deployer/pkg/deployer/pipeline/opchain.go @@ -58,6 +58,7 @@ func DeployOPChain(env *Env, intent *state.Intent, st *state.State, chainID comm st.ImplementationsDeployment.DelayedWETHImplAddress = impls.DelayedWETH st.ImplementationsDeployment.OptimismPortalImplAddress = impls.OptimismPortal + st.ImplementationsDeployment.ETHLockboxImplAddress = impls.ETHLockbox st.ImplementationsDeployment.SystemConfigImplAddress = impls.SystemConfig st.ImplementationsDeployment.L1CrossDomainMessengerImplAddress = impls.L1CrossDomainMessenger st.ImplementationsDeployment.L1ERC721BridgeImplAddress = impls.L1ERC721Bridge diff --git a/op-deployer/pkg/deployer/state/state.go b/op-deployer/pkg/deployer/state/state.go index 0ec6c923f30bc..86e5be0e8936f 100644 --- a/op-deployer/pkg/deployer/state/state.go +++ b/op-deployer/pkg/deployer/state/state.go @@ -76,6 +76,7 @@ type ImplementationsDeployment struct { OpcmUpgraderAddress common.Address `json:"opcmUpgraderAddress"` DelayedWETHImplAddress common.Address `json:"delayedWETHImplAddress"` OptimismPortalImplAddress common.Address `json:"optimismPortalImplAddress"` + ETHLockboxImplAddress common.Address `json:"ethLockboxImplAddress"` PreimageOracleSingletonAddress common.Address `json:"preimageOracleSingletonAddress"` MipsSingletonAddress common.Address `json:"mipsSingletonAddress"` SystemConfigImplAddress common.Address `json:"systemConfigImplAddress"` From 151b18f75eaf62054de38077e67861641943f6df Mon Sep 17 00:00:00 2001 From: Kelvin Fichter Date: Thu, 6 Mar 2025 16:01:38 -0600 Subject: [PATCH 35/50] fix: add evm tags to ETHLockboxImpl --- op-deployer/pkg/deployer/bootstrap/validator.go | 2 +- op-deployer/pkg/deployer/opcm/implementations.go | 2 +- packages/contracts-bedrock/justfile | 4 ++++ 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/op-deployer/pkg/deployer/bootstrap/validator.go b/op-deployer/pkg/deployer/bootstrap/validator.go index 25ff1668b9f4e..5d5c5f0bb4d9a 100644 --- a/op-deployer/pkg/deployer/bootstrap/validator.go +++ b/op-deployer/pkg/deployer/bootstrap/validator.go @@ -47,7 +47,7 @@ type ValidatorInput struct { ProtocolVersionsImpl common.Address `json:"protocolVersionsImpl"` L1ERC721BridgeImpl common.Address `json:"l1ERC721BridgeImpl"` OptimismPortalImpl common.Address `json:"optimismPortalImpl"` - ETHLockboxImpl common.Address `json:"ethLockboxImpl"` + ETHLockboxImpl common.Address `json:"ethLockboxImpl" evm:"ethLockboxImpl"` SystemConfigImpl common.Address `json:"systemConfigImpl"` OptimismMintableERC20FactoryImpl common.Address `json:"optimismMintableERC20FactoryImpl"` L1CrossDomainMessengerImpl common.Address `json:"l1CrossDomainMessengerImpl"` diff --git a/op-deployer/pkg/deployer/opcm/implementations.go b/op-deployer/pkg/deployer/opcm/implementations.go index 3acc0395c5ce6..b7534f84a3ec2 100644 --- a/op-deployer/pkg/deployer/opcm/implementations.go +++ b/op-deployer/pkg/deployer/opcm/implementations.go @@ -37,7 +37,7 @@ type DeployImplementationsOutput struct { OpcmUpgrader common.Address DelayedWETHImpl common.Address OptimismPortalImpl common.Address - ETHLockboxImpl common.Address + ETHLockboxImpl common.Address `evm:"ethLockboxImpl"` PreimageOracleSingleton common.Address MipsSingleton common.Address SystemConfigImpl common.Address diff --git a/packages/contracts-bedrock/justfile b/packages/contracts-bedrock/justfile index 1908267920fe3..0cde991fcc278 100644 --- a/packages/contracts-bedrock/justfile +++ b/packages/contracts-bedrock/justfile @@ -27,6 +27,10 @@ forge-build-dev *ARGS: build-source: forge build --skip "/**/test/**" --skip "/**/scripts/**" +# Builds source contracts and scripts, skipping tests. +build-no-tests: + forge build --skip "/**/test/**" + # Builds the contracts. build *ARGS: lint-fix-no-fail just forge-build {{ARGS}} From 5c591508d6e1e5584e13702d02531417f14810f4 Mon Sep 17 00:00:00 2001 From: Kelvin Fichter Date: Thu, 6 Mar 2025 16:17:40 -0600 Subject: [PATCH 36/50] fix: evm tags for ethLockboxProxy too --- op-deployer/pkg/deployer/opcm/opchain.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/op-deployer/pkg/deployer/opcm/opchain.go b/op-deployer/pkg/deployer/opcm/opchain.go index 96237782c8122..fce8db7afebe2 100644 --- a/op-deployer/pkg/deployer/opcm/opchain.go +++ b/op-deployer/pkg/deployer/opcm/opchain.go @@ -64,7 +64,7 @@ type DeployOPChainOutput struct { L1CrossDomainMessengerProxy common.Address // Fault proof contracts below. OptimismPortalProxy common.Address - ETHLockboxProxy common.Address + ETHLockboxProxy common.Address `evm:"ethLockboxProxy"` DisputeGameFactoryProxy common.Address AnchorStateRegistryProxy common.Address FaultDisputeGame common.Address From 66634b6692b4f9f0f20821d69df0e60d39da5c5d Mon Sep 17 00:00:00 2001 From: Kelvin Fichter Date: Fri, 7 Mar 2025 09:47:41 -0600 Subject: [PATCH 37/50] fix: remove certain ETHLockbox checks --- op-chain-ops/genesis/config.go | 3 +++ .../pkg/deployer/integration_test/apply_test.go | 11 ++++++++--- op-deployer/pkg/deployer/opcm/opchain.go | 2 +- .../scripts/deploy/ReadImplementationAddresses.s.sol | 10 ++++++++++ 4 files changed, 22 insertions(+), 4 deletions(-) diff --git a/op-chain-ops/genesis/config.go b/op-chain-ops/genesis/config.go index 8821e9b60fd28..110c2d1612a83 100644 --- a/op-chain-ops/genesis/config.go +++ b/op-chain-ops/genesis/config.go @@ -1147,6 +1147,9 @@ func (d *L1Deployments) Check(deployConfig *DeployConfig) error { name == "DataAvailabilityChallengeProxy") { continue } + if name == "ETHLockbox" || name == "ETHLockboxProxy" { + continue + } if val.Field(i).Interface().(common.Address) == (common.Address{}) { return fmt.Errorf("%s is not set", name) } diff --git a/op-deployer/pkg/deployer/integration_test/apply_test.go b/op-deployer/pkg/deployer/integration_test/apply_test.go index fda137d0a093a..af9ae8e57c61d 100644 --- a/op-deployer/pkg/deployer/integration_test/apply_test.go +++ b/op-deployer/pkg/deployer/integration_test/apply_test.go @@ -676,13 +676,13 @@ func validateSuperchainDeployment(t *testing.T, st *state.State, cg codeGetter) func validateOPChainDeployment(t *testing.T, cg codeGetter, st *state.State, intent *state.Intent, govEnabled bool) { // Validate that the implementation addresses are always set, even in subsequent deployments // that pull from an existing OPCM deployment. - implAddrs := []struct { + type addrTuple struct { name string addr common.Address - }{ + } + implAddrs := []addrTuple{ {"DelayedWETHImplAddress", st.ImplementationsDeployment.DelayedWETHImplAddress}, {"OptimismPortalImplAddress", st.ImplementationsDeployment.OptimismPortalImplAddress}, - {"ETHLockboxImplAddress", st.ImplementationsDeployment.ETHLockboxImplAddress}, {"SystemConfigImplAddress", st.ImplementationsDeployment.SystemConfigImplAddress}, {"L1CrossDomainMessengerImplAddress", st.ImplementationsDeployment.L1CrossDomainMessengerImplAddress}, {"L1ERC721BridgeImplAddress", st.ImplementationsDeployment.L1ERC721BridgeImplAddress}, @@ -692,6 +692,11 @@ func validateOPChainDeployment(t *testing.T, cg codeGetter, st *state.State, int {"MipsSingletonAddress", st.ImplementationsDeployment.MipsSingletonAddress}, {"PreimageOracleSingletonAddress", st.ImplementationsDeployment.PreimageOracleSingletonAddress}, } + + if !intent.L1ContractsLocator.IsTag() { + implAddrs = append(implAddrs, addrTuple{"ETHLockboxImplAddress", st.ImplementationsDeployment.ETHLockboxImplAddress}) + } + for _, addr := range implAddrs { require.NotEmpty(t, addr.addr, "%s should be set", addr.name) code := cg(t, addr.addr) diff --git a/op-deployer/pkg/deployer/opcm/opchain.go b/op-deployer/pkg/deployer/opcm/opchain.go index fce8db7afebe2..813b7435170ad 100644 --- a/op-deployer/pkg/deployer/opcm/opchain.go +++ b/op-deployer/pkg/deployer/opcm/opchain.go @@ -94,7 +94,7 @@ type ReadImplementationAddressesInput struct { type ReadImplementationAddressesOutput struct { DelayedWETH common.Address OptimismPortal common.Address - ETHLockbox common.Address + ETHLockbox common.Address `evm:"ethLockbox"` SystemConfig common.Address L1CrossDomainMessenger common.Address L1ERC721Bridge common.Address diff --git a/packages/contracts-bedrock/scripts/deploy/ReadImplementationAddresses.s.sol b/packages/contracts-bedrock/scripts/deploy/ReadImplementationAddresses.s.sol index 211281b506d37..e7b7f76edfd7c 100644 --- a/packages/contracts-bedrock/scripts/deploy/ReadImplementationAddresses.s.sol +++ b/packages/contracts-bedrock/scripts/deploy/ReadImplementationAddresses.s.sol @@ -30,6 +30,7 @@ contract ReadImplementationAddressesInput is DeployOPChainOutput { contract ReadImplementationAddressesOutput is BaseDeployIO { address internal _delayedWETH; address internal _optimismPortal; + address internal _ethLockbox; address internal _systemConfig; address internal _l1CrossDomainMessenger; address internal _l1ERC721Bridge; @@ -43,6 +44,7 @@ contract ReadImplementationAddressesOutput is BaseDeployIO { require(_addr != address(0), "ReadImplementationAddressesOutput: cannot set zero address"); if (_sel == this.delayedWETH.selector) _delayedWETH = _addr; else if (_sel == this.optimismPortal.selector) _optimismPortal = _addr; + else if (_sel == this.ethLockbox.selector) _ethLockbox = _addr; else if (_sel == this.systemConfig.selector) _systemConfig = _addr; else if (_sel == this.l1CrossDomainMessenger.selector) _l1CrossDomainMessenger = _addr; else if (_sel == this.l1ERC721Bridge.selector) _l1ERC721Bridge = _addr; @@ -64,6 +66,11 @@ contract ReadImplementationAddressesOutput is BaseDeployIO { return _optimismPortal; } + function ethLockbox() public view returns (address) { + require(_ethLockbox != address(0), "ReadImplementationAddressesOutput: ethLockbox not set"); + return _ethLockbox; + } + function systemConfig() public view returns (address) { require(_systemConfig != address(0), "ReadImplementationAddressesOutput: systemConfig not set"); return _systemConfig; @@ -154,5 +161,8 @@ contract ReadImplementationAddresses is Script { address preimageOracle = address(IMIPS(mipsLogic).oracle()); _rio.set(_rio.preimageOracleSingleton.selector, preimageOracle); + + address ethLockbox = _rii.opcm().implementations().ethLockboxImpl; + _rio.set(_rio.ethLockbox.selector, ethLockbox); } } From 5311c21b33c1b200cfa7fe68a89f6987b295d761 Mon Sep 17 00:00:00 2001 From: Kelvin Fichter Date: Fri, 7 Mar 2025 10:06:55 -0600 Subject: [PATCH 38/50] fix: properly add ETHLockbox to state --- op-chain-ops/genesis/config.go | 3 --- op-deployer/pkg/deployer/inspect/l1.go | 1 + op-deployer/pkg/deployer/pipeline/opchain.go | 1 + op-deployer/pkg/deployer/state/state.go | 1 + 4 files changed, 3 insertions(+), 3 deletions(-) diff --git a/op-chain-ops/genesis/config.go b/op-chain-ops/genesis/config.go index 110c2d1612a83..8821e9b60fd28 100644 --- a/op-chain-ops/genesis/config.go +++ b/op-chain-ops/genesis/config.go @@ -1147,9 +1147,6 @@ func (d *L1Deployments) Check(deployConfig *DeployConfig) error { name == "DataAvailabilityChallengeProxy") { continue } - if name == "ETHLockbox" || name == "ETHLockboxProxy" { - continue - } if val.Field(i).Interface().(common.Address) == (common.Address{}) { return fmt.Errorf("%s is not set", name) } diff --git a/op-deployer/pkg/deployer/inspect/l1.go b/op-deployer/pkg/deployer/inspect/l1.go index d432f0cac7fca..00308a87b88af 100644 --- a/op-deployer/pkg/deployer/inspect/l1.go +++ b/op-deployer/pkg/deployer/inspect/l1.go @@ -139,6 +139,7 @@ func L1(globalState *state.State, chainID common.Hash) (*L1Contracts, error) { L1StandardBridgeProxyAddress: chainState.L1StandardBridgeProxyAddress, L1CrossDomainMessengerProxyAddress: chainState.L1CrossDomainMessengerProxyAddress, OptimismPortalProxyAddress: chainState.OptimismPortalProxyAddress, + ETHLockboxProxyAddress: chainState.ETHLockboxProxyAddress, DisputeGameFactoryProxyAddress: chainState.DisputeGameFactoryProxyAddress, AnchorStateRegistryProxyAddress: chainState.AnchorStateRegistryProxyAddress, FaultDisputeGameAddress: chainState.FaultDisputeGameAddress, diff --git a/op-deployer/pkg/deployer/pipeline/opchain.go b/op-deployer/pkg/deployer/pipeline/opchain.go index 6110feda2fc72..3007aaa1f47fe 100644 --- a/op-deployer/pkg/deployer/pipeline/opchain.go +++ b/op-deployer/pkg/deployer/pipeline/opchain.go @@ -126,6 +126,7 @@ func makeChainState(chainID common.Hash, dco opcm.DeployOPChainOutput) *state.Ch L1StandardBridgeProxyAddress: dco.L1StandardBridgeProxy, L1CrossDomainMessengerProxyAddress: dco.L1CrossDomainMessengerProxy, OptimismPortalProxyAddress: dco.OptimismPortalProxy, + ETHLockboxProxyAddress: dco.ETHLockboxProxy, DisputeGameFactoryProxyAddress: dco.DisputeGameFactoryProxy, AnchorStateRegistryProxyAddress: dco.AnchorStateRegistryProxy, FaultDisputeGameAddress: dco.FaultDisputeGame, diff --git a/op-deployer/pkg/deployer/state/state.go b/op-deployer/pkg/deployer/state/state.go index 86e5be0e8936f..25f99234d9e80 100644 --- a/op-deployer/pkg/deployer/state/state.go +++ b/op-deployer/pkg/deployer/state/state.go @@ -107,6 +107,7 @@ type ChainState struct { L1StandardBridgeProxyAddress common.Address `json:"l1StandardBridgeProxyAddress"` L1CrossDomainMessengerProxyAddress common.Address `json:"l1CrossDomainMessengerProxyAddress"` OptimismPortalProxyAddress common.Address `json:"optimismPortalProxyAddress"` + ETHLockboxProxyAddress common.Address `json:"ethLockboxProxyAddress"` DisputeGameFactoryProxyAddress common.Address `json:"disputeGameFactoryProxyAddress"` AnchorStateRegistryProxyAddress common.Address `json:"anchorStateRegistryProxyAddress"` FaultDisputeGameAddress common.Address `json:"faultDisputeGameAddress"` From b1b6ecced2df795d59952f84fca1948a9d3ab8c2 Mon Sep 17 00:00:00 2001 From: Kelvin Fichter Date: Fri, 7 Mar 2025 10:58:40 -0600 Subject: [PATCH 39/50] fix: handle deposit tests and kill L2oo tests --- op-chain-ops/genesis/config.go | 1 + op-e2e/actions/helpers/user.go | 2 +- op-e2e/actions/helpers/user_test.go | 4 ---- op-e2e/system/bridge/validity_test.go | 4 ---- op-e2e/system/bridge/withdrawal_test.go | 4 ---- op-e2e/system/helpers/tx_helper.go | 3 ++- 6 files changed, 4 insertions(+), 14 deletions(-) diff --git a/op-chain-ops/genesis/config.go b/op-chain-ops/genesis/config.go index 8821e9b60fd28..ff70ea3d62fdf 100644 --- a/op-chain-ops/genesis/config.go +++ b/op-chain-ops/genesis/config.go @@ -1147,6 +1147,7 @@ func (d *L1Deployments) Check(deployConfig *DeployConfig) error { name == "DataAvailabilityChallengeProxy") { continue } + if val.Field(i).Interface().(common.Address) == (common.Address{}) { return fmt.Errorf("%s is not set", name) } diff --git a/op-e2e/actions/helpers/user.go b/op-e2e/actions/helpers/user.go index d7215b80650f7..7f9c948e891b6 100644 --- a/op-e2e/actions/helpers/user.go +++ b/op-e2e/actions/helpers/user.go @@ -382,7 +382,7 @@ func (s *CrossLayerUser) ActDeposit(t Testing) { func (s *CrossLayerUser) ActCheckDepositStatus(l1Success, l2Success bool) Action { return func(t Testing) { - s.CheckDepositTx(t, s.lastL1DepositTxHash, 0, l1Success, l2Success) + s.CheckDepositTx(t, s.lastL1DepositTxHash, 1, l1Success, l2Success) } } diff --git a/op-e2e/actions/helpers/user_test.go b/op-e2e/actions/helpers/user_test.go index 2c5822f988942..ae3a30feabf6f 100644 --- a/op-e2e/actions/helpers/user_test.go +++ b/op-e2e/actions/helpers/user_test.go @@ -67,10 +67,6 @@ func TestCrossLayerUser_Standard(t *testing.T) { testCrossLayerUser(t, config.AllocTypeStandard) } -func TestCrossLayerUser_L2OO(t *testing.T) { - testCrossLayerUser(t, config.AllocTypeL2OO) -} - // TestCrossLayerUser tests that common actions of the CrossLayerUser actor work in various hardfork configurations: // - transact on L1 // - transact on L2 diff --git a/op-e2e/system/bridge/validity_test.go b/op-e2e/system/bridge/validity_test.go index ebc141436a4de..ed8e3a1edcd06 100644 --- a/op-e2e/system/bridge/validity_test.go +++ b/op-e2e/system/bridge/validity_test.go @@ -266,10 +266,6 @@ func TestMixedDepositValidity(t *testing.T) { } } -func TestMixedWithdrawalValidity_L2OO(t *testing.T) { - testMixedWithdrawalValidity(t, config.AllocTypeL2OO) -} - func TestMixedWithdrawalValidity_Standard(t *testing.T) { testMixedWithdrawalValidity(t, config.AllocTypeStandard) } diff --git a/op-e2e/system/bridge/withdrawal_test.go b/op-e2e/system/bridge/withdrawal_test.go index 1f56fe4c4adec..12b5fe12e2249 100644 --- a/op-e2e/system/bridge/withdrawal_test.go +++ b/op-e2e/system/bridge/withdrawal_test.go @@ -9,10 +9,6 @@ import ( "github.com/stretchr/testify/require" ) -func TestWithdrawals_L2OO(t *testing.T) { - testWithdrawals(t, config.AllocTypeL2OO) -} - func TestWithdrawals_Standard(t *testing.T) { testWithdrawals(t, config.AllocTypeStandard) } diff --git a/op-e2e/system/helpers/tx_helper.go b/op-e2e/system/helpers/tx_helper.go index 10c16c0e74656..0c8d8177684eb 100644 --- a/op-e2e/system/helpers/tx_helper.go +++ b/op-e2e/system/helpers/tx_helper.go @@ -53,7 +53,8 @@ func SendDepositTx(t *testing.T, cfg e2esys.SystemConfig, l1Client *ethclient.Cl t.Logf("SendDepositTx: included on L1") // Wait for transaction to be included on L2 - reconstructedDep, err := derive.UnmarshalDepositLogEvent(l1Receipt.Logs[0]) + // Second log is the deposit log + reconstructedDep, err := derive.UnmarshalDepositLogEvent(l1Receipt.Logs[1]) require.NoError(t, err, "Could not reconstruct L2 Deposit") tx = types.NewTx(reconstructedDep) l2Receipt, err := wait.ForReceipt(ctx, l2Client, tx.Hash(), l2Opts.ExpectedStatus) From 8f150dff0ad7f36749740d013fdae7a8aa2d49a5 Mon Sep 17 00:00:00 2001 From: Kelvin Fichter Date: Fri, 7 Mar 2025 11:43:45 -0600 Subject: [PATCH 40/50] fix: undo change in user test --- op-e2e/actions/helpers/user.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/op-e2e/actions/helpers/user.go b/op-e2e/actions/helpers/user.go index 7f9c948e891b6..d7215b80650f7 100644 --- a/op-e2e/actions/helpers/user.go +++ b/op-e2e/actions/helpers/user.go @@ -382,7 +382,7 @@ func (s *CrossLayerUser) ActDeposit(t Testing) { func (s *CrossLayerUser) ActCheckDepositStatus(l1Success, l2Success bool) Action { return func(t Testing) { - s.CheckDepositTx(t, s.lastL1DepositTxHash, 1, l1Success, l2Success) + s.CheckDepositTx(t, s.lastL1DepositTxHash, 0, l1Success, l2Success) } } From 93c578da8b04443207d07a73769034a076447c16 Mon Sep 17 00:00:00 2001 From: Kelvin Fichter Date: Fri, 7 Mar 2025 11:57:16 -0600 Subject: [PATCH 41/50] fix: kill more L2OO tests --- op-e2e/actions/proposer/l2_proposer_test.go | 7 -- op-e2e/system/helpers/tx_helper.go | 5 +- op-e2e/system/proofs/proposer_l2oo_test.go | 83 --------------------- 3 files changed, 3 insertions(+), 92 deletions(-) delete mode 100644 op-e2e/system/proofs/proposer_l2oo_test.go diff --git a/op-e2e/actions/proposer/l2_proposer_test.go b/op-e2e/actions/proposer/l2_proposer_test.go index 917fff2bafd5c..da5000d855004 100644 --- a/op-e2e/actions/proposer/l2_proposer_test.go +++ b/op-e2e/actions/proposer/l2_proposer_test.go @@ -28,17 +28,10 @@ func TestProposerBatchType(t *testing.T) { t.Run("SingularBatch/Standard", func(t *testing.T) { runProposerTest(t, nil, config.AllocTypeStandard) }) - t.Run("SingularBatch/L2OO", func(t *testing.T) { - runProposerTest(t, nil, config.AllocTypeL2OO) - }) t.Run("SpanBatch/Standard", func(t *testing.T) { deltaTimeOffset := hexutil.Uint64(0) runProposerTest(t, &deltaTimeOffset, config.AllocTypeStandard) }) - t.Run("SpanBatch/L2OO", func(t *testing.T) { - deltaTimeOffset := hexutil.Uint64(0) - runProposerTest(t, &deltaTimeOffset, config.AllocTypeL2OO) - }) } func runProposerTest(gt *testing.T, deltaTimeOffset *hexutil.Uint64, allocType config.AllocType) { diff --git a/op-e2e/system/helpers/tx_helper.go b/op-e2e/system/helpers/tx_helper.go index 0c8d8177684eb..fefddf8f98b6d 100644 --- a/op-e2e/system/helpers/tx_helper.go +++ b/op-e2e/system/helpers/tx_helper.go @@ -53,8 +53,9 @@ func SendDepositTx(t *testing.T, cfg e2esys.SystemConfig, l1Client *ethclient.Cl t.Logf("SendDepositTx: included on L1") // Wait for transaction to be included on L2 - // Second log is the deposit log - reconstructedDep, err := derive.UnmarshalDepositLogEvent(l1Receipt.Logs[1]) + // The last log is the deposit log + idx := len(l1Receipt.Logs) - 1 + reconstructedDep, err := derive.UnmarshalDepositLogEvent(l1Receipt.Logs[idx]) require.NoError(t, err, "Could not reconstruct L2 Deposit") tx = types.NewTx(reconstructedDep) l2Receipt, err := wait.ForReceipt(ctx, l2Client, tx.Hash(), l2Opts.ExpectedStatus) diff --git a/op-e2e/system/proofs/proposer_l2oo_test.go b/op-e2e/system/proofs/proposer_l2oo_test.go deleted file mode 100644 index bf718c616538b..0000000000000 --- a/op-e2e/system/proofs/proposer_l2oo_test.go +++ /dev/null @@ -1,83 +0,0 @@ -package proofs - -import ( - "context" - "math/big" - "testing" - "time" - - op_e2e "github.com/ethereum-optimism/optimism/op-e2e" - "github.com/ethereum-optimism/optimism/op-e2e/config" - - "github.com/ethereum-optimism/optimism/op-e2e/bindings" - "github.com/ethereum-optimism/optimism/op-e2e/e2eutils/geth" - "github.com/ethereum-optimism/optimism/op-e2e/system/e2esys" - "github.com/ethereum/go-ethereum/accounts/abi/bind" - "github.com/stretchr/testify/require" -) - -func TestL2OutputSubmitter(t *testing.T) { - op_e2e.InitParallel(t) - - cfg := e2esys.DefaultSystemConfig(t, e2esys.WithAllocType(config.AllocTypeL2OO)) - cfg.NonFinalizedProposals = true // speed up the time till we see output proposals - - sys, err := cfg.Start(t) - require.Nil(t, err, "Error starting up system") - - l1Client := sys.NodeClient("l1") - - rollupClient := sys.RollupClient("sequencer") - - // OutputOracle is already deployed - l2OutputOracle, err := bindings.NewL2OutputOracleCaller(cfg.L1Deployments.L2OutputOracleProxy, l1Client) - require.Nil(t, err) - - initialOutputBlockNumber, err := l2OutputOracle.LatestBlockNumber(&bind.CallOpts{}) - require.Nil(t, err) - - // Wait until the second output submission from L2. The output submitter submits outputs from the - // unsafe portion of the chain which gets reorged on startup. The sequencer has an out of date view - // when it creates it's first block and uses and old L1 Origin. It then does not submit a batch - // for that block and subsequently reorgs to match what the verifier derives when running the - // reconcillation process. - l2Verif := sys.NodeClient("verifier") - _, err = geth.WaitForBlock(big.NewInt(6), l2Verif) - require.Nil(t, err) - - // Wait for batch submitter to update L2 output oracle. - timeoutCh := time.After(15 * time.Second) - ticker := time.NewTicker(1 * time.Second) - defer ticker.Stop() - for { - l2ooBlockNumber, err := l2OutputOracle.LatestBlockNumber(&bind.CallOpts{}) - require.Nil(t, err) - - // Wait for the L2 output oracle to have been changed from the initial - // timestamp set in the contract constructor. - if l2ooBlockNumber.Cmp(initialOutputBlockNumber) > 0 { - // Retrieve the l2 output committed at this updated timestamp. - committedL2Output, err := l2OutputOracle.GetL2OutputAfter(&bind.CallOpts{}, l2ooBlockNumber) - require.NotEqual(t, [32]byte{}, committedL2Output.OutputRoot, "Empty L2 Output") - require.Nil(t, err) - - // Fetch the corresponding L2 block and assert the committed L2 - // output matches the block's state root. - // - // NOTE: This assertion will change once the L2 output format is - // finalized. - ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) - defer cancel() - l2Output, err := rollupClient.OutputAtBlock(ctx, l2ooBlockNumber.Uint64()) - require.Nil(t, err) - require.Equal(t, l2Output.OutputRoot[:], committedL2Output.OutputRoot[:]) - break - } - - select { - case <-timeoutCh: - t.Fatalf("State root oracle not updated") - case <-ticker.C: - } - } -} From ae4dfadaa3e70fe4b528c8859b39779a7e87f616 Mon Sep 17 00:00:00 2001 From: Kelvin Fichter Date: Fri, 7 Mar 2025 13:19:48 -0600 Subject: [PATCH 42/50] fix: bug in withdrawal validity test --- op-e2e/system/bridge/validity_test.go | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/op-e2e/system/bridge/validity_test.go b/op-e2e/system/bridge/validity_test.go index ed8e3a1edcd06..efd5900889d18 100644 --- a/op-e2e/system/bridge/validity_test.go +++ b/op-e2e/system/bridge/validity_test.go @@ -577,7 +577,20 @@ func testMixedWithdrawalValidity(t *testing.T, allocType config.AllocType) { require.NoError(t, err) } + // Do a large deposit into the OptimismPortal so there's a balance to withdraw + depositAmount := big.NewInt(1_000_000_000_000) + transactor.Account.L1Opts.Value = depositAmount + tx, err := depositContract.DepositTransaction(transactor.Account.L1Opts, fromAddr, depositAmount, 120_000, false, nil) + require.NoError(t, err) + receipt, err := wait.ForReceiptOK(ctx, l1Client, tx.Hash()) + require.NoError(t, err) + require.Equal(t, receipt.Status, types.ReceiptStatusSuccessful) + + // Increase the expected nonce + transactor.ExpectedL1Nonce++ + // Finalize withdrawal + transactor.Account.L1Opts.Value = big.NewInt(0) _, err = depositContract.FinalizeWithdrawalTransaction( transactor.Account.L1Opts, withdrawalTransaction, From 3f59cf0be487f4f7e5a27ac0813ad40d06c86c50 Mon Sep 17 00:00:00 2001 From: Disco <131301107+0xDiscotech@users.noreply.github.com> Date: Fri, 7 Mar 2025 19:00:38 -0300 Subject: [PATCH 43/50] fix: pr review (#309) * chore: remove unnecessary cast * refactor: add lockbox on eth migrated event * chore: rename pa owner to pa owned base * refactor: add amount on liquidity received and migrated events * fix: format * refactor: add amount on liquidity migrated event * chore: pre-pr --------- Co-authored-by: agusduha --- .../interfaces/L1/IETHLockbox.sol | 8 ++--- .../interfaces/L1/IOptimismPortal2.sol | 6 ++-- .../interfaces/L1/IOptimismPortalInterop.sol | 6 ++-- ...OwnerBase.sol => IProxyAdminOwnedBase.sol} | 2 +- .../snapshots/abi/ETHLockbox.json | 12 +++++++ .../snapshots/abi/OptimismPortal2.json | 6 ++++ .../snapshots/abi/OptimismPortalInterop.json | 6 ++++ .../snapshots/semver-lock.json | 10 +++--- .../contracts-bedrock/src/L1/ETHLockbox.sol | 13 +++---- .../src/L1/OptimismPortal2.sol | 11 +++--- ...nOwnerBase.sol => ProxyAdminOwnedBase.sol} | 4 +-- .../test/L1/ETHLockbox.t.sol | 36 ++++++++++--------- .../test/L1/OptimismPortal2.t.sol | 2 +- .../contracts-bedrock/test/setup/Events.sol | 2 +- .../test/universal/Specs.t.sol | 4 +-- 15 files changed, 78 insertions(+), 50 deletions(-) rename packages/contracts-bedrock/interfaces/L1/{IProxyAdminOwnerBase.sol => IProxyAdminOwnedBase.sol} (78%) rename packages/contracts-bedrock/src/L1/{ProxyAdminOwnerBase.sol => ProxyAdminOwnedBase.sol} (92%) diff --git a/packages/contracts-bedrock/interfaces/L1/IETHLockbox.sol b/packages/contracts-bedrock/interfaces/L1/IETHLockbox.sol index 27df1600dd686..5d4abd72c79f5 100644 --- a/packages/contracts-bedrock/interfaces/L1/IETHLockbox.sol +++ b/packages/contracts-bedrock/interfaces/L1/IETHLockbox.sol @@ -3,10 +3,10 @@ pragma solidity ^0.8.0; import { ISemver } from "interfaces/universal/ISemver.sol"; import { ISuperchainConfig } from "interfaces/L1/ISuperchainConfig.sol"; -import { IProxyAdminOwnerBase } from "interfaces/L1/IProxyAdminOwnerBase.sol"; +import { IProxyAdminOwnedBase } from "interfaces/L1/IProxyAdminOwnedBase.sol"; import { IOptimismPortal2 } from "interfaces/L1/IOptimismPortal2.sol"; -interface IETHLockbox is IProxyAdminOwnerBase, ISemver { +interface IETHLockbox is IProxyAdminOwnedBase, ISemver { error ETHLockbox_Unauthorized(); error ETHLockbox_Paused(); error ETHLockbox_InsufficientBalance(); @@ -18,8 +18,8 @@ interface IETHLockbox is IProxyAdminOwnerBase, ISemver { event ETHUnlocked(address indexed portal, uint256 amount); event PortalAuthorized(address indexed portal); event LockboxAuthorized(address indexed lockbox); - event LiquidityMigrated(address indexed lockbox); - event LiquidityReceived(address indexed lockbox); + event LiquidityMigrated(address indexed lockbox, uint256 amount); + event LiquidityReceived(address indexed lockbox, uint256 amount); function initialize(ISuperchainConfig _superchainConfig, IOptimismPortal2[] calldata _portals) external; function superchainConfig() external view returns (ISuperchainConfig); diff --git a/packages/contracts-bedrock/interfaces/L1/IOptimismPortal2.sol b/packages/contracts-bedrock/interfaces/L1/IOptimismPortal2.sol index 0b49e2c79814e..f9cfc5325436c 100644 --- a/packages/contracts-bedrock/interfaces/L1/IOptimismPortal2.sol +++ b/packages/contracts-bedrock/interfaces/L1/IOptimismPortal2.sol @@ -8,10 +8,10 @@ import { IDisputeGameFactory } from "interfaces/dispute/IDisputeGameFactory.sol" import { ISystemConfig } from "interfaces/L1/ISystemConfig.sol"; import { ISuperchainConfig } from "interfaces/L1/ISuperchainConfig.sol"; import { IAnchorStateRegistry } from "interfaces/dispute/IAnchorStateRegistry.sol"; -import { IProxyAdminOwnerBase } from "interfaces/L1/IProxyAdminOwnerBase.sol"; +import { IProxyAdminOwnedBase } from "interfaces/L1/IProxyAdminOwnedBase.sol"; import { IETHLockbox } from "interfaces/L1/IETHLockbox.sol"; -interface IOptimismPortal2 is IProxyAdminOwnerBase { +interface IOptimismPortal2 is IProxyAdminOwnedBase { error OptimismPortal_Unauthorized(); error ContentLengthMismatch(); error EmptyItem(); @@ -48,7 +48,7 @@ interface IOptimismPortal2 is IProxyAdminOwnerBase { 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 ethBalance); + event ETHMigrated(address indexed lockbox, uint256 ethBalance); event LockboxUpdated(address oldLockbox, address newLockbox); receive() external payable; diff --git a/packages/contracts-bedrock/interfaces/L1/IOptimismPortalInterop.sol b/packages/contracts-bedrock/interfaces/L1/IOptimismPortalInterop.sol index f6de9ee89f59f..7cd674a0c1835 100644 --- a/packages/contracts-bedrock/interfaces/L1/IOptimismPortalInterop.sol +++ b/packages/contracts-bedrock/interfaces/L1/IOptimismPortalInterop.sol @@ -10,9 +10,9 @@ import { ISuperchainConfig } from "interfaces/L1/ISuperchainConfig.sol"; import { ConfigType } from "interfaces/L2/IL1BlockInterop.sol"; import { IAnchorStateRegistry } from "interfaces/dispute/IAnchorStateRegistry.sol"; import { IETHLockbox } from "interfaces/L1/IETHLockbox.sol"; -import { IProxyAdminOwnerBase } from "interfaces/L1/IProxyAdminOwnerBase.sol"; +import { IProxyAdminOwnedBase } from "interfaces/L1/IProxyAdminOwnedBase.sol"; -interface IOptimismPortalInterop is IProxyAdminOwnerBase { +interface IOptimismPortalInterop is IProxyAdminOwnedBase { error ContentLengthMismatch(); error EmptyItem(); error InvalidDataRemainder(); @@ -49,7 +49,7 @@ interface IOptimismPortalInterop is IProxyAdminOwnerBase { 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 ethBalance); + event ETHMigrated(address indexed lockbox, uint256 ethBalance); event LockboxUpdated(address oldLockbox, address newLockbox); receive() external payable; diff --git a/packages/contracts-bedrock/interfaces/L1/IProxyAdminOwnerBase.sol b/packages/contracts-bedrock/interfaces/L1/IProxyAdminOwnedBase.sol similarity index 78% rename from packages/contracts-bedrock/interfaces/L1/IProxyAdminOwnerBase.sol rename to packages/contracts-bedrock/interfaces/L1/IProxyAdminOwnedBase.sol index fa04187811c57..73346bff11294 100644 --- a/packages/contracts-bedrock/interfaces/L1/IProxyAdminOwnerBase.sol +++ b/packages/contracts-bedrock/interfaces/L1/IProxyAdminOwnedBase.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; -interface IProxyAdminOwnerBase { +interface IProxyAdminOwnedBase { function proxyAdminOwner() external view returns (address); } diff --git a/packages/contracts-bedrock/snapshots/abi/ETHLockbox.json b/packages/contracts-bedrock/snapshots/abi/ETHLockbox.json index 4b7bd30e568ef..d0e9c8fb9a626 100644 --- a/packages/contracts-bedrock/snapshots/abi/ETHLockbox.json +++ b/packages/contracts-bedrock/snapshots/abi/ETHLockbox.json @@ -237,6 +237,12 @@ "internalType": "address", "name": "lockbox", "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" } ], "name": "LiquidityMigrated", @@ -250,6 +256,12 @@ "internalType": "address", "name": "lockbox", "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" } ], "name": "LiquidityReceived", diff --git a/packages/contracts-bedrock/snapshots/abi/OptimismPortal2.json b/packages/contracts-bedrock/snapshots/abi/OptimismPortal2.json index ff90cc3ca3701..5a60bf69cd620 100644 --- a/packages/contracts-bedrock/snapshots/abi/OptimismPortal2.json +++ b/packages/contracts-bedrock/snapshots/abi/OptimismPortal2.json @@ -790,6 +790,12 @@ { "anonymous": false, "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "lockbox", + "type": "address" + }, { "indexed": false, "internalType": "uint256", diff --git a/packages/contracts-bedrock/snapshots/abi/OptimismPortalInterop.json b/packages/contracts-bedrock/snapshots/abi/OptimismPortalInterop.json index b94c51d314cd7..4835f00aa2ca9 100644 --- a/packages/contracts-bedrock/snapshots/abi/OptimismPortalInterop.json +++ b/packages/contracts-bedrock/snapshots/abi/OptimismPortalInterop.json @@ -808,6 +808,12 @@ { "anonymous": false, "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "lockbox", + "type": "address" + }, { "indexed": false, "internalType": "uint256", diff --git a/packages/contracts-bedrock/snapshots/semver-lock.json b/packages/contracts-bedrock/snapshots/semver-lock.json index 152b1dee29c76..ea58a3a2475b1 100644 --- a/packages/contracts-bedrock/snapshots/semver-lock.json +++ b/packages/contracts-bedrock/snapshots/semver-lock.json @@ -4,8 +4,8 @@ "sourceCodeHash": "0xe772f7db8033e4a738850cb28ac4849d3a454c93732135a8a10d4f7cb498088e" }, "src/L1/ETHLockbox.sol": { - "initCodeHash": "0x4ad4f1719b2092cf224d7083c4816c2ab9fa39331eb016557d17aa2ff43ef909", - "sourceCodeHash": "0x45c7ee45398ab4be3a59d93d36b897bbcc1c12adb71c002e17fd6142ce95ef52" + "initCodeHash": "0x599593bf96170081d2fd191bb2f61eaa2d094faafb6a583a34b3eea2dc49e5c6", + "sourceCodeHash": "0x34da4a8a0e32cabce97056f10cc1c8231f14a1cd706ae63ee0bef3c1f65f3e13" }, "src/L1/L1CrossDomainMessenger.sol": { "initCodeHash": "0x5a6272f6bd4346da460b7ff2ebc426d158d7ffc65dc0f2c0651a9e6d545bfd03", @@ -24,11 +24,11 @@ "sourceCodeHash": "0x8302de0ccd551512f2430d545ea55c938b002701d2292fd7aad2b4d2b975727b" }, "src/L1/OptimismPortal2.sol": { - "initCodeHash": "0xbfb7f71d76023a02e04e758399bf7db73f83063e989ade28b0432e47a2b86f1a", - "sourceCodeHash": "0xddc6fb2d5e6a6864f9ea29e89ea57d03ca7e527477c4862d4b262164003dbc58" + "initCodeHash": "0x774dd0c79fb11ea7ecbbffb28d1c005b72790577adbeeeb881c2a180e2feaadf", + "sourceCodeHash": "0x2d36fbd2326ceafe89ca1a3b80c223ad7f934e89e996d20b847498f015316bdf" }, "src/L1/OptimismPortalInterop.sol": { - "initCodeHash": "0x333b0db068574b43e3efb7a5d2e756a2ec01291407d07c6b216e42bcdbdfd4a5", + "initCodeHash": "0x9c35223461ee313b77eafb727c51300596975b5134ff51847d8fab424b4dcd0b", "sourceCodeHash": "0xa1ecf8a19638dd24d80f62e2faef132afa15c6945c14133fe58709015fcb14a6" }, "src/L1/ProtocolVersions.sol": { diff --git a/packages/contracts-bedrock/src/L1/ETHLockbox.sol b/packages/contracts-bedrock/src/L1/ETHLockbox.sol index e08ac219c8cf6..3931c90fb3e0e 100644 --- a/packages/contracts-bedrock/src/L1/ETHLockbox.sol +++ b/packages/contracts-bedrock/src/L1/ETHLockbox.sol @@ -2,7 +2,7 @@ pragma solidity 0.8.25; // Contracts -import { ProxyAdminOwnerBase } from "src/L1/ProxyAdminOwnerBase.sol"; +import { ProxyAdminOwnedBase } from "src/L1/ProxyAdminOwnedBase.sol"; import { Initializable } from "@openzeppelin/contracts/proxy/utils/Initializable.sol"; // Libraries @@ -18,7 +18,7 @@ import { IETHLockbox } from "interfaces/L1/IETHLockbox.sol"; /// @title ETHLockbox /// @notice Manages ETH liquidity locking and unlocking for authorized OptimismPortals, enabling unified ETH liquidity /// management across chains in the superchain cluster. -contract ETHLockbox is ProxyAdminOwnerBase, Initializable, ISemver { +contract ETHLockbox is ProxyAdminOwnedBase, Initializable, ISemver { /// @notice Thrown when the lockbox is paused. error ETHLockbox_Paused(); @@ -54,11 +54,12 @@ contract ETHLockbox is ProxyAdminOwnerBase, Initializable, ISemver { /// @notice Emitted when ETH liquidity is migrated from the current ETH lockbox to another. /// @param lockbox The address of the ETH lockbox that was migrated. - event LiquidityMigrated(address indexed lockbox); + event LiquidityMigrated(address indexed lockbox, uint256 amount); /// @notice Emitted when ETH liquidity is received during an authorized lockbox migration. /// @param lockbox The address of the ETH lockbox that received the liquidity. - event LiquidityReceived(address indexed lockbox); + /// @param amount The amount of ETH received. + event LiquidityReceived(address indexed lockbox, uint256 amount); /// @notice The address of the SuperchainConfig contract. ISuperchainConfig public superchainConfig; @@ -111,7 +112,7 @@ contract ETHLockbox is ProxyAdminOwnerBase, Initializable, ISemver { /// @notice Receives the ETH liquidity migrated from an authorized lockbox. function receiveLiquidity() external payable { if (!authorizedLockboxes[msg.sender]) revert ETHLockbox_Unauthorized(); - emit LiquidityReceived(msg.sender); + emit LiquidityReceived(msg.sender, msg.value); } /// @notice Locks ETH in the lockbox. @@ -159,7 +160,7 @@ contract ETHLockbox is ProxyAdminOwnerBase, Initializable, ISemver { if (!_sameProxyAdminOwner(address(_lockbox))) revert ETHLockbox_DifferentProxyAdminOwner(); IETHLockbox(_lockbox).receiveLiquidity{ value: address(this).balance }(); - emit LiquidityMigrated(address(_lockbox)); + emit LiquidityMigrated(address(_lockbox), address(this).balance); } /// @notice Authorizes a portal to lock and unlock ETH. diff --git a/packages/contracts-bedrock/src/L1/OptimismPortal2.sol b/packages/contracts-bedrock/src/L1/OptimismPortal2.sol index a72f0e117b7b0..5b8cef6cc83dd 100644 --- a/packages/contracts-bedrock/src/L1/OptimismPortal2.sol +++ b/packages/contracts-bedrock/src/L1/OptimismPortal2.sol @@ -2,7 +2,7 @@ pragma solidity 0.8.15; // Contracts -import { ProxyAdminOwnerBase } from "src/L1/ProxyAdminOwnerBase.sol"; +import { ProxyAdminOwnedBase } from "src/L1/ProxyAdminOwnedBase.sol"; import { Initializable } from "@openzeppelin/contracts/proxy/utils/Initializable.sol"; import { ResourceMetering } from "src/L1/ResourceMetering.sol"; import { ReinitializableBase } from "src/universal/ReinitializableBase.sol"; @@ -32,7 +32,7 @@ import { IETHLockbox } from "interfaces/L1/IETHLockbox.sol"; /// @notice The OptimismPortal is a low-level contract responsible for passing messages between L1 /// 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 OptimismPortal2 is Initializable, ResourceMetering, ReinitializableBase, ProxyAdminOwnerBase, ISemver { +contract OptimismPortal2 is Initializable, ResourceMetering, ReinitializableBase, ProxyAdminOwnedBase, ISemver { /// @notice Represents a proven withdrawal. /// @custom:field disputeGameProxy Game that the withdrawal was proven against. /// @custom:field timestamp Timestamp at which the withdrawal was proven. @@ -153,8 +153,9 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ReinitializableBase event WithdrawalFinalized(bytes32 indexed withdrawalHash, bool success); /// @notice Emitted when the total ETH balance is migrated to the ETHLockbox. + /// @param lockbox The address of the ETHLockbox contract. /// @param ethBalance Amount of ETH migrated. - event ETHMigrated(uint256 ethBalance); + event ETHMigrated(address indexed lockbox, uint256 ethBalance); /// @notice Emitted when the ETHLockbox contract is updated. /// @param oldLockbox The address of the old ETHLockbox contract. @@ -362,7 +363,7 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ReinitializableBase if (msg.sender != proxyAdminOwner()) revert OptimismPortal_Unauthorized(); address oldLockbox = address(ethLockbox); - ethLockbox = IETHLockbox(_newLockbox); + ethLockbox = _newLockbox; emit LockboxUpdated(oldLockbox, address(_newLockbox)); } @@ -738,7 +739,7 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ReinitializableBase uint256 ethBalance = address(this).balance; ethLockbox.lockETH{ value: ethBalance }(); - emit ETHMigrated(ethBalance); + emit ETHMigrated(address(ethLockbox), ethBalance); } /// @notice Getter for the resource config. Used internally by the ResourceMetering contract. diff --git a/packages/contracts-bedrock/src/L1/ProxyAdminOwnerBase.sol b/packages/contracts-bedrock/src/L1/ProxyAdminOwnedBase.sol similarity index 92% rename from packages/contracts-bedrock/src/L1/ProxyAdminOwnerBase.sol rename to packages/contracts-bedrock/src/L1/ProxyAdminOwnedBase.sol index 9f5861034e566..350b91cf10c87 100644 --- a/packages/contracts-bedrock/src/L1/ProxyAdminOwnerBase.sol +++ b/packages/contracts-bedrock/src/L1/ProxyAdminOwnedBase.sol @@ -7,7 +7,7 @@ import { Constants } from "src/libraries/Constants.sol"; /// @notice Base contract for ProxyAdmin-owned contracts. It's main goal is to expose the ProxyAdmin owner address on /// a function and also to check if the current contract and a given proxy have the same ProxyAdmin owner. -abstract contract ProxyAdminOwnerBase { +abstract contract ProxyAdminOwnedBase { /// @notice Getter for the owner of the ProxyAdmin. /// The ProxyAdmin is the owner of the current proxy contract. function proxyAdminOwner() public view returns (address) { @@ -21,6 +21,6 @@ abstract contract ProxyAdminOwnerBase { /// proxy. /// @param _proxy The address of the proxy to check. function _sameProxyAdminOwner(address _proxy) internal view returns (bool) { - return proxyAdminOwner() == ProxyAdminOwnerBase(_proxy).proxyAdminOwner(); + return proxyAdminOwner() == ProxyAdminOwnedBase(_proxy).proxyAdminOwner(); } } diff --git a/packages/contracts-bedrock/test/L1/ETHLockbox.t.sol b/packages/contracts-bedrock/test/L1/ETHLockbox.t.sol index 3cf10cf7fc766..17ade24455429 100644 --- a/packages/contracts-bedrock/test/L1/ETHLockbox.t.sol +++ b/packages/contracts-bedrock/test/L1/ETHLockbox.t.sol @@ -10,7 +10,7 @@ import { IOptimismPortal2 as IOptimismPortal } from "interfaces/L1/IOptimismPort import { IETHLockbox } from "interfaces/L1/IETHLockbox.sol"; import { ISuperchainConfig } from "interfaces/L1/ISuperchainConfig.sol"; -import { IProxyAdminOwnerBase } from "interfaces/L1/IProxyAdminOwnerBase.sol"; +import { IProxyAdminOwnedBase } from "interfaces/L1/IProxyAdminOwnedBase.sol"; import { IOptimismPortal2 } from "interfaces/L1/IOptimismPortal2.sol"; // Test @@ -25,8 +25,8 @@ contract ETHLockboxTest is CommonTest { event ETHUnlocked(address indexed portal, uint256 amount); event PortalAuthorized(address indexed portal); event LockboxAuthorized(address indexed lockbox); - event LiquidityMigrated(address indexed lockbox); - event LiquidityReceived(address indexed lockbox); + event LiquidityMigrated(address indexed lockbox, uint256 amount); + event LiquidityReceived(address indexed lockbox, uint256 amount); ProxyAdmin public proxyAdmin; address public proxyAdminOwner; @@ -84,7 +84,7 @@ contract ETHLockboxTest is CommonTest { // Mock the admin owner of the lockbox to be the same as the current lockbox proxy admin owner vm.mockCall( - address(_lockbox), abi.encodeCall(IProxyAdminOwnerBase.proxyAdminOwner, ()), abi.encode(proxyAdminOwner) + address(_lockbox), abi.encodeCall(IProxyAdminOwnedBase.proxyAdminOwner, ()), abi.encode(proxyAdminOwner) ); // Authorize the lockbox if needed @@ -98,7 +98,7 @@ contract ETHLockboxTest is CommonTest { // Expect the `LiquidityReceived` event to be emitted vm.expectEmit(address(ethLockbox)); - emit LiquidityReceived(_lockbox); + emit LiquidityReceived(_lockbox, _value); // Call the `receiveLiquidity` function vm.prank(address(_lockbox)); @@ -154,7 +154,7 @@ contract ETHLockboxTest is CommonTest { // Mock the admin owner of the portal to be the same as the current lockbox proxy admin owner vm.mockCall( - address(_portal), abi.encodeCall(IProxyAdminOwnerBase.proxyAdminOwner, ()), abi.encode(proxyAdminOwner) + address(_portal), abi.encodeCall(IProxyAdminOwnedBase.proxyAdminOwner, ()), abi.encode(proxyAdminOwner) ); // Set the portal as an authorized portal if needed @@ -266,7 +266,7 @@ contract ETHLockboxTest is CommonTest { // Mock the admin owner of the portal to be the same as the current lockbox proxy admin owner vm.mockCall( - address(_portal), abi.encodeCall(IProxyAdminOwnerBase.proxyAdminOwner, ()), abi.encode(proxyAdminOwner) + address(_portal), abi.encodeCall(IProxyAdminOwnedBase.proxyAdminOwner, ()), abi.encode(proxyAdminOwner) ); // Set the portal as an authorized portal if needed @@ -314,7 +314,7 @@ contract ETHLockboxTest is CommonTest { /// the one of the lockbox. function testFuzz_authorizePortal_differentProxyAdminOwner_reverts(IOptimismPortal2 _portal) public { assumeNotForgeAddress(address(_portal)); - vm.mockCall(address(_portal), abi.encodeCall(IProxyAdminOwnerBase.proxyAdminOwner, ()), abi.encode(address(0))); + vm.mockCall(address(_portal), abi.encodeCall(IProxyAdminOwnedBase.proxyAdminOwner, ()), abi.encode(address(0))); // Expect the revert with `DifferentOwner` selector vm.expectRevert(IETHLockbox.ETHLockbox_DifferentProxyAdminOwner.selector); @@ -352,7 +352,7 @@ contract ETHLockboxTest is CommonTest { // Mock the admin owner of the portal to be the same as the current lockbox proxy admin owner vm.mockCall( - address(_portal), abi.encodeCall(IProxyAdminOwnerBase.proxyAdminOwner, ()), abi.encode(proxyAdminOwner) + address(_portal), abi.encodeCall(IProxyAdminOwnedBase.proxyAdminOwner, ()), abi.encode(proxyAdminOwner) ); // Expect the `PortalAuthorized` event to be emitted @@ -385,7 +385,7 @@ contract ETHLockboxTest is CommonTest { function testFuzz_authorizeLockbox_differentProxyAdminOwner_reverts(address _lockbox) public { assumeNotForgeAddress(_lockbox); - vm.mockCall(address(_lockbox), abi.encodeCall(IProxyAdminOwnerBase.proxyAdminOwner, ()), abi.encode(address(0))); + vm.mockCall(address(_lockbox), abi.encodeCall(IProxyAdminOwnedBase.proxyAdminOwner, ()), abi.encode(address(0))); // Expect the revert with `ETHLockbox_DifferentProxyAdminOwner` selector vm.expectRevert(IETHLockbox.ETHLockbox_DifferentProxyAdminOwner.selector); @@ -401,7 +401,7 @@ contract ETHLockboxTest is CommonTest { // Mock the admin owner of the lockbox to be the same as the current lockbox proxy admin owner vm.mockCall( - address(_lockbox), abi.encodeCall(IProxyAdminOwnerBase.proxyAdminOwner, ()), abi.encode(proxyAdminOwner) + address(_lockbox), abi.encodeCall(IProxyAdminOwnedBase.proxyAdminOwner, ()), abi.encode(proxyAdminOwner) ); // Expect the `LockboxAuthorized` event to be emitted @@ -434,7 +434,7 @@ contract ETHLockboxTest is CommonTest { function testFuzz_migrateLiquidity_differentProxyAdminOwner_reverts(address _lockbox) public { assumeNotForgeAddress(_lockbox); - vm.mockCall(address(_lockbox), abi.encodeCall(IProxyAdminOwnerBase.proxyAdminOwner, ()), abi.encode(address(0))); + vm.mockCall(address(_lockbox), abi.encodeCall(IProxyAdminOwnedBase.proxyAdminOwner, ()), abi.encode(address(0))); // Expect the revert with `ETHLockbox_DifferentProxyAdminOwner` selector vm.expectRevert(IETHLockbox.ETHLockbox_DifferentProxyAdminOwner.selector); @@ -446,6 +446,8 @@ contract ETHLockboxTest is CommonTest { /// @notice Tests the `migrateLiquidity` function succeeds function testFuzz_migrateLiquidity_succeeds(uint256 _balance, address _lockbox) public { + _balance = bound(_balance, 0, type(uint256).max - address(ethLockbox).balance); + // Since on the fork the `_lockbox` fuzzed address doesn't exist, we skip the test if (isForkTest()) vm.skip(true); assumeNotForgeAddress(_lockbox); @@ -453,7 +455,7 @@ contract ETHLockboxTest is CommonTest { // Mock on the lockbox that will receive the migration for it to succeed vm.mockCall( - address(_lockbox), abi.encodeCall(IProxyAdminOwnerBase.proxyAdminOwner, ()), abi.encode(proxyAdminOwner) + address(_lockbox), abi.encodeCall(IProxyAdminOwnedBase.proxyAdminOwner, ()), abi.encode(proxyAdminOwner) ); vm.mockCall( address(_lockbox), abi.encodeCall(IETHLockbox.authorizedLockboxes, (address(ethLockbox))), abi.encode(true) @@ -463,14 +465,14 @@ contract ETHLockboxTest is CommonTest { // Deal the balance to the lockbox deal(address(_lockbox), _balance); - // Expect the `LiquidityMigrated` event to be emitted - vm.expectEmit(address(ethLockbox)); - emit LiquidityMigrated(_lockbox); - // Get balances before the migration uint256 ethLockboxBalanceBefore = address(ethLockbox).balance; uint256 newLockboxBalanceBefore = address(_lockbox).balance; + // Expect the `LiquidityMigrated` event to be emitted + vm.expectEmit(address(ethLockbox)); + emit LiquidityMigrated(_lockbox, ethLockboxBalanceBefore); + // Call the `migrateLiquidity` function with the lockbox vm.prank(proxyAdminOwner); ethLockbox.migrateLiquidity(IETHLockbox(_lockbox)); diff --git a/packages/contracts-bedrock/test/L1/OptimismPortal2.t.sol b/packages/contracts-bedrock/test/L1/OptimismPortal2.t.sol index 9c15c26a6a604..039093b81ecf2 100644 --- a/packages/contracts-bedrock/test/L1/OptimismPortal2.t.sol +++ b/packages/contracts-bedrock/test/L1/OptimismPortal2.t.sol @@ -2123,7 +2123,7 @@ contract OptimismPortal2_LiquidityMigration_Test is CommonTest { vm.expectCall(address(ethLockbox), _portalBalance, abi.encodeCall(ethLockbox.lockETH, ())); vm.expectEmit(address(optimismPortal2)); - emit ETHMigrated(_portalBalance); + emit ETHMigrated(address(ethLockbox), _portalBalance); vm.prank(proxyAdminOwner); optimismPortal2.migrateLiquidity(); diff --git a/packages/contracts-bedrock/test/setup/Events.sol b/packages/contracts-bedrock/test/setup/Events.sol index 59a348fb27d19..022258e02baf9 100644 --- a/packages/contracts-bedrock/test/setup/Events.sol +++ b/packages/contracts-bedrock/test/setup/Events.sol @@ -107,7 +107,7 @@ contract Events { event BalanceChanged(address account, uint256 balance); - event ETHMigrated(uint256 ethBalance); + event ETHMigrated(address indexed lockbox, uint256 ethBalance); event LockboxUpdated(address oldLockbox, address newLockbox); } diff --git a/packages/contracts-bedrock/test/universal/Specs.t.sol b/packages/contracts-bedrock/test/universal/Specs.t.sol index 9f820c1599688..236294d0b0fc9 100644 --- a/packages/contracts-bedrock/test/universal/Specs.t.sol +++ b/packages/contracts-bedrock/test/universal/Specs.t.sol @@ -326,8 +326,8 @@ contract Specification_Test is CommonTest { _addSpec({ _name: "OptimismPortal2", _sel: _getSel("migrateLiquidity()") }); _addSpec({ _name: "OptimismPortal2", _sel: _getSel("proxyAdminOwner()") }); - // ProxyAdminOwnerBase - _addSpec({ _name: "ProxyAdminOwnerBase", _sel: _getSel("proxyAdminOwner()") }); + // ProxyAdminOwnedBase + _addSpec({ _name: "ProxyAdminOwnedBase", _sel: _getSel("proxyAdminOwner()") }); // ProtocolVersions _addSpec({ _name: "ProtocolVersions", _sel: _getSel("RECOMMENDED_SLOT()") }); From 94ee1358d40641097102aad751fdf52f626891ca Mon Sep 17 00:00:00 2001 From: Kelvin Fichter Date: Sun, 9 Mar 2025 10:09:15 -0600 Subject: [PATCH 44/50] fix: some remaining merge issues --- .../scripts/checks/check-frozen-files.sh | 1 + packages/contracts-bedrock/test/L1/OptimismPortal2.t.sol | 8 -------- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/packages/contracts-bedrock/scripts/checks/check-frozen-files.sh b/packages/contracts-bedrock/scripts/checks/check-frozen-files.sh index c2a9e6688afbd..435cd301a35ce 100755 --- a/packages/contracts-bedrock/scripts/checks/check-frozen-files.sh +++ b/packages/contracts-bedrock/scripts/checks/check-frozen-files.sh @@ -49,6 +49,7 @@ changed_contracts=$(jq -r ' # All files in semver-lock.json should be in this list. ALLOWED_FILES=( "src/L1/DataAvailabilityChallenge.sol:DataAvailabilityChallenge" + "src/L1/ETHLockbox.sol:ETHLockbox" "src/L1/L1CrossDomainMessenger.sol:L1CrossDomainMessenger" "src/L1/L1ERC721Bridge.sol:L1ERC721Bridge" "src/L1/L1StandardBridge.sol:L1StandardBridge" diff --git a/packages/contracts-bedrock/test/L1/OptimismPortal2.t.sol b/packages/contracts-bedrock/test/L1/OptimismPortal2.t.sol index 039093b81ecf2..12767741da676 100644 --- a/packages/contracts-bedrock/test/L1/OptimismPortal2.t.sol +++ b/packages/contracts-bedrock/test/L1/OptimismPortal2.t.sol @@ -56,16 +56,8 @@ contract OptimismPortal2_Test is CommonTest { assertEq(address(opImpl.anchorStateRegistry()), address(0)); assertEq(address(opImpl.systemConfig()), address(0)); assertEq(address(opImpl.superchainConfig()), address(0)); - - // TODO(opcm upgrades): remove skip once upgrade path is implemented - returnIfForkTest("OptimismPortal2_Test: l2Sender is nonzero on OP mainnet"); assertEq(opImpl.l2Sender(), address(0)); - - // TODO(opcm upgrades): remove skip once upgrade path is implemented - returnIfForkTest("OptimismPortal2_Test: anchorStateRegistry getter doesn't yet exist on OP mainnet"); assertEq(address(opImpl.anchorStateRegistry()), address(0)); - - returnIfForkTest("OptimismPortal2_Test: ethLockbox getter doesn't yet exist on OP mainnet"); assertEq(address(opImpl.ethLockbox()), address(0)); } From f7dc5ab3adf4fc0f49d8f756f7ad27e076584b65 Mon Sep 17 00:00:00 2001 From: Kelvin Fichter Date: Sun, 9 Mar 2025 10:17:47 -0600 Subject: [PATCH 45/50] fix: go linting error --- op-deployer/pkg/deployer/opcm/implementations.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/op-deployer/pkg/deployer/opcm/implementations.go b/op-deployer/pkg/deployer/opcm/implementations.go index 5df6c3299a93e..917754cbd5620 100644 --- a/op-deployer/pkg/deployer/opcm/implementations.go +++ b/op-deployer/pkg/deployer/opcm/implementations.go @@ -37,7 +37,7 @@ type DeployImplementationsOutput struct { OpcmUpgrader common.Address `json:"opcmUpgraderAddress"` DelayedWETHImpl common.Address `json:"delayedWETHImplAddress"` OptimismPortalImpl common.Address `json:"optimismPortalImplAddress"` - ETHLockboxImpl common.Address `json:"ethLockboxImplAddress" evm:"ethLockboxImpl"` + ETHLockboxImpl common.Address `json:"ethLockboxImplAddress" evm:"ethLockboxImpl"` PreimageOracleSingleton common.Address `json:"preimageOracleSingletonAddress"` MipsSingleton common.Address `json:"mipsSingletonAddress"` SystemConfigImpl common.Address `json:"systemConfigImplAddress"` From fa40bbc31faba694ecfedb18562c93f76b3b290d Mon Sep 17 00:00:00 2001 From: Kelvin Fichter Date: Mon, 10 Mar 2025 09:01:36 -0600 Subject: [PATCH 46/50] fix: remove old test skips --- packages/contracts-bedrock/test/L1/OptimismPortal2.t.sol | 8 -------- 1 file changed, 8 deletions(-) diff --git a/packages/contracts-bedrock/test/L1/OptimismPortal2.t.sol b/packages/contracts-bedrock/test/L1/OptimismPortal2.t.sol index 12767741da676..c0c05e6f327e4 100644 --- a/packages/contracts-bedrock/test/L1/OptimismPortal2.t.sol +++ b/packages/contracts-bedrock/test/L1/OptimismPortal2.t.sol @@ -206,8 +206,6 @@ contract OptimismPortal2_Test is CommonTest { /// @dev Tests that `depositTransaction` reverts when the destination address is non-zero /// for a contract creation deposit. function test_depositTransaction_contractCreation_reverts() external { - // TODO(opcm upgrades): remove skip once upgrade path is implemented - skipIfForkTest("OptimismPortal2_Test: error is different on OP Mainnet"); // contract creation must have a target of address(0) vm.expectRevert(IOptimismPortal.OptimismPortal_BadTarget.selector); optimismPortal2.depositTransaction(address(1), 1, 0, true, hex""); @@ -216,8 +214,6 @@ contract OptimismPortal2_Test is CommonTest { /// @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 { - // TODO(opcm upgrades): remove skip once upgrade path is implemented - skipIfForkTest("OptimismPortal2_Test: error is different on OP Mainnet"); uint256 size = 120_001; uint64 gasLimit = optimismPortal2.minimumGasLimit(uint64(size)); vm.expectRevert(IOptimismPortal.OptimismPortal_CalldataTooLarge.selector); @@ -232,8 +228,6 @@ contract OptimismPortal2_Test is CommonTest { /// @dev Tests that `depositTransaction` reverts when the gas limit is too small. function test_depositTransaction_smallGasLimit_reverts() external { - // TODO(opcm upgrades): remove skip once upgrade path is implemented - skipIfForkTest("OptimismPortal2_Test: error is different on OP Mainnet"); vm.expectRevert(IOptimismPortal.OptimismPortal_GasLimitTooLow.selector); optimismPortal2.depositTransaction({ _to: address(1), _value: 0, _gasLimit: 0, _isCreation: false, _data: hex"" }); } @@ -244,8 +238,6 @@ contract OptimismPortal2_Test is CommonTest { uint64 gasLimit = optimismPortal2.minimumGasLimit(uint64(_data.length)); if (_shouldFail) { gasLimit = uint64(bound(gasLimit, 0, gasLimit - 1)); - // TODO(opcm upgrades): remove skip once upgrade path is implemented - skipIfForkTest("OptimismPortal2_Test: error is different on OP Mainnet"); vm.expectRevert(IOptimismPortal.OptimismPortal_GasLimitTooLow.selector); } From 45899f1574c4140427a15d42c2d1f64a23a81c8a Mon Sep 17 00:00:00 2001 From: Kelvin Fichter Date: Mon, 10 Mar 2025 11:03:18 -0600 Subject: [PATCH 47/50] fix: unskip v2 upgrade tests --- op-deployer/pkg/deployer/upgrade/v2_0_0/upgrade_test.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/op-deployer/pkg/deployer/upgrade/v2_0_0/upgrade_test.go b/op-deployer/pkg/deployer/upgrade/v2_0_0/upgrade_test.go index b025855751b11..95585401b1182 100644 --- a/op-deployer/pkg/deployer/upgrade/v2_0_0/upgrade_test.go +++ b/op-deployer/pkg/deployer/upgrade/v2_0_0/upgrade_test.go @@ -21,7 +21,6 @@ import ( ) func TestUpgradeOPChainInput_OpChainConfigs(t *testing.T) { - t.Skip("skipping for upgrade 15") input := &UpgradeOPChainInput{ Prank: common.Address{0xaa}, Opcm: common.Address{0xbb}, @@ -55,7 +54,6 @@ func TestUpgradeOPChainInput_OpChainConfigs(t *testing.T) { } func TestUpgrader_Upgrade(t *testing.T) { - t.Skip("skipping for upgrade 15") lgr := testlog.Logger(t, slog.LevelDebug) forkedL1, stopL1, err := devnet.NewForkedSepolia(lgr) From ca2275c5b68ead2a36ec7b32e255b98d8fff3b9b Mon Sep 17 00:00:00 2001 From: Kelvin Fichter Date: Mon, 10 Mar 2025 11:11:22 -0600 Subject: [PATCH 48/50] fix: skip v2 upgrade tests again --- op-deployer/pkg/deployer/upgrade/v2_0_0/upgrade_test.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/op-deployer/pkg/deployer/upgrade/v2_0_0/upgrade_test.go b/op-deployer/pkg/deployer/upgrade/v2_0_0/upgrade_test.go index 95585401b1182..b025855751b11 100644 --- a/op-deployer/pkg/deployer/upgrade/v2_0_0/upgrade_test.go +++ b/op-deployer/pkg/deployer/upgrade/v2_0_0/upgrade_test.go @@ -21,6 +21,7 @@ import ( ) func TestUpgradeOPChainInput_OpChainConfigs(t *testing.T) { + t.Skip("skipping for upgrade 15") input := &UpgradeOPChainInput{ Prank: common.Address{0xaa}, Opcm: common.Address{0xbb}, @@ -54,6 +55,7 @@ func TestUpgradeOPChainInput_OpChainConfigs(t *testing.T) { } func TestUpgrader_Upgrade(t *testing.T) { + t.Skip("skipping for upgrade 15") lgr := testlog.Logger(t, slog.LevelDebug) forkedL1, stopL1, err := devnet.NewForkedSepolia(lgr) From 99271b16794efe2095dfa52e8263c08967323ab2 Mon Sep 17 00:00:00 2001 From: Maurelian Date: Mon, 10 Mar 2025 15:25:33 -0400 Subject: [PATCH 49/50] Keep prestate as before if input is zero --- .../interfaces/L1/IOPContractsManager.sol | 2 -- packages/contracts-bedrock/lib/automate | 1 + .../src/L1/OPContractsManager.sol | 8 ++---- .../test/L1/OPContractsManager.t.sol | 28 +++++++++++++++---- 4 files changed, 25 insertions(+), 14 deletions(-) create mode 160000 packages/contracts-bedrock/lib/automate diff --git a/packages/contracts-bedrock/interfaces/L1/IOPContractsManager.sol b/packages/contracts-bedrock/interfaces/L1/IOPContractsManager.sol index d2f83d685d99e..aecb6d4604642 100644 --- a/packages/contracts-bedrock/interfaces/L1/IOPContractsManager.sol +++ b/packages/contracts-bedrock/interfaces/L1/IOPContractsManager.sol @@ -253,8 +253,6 @@ interface IOPContractsManager { error SuperchainProxyAdminMismatch(); - error PrestateNotSet(); - error PrestateRequired(); // -------- Methods -------- diff --git a/packages/contracts-bedrock/lib/automate b/packages/contracts-bedrock/lib/automate new file mode 160000 index 0000000000000..0117585fea20f --- /dev/null +++ b/packages/contracts-bedrock/lib/automate @@ -0,0 +1 @@ +Subproject commit 0117585fea20ff0cd24fd17bf74a6debaa4d57d2 diff --git a/packages/contracts-bedrock/src/L1/OPContractsManager.sol b/packages/contracts-bedrock/src/L1/OPContractsManager.sol index 096b46c5bd477..50f80f588c727 100644 --- a/packages/contracts-bedrock/src/L1/OPContractsManager.sol +++ b/packages/contracts-bedrock/src/L1/OPContractsManager.sol @@ -724,10 +724,9 @@ contract OPContractsManagerUpgrader is OPContractsManagerBase { // Modify the params with the new vm values. params.anchorStateRegistry = IAnchorStateRegistry(address(_newAnchorStateRegistryProxy)); params.vm = IBigStepper(impls.mipsImpl); - if (Claim.unwrap(_opChainConfig.absolutePrestate) == bytes32(0)) { - revert OPContractsManager.PrestateNotSet(); + if (Claim.unwrap(_opChainConfig.absolutePrestate) != bytes32(0)) { + params.absolutePrestate = _opChainConfig.absolutePrestate; } - params.absolutePrestate = _opChainConfig.absolutePrestate; IDisputeGame newGame; if (GameType.unwrap(_gameType) == GameType.unwrap(GameTypes.PERMISSIONED_CANNON)) { @@ -1422,9 +1421,6 @@ contract OPContractsManager is ISemver { /// @notice Thrown when the SuperchainProxyAdmin does not match the SuperchainConfig's admin. error SuperchainProxyAdminMismatch(); - /// @notice Thrown when a prestate is not set for a game. - error PrestateNotSet(); - /// @notice Thrown when the prestate of a permissioned disputed game is 0. error PrestateRequired(); diff --git a/packages/contracts-bedrock/test/L1/OPContractsManager.t.sol b/packages/contracts-bedrock/test/L1/OPContractsManager.t.sol index 80880840d8bef..6ea41d3fd9c41 100644 --- a/packages/contracts-bedrock/test/L1/OPContractsManager.t.sol +++ b/packages/contracts-bedrock/test/L1/OPContractsManager.t.sol @@ -677,6 +677,28 @@ contract OPContractsManager_Upgrade_Test is OPContractsManager_Upgrade_Harness { // Try to upgrade the current OPChain runUpgradeTestAndChecks(upgrader); } + + function test_upgrade_absolutePrestateNotSet_succeeds() public { + // Get the pdg and fdg before the upgrade + Claim pdgPrestateBefore = IPermissionedDisputeGame(address(disputeGameFactory.gameImpls(GameTypes.PERMISSIONED_CANNON))).absolutePrestate(); + Claim fdgPrestateBefore = IFaultDisputeGame(address(disputeGameFactory.gameImpls(GameTypes.CANNON))).absolutePrestate(); + + runV200UpgradeAndChecks(upgrader); + runUpgrade14UpgradeAndChecks(upgrader); + + // Set the absolute prestate input to 0 + opChainConfigs[0].absolutePrestate = Claim.wrap(bytes32(0)); + + runUpgrade15UpgradeAndChecks(upgrader); + + // Get the absolute prestate after the upgrade + Claim pdgPrestateAfter = IPermissionedDisputeGame(address(disputeGameFactory.gameImpls(GameTypes.PERMISSIONED_CANNON))).absolutePrestate(); + Claim fdgPrestateAfter = IFaultDisputeGame(address(disputeGameFactory.gameImpls(GameTypes.CANNON))).absolutePrestate(); + + // Assert that the absolute prestate is 0 + assertEq(Claim.unwrap(pdgPrestateAfter), Claim.unwrap(pdgPrestateBefore)); + assertEq(Claim.unwrap(fdgPrestateAfter), Claim.unwrap(fdgPrestateBefore)); + } } contract OPContractsManager_Upgrade_TestFails is OPContractsManager_Upgrade_Harness { @@ -704,12 +726,6 @@ contract OPContractsManager_Upgrade_TestFails is OPContractsManager_Upgrade_Harn address(opcm), abi.encodeCall(IOPContractsManager.upgrade, (opChainConfigs)) ); } - - function test_upgrade_absolutePrestateNotSet_reverts() public { - opChainConfigs[0].absolutePrestate = Claim.wrap(bytes32(0)); - vm.expectRevert(IOPContractsManager.PrestateNotSet.selector); - DelegateCaller(upgrader).dcForward(address(opcm), abi.encodeCall(IOPContractsManager.upgrade, (opChainConfigs))); - } } contract OPContractsManager_SetRC_Test is OPContractsManager_Upgrade_Harness { From d669db6eeb0a5beeda1b2702394e2b1ce5961c17 Mon Sep 17 00:00:00 2001 From: Maurelian Date: Mon, 10 Mar 2025 16:56:38 -0400 Subject: [PATCH 50/50] pre-pr --- .../snapshots/abi/OPContractsManager.json | 5 - .../abi/OPContractsManagerInterop.json | 950 ++++++++++++++++++ .../abi/OPContractsManagerUpgrader.json | 5 - .../snapshots/abi/OPPrestateUpdater.json | 908 +++++++++++++++++ .../OPContractsManagerInterop.json | 30 + .../storageLayout/OPPrestateUpdater.json | 30 + .../test/L1/OPContractsManager.t.sol | 14 +- 7 files changed, 1928 insertions(+), 14 deletions(-) create mode 100644 packages/contracts-bedrock/snapshots/abi/OPContractsManagerInterop.json create mode 100644 packages/contracts-bedrock/snapshots/abi/OPPrestateUpdater.json create mode 100644 packages/contracts-bedrock/snapshots/storageLayout/OPContractsManagerInterop.json create mode 100644 packages/contracts-bedrock/snapshots/storageLayout/OPPrestateUpdater.json diff --git a/packages/contracts-bedrock/snapshots/abi/OPContractsManager.json b/packages/contracts-bedrock/snapshots/abi/OPContractsManager.json index 381040d946efc..a59cc1337d061 100644 --- a/packages/contracts-bedrock/snapshots/abi/OPContractsManager.json +++ b/packages/contracts-bedrock/snapshots/abi/OPContractsManager.json @@ -783,11 +783,6 @@ "name": "OnlyUpgradeController", "type": "error" }, - { - "inputs": [], - "name": "PrestateNotSet", - "type": "error" - }, { "inputs": [], "name": "PrestateRequired", diff --git a/packages/contracts-bedrock/snapshots/abi/OPContractsManagerInterop.json b/packages/contracts-bedrock/snapshots/abi/OPContractsManagerInterop.json new file mode 100644 index 0000000000000..22c1d40b77cd2 --- /dev/null +++ b/packages/contracts-bedrock/snapshots/abi/OPContractsManagerInterop.json @@ -0,0 +1,950 @@ +[ + { + "inputs": [ + { + "internalType": "contract ISuperchainConfig", + "name": "_superchainConfig", + "type": "address" + }, + { + "internalType": "contract IProtocolVersions", + "name": "_protocolVersions", + "type": "address" + }, + { + "internalType": "contract IProxyAdmin", + "name": "_superchainProxyAdmin", + "type": "address" + }, + { + "internalType": "string", + "name": "_l1ContractsRelease", + "type": "string" + }, + { + "components": [ + { + "internalType": "address", + "name": "addressManager", + "type": "address" + }, + { + "internalType": "address", + "name": "proxy", + "type": "address" + }, + { + "internalType": "address", + "name": "proxyAdmin", + "type": "address" + }, + { + "internalType": "address", + "name": "l1ChugSplashProxy", + "type": "address" + }, + { + "internalType": "address", + "name": "resolvedDelegateProxy", + "type": "address" + }, + { + "internalType": "address", + "name": "permissionedDisputeGame1", + "type": "address" + }, + { + "internalType": "address", + "name": "permissionedDisputeGame2", + "type": "address" + }, + { + "internalType": "address", + "name": "permissionlessDisputeGame1", + "type": "address" + }, + { + "internalType": "address", + "name": "permissionlessDisputeGame2", + "type": "address" + } + ], + "internalType": "struct OPContractsManager.Blueprints", + "name": "_blueprints", + "type": "tuple" + }, + { + "components": [ + { + "internalType": "address", + "name": "superchainConfigImpl", + "type": "address" + }, + { + "internalType": "address", + "name": "protocolVersionsImpl", + "type": "address" + }, + { + "internalType": "address", + "name": "l1ERC721BridgeImpl", + "type": "address" + }, + { + "internalType": "address", + "name": "optimismPortalImpl", + "type": "address" + }, + { + "internalType": "address", + "name": "systemConfigImpl", + "type": "address" + }, + { + "internalType": "address", + "name": "optimismMintableERC20FactoryImpl", + "type": "address" + }, + { + "internalType": "address", + "name": "l1CrossDomainMessengerImpl", + "type": "address" + }, + { + "internalType": "address", + "name": "l1StandardBridgeImpl", + "type": "address" + }, + { + "internalType": "address", + "name": "disputeGameFactoryImpl", + "type": "address" + }, + { + "internalType": "address", + "name": "anchorStateRegistryImpl", + "type": "address" + }, + { + "internalType": "address", + "name": "delayedWETHImpl", + "type": "address" + }, + { + "internalType": "address", + "name": "mipsImpl", + "type": "address" + } + ], + "internalType": "struct OPContractsManager.Implementations", + "name": "_implementations", + "type": "tuple" + }, + { + "internalType": "address", + "name": "_upgradeController", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "string", + "name": "saltMixer", + "type": "string" + }, + { + "internalType": "contract ISystemConfig", + "name": "systemConfig", + "type": "address" + }, + { + "internalType": "contract IProxyAdmin", + "name": "proxyAdmin", + "type": "address" + }, + { + "internalType": "contract IDelayedWETH", + "name": "delayedWETH", + "type": "address" + }, + { + "internalType": "GameType", + "name": "disputeGameType", + "type": "uint32" + }, + { + "internalType": "Claim", + "name": "disputeAbsolutePrestate", + "type": "bytes32" + }, + { + "internalType": "uint256", + "name": "disputeMaxGameDepth", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "disputeSplitDepth", + "type": "uint256" + }, + { + "internalType": "Duration", + "name": "disputeClockExtension", + "type": "uint64" + }, + { + "internalType": "Duration", + "name": "disputeMaxClockDuration", + "type": "uint64" + }, + { + "internalType": "uint256", + "name": "initialBond", + "type": "uint256" + }, + { + "internalType": "contract IBigStepper", + "name": "vm", + "type": "address" + }, + { + "internalType": "bool", + "name": "permissioned", + "type": "bool" + } + ], + "internalType": "struct OPContractsManager.AddGameInput[]", + "name": "_gameConfigs", + "type": "tuple[]" + } + ], + "name": "addGameType", + "outputs": [ + { + "components": [ + { + "internalType": "contract IDelayedWETH", + "name": "delayedWETH", + "type": "address" + }, + { + "internalType": "contract IFaultDisputeGame", + "name": "faultDisputeGame", + "type": "address" + } + ], + "internalType": "struct OPContractsManager.AddGameOutput[]", + "name": "", + "type": "tuple[]" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "blueprints", + "outputs": [ + { + "components": [ + { + "internalType": "address", + "name": "addressManager", + "type": "address" + }, + { + "internalType": "address", + "name": "proxy", + "type": "address" + }, + { + "internalType": "address", + "name": "proxyAdmin", + "type": "address" + }, + { + "internalType": "address", + "name": "l1ChugSplashProxy", + "type": "address" + }, + { + "internalType": "address", + "name": "resolvedDelegateProxy", + "type": "address" + }, + { + "internalType": "address", + "name": "permissionedDisputeGame1", + "type": "address" + }, + { + "internalType": "address", + "name": "permissionedDisputeGame2", + "type": "address" + }, + { + "internalType": "address", + "name": "permissionlessDisputeGame1", + "type": "address" + }, + { + "internalType": "address", + "name": "permissionlessDisputeGame2", + "type": "address" + } + ], + "internalType": "struct OPContractsManager.Blueprints", + "name": "", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_l2ChainId", + "type": "uint256" + } + ], + "name": "chainIdToBatchInboxAddress", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "components": [ + { + "internalType": "address", + "name": "opChainProxyAdminOwner", + "type": "address" + }, + { + "internalType": "address", + "name": "systemConfigOwner", + "type": "address" + }, + { + "internalType": "address", + "name": "batcher", + "type": "address" + }, + { + "internalType": "address", + "name": "unsafeBlockSigner", + "type": "address" + }, + { + "internalType": "address", + "name": "proposer", + "type": "address" + }, + { + "internalType": "address", + "name": "challenger", + "type": "address" + } + ], + "internalType": "struct OPContractsManager.Roles", + "name": "roles", + "type": "tuple" + }, + { + "internalType": "uint32", + "name": "basefeeScalar", + "type": "uint32" + }, + { + "internalType": "uint32", + "name": "blobBasefeeScalar", + "type": "uint32" + }, + { + "internalType": "uint256", + "name": "l2ChainId", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "startingAnchorRoot", + "type": "bytes" + }, + { + "internalType": "string", + "name": "saltMixer", + "type": "string" + }, + { + "internalType": "uint64", + "name": "gasLimit", + "type": "uint64" + }, + { + "internalType": "GameType", + "name": "disputeGameType", + "type": "uint32" + }, + { + "internalType": "Claim", + "name": "disputeAbsolutePrestate", + "type": "bytes32" + }, + { + "internalType": "uint256", + "name": "disputeMaxGameDepth", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "disputeSplitDepth", + "type": "uint256" + }, + { + "internalType": "Duration", + "name": "disputeClockExtension", + "type": "uint64" + }, + { + "internalType": "Duration", + "name": "disputeMaxClockDuration", + "type": "uint64" + } + ], + "internalType": "struct OPContractsManager.DeployInput", + "name": "_input", + "type": "tuple" + } + ], + "name": "deploy", + "outputs": [ + { + "components": [ + { + "internalType": "contract IProxyAdmin", + "name": "opChainProxyAdmin", + "type": "address" + }, + { + "internalType": "contract IAddressManager", + "name": "addressManager", + "type": "address" + }, + { + "internalType": "contract IL1ERC721Bridge", + "name": "l1ERC721BridgeProxy", + "type": "address" + }, + { + "internalType": "contract ISystemConfig", + "name": "systemConfigProxy", + "type": "address" + }, + { + "internalType": "contract IOptimismMintableERC20Factory", + "name": "optimismMintableERC20FactoryProxy", + "type": "address" + }, + { + "internalType": "contract IL1StandardBridge", + "name": "l1StandardBridgeProxy", + "type": "address" + }, + { + "internalType": "contract IL1CrossDomainMessenger", + "name": "l1CrossDomainMessengerProxy", + "type": "address" + }, + { + "internalType": "contract IOptimismPortal2", + "name": "optimismPortalProxy", + "type": "address" + }, + { + "internalType": "contract IDisputeGameFactory", + "name": "disputeGameFactoryProxy", + "type": "address" + }, + { + "internalType": "contract IAnchorStateRegistry", + "name": "anchorStateRegistryProxy", + "type": "address" + }, + { + "internalType": "contract IFaultDisputeGame", + "name": "faultDisputeGame", + "type": "address" + }, + { + "internalType": "contract IPermissionedDisputeGame", + "name": "permissionedDisputeGame", + "type": "address" + }, + { + "internalType": "contract IDelayedWETH", + "name": "delayedWETHPermissionedGameProxy", + "type": "address" + }, + { + "internalType": "contract IDelayedWETH", + "name": "delayedWETHPermissionlessGameProxy", + "type": "address" + } + ], + "internalType": "struct OPContractsManager.DeployOutput", + "name": "", + "type": "tuple" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "implementations", + "outputs": [ + { + "components": [ + { + "internalType": "address", + "name": "superchainConfigImpl", + "type": "address" + }, + { + "internalType": "address", + "name": "protocolVersionsImpl", + "type": "address" + }, + { + "internalType": "address", + "name": "l1ERC721BridgeImpl", + "type": "address" + }, + { + "internalType": "address", + "name": "optimismPortalImpl", + "type": "address" + }, + { + "internalType": "address", + "name": "systemConfigImpl", + "type": "address" + }, + { + "internalType": "address", + "name": "optimismMintableERC20FactoryImpl", + "type": "address" + }, + { + "internalType": "address", + "name": "l1CrossDomainMessengerImpl", + "type": "address" + }, + { + "internalType": "address", + "name": "l1StandardBridgeImpl", + "type": "address" + }, + { + "internalType": "address", + "name": "disputeGameFactoryImpl", + "type": "address" + }, + { + "internalType": "address", + "name": "anchorStateRegistryImpl", + "type": "address" + }, + { + "internalType": "address", + "name": "delayedWETHImpl", + "type": "address" + }, + { + "internalType": "address", + "name": "mipsImpl", + "type": "address" + } + ], + "internalType": "struct OPContractsManager.Implementations", + "name": "", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "isRC", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "l1ContractsRelease", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "protocolVersions", + "outputs": [ + { + "internalType": "contract IProtocolVersions", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bool", + "name": "_isRC", + "type": "bool" + } + ], + "name": "setRC", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "superchainConfig", + "outputs": [ + { + "internalType": "contract ISuperchainConfig", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "superchainProxyAdmin", + "outputs": [ + { + "internalType": "contract IProxyAdmin", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "contract ISystemConfig", + "name": "systemConfigProxy", + "type": "address" + }, + { + "internalType": "contract IProxyAdmin", + "name": "proxyAdmin", + "type": "address" + }, + { + "internalType": "Claim", + "name": "absolutePrestate", + "type": "bytes32" + } + ], + "internalType": "struct OPContractsManager.OpChainConfig[]", + "name": "_opChainConfigs", + "type": "tuple[]" + } + ], + "name": "upgrade", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "upgradeController", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "version", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "l2ChainId", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "address", + "name": "deployer", + "type": "address" + }, + { + "indexed": false, + "internalType": "bytes", + "name": "deployOutput", + "type": "bytes" + } + ], + "name": "Deployed", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "l2ChainId", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "GameType", + "name": "gameType", + "type": "uint32" + }, + { + "indexed": false, + "internalType": "contract IDisputeGame", + "name": "newDisputeGame", + "type": "address" + }, + { + "indexed": false, + "internalType": "contract IDisputeGame", + "name": "oldDisputeGame", + "type": "address" + } + ], + "name": "GameTypeAdded", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "l2ChainId", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "contract ISystemConfig", + "name": "systemConfig", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "upgrader", + "type": "address" + } + ], + "name": "Upgraded", + "type": "event" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "who", + "type": "address" + } + ], + "name": "AddressHasNoCode", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "who", + "type": "address" + } + ], + "name": "AddressNotFound", + "type": "error" + }, + { + "inputs": [], + "name": "AlreadyReleased", + "type": "error" + }, + { + "inputs": [], + "name": "BytesArrayTooLong", + "type": "error" + }, + { + "inputs": [], + "name": "DeploymentFailed", + "type": "error" + }, + { + "inputs": [], + "name": "EmptyInitcode", + "type": "error" + }, + { + "inputs": [], + "name": "IdentityPrecompileCallFailed", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidChainId", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidGameConfigs", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "string", + "name": "role", + "type": "string" + } + ], + "name": "InvalidRoleAddress", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidStartingAnchorRoot", + "type": "error" + }, + { + "inputs": [], + "name": "LatestReleaseNotSet", + "type": "error" + }, + { + "inputs": [], + "name": "NotABlueprint", + "type": "error" + }, + { + "inputs": [], + "name": "OnlyDelegatecall", + "type": "error" + }, + { + "inputs": [], + "name": "OnlyUpgradeController", + "type": "error" + }, + { + "inputs": [], + "name": "PrestateNotSet", + "type": "error" + }, + { + "inputs": [], + "name": "ReservedBitsSet", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "contract ISystemConfig", + "name": "systemConfig", + "type": "address" + } + ], + "name": "SuperchainConfigMismatch", + "type": "error" + }, + { + "inputs": [], + "name": "SuperchainProxyAdminMismatch", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "UnexpectedPreambleData", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint8", + "name": "version", + "type": "uint8" + } + ], + "name": "UnsupportedERCVersion", + "type": "error" + } +] \ No newline at end of file diff --git a/packages/contracts-bedrock/snapshots/abi/OPContractsManagerUpgrader.json b/packages/contracts-bedrock/snapshots/abi/OPContractsManagerUpgrader.json index d9ac40df7ca94..bcb065640164d 100644 --- a/packages/contracts-bedrock/snapshots/abi/OPContractsManagerUpgrader.json +++ b/packages/contracts-bedrock/snapshots/abi/OPContractsManagerUpgrader.json @@ -291,11 +291,6 @@ "name": "NotABlueprint", "type": "error" }, - { - "inputs": [], - "name": "PrestateNotSet", - "type": "error" - }, { "inputs": [], "name": "ReservedBitsSet", diff --git a/packages/contracts-bedrock/snapshots/abi/OPPrestateUpdater.json b/packages/contracts-bedrock/snapshots/abi/OPPrestateUpdater.json new file mode 100644 index 0000000000000..d44b099737a6f --- /dev/null +++ b/packages/contracts-bedrock/snapshots/abi/OPPrestateUpdater.json @@ -0,0 +1,908 @@ +[ + { + "inputs": [ + { + "internalType": "contract ISuperchainConfig", + "name": "_superchainConfig", + "type": "address" + }, + { + "internalType": "contract IProtocolVersions", + "name": "_protocolVersions", + "type": "address" + }, + { + "components": [ + { + "internalType": "address", + "name": "addressManager", + "type": "address" + }, + { + "internalType": "address", + "name": "proxy", + "type": "address" + }, + { + "internalType": "address", + "name": "proxyAdmin", + "type": "address" + }, + { + "internalType": "address", + "name": "l1ChugSplashProxy", + "type": "address" + }, + { + "internalType": "address", + "name": "resolvedDelegateProxy", + "type": "address" + }, + { + "internalType": "address", + "name": "permissionedDisputeGame1", + "type": "address" + }, + { + "internalType": "address", + "name": "permissionedDisputeGame2", + "type": "address" + }, + { + "internalType": "address", + "name": "permissionlessDisputeGame1", + "type": "address" + }, + { + "internalType": "address", + "name": "permissionlessDisputeGame2", + "type": "address" + } + ], + "internalType": "struct OPContractsManager.Blueprints", + "name": "_blueprints", + "type": "tuple" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "string", + "name": "saltMixer", + "type": "string" + }, + { + "internalType": "contract ISystemConfig", + "name": "systemConfig", + "type": "address" + }, + { + "internalType": "contract IProxyAdmin", + "name": "proxyAdmin", + "type": "address" + }, + { + "internalType": "contract IDelayedWETH", + "name": "delayedWETH", + "type": "address" + }, + { + "internalType": "GameType", + "name": "disputeGameType", + "type": "uint32" + }, + { + "internalType": "Claim", + "name": "disputeAbsolutePrestate", + "type": "bytes32" + }, + { + "internalType": "uint256", + "name": "disputeMaxGameDepth", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "disputeSplitDepth", + "type": "uint256" + }, + { + "internalType": "Duration", + "name": "disputeClockExtension", + "type": "uint64" + }, + { + "internalType": "Duration", + "name": "disputeMaxClockDuration", + "type": "uint64" + }, + { + "internalType": "uint256", + "name": "initialBond", + "type": "uint256" + }, + { + "internalType": "contract IBigStepper", + "name": "vm", + "type": "address" + }, + { + "internalType": "bool", + "name": "permissioned", + "type": "bool" + } + ], + "internalType": "struct OPContractsManager.AddGameInput[]", + "name": "_gameConfigs", + "type": "tuple[]" + } + ], + "name": "addGameType", + "outputs": [ + { + "components": [ + { + "internalType": "contract IDelayedWETH", + "name": "delayedWETH", + "type": "address" + }, + { + "internalType": "contract IFaultDisputeGame", + "name": "faultDisputeGame", + "type": "address" + } + ], + "internalType": "struct OPContractsManager.AddGameOutput[]", + "name": "", + "type": "tuple[]" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [], + "name": "blueprints", + "outputs": [ + { + "components": [ + { + "internalType": "address", + "name": "addressManager", + "type": "address" + }, + { + "internalType": "address", + "name": "proxy", + "type": "address" + }, + { + "internalType": "address", + "name": "proxyAdmin", + "type": "address" + }, + { + "internalType": "address", + "name": "l1ChugSplashProxy", + "type": "address" + }, + { + "internalType": "address", + "name": "resolvedDelegateProxy", + "type": "address" + }, + { + "internalType": "address", + "name": "permissionedDisputeGame1", + "type": "address" + }, + { + "internalType": "address", + "name": "permissionedDisputeGame2", + "type": "address" + }, + { + "internalType": "address", + "name": "permissionlessDisputeGame1", + "type": "address" + }, + { + "internalType": "address", + "name": "permissionlessDisputeGame2", + "type": "address" + } + ], + "internalType": "struct OPContractsManager.Blueprints", + "name": "", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_l2ChainId", + "type": "uint256" + } + ], + "name": "chainIdToBatchInboxAddress", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "components": [ + { + "internalType": "address", + "name": "opChainProxyAdminOwner", + "type": "address" + }, + { + "internalType": "address", + "name": "systemConfigOwner", + "type": "address" + }, + { + "internalType": "address", + "name": "batcher", + "type": "address" + }, + { + "internalType": "address", + "name": "unsafeBlockSigner", + "type": "address" + }, + { + "internalType": "address", + "name": "proposer", + "type": "address" + }, + { + "internalType": "address", + "name": "challenger", + "type": "address" + } + ], + "internalType": "struct OPContractsManager.Roles", + "name": "roles", + "type": "tuple" + }, + { + "internalType": "uint32", + "name": "basefeeScalar", + "type": "uint32" + }, + { + "internalType": "uint32", + "name": "blobBasefeeScalar", + "type": "uint32" + }, + { + "internalType": "uint256", + "name": "l2ChainId", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "startingAnchorRoot", + "type": "bytes" + }, + { + "internalType": "string", + "name": "saltMixer", + "type": "string" + }, + { + "internalType": "uint64", + "name": "gasLimit", + "type": "uint64" + }, + { + "internalType": "GameType", + "name": "disputeGameType", + "type": "uint32" + }, + { + "internalType": "Claim", + "name": "disputeAbsolutePrestate", + "type": "bytes32" + }, + { + "internalType": "uint256", + "name": "disputeMaxGameDepth", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "disputeSplitDepth", + "type": "uint256" + }, + { + "internalType": "Duration", + "name": "disputeClockExtension", + "type": "uint64" + }, + { + "internalType": "Duration", + "name": "disputeMaxClockDuration", + "type": "uint64" + } + ], + "internalType": "struct OPContractsManager.DeployInput", + "name": "_input", + "type": "tuple" + } + ], + "name": "deploy", + "outputs": [ + { + "components": [ + { + "internalType": "contract IProxyAdmin", + "name": "opChainProxyAdmin", + "type": "address" + }, + { + "internalType": "contract IAddressManager", + "name": "addressManager", + "type": "address" + }, + { + "internalType": "contract IL1ERC721Bridge", + "name": "l1ERC721BridgeProxy", + "type": "address" + }, + { + "internalType": "contract ISystemConfig", + "name": "systemConfigProxy", + "type": "address" + }, + { + "internalType": "contract IOptimismMintableERC20Factory", + "name": "optimismMintableERC20FactoryProxy", + "type": "address" + }, + { + "internalType": "contract IL1StandardBridge", + "name": "l1StandardBridgeProxy", + "type": "address" + }, + { + "internalType": "contract IL1CrossDomainMessenger", + "name": "l1CrossDomainMessengerProxy", + "type": "address" + }, + { + "internalType": "contract IOptimismPortal2", + "name": "optimismPortalProxy", + "type": "address" + }, + { + "internalType": "contract IDisputeGameFactory", + "name": "disputeGameFactoryProxy", + "type": "address" + }, + { + "internalType": "contract IAnchorStateRegistry", + "name": "anchorStateRegistryProxy", + "type": "address" + }, + { + "internalType": "contract IFaultDisputeGame", + "name": "faultDisputeGame", + "type": "address" + }, + { + "internalType": "contract IPermissionedDisputeGame", + "name": "permissionedDisputeGame", + "type": "address" + }, + { + "internalType": "contract IDelayedWETH", + "name": "delayedWETHPermissionedGameProxy", + "type": "address" + }, + { + "internalType": "contract IDelayedWETH", + "name": "delayedWETHPermissionlessGameProxy", + "type": "address" + } + ], + "internalType": "struct OPContractsManager.DeployOutput", + "name": "", + "type": "tuple" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [], + "name": "implementations", + "outputs": [ + { + "components": [ + { + "internalType": "address", + "name": "superchainConfigImpl", + "type": "address" + }, + { + "internalType": "address", + "name": "protocolVersionsImpl", + "type": "address" + }, + { + "internalType": "address", + "name": "l1ERC721BridgeImpl", + "type": "address" + }, + { + "internalType": "address", + "name": "optimismPortalImpl", + "type": "address" + }, + { + "internalType": "address", + "name": "systemConfigImpl", + "type": "address" + }, + { + "internalType": "address", + "name": "optimismMintableERC20FactoryImpl", + "type": "address" + }, + { + "internalType": "address", + "name": "l1CrossDomainMessengerImpl", + "type": "address" + }, + { + "internalType": "address", + "name": "l1StandardBridgeImpl", + "type": "address" + }, + { + "internalType": "address", + "name": "disputeGameFactoryImpl", + "type": "address" + }, + { + "internalType": "address", + "name": "anchorStateRegistryImpl", + "type": "address" + }, + { + "internalType": "address", + "name": "delayedWETHImpl", + "type": "address" + }, + { + "internalType": "address", + "name": "mipsImpl", + "type": "address" + } + ], + "internalType": "struct OPContractsManager.Implementations", + "name": "", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "isRC", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "l1ContractsRelease", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [], + "name": "protocolVersions", + "outputs": [ + { + "internalType": "contract IProtocolVersions", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bool", + "name": "_isRC", + "type": "bool" + } + ], + "name": "setRC", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "superchainConfig", + "outputs": [ + { + "internalType": "contract ISuperchainConfig", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "superchainProxyAdmin", + "outputs": [ + { + "internalType": "contract IProxyAdmin", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "contract ISystemConfig", + "name": "systemConfigProxy", + "type": "address" + }, + { + "internalType": "contract IProxyAdmin", + "name": "proxyAdmin", + "type": "address" + }, + { + "internalType": "Claim", + "name": "absolutePrestate", + "type": "bytes32" + } + ], + "internalType": "struct OPContractsManager.OpChainConfig[]", + "name": "_prestateUpdateInputs", + "type": "tuple[]" + } + ], + "name": "updatePrestate", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "contract ISystemConfig", + "name": "systemConfigProxy", + "type": "address" + }, + { + "internalType": "contract IProxyAdmin", + "name": "proxyAdmin", + "type": "address" + }, + { + "internalType": "Claim", + "name": "absolutePrestate", + "type": "bytes32" + } + ], + "internalType": "struct OPContractsManager.OpChainConfig[]", + "name": "_opChainConfigs", + "type": "tuple[]" + } + ], + "name": "upgrade", + "outputs": [], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [], + "name": "upgradeController", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "version", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "l2ChainId", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "address", + "name": "deployer", + "type": "address" + }, + { + "indexed": false, + "internalType": "bytes", + "name": "deployOutput", + "type": "bytes" + } + ], + "name": "Deployed", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "l2ChainId", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "GameType", + "name": "gameType", + "type": "uint32" + }, + { + "indexed": false, + "internalType": "contract IDisputeGame", + "name": "newDisputeGame", + "type": "address" + }, + { + "indexed": false, + "internalType": "contract IDisputeGame", + "name": "oldDisputeGame", + "type": "address" + } + ], + "name": "GameTypeAdded", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "l2ChainId", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "contract ISystemConfig", + "name": "systemConfig", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "upgrader", + "type": "address" + } + ], + "name": "Upgraded", + "type": "event" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "who", + "type": "address" + } + ], + "name": "AddressHasNoCode", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "who", + "type": "address" + } + ], + "name": "AddressNotFound", + "type": "error" + }, + { + "inputs": [], + "name": "AlreadyReleased", + "type": "error" + }, + { + "inputs": [], + "name": "BytesArrayTooLong", + "type": "error" + }, + { + "inputs": [], + "name": "DeploymentFailed", + "type": "error" + }, + { + "inputs": [], + "name": "EmptyInitcode", + "type": "error" + }, + { + "inputs": [], + "name": "IdentityPrecompileCallFailed", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidChainId", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidGameConfigs", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "string", + "name": "role", + "type": "string" + } + ], + "name": "InvalidRoleAddress", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidStartingAnchorRoot", + "type": "error" + }, + { + "inputs": [], + "name": "LatestReleaseNotSet", + "type": "error" + }, + { + "inputs": [], + "name": "NotABlueprint", + "type": "error" + }, + { + "inputs": [], + "name": "NotImplemented", + "type": "error" + }, + { + "inputs": [], + "name": "OnlyDelegatecall", + "type": "error" + }, + { + "inputs": [], + "name": "OnlyUpgradeController", + "type": "error" + }, + { + "inputs": [], + "name": "PrestateNotSet", + "type": "error" + }, + { + "inputs": [], + "name": "PrestateRequired", + "type": "error" + }, + { + "inputs": [], + "name": "ReservedBitsSet", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "contract ISystemConfig", + "name": "systemConfig", + "type": "address" + } + ], + "name": "SuperchainConfigMismatch", + "type": "error" + }, + { + "inputs": [], + "name": "SuperchainProxyAdminMismatch", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "UnexpectedPreambleData", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint8", + "name": "version", + "type": "uint8" + } + ], + "name": "UnsupportedERCVersion", + "type": "error" + } +] \ No newline at end of file diff --git a/packages/contracts-bedrock/snapshots/storageLayout/OPContractsManagerInterop.json b/packages/contracts-bedrock/snapshots/storageLayout/OPContractsManagerInterop.json new file mode 100644 index 0000000000000..3c1445a982928 --- /dev/null +++ b/packages/contracts-bedrock/snapshots/storageLayout/OPContractsManagerInterop.json @@ -0,0 +1,30 @@ +[ + { + "bytes": "32", + "label": "L1_CONTRACTS_RELEASE", + "offset": 0, + "slot": "0", + "type": "string" + }, + { + "bytes": "288", + "label": "blueprint", + "offset": 0, + "slot": "1", + "type": "struct OPContractsManager.Blueprints" + }, + { + "bytes": "384", + "label": "implementation", + "offset": 0, + "slot": "10", + "type": "struct OPContractsManager.Implementations" + }, + { + "bytes": "1", + "label": "isRC", + "offset": 0, + "slot": "22", + "type": "bool" + } +] \ No newline at end of file diff --git a/packages/contracts-bedrock/snapshots/storageLayout/OPPrestateUpdater.json b/packages/contracts-bedrock/snapshots/storageLayout/OPPrestateUpdater.json new file mode 100644 index 0000000000000..3c1445a982928 --- /dev/null +++ b/packages/contracts-bedrock/snapshots/storageLayout/OPPrestateUpdater.json @@ -0,0 +1,30 @@ +[ + { + "bytes": "32", + "label": "L1_CONTRACTS_RELEASE", + "offset": 0, + "slot": "0", + "type": "string" + }, + { + "bytes": "288", + "label": "blueprint", + "offset": 0, + "slot": "1", + "type": "struct OPContractsManager.Blueprints" + }, + { + "bytes": "384", + "label": "implementation", + "offset": 0, + "slot": "10", + "type": "struct OPContractsManager.Implementations" + }, + { + "bytes": "1", + "label": "isRC", + "offset": 0, + "slot": "22", + "type": "bool" + } +] \ No newline at end of file diff --git a/packages/contracts-bedrock/test/L1/OPContractsManager.t.sol b/packages/contracts-bedrock/test/L1/OPContractsManager.t.sol index 6ea41d3fd9c41..185ab8f79f31c 100644 --- a/packages/contracts-bedrock/test/L1/OPContractsManager.t.sol +++ b/packages/contracts-bedrock/test/L1/OPContractsManager.t.sol @@ -680,8 +680,11 @@ contract OPContractsManager_Upgrade_Test is OPContractsManager_Upgrade_Harness { function test_upgrade_absolutePrestateNotSet_succeeds() public { // Get the pdg and fdg before the upgrade - Claim pdgPrestateBefore = IPermissionedDisputeGame(address(disputeGameFactory.gameImpls(GameTypes.PERMISSIONED_CANNON))).absolutePrestate(); - Claim fdgPrestateBefore = IFaultDisputeGame(address(disputeGameFactory.gameImpls(GameTypes.CANNON))).absolutePrestate(); + Claim pdgPrestateBefore = IPermissionedDisputeGame( + address(disputeGameFactory.gameImpls(GameTypes.PERMISSIONED_CANNON)) + ).absolutePrestate(); + Claim fdgPrestateBefore = + IFaultDisputeGame(address(disputeGameFactory.gameImpls(GameTypes.CANNON))).absolutePrestate(); runV200UpgradeAndChecks(upgrader); runUpgrade14UpgradeAndChecks(upgrader); @@ -692,8 +695,11 @@ contract OPContractsManager_Upgrade_Test is OPContractsManager_Upgrade_Harness { runUpgrade15UpgradeAndChecks(upgrader); // Get the absolute prestate after the upgrade - Claim pdgPrestateAfter = IPermissionedDisputeGame(address(disputeGameFactory.gameImpls(GameTypes.PERMISSIONED_CANNON))).absolutePrestate(); - Claim fdgPrestateAfter = IFaultDisputeGame(address(disputeGameFactory.gameImpls(GameTypes.CANNON))).absolutePrestate(); + Claim pdgPrestateAfter = IPermissionedDisputeGame( + address(disputeGameFactory.gameImpls(GameTypes.PERMISSIONED_CANNON)) + ).absolutePrestate(); + Claim fdgPrestateAfter = + IFaultDisputeGame(address(disputeGameFactory.gameImpls(GameTypes.CANNON))).absolutePrestate(); // Assert that the absolute prestate is 0 assertEq(Claim.unwrap(pdgPrestateAfter), Claim.unwrap(pdgPrestateBefore));