diff --git a/op-chain-ops/interopgen/deploy.go b/op-chain-ops/interopgen/deploy.go index eeeedf44e63..f7cec37204e 100644 --- a/op-chain-ops/interopgen/deploy.go +++ b/op-chain-ops/interopgen/deploy.go @@ -244,6 +244,7 @@ func DeployL2ToL1(l1Host *script.Host, superCfg *SuperchainConfig, superDeployme GasLimit: cfg.GasLimit, DisputeGameType: cfg.DisputeGameType, DisputeAbsolutePrestate: cfg.DisputeAbsolutePrestate, + StartingAnchorRoot: opcm.DefaultStartingAnchorRoot.Root, DisputeMaxGameDepth: new(big.Int).SetUint64(cfg.DisputeMaxGameDepth), DisputeSplitDepth: new(big.Int).SetUint64(cfg.DisputeSplitDepth), DisputeClockExtension: cfg.DisputeClockExtension, diff --git a/op-deployer/pkg/deployer/opcm/opchain.go b/op-deployer/pkg/deployer/opcm/opchain.go index 8e2223f5957..c7b85bd21a0 100644 --- a/op-deployer/pkg/deployer/opcm/opchain.go +++ b/op-deployer/pkg/deployer/opcm/opchain.go @@ -36,6 +36,7 @@ type DeployOPChainInput struct { DisputeGameType uint32 DisputeAbsolutePrestate common.Hash + StartingAnchorRoot common.Hash DisputeMaxGameDepth *big.Int DisputeSplitDepth *big.Int DisputeClockExtension uint64 diff --git a/op-deployer/pkg/deployer/pipeline/opchain.go b/op-deployer/pkg/deployer/pipeline/opchain.go index a7a5f87cf8d..c959c8cfc0a 100644 --- a/op-deployer/pkg/deployer/pipeline/opchain.go +++ b/op-deployer/pkg/deployer/pipeline/opchain.go @@ -134,6 +134,7 @@ func makeDCI(intent *state.Intent, thisIntent *state.ChainIntent, chainID common return opcm.DeployOPChainInput{}, fmt.Errorf("OPCM implementation is not deployed") } + // TODO(#20912): Populate StartingAnchorRoot and DisputeAbsolutePrestate from pipeline state for permissionless deploys. return opcm.DeployOPChainInput{ OpChainProxyAdminOwner: thisIntent.Roles.L1ProxyAdminOwner, SystemConfigOwner: thisIntent.Roles.SystemConfigOwner, @@ -149,6 +150,7 @@ func makeDCI(intent *state.Intent, thisIntent *state.ChainIntent, chainID common GasLimit: thisIntent.GasLimit, DisputeGameType: proofParams.DisputeGameType, DisputeAbsolutePrestate: proofParams.DisputeAbsolutePrestate, + StartingAnchorRoot: opcm.DefaultStartingAnchorRoot.Root, DisputeMaxGameDepth: new(big.Int).SetUint64(proofParams.DisputeMaxGameDepth), DisputeSplitDepth: new(big.Int).SetUint64(proofParams.DisputeSplitDepth), DisputeClockExtension: proofParams.DisputeClockExtension, // 3 hours (input in seconds) diff --git a/packages/contracts-bedrock/scripts/checks/test-validation/exclusions.toml b/packages/contracts-bedrock/scripts/checks/test-validation/exclusions.toml index 65c05ad9aa1..97bd9734c69 100644 --- a/packages/contracts-bedrock/scripts/checks/test-validation/exclusions.toml +++ b/packages/contracts-bedrock/scripts/checks/test-validation/exclusions.toml @@ -25,6 +25,7 @@ src_validation = [ "test/dispute/lib/LibGameId.t.sol", # Tests library utilities "test/libraries/DeployUtils.t.sol", # Tests library utilities - no direct src counterpart "test/setup/DeployVariations.t.sol", # Tests deployment variations + "test/setup/DisputeGames.t.sol", # Tests a test-only library (test/setup/DisputeGames.sol) "test/setup/PastNUTBundles.t.sol", # Tests a test-only library (test/setup/PastNUTBundles.sol) "test/universal/BenchmarkTest.t.sol", # Performance benchmarking tests "test/universal/ExtendedPause.t.sol", # Tests extended functionality diff --git a/packages/contracts-bedrock/scripts/deploy/ChainAssertions.sol b/packages/contracts-bedrock/scripts/deploy/ChainAssertions.sol index d8bfbb52164..2ae94f6a17a 100644 --- a/packages/contracts-bedrock/scripts/deploy/ChainAssertions.sol +++ b/packages/contracts-bedrock/scripts/deploy/ChainAssertions.sol @@ -14,8 +14,7 @@ import { DeployUtils } from "scripts/libraries/DeployUtils.sol"; import { Constants } from "src/libraries/Constants.sol"; import { Types } from "scripts/libraries/Types.sol"; import { Blueprint } from "src/libraries/Blueprint.sol"; -import { GameType, GameTypes } from "src/dispute/lib/Types.sol"; -import { Hash } from "src/dispute/lib/Types.sol"; +import { GameType, Hash, Proposal } from "src/dispute/lib/Types.sol"; // Interfaces import { IOPContractsManagerV2 } from "interfaces/L1/opcm/IOPContractsManagerV2.sol"; import { IOPContractsManagerContainer } from "interfaces/L1/opcm/IOPContractsManagerContainer.sol"; @@ -402,7 +401,14 @@ library ChainAssertions { ); } - function checkAnchorStateRegistryProxy(IAnchorStateRegistry _anchorStateRegistryProxy, bool _isProxy) internal { + function checkAnchorStateRegistryProxy( + IAnchorStateRegistry _anchorStateRegistryProxy, + bool _isProxy, + GameType _expectedRespectedGameType, + Hash _expectedRoot + ) + internal + { DeployUtils.assertValidContractAddress(address(_anchorStateRegistryProxy)); if (_isProxy) { DeployUtils.assertERC1967ImplementationSet(address(_anchorStateRegistryProxy)); @@ -416,15 +422,11 @@ library ChainAssertions { }); // The below check cannot be done in the standard validator because the assertion only applies at deploy time. - (Hash actualRoot,) = _anchorStateRegistryProxy.anchors(GameTypes.PERMISSIONED_CANNON); - if (_isProxy) { - require( - Hash.unwrap(actualRoot) == 0xdead000000000000000000000000000000000000000000000000000000000000, - "ANCHORP-40" - ); - } else { - require(Hash.unwrap(actualRoot) == bytes32(0), "ANCHORP-40"); - } + Proposal memory actualRoot = _anchorStateRegistryProxy.getStartingAnchorRoot(); + + require(_anchorStateRegistryProxy.respectedGameType().raw() == _expectedRespectedGameType.raw(), "ANCHORP-30"); + require(actualRoot.root.raw() == _expectedRoot.raw(), "ANCHORP-40"); + require(actualRoot.l2SequenceNumber == 0, "ANCHORP-50"); } /// @notice Asserts that the ZKDisputeGame implementation is setup correctly. diff --git a/packages/contracts-bedrock/scripts/deploy/Deploy.s.sol b/packages/contracts-bedrock/scripts/deploy/Deploy.s.sol index 778578ef387..c08342748a8 100644 --- a/packages/contracts-bedrock/scripts/deploy/Deploy.s.sol +++ b/packages/contracts-bedrock/scripts/deploy/Deploy.s.sol @@ -315,7 +315,9 @@ contract Deploy is Deployer { _mips: IMIPS64(address(dio.mipsSingleton)) }); ChainAssertions.checkSystemConfigImpls(impls); - ChainAssertions.checkAnchorStateRegistryProxy(IAnchorStateRegistry(impls.AnchorStateRegistry), false); + ChainAssertions.checkAnchorStateRegistryProxy( + IAnchorStateRegistry(impls.AnchorStateRegistry), false, GameType.wrap(0), Hash.wrap(bytes32(0)) + ); } /// @notice Deploy all of the OP Chain specific contracts diff --git a/packages/contracts-bedrock/scripts/deploy/DeployImplementations.s.sol b/packages/contracts-bedrock/scripts/deploy/DeployImplementations.s.sol index 33883fdcafc..f464532f444 100644 --- a/packages/contracts-bedrock/scripts/deploy/DeployImplementations.s.sol +++ b/packages/contracts-bedrock/scripts/deploy/DeployImplementations.s.sol @@ -19,7 +19,7 @@ import { ISuperFaultDisputeGame } from "interfaces/dispute/ISuperFaultDisputeGam import { ISuperPermissionedDisputeGame } from "interfaces/dispute/ISuperPermissionedDisputeGame.sol"; import { IPermissionedDisputeGame } from "interfaces/dispute/IPermissionedDisputeGame.sol"; import { IZKDisputeGame } from "interfaces/dispute/zk/IZKDisputeGame.sol"; -import { Duration, GameType, GameTypes } from "src/dispute/lib/Types.sol"; +import { Duration, GameType, GameTypes, Hash } from "src/dispute/lib/Types.sol"; import { IOPContractsManagerV2 } from "interfaces/L1/opcm/IOPContractsManagerV2.sol"; import { IOPContractsManagerContainer } from "interfaces/L1/opcm/IOPContractsManagerContainer.sol"; import { IOPContractsManagerUtils } from "interfaces/L1/opcm/IOPContractsManagerUtils.sol"; @@ -804,6 +804,8 @@ contract DeployImplementations is Script { }); ChainAssertions.checkETHLockboxImpl(_output.ethLockboxImpl, _output.optimismPortalImpl); ChainAssertions.checkSystemConfigImpls(impls); - ChainAssertions.checkAnchorStateRegistryProxy(IAnchorStateRegistry(impls.AnchorStateRegistry), false); + ChainAssertions.checkAnchorStateRegistryProxy( + IAnchorStateRegistry(impls.AnchorStateRegistry), false, GameType.wrap(0), Hash.wrap(bytes32(0)) + ); } } diff --git a/packages/contracts-bedrock/scripts/deploy/DeployOPChain.s.sol b/packages/contracts-bedrock/scripts/deploy/DeployOPChain.s.sol index 45378288c1f..f5a71600baa 100644 --- a/packages/contracts-bedrock/scripts/deploy/DeployOPChain.s.sol +++ b/packages/contracts-bedrock/scripts/deploy/DeployOPChain.s.sol @@ -11,6 +11,7 @@ import { Constants as ScriptConstants } from "scripts/libraries/Constants.sol"; import { Types } from "scripts/libraries/Types.sol"; import { IProxyAdmin } from "interfaces/universal/IProxyAdmin.sol"; +import { IOPContractsManagerContainer } from "interfaces/L1/opcm/IOPContractsManagerContainer.sol"; import { IOPContractsManagerV2 } from "interfaces/L1/opcm/IOPContractsManagerV2.sol"; import { IOPContractsManagerUtils } from "interfaces/L1/opcm/IOPContractsManagerUtils.sol"; import { IResourceMetering } from "interfaces/L1/IResourceMetering.sol"; @@ -27,7 +28,7 @@ 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"; -import { GameType, GameTypes } from "src/dispute/lib/Types.sol"; +import { GameType, GameTypes, Proposal } from "src/dispute/lib/Types.sol"; import { DevFeatures } from "src/libraries/DevFeatures.sol"; contract DeployOPChain is Script { @@ -76,7 +77,7 @@ contract DeployOPChain is Script { require(address(_input.opcm).code.length > 0, "DeployOPChain: OPCM address has no code"); IOPContractsManagerV2 opcmV2 = IOPContractsManagerV2(_input.opcm); - isSuperRoot = DevFeatures.isDevFeatureEnabled(opcmV2.devFeatureBitmap(), DevFeatures.SUPER_ROOT_GAMES_MIGRATION); + isSuperRoot = _isSuperRootEnabled(opcmV2); IOPContractsManagerV2.FullConfig memory config = _toOPCMV2DeployInput(_input); vm.broadcast(msg.sender); @@ -112,11 +113,12 @@ contract DeployOPChain is Script { view returns (IOPContractsManagerV2.FullConfig memory config_) { - // Only PERMISSIONED_CANNON is allowed for initial deployment since no prestate exists for permissionless games. - require( - _input.disputeGameType.raw() == GameTypes.PERMISSIONED_CANNON.raw(), - "DeployOPChain: only PERMISSIONED_CANNON game type is supported for initial deployment" - ); + (bool permissionless, GameType respectedGameType) = + _initialDeployGameSelection(_input.disputeGameType, isSuperRoot); + bool enableCannonKona = permissionless && _input.disputeGameType.raw() == GameTypes.CANNON_KONA.raw(); + // Non-super-root deploys always register PERMISSIONED_CANNON. In CANNON_KONA mode, + // the ASR respects CANNON_KONA first but the guardian can switch to PERMISSIONED_CANNON. + bool enablePermissionedCannon = !isSuperRoot; // Shared permissioned game config for legacy permissioned games. IOPContractsManagerUtils.PermissionedDisputeGameConfig memory pdgConfig = IOPContractsManagerUtils @@ -133,7 +135,7 @@ contract DeployOPChain is Script { IOPContractsManagerUtils.DisputeGameConfig[] memory disputeGameConfigs = new IOPContractsManagerUtils.DisputeGameConfig[](6); - // Config 0: CANNON (disabled for initial deployment — no prestate exists) + // Config 0: legacy CANNON slot, disabled after U19 and kept to satisfy OPCMV2's 6-config shape. disputeGameConfigs[0] = IOPContractsManagerUtils.DisputeGameConfig({ enabled: false, initBond: 0, @@ -141,28 +143,18 @@ contract DeployOPChain is Script { gameArgs: bytes("") }); - // Config 1: PERMISSIONED_CANNON — enabled only in non-super-root mode. - disputeGameConfigs[1] = isSuperRoot - ? IOPContractsManagerUtils.DisputeGameConfig({ - enabled: false, - initBond: 0, - gameType: GameTypes.PERMISSIONED_CANNON, - gameArgs: bytes("") - }) - : IOPContractsManagerUtils.DisputeGameConfig({ - enabled: true, - initBond: DEFAULT_INIT_BOND, - gameType: GameTypes.PERMISSIONED_CANNON, - gameArgs: abi.encode(pdgConfig) - }); - - // Config 2: CANNON_KONA (disabled for initial deployment — no prestate exists) - disputeGameConfigs[2] = IOPContractsManagerUtils.DisputeGameConfig({ - enabled: false, - initBond: 0, - gameType: GameTypes.CANNON_KONA, - gameArgs: bytes("") - }); + // Config 1: PERMISSIONED_CANNON + disputeGameConfigs[1] = + _createGameConfig(enablePermissionedCannon, GameTypes.PERMISSIONED_CANNON, abi.encode(pdgConfig)); + + // Config 2: CANNON_KONA + disputeGameConfigs[2] = _createGameConfig( + enableCannonKona, + GameTypes.CANNON_KONA, + abi.encode( + IOPContractsManagerUtils.FaultDisputeGameConfig({ absolutePrestate: _input.disputeAbsolutePrestate }) + ) + ); // Config 3: SUPER_PERMISSIONED — enabled only in super-root mode. disputeGameConfigs[3] = isSuperRoot @@ -202,8 +194,8 @@ contract DeployOPChain is Script { systemConfigOwner: _input.systemConfigOwner, unsafeBlockSigner: _input.unsafeBlockSigner, batcher: _input.batcher, - startingAnchorRoot: ScriptConstants.DEFAULT_OUTPUT_ROOT(), - startingRespectedGameType: isSuperRoot ? GameTypes.SUPER_PERMISSIONED : GameTypes.PERMISSIONED_CANNON, + startingAnchorRoot: Proposal({ root: _input.startingAnchorRoot, l2SequenceNumber: 0 }), + startingRespectedGameType: respectedGameType, basefeeScalar: _input.basefeeScalar, blobBasefeeScalar: _input.blobBaseFeeScalar, gasLimit: _input.gasLimit, @@ -281,6 +273,64 @@ contract DeployOPChain is Script { require(cfg_.maxResourceLimit > 0, "DeployOPChain: gasLimit too small for any deposit budget"); } + /// @notice Returns the permissionless mode and respected game type for an initial deployment. + /// @dev Permissionless deploys respect the requested game type before the permissioned/super-root default. + function _initialDeployGameSelection( + GameType _disputeGameType, + bool _isSuperRoot + ) + internal + pure + returns (bool permissionless_, GameType respectedGameType_) + { + permissionless_ = _disputeGameType.raw() == GameTypes.CANNON_KONA.raw(); + + // PERMISSIONED_CANNON is the only **permissioned** type supported for an initial deploy. + require( + permissionless_ || _disputeGameType.raw() == GameTypes.PERMISSIONED_CANNON.raw(), + "DeployOPChain: unsupported dispute game type" + ); + + // Super roots don't support permissionless games and they deploy with SUPER_PERMISSIONED instead. + require( + !(_isSuperRoot && permissionless_), "DeployOPChain: permissionless game type not supported with super roots" + ); + + respectedGameType_ = permissionless_ + ? _disputeGameType + : (_isSuperRoot ? GameTypes.SUPER_PERMISSIONED : GameTypes.PERMISSIONED_CANNON); + } + + /// @notice Returns whether the given OPCM has the SUPER_ROOT_GAMES_MIGRATION dev feature enabled. + /// @param _opcm The OPCM to check. + /// @return Whether SUPER_ROOT_GAMES_MIGRATION is enabled. + function _isSuperRootEnabled(IOPContractsManagerV2 _opcm) internal view returns (bool) { + return DevFeatures.isDevFeatureEnabled(_opcm.devFeatureBitmap(), DevFeatures.SUPER_ROOT_GAMES_MIGRATION); + } + + /// @notice Returns a DisputeGameConfig with the appropriate values based on the parameters passed in. + /// If the game is enabled the configuration is filled with the default init bond and the game + /// arguments passed as parameter otherwise 0 is used for the bond and empty bytes for the arguments. + /// @param _enabled Whether the dispute game is enabled or not + /// @param _gameType The type of this dispute game + /// @param _enabledArgs The arguments for the dispute game config + function _createGameConfig( + bool _enabled, + GameType _gameType, + bytes memory _enabledArgs + ) + internal + pure + returns (IOPContractsManagerUtils.DisputeGameConfig memory) + { + return IOPContractsManagerUtils.DisputeGameConfig({ + enabled: _enabled, + initBond: _enabled ? DEFAULT_INIT_BOND : 0, + gameType: _gameType, + gameArgs: _enabled ? _enabledArgs : bytes("") + }); + } + // -------- Validations -------- /// @notice Checks if the input is valid. @@ -302,11 +352,21 @@ contract DeployOPChain is Script { require(_i.opcm != address(0), "DeployOPChainInput: opcm not set"); DeployUtils.assertValidContractAddress(_i.opcm); + bool superRoot = _isSuperRootEnabled(IOPContractsManagerV2(_i.opcm)); + (bool permissionless,) = _initialDeployGameSelection(_i.disputeGameType, superRoot); require(_i.disputeMaxGameDepth != 0, "DeployOPChainInput: disputeMaxGameDepth not set"); require(_i.disputeSplitDepth != 0, "DeployOPChainInput: disputeSplitDepth not set"); require(_i.disputeMaxClockDuration.raw() != 0, "DeployOPChainInput: disputeMaxClockDuration not set"); require(_i.disputeAbsolutePrestate.raw() != bytes32(0), "DeployOPChainInput: disputeAbsolutePrestate not set"); + require(_i.startingAnchorRoot.raw() != bytes32(0), "DeployOPChainInput: startingAnchorRoot not set"); + + if (permissionless) { + require( + _i.startingAnchorRoot.raw() != ScriptConstants.DEFAULT_OUTPUT_ROOT().root.raw(), + "DeployOPChainInput: permissionless startingAnchorRoot cannot be placeholder" + ); + } } /// @notice Checks if the output is valid. @@ -358,15 +418,18 @@ contract DeployOPChain is Script { // Check dispute games and get superchain config IOPContractsManagerV2 opcmV2 = IOPContractsManagerV2(_i.opcm); - address expectedPDGImpl = isSuperRoot - ? opcmV2.implementations().superPermissionedDisputeGameImpl - : opcmV2.implementations().permissionedDisputeGameImpl; + IOPContractsManagerContainer.Implementations memory implementations = opcmV2.implementations(); - GameType permGameType = isSuperRoot ? GameTypes.SUPER_PERMISSIONED : GameTypes.PERMISSIONED_CANNON; + (bool permissionless, GameType respectedGameType) = _initialDeployGameSelection(_i.disputeGameType, isSuperRoot); + address expectedDGImpl = permissionless + ? implementations.faultDisputeGameImpl + : (isSuperRoot ? implementations.superPermissionedDisputeGameImpl : implementations.permissionedDisputeGameImpl); ChainAssertions.checkDisputeGameFactory( - _o.disputeGameFactoryProxy, _i.opChainProxyAdminOwner, expectedPDGImpl, true, permGameType + _o.disputeGameFactoryProxy, _i.opChainProxyAdminOwner, expectedDGImpl, true, respectedGameType + ); + ChainAssertions.checkAnchorStateRegistryProxy( + _o.anchorStateRegistryProxy, true, respectedGameType, _i.startingAnchorRoot ); - ChainAssertions.checkAnchorStateRegistryProxy(_o.anchorStateRegistryProxy, true); ChainAssertions.checkL1CrossDomainMessenger(_o.l1CrossDomainMessengerProxy, vm, true); ChainAssertions.checkOptimismPortal2({ _contracts: proxies, @@ -469,20 +532,4 @@ contract DeployOPChain is Script { "OPCPA-120" ); } - - /// @notice Returns the starting anchor root for the permissioned game. - function startingAnchorRoot() public pure returns (bytes memory) { - // WARNING: For now always hardcode the starting permissioned game anchor root to 0xdead, - // and we do not set anything for the permissioned game. This is because we currently only - // support deploying straight to permissioned games, and the starting root does not - // matter for that, as long as it is non-zero, since no games will be played. We do not - // deploy the permissionless game (and therefore do not set a starting root for it here) - // because updating to the permissionless game will require updating its starting - // anchor root and deploy a new permissioned dispute game contract anyway. - // - // You can `console.logBytes(abi.encode(ScriptConstants.DEFAULT_OUTPUT_ROOT()))` to get the bytes that - // are hardcoded into `op-chain-ops/deployer/opcm/opchain.go` - - return abi.encode(ScriptConstants.DEFAULT_OUTPUT_ROOT()); - } } diff --git a/packages/contracts-bedrock/scripts/libraries/Types.sol b/packages/contracts-bedrock/scripts/libraries/Types.sol index 317e27dc9d0..c42ac54be62 100644 --- a/packages/contracts-bedrock/scripts/libraries/Types.sol +++ b/packages/contracts-bedrock/scripts/libraries/Types.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; -import { Claim, Duration, GameType } from "src/dispute/lib/Types.sol"; +import { Claim, Duration, GameType, Hash } from "src/dispute/lib/Types.sol"; import { ISuperchainConfig } from "interfaces/L1/ISuperchainConfig.sol"; library Types { @@ -31,7 +31,6 @@ library Types { address unsafeBlockSigner; address proposer; address challenger; - // TODO Add fault proofs inputs in a future PR. uint32 basefeeScalar; uint32 blobBaseFeeScalar; uint256 l2ChainId; @@ -41,6 +40,7 @@ library Types { // Configurable dispute game inputs GameType disputeGameType; Claim disputeAbsolutePrestate; + Hash startingAnchorRoot; uint256 disputeMaxGameDepth; uint256 disputeSplitDepth; Duration disputeClockExtension; diff --git a/packages/contracts-bedrock/snapshots/semver-lock.json b/packages/contracts-bedrock/snapshots/semver-lock.json index aaba55ac8c1..ca1c9d371a2 100644 --- a/packages/contracts-bedrock/snapshots/semver-lock.json +++ b/packages/contracts-bedrock/snapshots/semver-lock.json @@ -36,8 +36,8 @@ "sourceCodeHash": "0x2650f2af1df017387e1f1aa6273decc4c46dd4f3ac7f0910c6f0edc92aef4747" }, "src/L1/opcm/OPContractsManagerV2.sol:OPContractsManagerV2": { - "initCodeHash": "0xfa7d85fbf5f1d26c4f264dbde9bd4fcb186818441ef6e096ad628f43f308f44e", - "sourceCodeHash": "0xd2382fa63191a6f313c685fc91609006e94e643031b2d3787921358a652f28bc" + "initCodeHash": "0xa99f0ccc0562b7030453cde2b02cb2c9a991d08b20eeb813a2d19c566e57187f", + "sourceCodeHash": "0x2b3571405432accb486109ff6b8088350359f2bad29b22c13c0d3e6e4bb271e5" }, "src/L2/BaseFeeVault.sol:BaseFeeVault": { "initCodeHash": "0x18650c65fa6f23fefc6f0c5fff605f12be99fce0bfbcbc0644403138e4c8bb12", diff --git a/packages/contracts-bedrock/src/L1/opcm/OPContractsManagerV2.sol b/packages/contracts-bedrock/src/L1/opcm/OPContractsManagerV2.sol index 2203cdde882..10479a30941 100644 --- a/packages/contracts-bedrock/src/L1/opcm/OPContractsManagerV2.sol +++ b/packages/contracts-bedrock/src/L1/opcm/OPContractsManagerV2.sol @@ -158,9 +158,9 @@ contract OPContractsManagerV2 is ISemver, OPContractsManagerUtilsCaller { /// - Major bump: New required sequential upgrade /// - Minor bump: Replacement OPCM for same upgrade /// - Patch bump: Development changes (expected for normal dev work) - /// @custom:semver 7.1.23 + /// @custom:semver 7.1.24 function version() public pure returns (string memory) { - return "7.1.23"; + return "7.1.24"; } /// @param _standardValidator The standard validator for this OPCM release. @@ -709,11 +709,23 @@ contract OPContractsManagerV2 is ISemver, OPContractsManagerUtilsCaller { revert OPContractsManagerV2_InvalidGameConfigs(); } + // The starting anchor root must be set for an initial deployment. + if (_isInitialDeployment && _cfg.startingAnchorRoot.root.raw() == bytes32(0)) { + revert OPContractsManagerV2_InvalidGameConfigs(); + } + // Iterate over each provided config and confirm that it matches the game type array. // This places a requirement on the user to order the configs properly but that's // probably a good thing, keeps the config consistent. for (uint256 i = 0; i < _cfg.disputeGameConfigs.length; i++) { - if (_cfg.disputeGameConfigs[i].gameType.raw() != validGameTypes[i].raw()) { + uint32 rawGameType = validGameTypes[i].raw(); + bool isCannonGame = rawGameType == GameTypes.CANNON.raw(); + bool isPermissionedCannonGame = rawGameType == GameTypes.PERMISSIONED_CANNON.raw(); + bool isCannonKonaGame = rawGameType == GameTypes.CANNON_KONA.raw(); + bool isSuperPermissionedGame = rawGameType == GameTypes.SUPER_PERMISSIONED.raw(); + bool isZkDisputeGame = rawGameType == GameTypes.ZK_DISPUTE_GAME.raw(); + + if (_cfg.disputeGameConfigs[i].gameType.raw() != rawGameType) { revert OPContractsManagerV2_InvalidGameConfigs(); } @@ -722,39 +734,52 @@ contract OPContractsManagerV2 is ISemver, OPContractsManagerUtilsCaller { revert OPContractsManagerV2_InvalidGameConfigs(); } - if ( - _cfg.disputeGameConfigs[i].gameType.raw() == GameTypes.SUPER_PERMISSIONED.raw() - && _cfg.disputeGameConfigs[i].initBond != 0 - ) { + if (isSuperPermissionedGame && _cfg.disputeGameConfigs[i].initBond != 0) { revert OPContractsManagerV2_InvalidGameConfigs(); } // If game is enabled, we must have a non-zero init bond, except // SUPER_PERMISSIONED which does not use bonds. if ( - _cfg.disputeGameConfigs[i].gameType.raw() != GameTypes.SUPER_PERMISSIONED.raw() - && _cfg.disputeGameConfigs[i].enabled && _cfg.disputeGameConfigs[i].initBond == 0 + !isSuperPermissionedGame && _cfg.disputeGameConfigs[i].enabled + && _cfg.disputeGameConfigs[i].initBond == 0 ) { revert OPContractsManagerV2_InvalidGameConfigs(); } - // Check if this is a permissioned type. - bool isPermissioned = validGameTypes[i].raw() == GameTypes.PERMISSIONED_CANNON.raw() - || validGameTypes[i].raw() == GameTypes.SUPER_PERMISSIONED.raw(); + // Post-U19 initial deployments may enable only PERMISSIONED_CANNON, + // CANNON_KONA, and SUPER_PERMISSIONED. Legacy CANNON is rejected. + bool enableableAtInitialDeployment = isPermissionedCannonGame || isCannonKonaGame || isSuperPermissionedGame; - // During initial deployment, only permissioned types can be enabled, because no - // prestate exists for permissionless games. - if (_isInitialDeployment && !isPermissioned && _cfg.disputeGameConfigs[i].enabled) { + if (_isInitialDeployment && !enableableAtInitialDeployment && _cfg.disputeGameConfigs[i].enabled) { revert OPContractsManagerV2_InvalidGameConfigs(); } // ZK_DISPUTE_GAME can only be enabled when the dev flag is on (upgrade path). if ( - validGameTypes[i].raw() == GameTypes.ZK_DISPUTE_GAME.raw() && _cfg.disputeGameConfigs[i].enabled + isZkDisputeGame && _cfg.disputeGameConfigs[i].enabled && !isDevFeatureEnabled(DevFeatures.ZK_DISPUTE_GAME) ) { revert OPContractsManagerV2_InvalidGameConfigs(); } + + if (_cfg.disputeGameConfigs[i].enabled && (isCannonGame || isCannonKonaGame)) { + // TODO(#20912): Remove once deploy pipelines provide real anchor roots. + // A permissionless initial deployment must not use the placeholder anchor root. + if ( + _isInitialDeployment + && _cfg.startingAnchorRoot.root.raw() == Constants.PLACEHOLDER_STARTING_ANCHOR_ROOT + ) { + revert OPContractsManagerV2_InvalidGameConfigs(); + } + + // If a permissionless game is being enabled the prestate must be not empty, otherwise revert. + IOPContractsManagerUtils.FaultDisputeGameConfig memory faultGameConfig = + abi.decode(_cfg.disputeGameConfigs[i].gameArgs, (IOPContractsManagerUtils.FaultDisputeGameConfig)); + if (faultGameConfig.absolutePrestate.raw() == bytes32(0)) { + revert OPContractsManagerV2_InvalidGameConfigs(); + } + } } // Validate that the starting respected game type corresponds to an enabled game config. diff --git a/packages/contracts-bedrock/src/libraries/Constants.sol b/packages/contracts-bedrock/src/libraries/Constants.sol index da3e689f840..5e5d5d12d28 100644 --- a/packages/contracts-bedrock/src/libraries/Constants.sol +++ b/packages/contracts-bedrock/src/libraries/Constants.sol @@ -57,6 +57,9 @@ library Constants { /// @notice Current bundle artifact path for Network Upgrade Transaction bundles. string internal constant CURRENT_BUNDLE_PATH = "snapshots/upgrades/current-upgrade-bundle.json"; + /// @notice Placeholder anchor root historically used for permissioned initial deployments. + bytes32 internal constant PLACEHOLDER_STARTING_ANCHOR_ROOT = bytes32(hex"dead"); + /// @notice Returns the default values for the ResourceConfig. These are the recommended values /// for a production network. function DEFAULT_RESOURCE_CONFIG() internal pure returns (IResourceMetering.ResourceConfig memory) { diff --git a/packages/contracts-bedrock/test/L1/OPContractsManagerStandardValidator.t.sol b/packages/contracts-bedrock/test/L1/OPContractsManagerStandardValidator.t.sol index 7ddcdb06bf1..e2d2f0d40e7 100644 --- a/packages/contracts-bedrock/test/L1/OPContractsManagerStandardValidator.t.sol +++ b/packages/contracts-bedrock/test/L1/OPContractsManagerStandardValidator.t.sol @@ -227,7 +227,7 @@ abstract contract OPContractsManagerStandardValidator_TestInit is CommonTest { ); } else { l2ChainId = deploy.cfg().l2ChainID(); - cannonPrestate = Claim.wrap(bytes32(deploy.cfg().faultGameAbsolutePrestate())); + cannonPrestate = Claim.wrap(keccak256("cannonPrestate")); proposer = deploy.cfg().l2OutputOracleProposer(); challenger = deploy.cfg().l2OutputOracleChallenger(); } @@ -2129,7 +2129,7 @@ abstract contract OPContractsManagerStandardValidator_ZKMode_TestInit is CommonT ); } else { l2ChainId = deploy.cfg().l2ChainID(); - cannonPrestate = Claim.wrap(bytes32(deploy.cfg().faultGameAbsolutePrestate())); + cannonPrestate = Claim.wrap(keccak256("cannonPrestate")); proposer = deploy.cfg().l2OutputOracleProposer(); challenger = deploy.cfg().l2OutputOracleChallenger(); diff --git a/packages/contracts-bedrock/test/L1/opcm/OPContractsManagerV2.t.sol b/packages/contracts-bedrock/test/L1/opcm/OPContractsManagerV2.t.sol index 4bd3b111b5d..a11f5557110 100644 --- a/packages/contracts-bedrock/test/L1/opcm/OPContractsManagerV2.t.sol +++ b/packages/contracts-bedrock/test/L1/opcm/OPContractsManagerV2.t.sol @@ -289,7 +289,9 @@ contract OPContractsManagerV2_Upgrade_TestInit is OPContractsManagerV2_TestInit v2UpgradeInput.disputeGameConfigs.push( IOPContractsManagerUtils.DisputeGameConfig({ enabled: true, - initBond: disputeGameFactory.initBonds(GameTypes.CANNON_KONA), + initBond: DisputeGames.permissionlessGameInitBondForUpgrade( + disputeGameFactory, GameTypes.CANNON_KONA, DEFAULT_DISPUTE_GAME_INIT_BOND + ), gameType: GameTypes.CANNON_KONA, gameArgs: abi.encode( IOPContractsManagerUtils.FaultDisputeGameConfig({ absolutePrestate: cannonKonaPrestate }) @@ -745,7 +747,9 @@ contract OPContractsManagerV2_Upgrade_Test is OPContractsManagerV2_Upgrade_TestI /// @notice Tests that repeatedly upgrading can enable a previously disabled game type. function test_upgrade_enableGameType_succeeds() public { - uint256 originalBond = disputeGameFactory.initBonds(GameTypes.CANNON_KONA); + uint256 originalBond = DisputeGames.permissionlessGameInitBondForUpgrade( + disputeGameFactory, GameTypes.CANNON_KONA, DEFAULT_DISPUTE_GAME_INIT_BOND + ); // First, disable CannonKona and clear its bond so the factory entry is removed. // CANNON_KONA is the respected game type, so we must override it to PERMISSIONED_CANNON @@ -1805,7 +1809,6 @@ contract OPContractsManagerV2_Deploy_Test is OPContractsManagerV2_TestInit { maximumBaseFee: type(uint128).max }); - // Set up dispute game configs. All 7 game types are required. // In super root mode, SUPER_PERMISSIONED is enabled; otherwise PERMISSIONED_CANNON. address initialChallenger = makeAddr("deployChallenger"); address initialProposer = makeAddr("deployProposer"); @@ -1960,9 +1963,11 @@ contract OPContractsManagerV2_Deploy_Test is OPContractsManagerV2_TestInit { /// @notice Tests that deploy reverts when an enabled game has a zero init bond. function test_deploy_enabledGameZeroBond_reverts() public { - // Enable Cannon but keep a zero init bond. - deployConfig.disputeGameConfigs[0].enabled = true; - deployConfig.disputeGameConfigs[0].initBond = 0; + // Enable CANNON_KONA but keep a zero init bond. + deployConfig.disputeGameConfigs[2].enabled = true; + deployConfig.disputeGameConfigs[2].initBond = 0; + deployConfig.disputeGameConfigs[2].gameArgs = + abi.encode(IOPContractsManagerUtils.FaultDisputeGameConfig({ absolutePrestate: cannonKonaPrestate })); // nosemgrep: sol-style-use-abi-encodecall runDeployV2( @@ -2007,9 +2012,60 @@ contract OPContractsManagerV2_Deploy_Test is OPContractsManagerV2_TestInit { ); } + /// @notice Deploy reverts when legacy CANNON is enabled, even with a valid prestate. function test_deploy_cannonGameEnabled_reverts() public { deployConfig.disputeGameConfigs[0].enabled = true; - deployConfig.disputeGameConfigs[0].initBond = 1 ether; + deployConfig.disputeGameConfigs[0].initBond = DEFAULT_DISPUTE_GAME_INIT_BOND; + deployConfig.disputeGameConfigs[0].gameArgs = + abi.encode(IOPContractsManagerUtils.FaultDisputeGameConfig({ absolutePrestate: cannonPrestate })); + deployConfig.startingRespectedGameType = GameTypes.CANNON; + + // nosemgrep: sol-style-use-abi-encodecall + runDeployV2( + deployConfig, abi.encodeWithSelector(IOPContractsManagerV2.OPContractsManagerV2_InvalidGameConfigs.selector) + ); + } + + /// @notice CANNON_KONA may be enabled at initial deployment now that its prestate is supplied via + /// the deploy config. + function test_deploy_cannonKonaGameEnabled_succeeds() public { + deployConfig.disputeGameConfigs[2].enabled = true; + deployConfig.disputeGameConfigs[2].initBond = DEFAULT_DISPUTE_GAME_INIT_BOND; + deployConfig.disputeGameConfigs[2].gameArgs = + abi.encode(IOPContractsManagerUtils.FaultDisputeGameConfig({ absolutePrestate: cannonKonaPrestate })); + deployConfig.startingRespectedGameType = GameTypes.CANNON_KONA; + + IOPContractsManagerV2.ChainContracts memory cts = opcmV2.deploy(deployConfig); + assertNotEq( + address(cts.disputeGameFactory.gameImpls(GameTypes.CANNON_KONA)), + address(0), + "CANNON_KONA impl should be set" + ); + if (!isDevFeatureEnabled(DevFeatures.SUPER_ROOT_GAMES_MIGRATION)) { + assertNotEq( + address(cts.disputeGameFactory.gameImpls(GameTypes.PERMISSIONED_CANNON)), + address(0), + "PERMISSIONED_CANNON fallback should be registered alongside CANNON_KONA" + ); + } + assertEq( + cts.anchorStateRegistry.respectedGameType().raw(), + GameTypes.CANNON_KONA.raw(), + "respected game type mismatch" + ); + } + + /// @notice Deploy reverts when legacy CANNON is enabled alongside CANNON_KONA. + function test_deploy_multiplePermissionlessGamesEnabled_reverts() public { + deployConfig.disputeGameConfigs[0].enabled = true; + deployConfig.disputeGameConfigs[0].initBond = DEFAULT_DISPUTE_GAME_INIT_BOND; + deployConfig.disputeGameConfigs[0].gameArgs = + abi.encode(IOPContractsManagerUtils.FaultDisputeGameConfig({ absolutePrestate: cannonPrestate })); + deployConfig.disputeGameConfigs[2].enabled = true; + deployConfig.disputeGameConfigs[2].initBond = DEFAULT_DISPUTE_GAME_INIT_BOND; + deployConfig.disputeGameConfigs[2].gameArgs = + abi.encode(IOPContractsManagerUtils.FaultDisputeGameConfig({ absolutePrestate: cannonKonaPrestate })); + deployConfig.startingRespectedGameType = GameTypes.CANNON_KONA; // nosemgrep: sol-style-use-abi-encodecall runDeployV2( @@ -2017,9 +2073,63 @@ contract OPContractsManagerV2_Deploy_Test is OPContractsManagerV2_TestInit { ); } - function test_deploy_cannonKonaGameEnabled_reverts() public { + /// @notice Deploy reverts when CANNON_KONA is enabled with a zero prestate. + function test_deploy_cannonKonaGameEnabledZeroPrestate_reverts() public { deployConfig.disputeGameConfigs[2].enabled = true; - deployConfig.disputeGameConfigs[2].initBond = 1 ether; + deployConfig.disputeGameConfigs[2].initBond = DEFAULT_DISPUTE_GAME_INIT_BOND; + deployConfig.disputeGameConfigs[2].gameArgs = + abi.encode(IOPContractsManagerUtils.FaultDisputeGameConfig({ absolutePrestate: Claim.wrap(bytes32(0)) })); + + // nosemgrep: sol-style-use-abi-encodecall + runDeployV2( + deployConfig, abi.encodeWithSelector(IOPContractsManagerV2.OPContractsManagerV2_InvalidGameConfigs.selector) + ); + } + + /// @notice Deploy reverts when an initial deployment uses a zero starting anchor root. + function test_deploy_zeroStartingAnchorRoot_reverts() public { + deployConfig.startingAnchorRoot = Proposal({ root: Hash.wrap(bytes32(0)), l2SequenceNumber: 0 }); + + // nosemgrep: sol-style-use-abi-encodecall + runDeployV2( + deployConfig, abi.encodeWithSelector(IOPContractsManagerV2.OPContractsManagerV2_InvalidGameConfigs.selector) + ); + } + + /// @notice Deploy reverts when an initial permissionless deploy uses a zero starting anchor root. + function test_deploy_permissionlessZeroStartingAnchorRoot_reverts() public { + deployConfig.disputeGameConfigs[2].enabled = true; + deployConfig.disputeGameConfigs[2].initBond = DEFAULT_DISPUTE_GAME_INIT_BOND; + deployConfig.disputeGameConfigs[2].gameArgs = + abi.encode(IOPContractsManagerUtils.FaultDisputeGameConfig({ absolutePrestate: cannonKonaPrestate })); + deployConfig.startingRespectedGameType = GameTypes.CANNON_KONA; + deployConfig.startingAnchorRoot = Proposal({ root: Hash.wrap(bytes32(0)), l2SequenceNumber: 0 }); + + // nosemgrep: sol-style-use-abi-encodecall + runDeployV2( + deployConfig, abi.encodeWithSelector(IOPContractsManagerV2.OPContractsManagerV2_InvalidGameConfigs.selector) + ); + } + + /// @notice Deploy reverts when an initial permissionless deploy uses the permissioned placeholder anchor root. + function test_deploy_permissionlessPlaceholderStartingAnchorRoot_reverts() public { + deployConfig.disputeGameConfigs[2].enabled = true; + deployConfig.disputeGameConfigs[2].initBond = DEFAULT_DISPUTE_GAME_INIT_BOND; + deployConfig.disputeGameConfigs[2].gameArgs = + abi.encode(IOPContractsManagerUtils.FaultDisputeGameConfig({ absolutePrestate: cannonKonaPrestate })); + deployConfig.startingAnchorRoot = + Proposal({ root: Hash.wrap(Constants.PLACEHOLDER_STARTING_ANCHOR_ROOT), l2SequenceNumber: 0 }); + + // nosemgrep: sol-style-use-abi-encodecall + runDeployV2( + deployConfig, abi.encodeWithSelector(IOPContractsManagerV2.OPContractsManagerV2_InvalidGameConfigs.selector) + ); + } + + /// @notice SUPER_CANNON_KONA may not be enabled at initial deployment. + function test_deploy_superCannonKonaGameEnabled_reverts() public { + deployConfig.disputeGameConfigs[4].enabled = true; + deployConfig.disputeGameConfigs[4].initBond = 1 ether; // nosemgrep: sol-style-use-abi-encodecall runDeployV2( @@ -2037,6 +2147,19 @@ contract OPContractsManagerV2_Deploy_Test is OPContractsManagerV2_TestInit { assertEq(cts.anchorStateRegistry.respectedGameType().raw(), permType.raw(), "respected game type mismatch"); } + /// @notice The 0xdead placeholder anchor remains allowed for initial permissioned deployments. + function test_deploy_permissionedPlaceholderStartingAnchorRoot_succeeds() public { + deployConfig.startingAnchorRoot = + Proposal({ root: Hash.wrap(Constants.PLACEHOLDER_STARTING_ANCHOR_ROOT), l2SequenceNumber: 0 }); + bool superRoot = isDevFeatureEnabled(DevFeatures.SUPER_ROOT_GAMES_MIGRATION); + string memory expectedErrors = superRoot ? "SCKDG-SHAPE,SCKDG-10" : "CKDG-NOSHAPE,CKDG-10"; + IOPContractsManagerV2.ChainContracts memory cts = runDeployV2(deployConfig, bytes(""), expectedErrors); + Proposal memory startingAnchorRoot = cts.anchorStateRegistry.getStartingAnchorRoot(); + assertEq( + startingAnchorRoot.root.raw(), Constants.PLACEHOLDER_STARTING_ANCHOR_ROOT, "starting anchor root mismatch" + ); + } + /// @notice CANNON_KONA as respected game type reverts because its dispute game is not enabled /// during initial deployment. function test_deploy_cannonKonaRespectedGameType_reverts() public { diff --git a/packages/contracts-bedrock/test/dispute/FaultDisputeGame.t.sol b/packages/contracts-bedrock/test/dispute/FaultDisputeGame.t.sol index 187a38303f0..1cc02d75ba0 100644 --- a/packages/contracts-bedrock/test/dispute/FaultDisputeGame.t.sol +++ b/packages/contracts-bedrock/test/dispute/FaultDisputeGame.t.sol @@ -139,6 +139,15 @@ abstract contract BaseFaultDisputeGame_TestInit is DisputeGameFactory_TestInit { receive() external payable { } + function respectGameType(GameType _gameType) internal { + if (anchorStateRegistry.respectedGameType().raw() == _gameType.raw()) { + return; + } + + vm.prank(superchainConfig.guardian()); + anchorStateRegistry.setRespectedGameType(_gameType); + } + function copyBytes(bytes memory src, bytes memory dest) internal pure returns (bytes memory) { uint256 byteCount = src.length < dest.length ? src.length : dest.length; for (uint256 i = 0; i < byteCount; i++) { @@ -168,6 +177,7 @@ abstract contract FaultDisputeGame_TestInit is BaseFaultDisputeGame_TestInit { absolutePrestate = _changeClaimStatus(Claim.wrap(keccak256(absolutePrestateData)), VMStatuses.UNFINISHED); super.setUp(); + respectGameType(GAME_TYPE); // Get the actual anchor roots (Hash root, uint256 l2Bn) = anchorStateRegistry.getAnchorRoot(); diff --git a/packages/contracts-bedrock/test/opcm/DeployOPChain.t.sol b/packages/contracts-bedrock/test/opcm/DeployOPChain.t.sol index 66a7b06abab..f9c65067f3f 100644 --- a/packages/contracts-bedrock/test/opcm/DeployOPChain.t.sol +++ b/packages/contracts-bedrock/test/opcm/DeployOPChain.t.sol @@ -16,13 +16,16 @@ import { Types } from "scripts/libraries/Types.sol"; import { Constants } from "src/libraries/Constants.sol"; import { Features } from "src/libraries/Features.sol"; import { DevFeatures } from "src/libraries/DevFeatures.sol"; +import { LibGameArgs } from "src/dispute/lib/LibGameArgs.sol"; // Interfaces import { IOPContractsManagerV2 } from "interfaces/L1/opcm/IOPContractsManagerV2.sol"; import { IOPContractsManagerContainer } from "interfaces/L1/opcm/IOPContractsManagerContainer.sol"; import { IResourceMetering } from "interfaces/L1/IResourceMetering.sol"; -import { Claim, Duration, GameType, GameTypes } from "src/dispute/lib/Types.sol"; +import { Claim, Duration, GameType, GameTypes, Hash, Proposal } from "src/dispute/lib/Types.sol"; +import { IDisputeGame } from "interfaces/dispute/IDisputeGame.sol"; import { IPermissionedDisputeGame } from "interfaces/dispute/IPermissionedDisputeGame.sol"; +import { IAnchorStateRegistry } from "interfaces/dispute/IAnchorStateRegistry.sol"; import { ISuperchainConfig } from "interfaces/L1/ISuperchainConfig.sol"; contract DeployOPChain_TestBase is Test, FeatureFlags { @@ -59,6 +62,9 @@ contract DeployOPChain_TestBase is Test, FeatureFlags { uint64 gasLimit = 60_000_000; GameType disputeGameType = GameTypes.PERMISSIONED_CANNON; Claim disputeAbsolutePrestate = Claim.wrap(0x038512e02c4c3f7bdaec27d00edf55b7155e0905301e1a88083e4e0a6764d54c); + Hash startingAnchorRoot = Hash.wrap(0xdead000000000000000000000000000000000000000000000000000000000000); + // Non-placeholder anchor root for the permissionless deploy tests. + Hash permissionlessAnchorRoot = Hash.wrap(0x02f4397b2de6fce03b3f9982378c2b4c4deff9c92c662dcc6f9643267aeb5e47); uint256 disputeMaxGameDepth = 73; uint256 disputeSplitDepth = 30; Duration disputeClockExtension = Duration.wrap(3 hours); @@ -126,6 +132,7 @@ contract DeployOPChain_TestBase is Test, FeatureFlags { gasLimit: gasLimit, disputeGameType: disputeGameType, disputeAbsolutePrestate: disputeAbsolutePrestate, + startingAnchorRoot: startingAnchorRoot, disputeMaxGameDepth: disputeMaxGameDepth, disputeSplitDepth: disputeSplitDepth, disputeClockExtension: disputeClockExtension, @@ -223,18 +230,103 @@ contract DeployOPChain_Test is DeployOPChain_TestBase { _checkDeploymentAssertions(doo); } + /// @notice Legacy CANNON is rejected as an initial deployment game type. function test_run_cannonGameType_reverts() public { deployOPChainInput.disputeGameType = GameTypes.CANNON; - vm.expectRevert("DeployOPChain: only PERMISSIONED_CANNON game type is supported for initial deployment"); + + vm.expectRevert("DeployOPChain: unsupported dispute game type"); deployOPChain.run(deployOPChainInput); } - function test_run_cannonKonaGameType_reverts() public { + /// @notice Non-super-root CANNON_KONA deploys respect CANNON_KONA and register + /// PERMISSIONED_CANNON for guardian fallback. + function test_run_cannonKonaGameType_succeeds() public { + skipIfDevFeatureEnabled(DevFeatures.SUPER_ROOT_GAMES_MIGRATION); + deployOPChainInput.disputeGameType = GameTypes.CANNON_KONA; + deployOPChainInput.startingAnchorRoot = permissionlessAnchorRoot; + + DeployOPChain.Output memory doo = deployOPChain.run(deployOPChainInput); + _checkCannonKonaPermissionlessDeployment(doo); + } + + /// @notice Verifies the guardian can switch a CANNON_KONA deploy to PERMISSIONED_CANNON + /// and the trusted proposer can create a respected fallback game. + function test_run_cannonKonaGameTypeFallback_succeeds() public { + skipIfDevFeatureEnabled(DevFeatures.SUPER_ROOT_GAMES_MIGRATION); + deployOPChainInput.disputeGameType = GameTypes.CANNON_KONA; + deployOPChainInput.startingAnchorRoot = permissionlessAnchorRoot; + DeployOPChain.Output memory doo = deployOPChain.run(deployOPChainInput); + + IAnchorStateRegistry asr = doo.anchorStateRegistryProxy; + vm.prank(doo.systemConfigProxy.guardian()); + asr.setRespectedGameType(GameTypes.PERMISSIONED_CANNON); + + uint256 bond = doo.disputeGameFactoryProxy.initBonds(GameTypes.PERMISSIONED_CANNON); + vm.deal(proposer, bond); + vm.prank(proposer, proposer); + IDisputeGame game = doo.disputeGameFactoryProxy.create{ value: bond }( + GameTypes.PERMISSIONED_CANNON, Claim.wrap(keccak256("fallback proposal")), abi.encode(uint256(1)) + ); + assertTrue(asr.isGameRespected(game), "fallback game must be respected"); + } + + /// @notice Permissionless game types are rejected when super roots are enabled. + function test_run_permissionlessGameTypeWithSuperRoot_reverts() public { + skipIfDevFeatureDisabled(DevFeatures.SUPER_ROOT_GAMES_MIGRATION); deployOPChainInput.disputeGameType = GameTypes.CANNON_KONA; - vm.expectRevert("DeployOPChain: only PERMISSIONED_CANNON game type is supported for initial deployment"); + deployOPChainInput.startingAnchorRoot = permissionlessAnchorRoot; + vm.expectRevert("DeployOPChain: permissionless game type not supported with super roots"); deployOPChain.run(deployOPChainInput); } + /// @notice Asserts non-super-root CANNON_KONA deploys register the permissioned fallback + /// with matching bond, prestate, proposer, and challenger. + /// + /// @param doo The deployment output. + function _checkCannonKonaPermissionlessDeployment(DeployOPChain.Output memory doo) internal view { + IOPContractsManagerContainer.Implementations memory impls = IOPContractsManagerV2(opcmAddr).implementations(); + assertEq( + doo.disputeGameFactoryProxy.initBonds(GameTypes.CANNON_KONA), + deployOPChain.DEFAULT_INIT_BOND(), + "selected init bond" + ); + assertEq( + address(doo.disputeGameFactoryProxy.gameImpls(GameTypes.CANNON_KONA)), + impls.faultDisputeGameImpl, + "selected impl" + ); + assertEq(doo.disputeGameFactoryProxy.initBonds(GameTypes.CANNON), 0, "unselected init bond"); + assertEq(address(doo.disputeGameFactoryProxy.gameImpls(GameTypes.CANNON)), address(0), "unselected impl"); + assertEq(doo.disputeGameFactoryProxy.gameArgs(GameTypes.CANNON).length, 0, "unselected args"); + assertEq( + doo.disputeGameFactoryProxy.initBonds(GameTypes.PERMISSIONED_CANNON), + deployOPChain.DEFAULT_INIT_BOND(), + "fallback init bond" + ); + assertEq( + address(doo.disputeGameFactoryProxy.gameImpls(GameTypes.PERMISSIONED_CANNON)), + impls.permissionedDisputeGameImpl, + "fallback impl" + ); + + assertEq( + LibGameArgs.decode(doo.disputeGameFactoryProxy.gameArgs(GameTypes.CANNON_KONA)).absolutePrestate, + deployOPChainInput.disputeAbsolutePrestate.raw(), + "selected prestate wiring" + ); + LibGameArgs.GameArgs memory pdgArgs = + LibGameArgs.decode(doo.disputeGameFactoryProxy.gameArgs(GameTypes.PERMISSIONED_CANNON)); + assertEq(pdgArgs.absolutePrestate, deployOPChainInput.disputeAbsolutePrestate.raw(), "fallback prestate"); + assertEq(pdgArgs.proposer, proposer, "fallback proposer"); + assertEq(pdgArgs.challenger, challenger, "fallback challenger"); + + IAnchorStateRegistry asr = doo.anchorStateRegistryProxy; + assertEq(asr.respectedGameType().raw(), GameTypes.CANNON_KONA.raw(), "respected game type"); + Proposal memory anchor = asr.getStartingAnchorRoot(); + assertEq(anchor.root.raw(), deployOPChainInput.startingAnchorRoot.raw(), "anchor root"); + assertEq(anchor.l2SequenceNumber, 0, "anchor seq"); + } + /// @notice Tests that faultDisputeGame is set to address(0) and permissionedDisputeGame is set to the correct /// implementation for GameTypes.PERMISSIONED_CANNON. function test_run_faultDisputeGamePermissionedCannon_succeeds() public { @@ -295,7 +387,7 @@ contract DeployOPChain_Test is DeployOPChain_TestBase { assertEq(doo.disputeGameFactoryProxy.initBonds(permType), expectedInitBond); assertNotEq(address(doo.disputeGameFactoryProxy.gameImpls(permType)), address(0)); - // CANNON must be disabled for initial deployment (not deployed for super root path) + // CANNON must be disabled for the default permissioned initial deployment. if (!isSuperRoot) { assertEq(doo.disputeGameFactoryProxy.initBonds(GameTypes.CANNON), 0, "CANNON init bond should be 0"); assertEq( @@ -305,13 +397,19 @@ contract DeployOPChain_Test is DeployOPChain_TestBase { ); } - // Kona must be disabled for initial deployment + // Kona must be disabled for the default permissioned initial deployment. assertEq(doo.disputeGameFactoryProxy.initBonds(konaType), 0, "CANNON_KONA init bond should be 0"); assertEq( address(doo.disputeGameFactoryProxy.gameImpls(konaType)), address(0), "CANNON_KONA impl should be the zero address" ); + + IAnchorStateRegistry asr = doo.anchorStateRegistryProxy; + assertEq(asr.respectedGameType().raw(), permType.raw(), "ASR respected game type"); + Proposal memory anchor = asr.getStartingAnchorRoot(); + assertEq(anchor.root.raw(), deployOPChainInput.startingAnchorRoot.raw(), "ASR anchor root"); + assertEq(anchor.l2SequenceNumber, 0, "ASR anchor seq"); } } @@ -421,6 +519,20 @@ contract DeployOPChain_TestFail is DeployOPChain_TestBase { deployOPChain.run(deployOPChainInput); } + function test_run_zeroStartingAnchorRoot_reverts() public { + deployOPChainInput.startingAnchorRoot = Hash.wrap(bytes32(0)); + vm.expectRevert("DeployOPChainInput: startingAnchorRoot not set"); + deployOPChain.run(deployOPChainInput); + } + + function test_run_permissionlessPlaceholderStartingAnchorRoot_reverts() public { + skipIfDevFeatureEnabled(DevFeatures.SUPER_ROOT_GAMES_MIGRATION); + deployOPChainInput.disputeGameType = GameTypes.CANNON_KONA; + // startingAnchorRoot stays at the 0xdead placeholder default. + vm.expectRevert("DeployOPChainInput: permissionless startingAnchorRoot cannot be placeholder"); + deployOPChain.run(deployOPChainInput); + } + function test_runWithBytes_invalidInput_reverts() public { // It should revert if the input bytes cannot be decoded. bytes memory invalidInput = "invalid"; diff --git a/packages/contracts-bedrock/test/setup/DisputeGames.sol b/packages/contracts-bedrock/test/setup/DisputeGames.sol index 0dcd4799497..482214616d4 100644 --- a/packages/contracts-bedrock/test/setup/DisputeGames.sol +++ b/packages/contracts-bedrock/test/setup/DisputeGames.sol @@ -168,6 +168,28 @@ library DisputeGames { } } + /// @notice Returns the live init bond for a registered game, or the default. + /// @param _dgf Dispute game factory. + /// @param _gameType Game type. + /// @param _defaultInitBond Fallback bond. + /// @return Init bond for upgrade config. + function permissionlessGameInitBondForUpgrade( + IDisputeGameFactory _dgf, + GameType _gameType, + uint256 _defaultInitBond + ) + internal + view + returns (uint256) + { + // Forks can retain bonds for game types with no implementation. + if (address(_dgf.gameImpls(_gameType)) == address(0)) { + return _defaultInitBond; + } + uint256 initBond = _dgf.initBonds(_gameType); + return initBond == 0 ? _defaultInitBond : initBond; + } + function mockGameImplPrestate(IDisputeGameFactory _dgf, GameType _gameType, bytes32 _prestate) internal { bytes memory value = abi.encodePacked(_prestate); _mockGameArg(_dgf, _gameType, GameArg.PRESTATE, value); diff --git a/packages/contracts-bedrock/test/setup/DisputeGames.t.sol b/packages/contracts-bedrock/test/setup/DisputeGames.t.sol new file mode 100644 index 00000000000..5501d704e6c --- /dev/null +++ b/packages/contracts-bedrock/test/setup/DisputeGames.t.sol @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.15; + +// Testing +import { CommonTest } from "test/setup/CommonTest.sol"; +import { DisputeGames } from "test/setup/DisputeGames.sol"; + +// Libraries +import { GameTypes } from "src/dispute/lib/Types.sol"; + +// Interfaces +import { IDisputeGame } from "interfaces/dispute/IDisputeGame.sol"; + +/// @title DisputeGames_PermissionlessGameInitBondForUpgrade_Test +/// @notice Tests for the `DisputeGames.permissionlessGameInitBondForUpgrade` helper. +contract DisputeGames_PermissionlessGameInitBondForUpgrade_Test is CommonTest { + /// @notice Non-default bond used in assertions. + uint256 internal constant STALE_INIT_BOND = 1 ether; + + /// @notice Sets the CANNON_KONA implementation and bond. + function _setFactoryState(address _impl, uint256 _initBond) internal { + vm.startPrank(disputeGameFactory.owner()); + disputeGameFactory.setImplementation(GameTypes.CANNON_KONA, IDisputeGame(_impl)); + disputeGameFactory.setInitBond(GameTypes.CANNON_KONA, _initBond); + vm.stopPrank(); + } + + /// @notice Uses the default when a stale bond has no implementation. + function test_permissionlessGameInitBondForUpgrade_staleBondWithoutImpl_succeeds() public { + _setFactoryState(address(0), STALE_INIT_BOND); + uint256 bond = DisputeGames.permissionlessGameInitBondForUpgrade( + disputeGameFactory, GameTypes.CANNON_KONA, DEFAULT_DISPUTE_GAME_INIT_BOND + ); + assertEq(bond, DEFAULT_DISPUTE_GAME_INIT_BOND, "stale bond must not override the default"); + } + + /// @notice Uses the default when a registered game's bond is zero. + function test_permissionlessGameInitBondForUpgrade_zeroBondWithImpl_succeeds() public { + _setFactoryState(address(0xdead), 0); + uint256 bond = DisputeGames.permissionlessGameInitBondForUpgrade( + disputeGameFactory, GameTypes.CANNON_KONA, DEFAULT_DISPUTE_GAME_INIT_BOND + ); + assertEq(bond, DEFAULT_DISPUTE_GAME_INIT_BOND, "zero bond must fall back to the default"); + } + + /// @notice Uses the live bond when a registered game's bond is nonzero. + function test_permissionlessGameInitBondForUpgrade_liveBondWithImpl_succeeds() public { + _setFactoryState(address(0xdead), STALE_INIT_BOND); + uint256 bond = DisputeGames.permissionlessGameInitBondForUpgrade( + disputeGameFactory, GameTypes.CANNON_KONA, DEFAULT_DISPUTE_GAME_INIT_BOND + ); + assertEq(bond, STALE_INIT_BOND, "live bond must be used when the game is registered"); + } +} diff --git a/packages/contracts-bedrock/test/setup/ForkL1Live.s.sol b/packages/contracts-bedrock/test/setup/ForkL1Live.s.sol index 1d1c7b265d5..d3cff3e52e7 100644 --- a/packages/contracts-bedrock/test/setup/ForkL1Live.s.sol +++ b/packages/contracts-bedrock/test/setup/ForkL1Live.s.sol @@ -49,6 +49,8 @@ contract ForkL1Live is Deployer, StdAssertions, FeatureFlags { using stdToml for string; using LibString for string; + uint256 internal constant DEFAULT_PERMISSIONLESS_INIT_BOND = 0.08 ether; + bool public useOpsRepo; /// @notice Returns the base chain name to use for forking @@ -365,6 +367,10 @@ contract ForkL1Live is Deployer, StdAssertions, FeatureFlags { address proposer = DisputeGames.permissionedGameProposer(disputeGameFactory); // Standard upgrade path: CANNON disabled, remaining legacy types enabled, super types disabled. // Order must match validGameTypes in OPContractsManagerV2._assertValidFullConfig(). + uint256 cannonKonaInitBond = DisputeGames.permissionlessGameInitBondForUpgrade( + disputeGameFactory, GameTypes.CANNON_KONA, DEFAULT_PERMISSIONLESS_INIT_BOND + ); + disputeGameConfigs = new IOPContractsManagerUtils.DisputeGameConfig[](6); disputeGameConfigs[0] = IOPContractsManagerUtils.DisputeGameConfig({ enabled: false, @@ -386,7 +392,7 @@ contract ForkL1Live is Deployer, StdAssertions, FeatureFlags { }); disputeGameConfigs[2] = IOPContractsManagerUtils.DisputeGameConfig({ enabled: true, - initBond: disputeGameFactory.initBonds(GameTypes.CANNON_KONA), + initBond: cannonKonaInitBond, gameType: GameTypes.CANNON_KONA, gameArgs: abi.encode( IOPContractsManagerUtils.FaultDisputeGameConfig({