From 08fa065dbef3168f2727560bc2f20d882dfe5c53 Mon Sep 17 00:00:00 2001 From: lumoswiz Date: Fri, 26 Jun 2026 13:39:39 +0200 Subject: [PATCH 01/33] feat: add permissionless deploy input fields --- op-chain-ops/interopgen/deploy.go | 3 +++ op-deployer/pkg/deployer/opcm/opchain.go | 3 +++ op-deployer/pkg/deployer/pipeline/opchain.go | 4 ++++ packages/contracts-bedrock/scripts/libraries/Types.sol | 6 ++++-- packages/contracts-bedrock/test/opcm/DeployOPChain.t.sol | 8 +++++++- 5 files changed, 21 insertions(+), 3 deletions(-) diff --git a/op-chain-ops/interopgen/deploy.go b/op-chain-ops/interopgen/deploy.go index eeeedf44e63..568e15196c3 100644 --- a/op-chain-ops/interopgen/deploy.go +++ b/op-chain-ops/interopgen/deploy.go @@ -244,6 +244,9 @@ func DeployL2ToL1(l1Host *script.Host, superCfg *SuperchainConfig, superDeployme GasLimit: cfg.GasLimit, DisputeGameType: cfg.DisputeGameType, DisputeAbsolutePrestate: cfg.DisputeAbsolutePrestate, + StartingAnchorRoot: opcm.DefaultStartingAnchorRoot.Root, + CannonAbsolutePrestate: cfg.DisputeAbsolutePrestate, + CannonKonaAbsolutePrestate: cfg.DisputeKonaAbsolutePrestate, 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..aa95c244213 100644 --- a/op-deployer/pkg/deployer/opcm/opchain.go +++ b/op-deployer/pkg/deployer/opcm/opchain.go @@ -36,6 +36,9 @@ type DeployOPChainInput struct { DisputeGameType uint32 DisputeAbsolutePrestate common.Hash + StartingAnchorRoot common.Hash + CannonAbsolutePrestate common.Hash + CannonKonaAbsolutePrestate 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..5b54c0685aa 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 the anchor root and permissionless prestates from pipeline state. return opcm.DeployOPChainInput{ OpChainProxyAdminOwner: thisIntent.Roles.L1ProxyAdminOwner, SystemConfigOwner: thisIntent.Roles.SystemConfigOwner, @@ -149,6 +150,9 @@ func makeDCI(intent *state.Intent, thisIntent *state.ChainIntent, chainID common GasLimit: thisIntent.GasLimit, DisputeGameType: proofParams.DisputeGameType, DisputeAbsolutePrestate: proofParams.DisputeAbsolutePrestate, + StartingAnchorRoot: opcm.DefaultStartingAnchorRoot.Root, + CannonAbsolutePrestate: common.Hash{}, + CannonKonaAbsolutePrestate: common.Hash{}, 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/libraries/Types.sol b/packages/contracts-bedrock/scripts/libraries/Types.sol index 317e27dc9d0..751f1eaa1e1 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,9 @@ library Types { // Configurable dispute game inputs GameType disputeGameType; Claim disputeAbsolutePrestate; + Hash startingAnchorRoot; + Claim cannonAbsolutePrestate; + Claim cannonKonaAbsolutePrestate; uint256 disputeMaxGameDepth; uint256 disputeSplitDepth; Duration disputeClockExtension; diff --git a/packages/contracts-bedrock/test/opcm/DeployOPChain.t.sol b/packages/contracts-bedrock/test/opcm/DeployOPChain.t.sol index 66a7b06abab..2ad6f589dcc 100644 --- a/packages/contracts-bedrock/test/opcm/DeployOPChain.t.sol +++ b/packages/contracts-bedrock/test/opcm/DeployOPChain.t.sol @@ -21,7 +21,7 @@ import { DevFeatures } from "src/libraries/DevFeatures.sol"; 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 } from "src/dispute/lib/Types.sol"; import { IPermissionedDisputeGame } from "interfaces/dispute/IPermissionedDisputeGame.sol"; import { ISuperchainConfig } from "interfaces/L1/ISuperchainConfig.sol"; @@ -59,6 +59,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); + Claim cannonAbsolutePrestate = Claim.wrap(0x1111111111111111111111111111111111111111111111111111111111111111); + Claim cannonKonaAbsolutePrestate = Claim.wrap(0x2222222222222222222222222222222222222222222222222222222222222222); uint256 disputeMaxGameDepth = 73; uint256 disputeSplitDepth = 30; Duration disputeClockExtension = Duration.wrap(3 hours); @@ -126,6 +129,9 @@ contract DeployOPChain_TestBase is Test, FeatureFlags { gasLimit: gasLimit, disputeGameType: disputeGameType, disputeAbsolutePrestate: disputeAbsolutePrestate, + startingAnchorRoot: startingAnchorRoot, + cannonAbsolutePrestate: cannonAbsolutePrestate, + cannonKonaAbsolutePrestate: cannonKonaAbsolutePrestate, disputeMaxGameDepth: disputeMaxGameDepth, disputeSplitDepth: disputeSplitDepth, disputeClockExtension: disputeClockExtension, From 5971a9e30aed013c8763160acc6ec7c41fbb88a6 Mon Sep 17 00:00:00 2001 From: lumoswiz Date: Fri, 26 Jun 2026 14:38:19 +0200 Subject: [PATCH 02/33] feat: consume permissionless deploy inputs --- .../scripts/deploy/ChainAssertions.sol | 23 ++-- .../scripts/deploy/Deploy.s.sol | 4 +- .../deploy/DeployImplementations.s.sol | 6 +- .../scripts/deploy/DeployOPChain.s.sol | 115 +++++++++++++----- 4 files changed, 107 insertions(+), 41 deletions(-) diff --git a/packages/contracts-bedrock/scripts/deploy/ChainAssertions.sol b/packages/contracts-bedrock/scripts/deploy/ChainAssertions.sol index d8bfbb52164..1ec36df8bd0 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,14 +422,17 @@ 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); + Proposal memory actualRoot = _anchorStateRegistryProxy.getStartingAnchorRoot(); if (_isProxy) { require( - Hash.unwrap(actualRoot) == 0xdead000000000000000000000000000000000000000000000000000000000000, - "ANCHORP-40" + _anchorStateRegistryProxy.respectedGameType().raw() == _expectedRespectedGameType.raw(), "ANCHORP-30" ); + require(actualRoot.root.raw() == _expectedRoot.raw(), "ANCHORP-40"); + require(actualRoot.l2SequenceNumber == 0, "ANCHORP-50"); } else { - require(Hash.unwrap(actualRoot) == bytes32(0), "ANCHORP-40"); + require(_anchorStateRegistryProxy.respectedGameType().raw() == GameType.wrap(0).raw(), "ANCHORP-30"); + require(actualRoot.root.raw() == bytes32(0), "ANCHORP-40"); + require(actualRoot.l2SequenceNumber == 0, "ANCHORP-50"); } } 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..ad7e9964394 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 { @@ -112,11 +113,7 @@ 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 = _isPermissionlessDeploy(_input.disputeGameType, isSuperRoot); // Shared permissioned game config for legacy permissioned games. IOPContractsManagerUtils.PermissionedDisputeGameConfig memory pdgConfig = IOPContractsManagerUtils @@ -133,16 +130,25 @@ contract DeployOPChain is Script { IOPContractsManagerUtils.DisputeGameConfig[] memory disputeGameConfigs = new IOPContractsManagerUtils.DisputeGameConfig[](6); - // Config 0: CANNON (disabled for initial deployment — no prestate exists) - disputeGameConfigs[0] = IOPContractsManagerUtils.DisputeGameConfig({ - enabled: false, - initBond: 0, - gameType: GameTypes.CANNON, - gameArgs: bytes("") - }); + // Config 0: CANNON + disputeGameConfigs[0] = permissionless + ? IOPContractsManagerUtils.DisputeGameConfig({ + enabled: true, + initBond: DEFAULT_INIT_BOND, + gameType: GameTypes.CANNON, + gameArgs: abi.encode( + IOPContractsManagerUtils.FaultDisputeGameConfig({ absolutePrestate: _input.cannonAbsolutePrestate }) + ) + }) + : IOPContractsManagerUtils.DisputeGameConfig({ + enabled: false, + initBond: 0, + gameType: GameTypes.CANNON, + gameArgs: bytes("") + }); - // Config 1: PERMISSIONED_CANNON — enabled only in non-super-root mode. - disputeGameConfigs[1] = isSuperRoot + // Config 1: PERMISSIONED_CANNON — enabled only in non-super-root permissioned mode. + disputeGameConfigs[1] = isSuperRoot || permissionless ? IOPContractsManagerUtils.DisputeGameConfig({ enabled: false, initBond: 0, @@ -156,13 +162,22 @@ contract DeployOPChain is Script { 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 2: CANNON_KONA + disputeGameConfigs[2] = permissionless + ? IOPContractsManagerUtils.DisputeGameConfig({ + enabled: true, + initBond: DEFAULT_INIT_BOND, + gameType: GameTypes.CANNON_KONA, + gameArgs: abi.encode( + IOPContractsManagerUtils.FaultDisputeGameConfig({ absolutePrestate: _input.cannonKonaAbsolutePrestate }) + ) + }) + : IOPContractsManagerUtils.DisputeGameConfig({ + enabled: false, + initBond: 0, + gameType: GameTypes.CANNON_KONA, + gameArgs: bytes("") + }); // Config 3: SUPER_PERMISSIONED — enabled only in super-root mode. disputeGameConfigs[3] = isSuperRoot @@ -202,8 +217,10 @@ 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: permissionless + ? _input.disputeGameType + : (isSuperRoot ? GameTypes.SUPER_PERMISSIONED : GameTypes.PERMISSIONED_CANNON), basefeeScalar: _input.basefeeScalar, blobBasefeeScalar: _input.blobBaseFeeScalar, gasLimit: _input.gasLimit, @@ -281,6 +298,19 @@ contract DeployOPChain is Script { require(cfg_.maxResourceLimit > 0, "DeployOPChain: gasLimit too small for any deposit budget"); } + /// @notice Returns whether the deployment should enable permissionless dispute games. + function _isPermissionlessDeploy(GameType _disputeGameType, bool _isSuperRoot) internal pure returns (bool) { + bool supportedGameType = _disputeGameType.raw() == GameTypes.PERMISSIONED_CANNON.raw() + || _disputeGameType.raw() == GameTypes.CANNON.raw() || _disputeGameType.raw() == GameTypes.CANNON_KONA.raw(); + require(supportedGameType, "DeployOPChain: unsupported dispute game type"); + require( + !_isSuperRoot || _disputeGameType.raw() == GameTypes.PERMISSIONED_CANNON.raw(), + "DeployOPChain: permissionless game type not supported with super roots" + ); + + return !_isSuperRoot && _disputeGameType.raw() != GameTypes.PERMISSIONED_CANNON.raw(); + } + // -------- Validations -------- /// @notice Checks if the input is valid. @@ -302,11 +332,28 @@ contract DeployOPChain is Script { require(_i.opcm != address(0), "DeployOPChainInput: opcm not set"); DeployUtils.assertValidContractAddress(_i.opcm); + bool superRoot = DevFeatures.isDevFeatureEnabled( + IOPContractsManagerV2(_i.opcm).devFeatureBitmap(), DevFeatures.SUPER_ROOT_GAMES_MIGRATION + ); + bool permissionless = _isPermissionlessDeploy(_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" + ); + require(_i.cannonAbsolutePrestate.raw() != bytes32(0), "DeployOPChainInput: cannonAbsolutePrestate not set"); + require( + _i.cannonKonaAbsolutePrestate.raw() != bytes32(0), + "DeployOPChainInput: cannonKonaAbsolutePrestate not set" + ); + } } /// @notice Checks if the output is valid. @@ -358,15 +405,21 @@ 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; - - GameType permGameType = isSuperRoot ? GameTypes.SUPER_PERMISSIONED : GameTypes.PERMISSIONED_CANNON; + IOPContractsManagerContainer.Implementations memory implementations = opcmV2.implementations(); + + bool permissionless = _isPermissionlessDeploy(_i.disputeGameType, isSuperRoot); + GameType respectedGameType = permissionless + ? _i.disputeGameType + : (isSuperRoot ? GameTypes.SUPER_PERMISSIONED : GameTypes.PERMISSIONED_CANNON); + 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, From 486bd896a7fb562dcc49c58786fe0b669576dcec Mon Sep 17 00:00:00 2001 From: lumoswiz Date: Fri, 26 Jun 2026 16:27:51 +0200 Subject: [PATCH 03/33] feat: allow permissionless games at initial deploy --- .../snapshots/semver-lock.json | 4 +- .../src/L1/opcm/OPContractsManagerV2.sol | 15 +-- .../test/L1/opcm/OPContractsManagerV2.t.sol | 35 ++++-- .../test/opcm/DeployOPChain.t.sol | 115 +++++++++++++++++- 4 files changed, 147 insertions(+), 22 deletions(-) diff --git a/packages/contracts-bedrock/snapshots/semver-lock.json b/packages/contracts-bedrock/snapshots/semver-lock.json index 88f929024f0..f29fa64e871 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": "0x74e351d34da94ef188cf693262b3670c2acc27fc3f71ec5145b5b97198af74f5", + "sourceCodeHash": "0x43a608acbe515e28066b166678a32dfd8cd77db84e0536ce6d4c7f42cac5dfd2" }, "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..2fa300b73cd 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. @@ -738,13 +738,14 @@ contract OPContractsManagerV2 is ISemver, OPContractsManagerUtilsCaller { revert OPContractsManagerV2_InvalidGameConfigs(); } - // Check if this is a permissioned type. - bool isPermissioned = validGameTypes[i].raw() == GameTypes.PERMISSIONED_CANNON.raw() + // During initial deployment, only CANNON, PERMISSIONED_CANNON, CANNON_KONA, and + // SUPER_PERMISSIONED may be enabled. + bool enableableAtInitialDeployment = validGameTypes[i].raw() == GameTypes.CANNON.raw() + || validGameTypes[i].raw() == GameTypes.PERMISSIONED_CANNON.raw() + || validGameTypes[i].raw() == GameTypes.CANNON_KONA.raw() || validGameTypes[i].raw() == GameTypes.SUPER_PERMISSIONED.raw(); - // 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(); } diff --git a/packages/contracts-bedrock/test/L1/opcm/OPContractsManagerV2.t.sol b/packages/contracts-bedrock/test/L1/opcm/OPContractsManagerV2.t.sol index 100c5128a81..002bee7566e 100644 --- a/packages/contracts-bedrock/test/L1/opcm/OPContractsManagerV2.t.sol +++ b/packages/contracts-bedrock/test/L1/opcm/OPContractsManagerV2.t.sol @@ -1950,19 +1950,40 @@ contract OPContractsManagerV2_Deploy_Test is OPContractsManagerV2_TestInit { ); } - function test_deploy_cannonGameEnabled_reverts() public { + /// @notice CANNON may be enabled at initial deployment now that its prestate is supplied via the + /// deploy config. + function test_deploy_cannonGameEnabled_succeeds() 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 })); - // nosemgrep: sol-style-use-abi-encodecall - runDeployV2( - deployConfig, abi.encodeWithSelector(IOPContractsManagerV2.OPContractsManagerV2_InvalidGameConfigs.selector) + IOPContractsManagerV2.ChainContracts memory cts = opcmV2.deploy(deployConfig); + assertNotEq( + address(cts.disputeGameFactory.gameImpls(GameTypes.CANNON)), address(0), "CANNON impl should be set" ); } - function test_deploy_cannonKonaGameEnabled_reverts() public { + /// @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 = 1 ether; + deployConfig.disputeGameConfigs[2].initBond = DEFAULT_DISPUTE_GAME_INIT_BOND; + deployConfig.disputeGameConfigs[2].gameArgs = + abi.encode(IOPContractsManagerUtils.FaultDisputeGameConfig({ absolutePrestate: cannonKonaPrestate })); + + IOPContractsManagerV2.ChainContracts memory cts = opcmV2.deploy(deployConfig); + assertNotEq( + address(cts.disputeGameFactory.gameImpls(GameTypes.CANNON_KONA)), + address(0), + "CANNON_KONA impl should be set" + ); + } + + /// @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( diff --git a/packages/contracts-bedrock/test/opcm/DeployOPChain.t.sol b/packages/contracts-bedrock/test/opcm/DeployOPChain.t.sol index 2ad6f589dcc..0e6ffbee28f 100644 --- a/packages/contracts-bedrock/test/opcm/DeployOPChain.t.sol +++ b/packages/contracts-bedrock/test/opcm/DeployOPChain.t.sol @@ -16,13 +16,15 @@ 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, Hash } from "src/dispute/lib/Types.sol"; +import { Claim, Duration, GameType, GameTypes, Hash, Proposal } from "src/dispute/lib/Types.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 { @@ -62,6 +64,8 @@ contract DeployOPChain_TestBase is Test, FeatureFlags { Hash startingAnchorRoot = Hash.wrap(0xdead000000000000000000000000000000000000000000000000000000000000); Claim cannonAbsolutePrestate = Claim.wrap(0x1111111111111111111111111111111111111111111111111111111111111111); Claim cannonKonaAbsolutePrestate = Claim.wrap(0x2222222222222222222222222222222222222222222222222222222222222222); + // Non-placeholder anchor root for the permissionless deploy tests. + Hash permissionlessAnchorRoot = Hash.wrap(bytes32(uint256(0xc0ffee))); uint256 disputeMaxGameDepth = 73; uint256 disputeSplitDepth = 30; Duration disputeClockExtension = Duration.wrap(3 hours); @@ -229,18 +233,79 @@ contract DeployOPChain_Test is DeployOPChain_TestBase { _checkDeploymentAssertions(doo); } - function test_run_cannonGameType_reverts() public { + /// @notice A CANNON initial deployment enables both permissionless fault games and seeds the + /// anchor root and respected game type from the input. + function test_run_cannonGameType_succeeds() public { + skipIfDevFeatureEnabled(DevFeatures.SUPER_ROOT_GAMES_MIGRATION); deployOPChainInput.disputeGameType = GameTypes.CANNON; - vm.expectRevert("DeployOPChain: only PERMISSIONED_CANNON game type is supported for initial deployment"); - deployOPChain.run(deployOPChainInput); + deployOPChainInput.startingAnchorRoot = permissionlessAnchorRoot; + + DeployOPChain.Output memory doo = deployOPChain.run(deployOPChainInput); + _checkPermissionlessDeployment(doo, GameTypes.CANNON); } - function test_run_cannonKonaGameType_reverts() public { + /// @notice A CANNON_KONA initial deployment behaves like CANNON but sets CANNON_KONA as the + /// respected game type. + function test_run_cannonKonaGameType_succeeds() public { + skipIfDevFeatureEnabled(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; + + DeployOPChain.Output memory doo = deployOPChain.run(deployOPChainInput); + _checkPermissionlessDeployment(doo, GameTypes.CANNON_KONA); + } + + /// @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; + deployOPChainInput.startingAnchorRoot = permissionlessAnchorRoot; + vm.expectRevert("DeployOPChain: permissionless game type not supported with super roots"); deployOPChain.run(deployOPChainInput); } + /// @notice Permissioned deploys do not require the permissionless prestate inputs. + function test_run_permissionedZeroPermissionlessPrestates_succeeds() public { + deployOPChainInput.cannonAbsolutePrestate = Claim.wrap(bytes32(0)); + deployOPChainInput.cannonKonaAbsolutePrestate = Claim.wrap(bytes32(0)); + DeployOPChain.Output memory doo = deployOPChain.run(deployOPChainInput); + _checkDeploymentAssertions(doo); + } + + /// @notice Asserts a permissionless deployment enabled both fault games with the default bond and + /// non-zero impls, and seeded the ASR with the input anchor root and respected game type. + /// @param doo The output of the deployment. + /// @param _respectedType The expected respected game type. + function _checkPermissionlessDeployment(DeployOPChain.Output memory doo, GameType _respectedType) internal view { + uint256 defaultBond = deployOPChain.DEFAULT_INIT_BOND(); + + assertEq(doo.disputeGameFactoryProxy.initBonds(GameTypes.CANNON), defaultBond, "CANNON init bond"); + assertNotEq(address(doo.disputeGameFactoryProxy.gameImpls(GameTypes.CANNON)), address(0), "CANNON impl"); + assertEq(doo.disputeGameFactoryProxy.initBonds(GameTypes.CANNON_KONA), defaultBond, "CANNON_KONA init bond"); + assertNotEq( + address(doo.disputeGameFactoryProxy.gameImpls(GameTypes.CANNON_KONA)), address(0), "CANNON_KONA impl" + ); + + // gameImpls is the shared faultDisputeGameImpl for both games, so only the per-game stored + // args distinguish their prestates. + assertEq( + LibGameArgs.decode(doo.disputeGameFactoryProxy.gameArgs(GameTypes.CANNON)).absolutePrestate, + deployOPChainInput.cannonAbsolutePrestate.raw(), + "CANNON prestate wiring" + ); + assertEq( + LibGameArgs.decode(doo.disputeGameFactoryProxy.gameArgs(GameTypes.CANNON_KONA)).absolutePrestate, + deployOPChainInput.cannonKonaAbsolutePrestate.raw(), + "CANNON_KONA prestate wiring" + ); + + IAnchorStateRegistry asr = doo.anchorStateRegistryProxy; + assertEq(asr.respectedGameType().raw(), _respectedType.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 { @@ -318,6 +383,12 @@ contract DeployOPChain_Test is DeployOPChain_TestBase { 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"); } } @@ -427,6 +498,38 @@ 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_placeholderStartingAnchorRoot_permissionless_reverts() public { + skipIfDevFeatureEnabled(DevFeatures.SUPER_ROOT_GAMES_MIGRATION); + deployOPChainInput.disputeGameType = GameTypes.CANNON; + // startingAnchorRoot stays at the 0xdead placeholder default. + vm.expectRevert("DeployOPChainInput: permissionless startingAnchorRoot cannot be placeholder"); + deployOPChain.run(deployOPChainInput); + } + + function test_run_zeroCannonAbsolutePrestate_permissionless_reverts() public { + skipIfDevFeatureEnabled(DevFeatures.SUPER_ROOT_GAMES_MIGRATION); + deployOPChainInput.disputeGameType = GameTypes.CANNON; + deployOPChainInput.startingAnchorRoot = permissionlessAnchorRoot; + deployOPChainInput.cannonAbsolutePrestate = Claim.wrap(bytes32(0)); + vm.expectRevert("DeployOPChainInput: cannonAbsolutePrestate not set"); + deployOPChain.run(deployOPChainInput); + } + + function test_run_zeroCannonKonaAbsolutePrestate_permissionless_reverts() public { + skipIfDevFeatureEnabled(DevFeatures.SUPER_ROOT_GAMES_MIGRATION); + deployOPChainInput.disputeGameType = GameTypes.CANNON; + deployOPChainInput.startingAnchorRoot = permissionlessAnchorRoot; + deployOPChainInput.cannonKonaAbsolutePrestate = Claim.wrap(bytes32(0)); + vm.expectRevert("DeployOPChainInput: cannonKonaAbsolutePrestate not set"); + deployOPChain.run(deployOPChainInput); + } + function test_runWithBytes_invalidInput_reverts() public { // It should revert if the input bytes cannot be decoded. bytes memory invalidInput = "invalid"; From de2b2ea52ddb70e8d31ba6b9caf7b38fcdfea11d Mon Sep 17 00:00:00 2001 From: lumoswiz Date: Mon, 29 Jun 2026 13:53:48 +0200 Subject: [PATCH 04/33] fix: validate permissionless deploy config --- .../snapshots/semver-lock.json | 4 +- .../src/L1/opcm/OPContractsManagerV2.sol | 49 +++++++++--- .../test/L1/opcm/OPContractsManagerV2.t.sol | 76 +++++++++++++++++++ .../test/opcm/DeployOPChain.t.sol | 6 +- 4 files changed, 118 insertions(+), 17 deletions(-) diff --git a/packages/contracts-bedrock/snapshots/semver-lock.json b/packages/contracts-bedrock/snapshots/semver-lock.json index f29fa64e871..775e197ef9c 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": "0x74e351d34da94ef188cf693262b3670c2acc27fc3f71ec5145b5b97198af74f5", - "sourceCodeHash": "0x43a608acbe515e28066b166678a32dfd8cd77db84e0536ce6d4c7f42cac5dfd2" + "initCodeHash": "0xa2a91101d962601f475cdd2cb78c01e1b1c8096b16496c8d113601836ff9fcb4", + "sourceCodeHash": "0x0f7699e6beeab595b6f445a9571b45a7b9fbab0f054374c96cb97aac6e81718e" }, "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 2fa300b73cd..718382bb360 100644 --- a/packages/contracts-bedrock/src/L1/opcm/OPContractsManagerV2.sol +++ b/packages/contracts-bedrock/src/L1/opcm/OPContractsManagerV2.sol @@ -54,6 +54,10 @@ import { IOPContractsManagerUtils } from "interfaces/L1/opcm/IOPContractsManager /// design. Look at _apply, squint, and imagine that it can output an upgrade plan rather than /// actually executing the upgrade, and then you'll see how it can be improved. contract OPContractsManagerV2 is ISemver, OPContractsManagerUtilsCaller { + // TODO(#20912): Remove once deploy pipelines provide real anchor roots. + /// @notice Placeholder anchor root historically used for permissioned initial deployments. + bytes32 internal constant PLACEHOLDER_STARTING_ANCHOR_ROOT = bytes32(hex"dead"); + /// @notice Contracts that represent the Superchain system. struct SuperchainContracts { ISuperchainConfig superchainConfig; @@ -713,7 +717,11 @@ contract OPContractsManagerV2 is ISemver, OPContractsManagerUtilsCaller { // 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 isCannonKonaGame = rawGameType == GameTypes.CANNON_KONA.raw(); + + if (_cfg.disputeGameConfigs[i].gameType.raw() != rawGameType) { revert OPContractsManagerV2_InvalidGameConfigs(); } @@ -722,28 +730,23 @@ contract OPContractsManagerV2 is ISemver, OPContractsManagerUtilsCaller { revert OPContractsManagerV2_InvalidGameConfigs(); } - if ( - _cfg.disputeGameConfigs[i].gameType.raw() == GameTypes.SUPER_PERMISSIONED.raw() - && _cfg.disputeGameConfigs[i].initBond != 0 - ) { + if (rawGameType == GameTypes.SUPER_PERMISSIONED.raw() && _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 + rawGameType != GameTypes.SUPER_PERMISSIONED.raw() && _cfg.disputeGameConfigs[i].enabled + && _cfg.disputeGameConfigs[i].initBond == 0 ) { revert OPContractsManagerV2_InvalidGameConfigs(); } // During initial deployment, only CANNON, PERMISSIONED_CANNON, CANNON_KONA, and // SUPER_PERMISSIONED may be enabled. - bool enableableAtInitialDeployment = validGameTypes[i].raw() == GameTypes.CANNON.raw() - || validGameTypes[i].raw() == GameTypes.PERMISSIONED_CANNON.raw() - || validGameTypes[i].raw() == GameTypes.CANNON_KONA.raw() - || validGameTypes[i].raw() == GameTypes.SUPER_PERMISSIONED.raw(); + bool enableableAtInitialDeployment = isCannonGame || rawGameType == GameTypes.PERMISSIONED_CANNON.raw() + || isCannonKonaGame || rawGameType == GameTypes.SUPER_PERMISSIONED.raw(); if (_isInitialDeployment && !enableableAtInitialDeployment && _cfg.disputeGameConfigs[i].enabled) { revert OPContractsManagerV2_InvalidGameConfigs(); @@ -751,11 +754,19 @@ contract OPContractsManagerV2 is ISemver, OPContractsManagerUtilsCaller { // 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 + rawGameType == GameTypes.ZK_DISPUTE_GAME.raw() && _cfg.disputeGameConfigs[i].enabled && !isDevFeatureEnabled(DevFeatures.ZK_DISPUTE_GAME) ) { revert OPContractsManagerV2_InvalidGameConfigs(); } + + if (_cfg.disputeGameConfigs[i].enabled && (isCannonGame || isCannonKonaGame)) { + 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. @@ -772,6 +783,20 @@ contract OPContractsManagerV2 is ISemver, OPContractsManagerUtilsCaller { if (!startingGameTypeFound) { revert OPContractsManagerV2_InvalidGameConfigs(); } + + if (_isInitialDeployment) { + bytes32 startingAnchorRoot = _cfg.startingAnchorRoot.root.raw(); + bool permissionlessInitialRespectedGameType = _cfg.startingRespectedGameType.raw() == GameTypes.CANNON.raw() + || _cfg.startingRespectedGameType.raw() == GameTypes.CANNON_KONA.raw(); + + if (startingAnchorRoot == bytes32(0)) { + revert OPContractsManagerV2_InvalidGameConfigs(); + } + + if (permissionlessInitialRespectedGameType && startingAnchorRoot == PLACEHOLDER_STARTING_ANCHOR_ROOT) { + revert OPContractsManagerV2_InvalidGameConfigs(); + } + } } /// @notice Executes the deployment/upgrade action. diff --git a/packages/contracts-bedrock/test/L1/opcm/OPContractsManagerV2.t.sol b/packages/contracts-bedrock/test/L1/opcm/OPContractsManagerV2.t.sol index 002bee7566e..4cc8aa057ce 100644 --- a/packages/contracts-bedrock/test/L1/opcm/OPContractsManagerV2.t.sol +++ b/packages/contracts-bedrock/test/L1/opcm/OPContractsManagerV2.t.sol @@ -1964,6 +1964,19 @@ contract OPContractsManagerV2_Deploy_Test is OPContractsManagerV2_TestInit { ); } + /// @notice Deploy reverts when CANNON is enabled with a zero prestate. + function test_deploy_cannonGameEnabledZeroPrestate_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: Claim.wrap(bytes32(0)) })); + + // 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 { @@ -1980,6 +1993,59 @@ contract OPContractsManagerV2_Deploy_Test is OPContractsManagerV2_TestInit { ); } + /// @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 = 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[0].enabled = true; + deployConfig.disputeGameConfigs[0].initBond = DEFAULT_DISPUTE_GAME_INIT_BOND; + deployConfig.disputeGameConfigs[0].gameArgs = + abi.encode(IOPContractsManagerUtils.FaultDisputeGameConfig({ absolutePrestate: cannonPrestate })); + deployConfig.startingRespectedGameType = GameTypes.CANNON; + 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[0].enabled = true; + deployConfig.disputeGameConfigs[0].initBond = DEFAULT_DISPUTE_GAME_INIT_BOND; + deployConfig.disputeGameConfigs[0].gameArgs = + abi.encode(IOPContractsManagerUtils.FaultDisputeGameConfig({ absolutePrestate: cannonPrestate })); + deployConfig.startingRespectedGameType = GameTypes.CANNON; + deployConfig.startingAnchorRoot = Proposal({ root: Hash.wrap(bytes32(hex"dead")), 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; @@ -2001,6 +2067,16 @@ 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(bytes32(hex"dead")), l2SequenceNumber: 0 }); + bool superRoot = isDevFeatureEnabled(DevFeatures.SUPER_ROOT_GAMES_MIGRATION); + string memory expectedErrors = superRoot ? "SCKDG-SHAPE,SCKDG-10" : "CKDG-NOSHAPE,PLDG-10,CKDG-10"; + IOPContractsManagerV2.ChainContracts memory cts = runDeployV2(deployConfig, bytes(""), expectedErrors); + Proposal memory startingAnchorRoot = cts.anchorStateRegistry.getStartingAnchorRoot(); + assertEq(startingAnchorRoot.root.raw(), bytes32(hex"dead"), "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/opcm/DeployOPChain.t.sol b/packages/contracts-bedrock/test/opcm/DeployOPChain.t.sol index 0e6ffbee28f..01fb7e65abb 100644 --- a/packages/contracts-bedrock/test/opcm/DeployOPChain.t.sol +++ b/packages/contracts-bedrock/test/opcm/DeployOPChain.t.sol @@ -504,7 +504,7 @@ contract DeployOPChain_TestFail is DeployOPChain_TestBase { deployOPChain.run(deployOPChainInput); } - function test_run_placeholderStartingAnchorRoot_permissionless_reverts() public { + function test_run_permissionlessPlaceholderStartingAnchorRoot_reverts() public { skipIfDevFeatureEnabled(DevFeatures.SUPER_ROOT_GAMES_MIGRATION); deployOPChainInput.disputeGameType = GameTypes.CANNON; // startingAnchorRoot stays at the 0xdead placeholder default. @@ -512,7 +512,7 @@ contract DeployOPChain_TestFail is DeployOPChain_TestBase { deployOPChain.run(deployOPChainInput); } - function test_run_zeroCannonAbsolutePrestate_permissionless_reverts() public { + function test_run_permissionlessZeroCannonAbsolutePrestate_reverts() public { skipIfDevFeatureEnabled(DevFeatures.SUPER_ROOT_GAMES_MIGRATION); deployOPChainInput.disputeGameType = GameTypes.CANNON; deployOPChainInput.startingAnchorRoot = permissionlessAnchorRoot; @@ -521,7 +521,7 @@ contract DeployOPChain_TestFail is DeployOPChain_TestBase { deployOPChain.run(deployOPChainInput); } - function test_run_zeroCannonKonaAbsolutePrestate_permissionless_reverts() public { + function test_run_permissionlessZeroCannonKonaAbsolutePrestate_reverts() public { skipIfDevFeatureEnabled(DevFeatures.SUPER_ROOT_GAMES_MIGRATION); deployOPChainInput.disputeGameType = GameTypes.CANNON; deployOPChainInput.startingAnchorRoot = permissionlessAnchorRoot; From 20302cab26a30735af3ab302385146ee7c118028 Mon Sep 17 00:00:00 2001 From: lumoswiz Date: Mon, 29 Jun 2026 16:52:58 +0200 Subject: [PATCH 05/33] test: use nonzero cannon prestate in validator fixtures --- .../test/L1/OPContractsManagerStandardValidator.t.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/contracts-bedrock/test/L1/OPContractsManagerStandardValidator.t.sol b/packages/contracts-bedrock/test/L1/OPContractsManagerStandardValidator.t.sol index 71fb2288ec7..2c4d07ec94a 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(); } @@ -2178,7 +2178,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(); From 950a781aa977aec841fe4c1ea66904b67f03dcbe Mon Sep 17 00:00:00 2001 From: lumoswiz Date: Mon, 29 Jun 2026 17:05:04 +0200 Subject: [PATCH 06/33] refactor: centralize initial deploy game selection --- .../scripts/deploy/DeployOPChain.s.sol | 27 +++++++++++++------ 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/packages/contracts-bedrock/scripts/deploy/DeployOPChain.s.sol b/packages/contracts-bedrock/scripts/deploy/DeployOPChain.s.sol index ad7e9964394..1f1e332545e 100644 --- a/packages/contracts-bedrock/scripts/deploy/DeployOPChain.s.sol +++ b/packages/contracts-bedrock/scripts/deploy/DeployOPChain.s.sol @@ -113,7 +113,8 @@ contract DeployOPChain is Script { view returns (IOPContractsManagerV2.FullConfig memory config_) { - bool permissionless = _isPermissionlessDeploy(_input.disputeGameType, isSuperRoot); + (bool permissionless, GameType respectedGameType) = + _initialDeployGameSelection(_input.disputeGameType, isSuperRoot); // Shared permissioned game config for legacy permissioned games. IOPContractsManagerUtils.PermissionedDisputeGameConfig memory pdgConfig = IOPContractsManagerUtils @@ -218,9 +219,7 @@ contract DeployOPChain is Script { unsafeBlockSigner: _input.unsafeBlockSigner, batcher: _input.batcher, startingAnchorRoot: Proposal({ root: _input.startingAnchorRoot, l2SequenceNumber: 0 }), - startingRespectedGameType: permissionless - ? _input.disputeGameType - : (isSuperRoot ? GameTypes.SUPER_PERMISSIONED : GameTypes.PERMISSIONED_CANNON), + startingRespectedGameType: respectedGameType, basefeeScalar: _input.basefeeScalar, blobBasefeeScalar: _input.blobBaseFeeScalar, gasLimit: _input.gasLimit, @@ -311,6 +310,21 @@ contract DeployOPChain is Script { return !_isSuperRoot && _disputeGameType.raw() != GameTypes.PERMISSIONED_CANNON.raw(); } + /// @notice Returns the permissionless mode and respected game type for an initial deployment. + function _initialDeployGameSelection( + GameType _disputeGameType, + bool _isSuperRoot + ) + internal + pure + returns (bool permissionless_, GameType respectedGameType_) + { + permissionless_ = _isPermissionlessDeploy(_disputeGameType, _isSuperRoot); + respectedGameType_ = permissionless_ + ? _disputeGameType + : (_isSuperRoot ? GameTypes.SUPER_PERMISSIONED : GameTypes.PERMISSIONED_CANNON); + } + // -------- Validations -------- /// @notice Checks if the input is valid. @@ -407,10 +421,7 @@ contract DeployOPChain is Script { IOPContractsManagerV2 opcmV2 = IOPContractsManagerV2(_i.opcm); IOPContractsManagerContainer.Implementations memory implementations = opcmV2.implementations(); - bool permissionless = _isPermissionlessDeploy(_i.disputeGameType, isSuperRoot); - GameType respectedGameType = permissionless - ? _i.disputeGameType - : (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); From 896d1d38904f2a24f4f2a55727d2c34ddbdc1537 Mon Sep 17 00:00:00 2001 From: lumoswiz Date: Mon, 29 Jun 2026 21:18:26 +0200 Subject: [PATCH 07/33] test: fix fork tests for current permissionless game config --- .../test/L1/OptimismPortal2.t.sol | 10 +- .../test/L1/opcm/OPContractsManagerV2.t.sol | 15 ++- .../test/dispute/FaultDisputeGame.t.sol | 10 ++ .../test/setup/ForkL1Live.s.sol | 100 ++++++++++++++++-- 4 files changed, 122 insertions(+), 13 deletions(-) diff --git a/packages/contracts-bedrock/test/L1/OptimismPortal2.t.sol b/packages/contracts-bedrock/test/L1/OptimismPortal2.t.sol index f7c04f3b836..a9d9f839702 100644 --- a/packages/contracts-bedrock/test/L1/OptimismPortal2.t.sol +++ b/packages/contracts-bedrock/test/L1/OptimismPortal2.t.sol @@ -1028,10 +1028,12 @@ contract OptimismPortal2_ProveWithdrawalTransaction_Test is OptimismPortal2_Test _withdrawalProof: _withdrawalProof }); - // Create a new game. - IDisputeGame newGame = disputeGameFactory.create{ - value: disputeGameFactory.initBonds(optimismPortal2.respectedGameType()) - }(GameType.wrap(0), Claim.wrap(_outputRoot), abi.encode(_proposedBlockNumber + 1)); + // Super game extraData is incompatible here, so mock it below. + GameType replacementGameType = + DisputeGames.isSuperGame(respectedGameType) ? GameType.wrap(0) : respectedGameType; + IDisputeGame newGame = disputeGameFactory.create{ value: disputeGameFactory.initBonds(replacementGameType) }( + replacementGameType, Claim.wrap(_outputRoot), abi.encode(_proposedBlockNumber + 1) + ); // In super mode, the new game's type differs from the respected type, so mock this. if (DisputeGames.isSuperGame(respectedGameType)) { diff --git a/packages/contracts-bedrock/test/L1/opcm/OPContractsManagerV2.t.sol b/packages/contracts-bedrock/test/L1/opcm/OPContractsManagerV2.t.sol index 4cc8aa057ce..2d1b3f71b2a 100644 --- a/packages/contracts-bedrock/test/L1/opcm/OPContractsManagerV2.t.sol +++ b/packages/contracts-bedrock/test/L1/opcm/OPContractsManagerV2.t.sol @@ -266,7 +266,7 @@ contract OPContractsManagerV2_Upgrade_TestInit is OPContractsManagerV2_TestInit v2UpgradeInput.disputeGameConfigs.push( IOPContractsManagerUtils.DisputeGameConfig({ enabled: true, - initBond: disputeGameFactory.initBonds(GameTypes.CANNON), + initBond: _permissionlessGameInitBondForUpgrade(GameTypes.CANNON), gameType: GameTypes.CANNON, gameArgs: abi.encode(IOPContractsManagerUtils.FaultDisputeGameConfig({ absolutePrestate: cannonPrestate })) }) @@ -288,7 +288,7 @@ contract OPContractsManagerV2_Upgrade_TestInit is OPContractsManagerV2_TestInit v2UpgradeInput.disputeGameConfigs.push( IOPContractsManagerUtils.DisputeGameConfig({ enabled: true, - initBond: disputeGameFactory.initBonds(GameTypes.CANNON_KONA), + initBond: _permissionlessGameInitBondForUpgrade(GameTypes.CANNON_KONA), gameType: GameTypes.CANNON_KONA, gameArgs: abi.encode( IOPContractsManagerUtils.FaultDisputeGameConfig({ absolutePrestate: cannonKonaPrestate }) @@ -334,6 +334,15 @@ contract OPContractsManagerV2_Upgrade_TestInit is OPContractsManagerV2_TestInit ); } + /// @notice Returns the permissionless game init bond to use in upgrade inputs. + /// @dev Uses the live bond when present, otherwise falls back to a valid nonzero default. + /// @param _gameType Game type to check. + /// @return The init bond to use for the upgrade config. + function _permissionlessGameInitBondForUpgrade(GameType _gameType) internal view returns (uint256) { + uint256 initBond = disputeGameFactory.initBonds(_gameType); + return initBond == 0 ? DEFAULT_DISPUTE_GAME_INIT_BOND : initBond; + } + /// @notice Helper function that runs an OPCM V2 upgrade, asserts that the upgrade was successful, /// and runs post-upgrade smoke tests. /// @param _opcm The OPCM contract to reference for shared components. @@ -725,7 +734,7 @@ 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); + uint256 originalBond = _permissionlessGameInitBondForUpgrade(GameTypes.CANNON); // First, disable Cannon and clear its bond so the factory entry is removed. // If the chain's current respectedGameType is CANNON, we must override it to diff --git a/packages/contracts-bedrock/test/dispute/FaultDisputeGame.t.sol b/packages/contracts-bedrock/test/dispute/FaultDisputeGame.t.sol index 977bc529cd7..f210c85e1f1 100644 --- a/packages/contracts-bedrock/test/dispute/FaultDisputeGame.t.sol +++ b/packages/contracts-bedrock/test/dispute/FaultDisputeGame.t.sol @@ -136,6 +136,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++) { @@ -165,6 +174,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/setup/ForkL1Live.s.sol b/packages/contracts-bedrock/test/setup/ForkL1Live.s.sol index 9b662f439af..8067d6d349b 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 @@ -169,14 +171,14 @@ contract ForkL1Live is Deployer, StdAssertions, FeatureFlags { saveProxyAndImpl("DisputeGameFactory", opToml, ".addresses.DisputeGameFactoryProxy"); // Fault proof non-proxied contracts - // For chains that don't have a permissionless game, we save the dispute game and WETH - // addresses as the zero address. artifacts.save( "PreimageOracle", vm.parseJsonAddress(addressesJson, string.concat("$.", chainId, ".PreimageOracle")) ); artifacts.save("MipsSingleton", vm.parseJsonAddress(addressesJson, string.concat("$.", chainId, ".MIPS"))); IDisputeGameFactory disputeGameFactory = IDisputeGameFactory(artifacts.mustGetAddress("DisputeGameFactoryProxy")); + IAnchorStateRegistry anchorStateRegistry = + IAnchorStateRegistry(artifacts.mustGetAddress("AnchorStateRegistryProxy")); // The PermissionedDisputeGame and PermissionedDelayedWETHProxy are not listed in the registry for OP, so we // look it up onchain. @@ -188,15 +190,97 @@ contract ForkL1Live is Deployer, StdAssertions, FeatureFlags { DisputeGames.getGameImplDelayedWeth(disputeGameFactory, GameTypes.PERMISSIONED_CANNON); artifacts.save("PermissionedDelayedWETHProxy", address(permissionedDelayedWeth)); - // Get DelayedWETH for PERMISSIONLESS games (CANNON) + // Get DelayedWETH from the live permissionless game. The respected type is preferred when still registered, + // otherwise we fall back to the live permissionless types that current mainnet forks may use. + GameType permissionlessGameType = _selectPermissionlessGameType(anchorStateRegistry, disputeGameFactory); IDelayedWETH permissionlessDelayedWeth = - DisputeGames.getGameImplDelayedWeth(disputeGameFactory, GameTypes.CANNON); + DisputeGames.getGameImplDelayedWeth(disputeGameFactory, permissionlessGameType); + require(address(permissionlessDelayedWeth) != address(0), "ForkL1Live: permissionless WETH is zero"); // The SR seems out-of-date, so pull the DelayedWETH addresses from the games. artifacts.save("DelayedWETHProxy", address(permissionlessDelayedWeth)); artifacts.save("DelayedWETHImpl", EIP1967Helper.getImplementation(address(permissionlessDelayedWeth))); } + /// @notice Selects a permissionless game type with a live implementation and init bond. + /// @dev Prefers the ASR's respected game type, then falls back to CANNON_KONA and CANNON. + /// @param _anchorStateRegistry Anchor state registry used to read the respected game type. + /// @param _disputeGameFactory Dispute game factory used to inspect game registrations. + /// @return The selected permissionless game type. + function _selectPermissionlessGameType( + IAnchorStateRegistry _anchorStateRegistry, + IDisputeGameFactory _disputeGameFactory + ) + internal + view + returns (GameType) + { + GameType respectedGameType = _anchorStateRegistry.respectedGameType(); + if (_isLivePermissionlessGame(_disputeGameFactory, respectedGameType)) { + return respectedGameType; + } + if (_isLivePermissionlessGame(_disputeGameFactory, GameTypes.CANNON_KONA)) { + return GameTypes.CANNON_KONA; + } + if (_isLivePermissionlessGame(_disputeGameFactory, GameTypes.CANNON)) { + return GameTypes.CANNON; + } + revert("ForkL1Live: no live permissionless game"); + } + + /// @notice Returns true if a game type is a live permissionless game. + /// @param _disputeGameFactory Dispute game factory used to inspect game config. + /// @param _gameType Game type to check. + /// @return True if the game type has a live permissionless init bond. + function _isLivePermissionlessGame( + IDisputeGameFactory _disputeGameFactory, + GameType _gameType + ) + internal + view + returns (bool) + { + return _livePermissionlessGameInitBond(_disputeGameFactory, _gameType) != 0; + } + + /// @notice Returns the live init bond for a supported permissionless game type. + /// @dev Returns zero for unsupported, unregistered, or zero-bond game types. + /// @param _disputeGameFactory Dispute game factory used to inspect game config. + /// @param _gameType Game type to check. + /// @return The live init bond, or zero if the game type is not live. + function _livePermissionlessGameInitBond( + IDisputeGameFactory _disputeGameFactory, + GameType _gameType + ) + internal + view + returns (uint256) + { + uint32 rawGameType = _gameType.raw(); + bool permissionless = rawGameType == GameTypes.CANNON.raw() || rawGameType == GameTypes.CANNON_KONA.raw(); + if (!permissionless || address(_disputeGameFactory.gameImpls(_gameType)) == address(0)) { + return 0; + } + return _disputeGameFactory.initBonds(_gameType); + } + + /// @notice Returns the permissionless game init bond to use in upgrade inputs. + /// @dev Uses the live bond when present, otherwise falls back to a valid nonzero default. + /// @param _disputeGameFactory Dispute game factory used to inspect game config. + /// @param _gameType Game type to check. + /// @return The init bond to use for the upgrade config. + function _permissionlessGameInitBondForUpgrade( + IDisputeGameFactory _disputeGameFactory, + GameType _gameType + ) + internal + view + returns (uint256) + { + uint256 initBond = _livePermissionlessGameInitBond(_disputeGameFactory, _gameType); + return initBond == 0 ? DEFAULT_PERMISSIONLESS_INIT_BOND : initBond; + } + /// @notice Calls to the Deploy.s.sol contract etched by Setup.sol to a deterministic address, sets up the /// environment, and deploys new implementations. function _deployNewImplementations() internal { @@ -332,10 +416,14 @@ contract ForkL1Live is Deployer, StdAssertions, FeatureFlags { } else { // Standard upgrade path: legacy types enabled, super types disabled. // Order must match validGameTypes in OPContractsManagerV2._assertValidFullConfig(). + uint256 cannonInitBond = _permissionlessGameInitBondForUpgrade(disputeGameFactory, GameTypes.CANNON); + uint256 cannonKonaInitBond = + _permissionlessGameInitBondForUpgrade(disputeGameFactory, GameTypes.CANNON_KONA); + disputeGameConfigs = new IOPContractsManagerUtils.DisputeGameConfig[](6); disputeGameConfigs[0] = IOPContractsManagerUtils.DisputeGameConfig({ enabled: true, - initBond: disputeGameFactory.initBonds(GameTypes.CANNON), + initBond: cannonInitBond, gameType: GameTypes.CANNON, gameArgs: abi.encode( IOPContractsManagerUtils.FaultDisputeGameConfig({ @@ -357,7 +445,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({ From 7185407dbf9b877d5b8d3e8d485d39d851a0c0ae Mon Sep 17 00:00:00 2001 From: lumoswiz Date: Wed, 1 Jul 2026 11:01:14 +0200 Subject: [PATCH 08/33] refactor: reduce DeployOPChain permissionless prestate input --- op-chain-ops/interopgen/deploy.go | 2 -- op-deployer/pkg/deployer/opcm/opchain.go | 2 -- op-deployer/pkg/deployer/pipeline/opchain.go | 4 +-- .../scripts/deploy/DeployOPChain.s.sol | 9 ++--- .../scripts/libraries/Types.sol | 2 -- .../test/opcm/DeployOPChain.t.sol | 36 ++----------------- 6 files changed, 6 insertions(+), 49 deletions(-) diff --git a/op-chain-ops/interopgen/deploy.go b/op-chain-ops/interopgen/deploy.go index 568e15196c3..f7cec37204e 100644 --- a/op-chain-ops/interopgen/deploy.go +++ b/op-chain-ops/interopgen/deploy.go @@ -245,8 +245,6 @@ func DeployL2ToL1(l1Host *script.Host, superCfg *SuperchainConfig, superDeployme DisputeGameType: cfg.DisputeGameType, DisputeAbsolutePrestate: cfg.DisputeAbsolutePrestate, StartingAnchorRoot: opcm.DefaultStartingAnchorRoot.Root, - CannonAbsolutePrestate: cfg.DisputeAbsolutePrestate, - CannonKonaAbsolutePrestate: cfg.DisputeKonaAbsolutePrestate, 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 aa95c244213..c7b85bd21a0 100644 --- a/op-deployer/pkg/deployer/opcm/opchain.go +++ b/op-deployer/pkg/deployer/opcm/opchain.go @@ -37,8 +37,6 @@ type DeployOPChainInput struct { DisputeGameType uint32 DisputeAbsolutePrestate common.Hash StartingAnchorRoot common.Hash - CannonAbsolutePrestate common.Hash - CannonKonaAbsolutePrestate 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 5b54c0685aa..c959c8cfc0a 100644 --- a/op-deployer/pkg/deployer/pipeline/opchain.go +++ b/op-deployer/pkg/deployer/pipeline/opchain.go @@ -134,7 +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 the anchor root and permissionless prestates from pipeline state. + // TODO(#20912): Populate StartingAnchorRoot and DisputeAbsolutePrestate from pipeline state for permissionless deploys. return opcm.DeployOPChainInput{ OpChainProxyAdminOwner: thisIntent.Roles.L1ProxyAdminOwner, SystemConfigOwner: thisIntent.Roles.SystemConfigOwner, @@ -151,8 +151,6 @@ func makeDCI(intent *state.Intent, thisIntent *state.ChainIntent, chainID common DisputeGameType: proofParams.DisputeGameType, DisputeAbsolutePrestate: proofParams.DisputeAbsolutePrestate, StartingAnchorRoot: opcm.DefaultStartingAnchorRoot.Root, - CannonAbsolutePrestate: common.Hash{}, - CannonKonaAbsolutePrestate: common.Hash{}, 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/deploy/DeployOPChain.s.sol b/packages/contracts-bedrock/scripts/deploy/DeployOPChain.s.sol index 1f1e332545e..a5133036220 100644 --- a/packages/contracts-bedrock/scripts/deploy/DeployOPChain.s.sol +++ b/packages/contracts-bedrock/scripts/deploy/DeployOPChain.s.sol @@ -138,7 +138,7 @@ contract DeployOPChain is Script { initBond: DEFAULT_INIT_BOND, gameType: GameTypes.CANNON, gameArgs: abi.encode( - IOPContractsManagerUtils.FaultDisputeGameConfig({ absolutePrestate: _input.cannonAbsolutePrestate }) + IOPContractsManagerUtils.FaultDisputeGameConfig({ absolutePrestate: _input.disputeAbsolutePrestate }) ) }) : IOPContractsManagerUtils.DisputeGameConfig({ @@ -170,7 +170,7 @@ contract DeployOPChain is Script { initBond: DEFAULT_INIT_BOND, gameType: GameTypes.CANNON_KONA, gameArgs: abi.encode( - IOPContractsManagerUtils.FaultDisputeGameConfig({ absolutePrestate: _input.cannonKonaAbsolutePrestate }) + IOPContractsManagerUtils.FaultDisputeGameConfig({ absolutePrestate: _input.disputeAbsolutePrestate }) ) }) : IOPContractsManagerUtils.DisputeGameConfig({ @@ -362,11 +362,6 @@ contract DeployOPChain is Script { _i.startingAnchorRoot.raw() != ScriptConstants.DEFAULT_OUTPUT_ROOT().root.raw(), "DeployOPChainInput: permissionless startingAnchorRoot cannot be placeholder" ); - require(_i.cannonAbsolutePrestate.raw() != bytes32(0), "DeployOPChainInput: cannonAbsolutePrestate not set"); - require( - _i.cannonKonaAbsolutePrestate.raw() != bytes32(0), - "DeployOPChainInput: cannonKonaAbsolutePrestate not set" - ); } } diff --git a/packages/contracts-bedrock/scripts/libraries/Types.sol b/packages/contracts-bedrock/scripts/libraries/Types.sol index 751f1eaa1e1..c42ac54be62 100644 --- a/packages/contracts-bedrock/scripts/libraries/Types.sol +++ b/packages/contracts-bedrock/scripts/libraries/Types.sol @@ -41,8 +41,6 @@ library Types { GameType disputeGameType; Claim disputeAbsolutePrestate; Hash startingAnchorRoot; - Claim cannonAbsolutePrestate; - Claim cannonKonaAbsolutePrestate; uint256 disputeMaxGameDepth; uint256 disputeSplitDepth; Duration disputeClockExtension; diff --git a/packages/contracts-bedrock/test/opcm/DeployOPChain.t.sol b/packages/contracts-bedrock/test/opcm/DeployOPChain.t.sol index 01fb7e65abb..5acac63fc80 100644 --- a/packages/contracts-bedrock/test/opcm/DeployOPChain.t.sol +++ b/packages/contracts-bedrock/test/opcm/DeployOPChain.t.sol @@ -62,10 +62,8 @@ contract DeployOPChain_TestBase is Test, FeatureFlags { GameType disputeGameType = GameTypes.PERMISSIONED_CANNON; Claim disputeAbsolutePrestate = Claim.wrap(0x038512e02c4c3f7bdaec27d00edf55b7155e0905301e1a88083e4e0a6764d54c); Hash startingAnchorRoot = Hash.wrap(0xdead000000000000000000000000000000000000000000000000000000000000); - Claim cannonAbsolutePrestate = Claim.wrap(0x1111111111111111111111111111111111111111111111111111111111111111); - Claim cannonKonaAbsolutePrestate = Claim.wrap(0x2222222222222222222222222222222222222222222222222222222222222222); // Non-placeholder anchor root for the permissionless deploy tests. - Hash permissionlessAnchorRoot = Hash.wrap(bytes32(uint256(0xc0ffee))); + Hash permissionlessAnchorRoot = Hash.wrap(0x02f4397b2de6fce03b3f9982378c2b4c4deff9c92c662dcc6f9643267aeb5e47); uint256 disputeMaxGameDepth = 73; uint256 disputeSplitDepth = 30; Duration disputeClockExtension = Duration.wrap(3 hours); @@ -134,8 +132,6 @@ contract DeployOPChain_TestBase is Test, FeatureFlags { disputeGameType: disputeGameType, disputeAbsolutePrestate: disputeAbsolutePrestate, startingAnchorRoot: startingAnchorRoot, - cannonAbsolutePrestate: cannonAbsolutePrestate, - cannonKonaAbsolutePrestate: cannonKonaAbsolutePrestate, disputeMaxGameDepth: disputeMaxGameDepth, disputeSplitDepth: disputeSplitDepth, disputeClockExtension: disputeClockExtension, @@ -264,14 +260,6 @@ contract DeployOPChain_Test is DeployOPChain_TestBase { deployOPChain.run(deployOPChainInput); } - /// @notice Permissioned deploys do not require the permissionless prestate inputs. - function test_run_permissionedZeroPermissionlessPrestates_succeeds() public { - deployOPChainInput.cannonAbsolutePrestate = Claim.wrap(bytes32(0)); - deployOPChainInput.cannonKonaAbsolutePrestate = Claim.wrap(bytes32(0)); - DeployOPChain.Output memory doo = deployOPChain.run(deployOPChainInput); - _checkDeploymentAssertions(doo); - } - /// @notice Asserts a permissionless deployment enabled both fault games with the default bond and /// non-zero impls, and seeded the ASR with the input anchor root and respected game type. /// @param doo The output of the deployment. @@ -290,12 +278,12 @@ contract DeployOPChain_Test is DeployOPChain_TestBase { // args distinguish their prestates. assertEq( LibGameArgs.decode(doo.disputeGameFactoryProxy.gameArgs(GameTypes.CANNON)).absolutePrestate, - deployOPChainInput.cannonAbsolutePrestate.raw(), + deployOPChainInput.disputeAbsolutePrestate.raw(), "CANNON prestate wiring" ); assertEq( LibGameArgs.decode(doo.disputeGameFactoryProxy.gameArgs(GameTypes.CANNON_KONA)).absolutePrestate, - deployOPChainInput.cannonKonaAbsolutePrestate.raw(), + deployOPChainInput.disputeAbsolutePrestate.raw(), "CANNON_KONA prestate wiring" ); @@ -512,24 +500,6 @@ contract DeployOPChain_TestFail is DeployOPChain_TestBase { deployOPChain.run(deployOPChainInput); } - function test_run_permissionlessZeroCannonAbsolutePrestate_reverts() public { - skipIfDevFeatureEnabled(DevFeatures.SUPER_ROOT_GAMES_MIGRATION); - deployOPChainInput.disputeGameType = GameTypes.CANNON; - deployOPChainInput.startingAnchorRoot = permissionlessAnchorRoot; - deployOPChainInput.cannonAbsolutePrestate = Claim.wrap(bytes32(0)); - vm.expectRevert("DeployOPChainInput: cannonAbsolutePrestate not set"); - deployOPChain.run(deployOPChainInput); - } - - function test_run_permissionlessZeroCannonKonaAbsolutePrestate_reverts() public { - skipIfDevFeatureEnabled(DevFeatures.SUPER_ROOT_GAMES_MIGRATION); - deployOPChainInput.disputeGameType = GameTypes.CANNON; - deployOPChainInput.startingAnchorRoot = permissionlessAnchorRoot; - deployOPChainInput.cannonKonaAbsolutePrestate = Claim.wrap(bytes32(0)); - vm.expectRevert("DeployOPChainInput: cannonKonaAbsolutePrestate not set"); - deployOPChain.run(deployOPChainInput); - } - function test_runWithBytes_invalidInput_reverts() public { // It should revert if the input bytes cannot be decoded. bytes memory invalidInput = "invalid"; From ab659c49353288d5dcc4eb51949c3f78a46f0428 Mon Sep 17 00:00:00 2001 From: lumoswiz Date: Wed, 1 Jul 2026 11:09:33 +0200 Subject: [PATCH 09/33] refactor: enable selected initial permissionless game only --- .../scripts/deploy/DeployOPChain.s.sol | 6 ++- .../test/opcm/DeployOPChain.t.sol | 47 ++++++++++--------- 2 files changed, 30 insertions(+), 23 deletions(-) diff --git a/packages/contracts-bedrock/scripts/deploy/DeployOPChain.s.sol b/packages/contracts-bedrock/scripts/deploy/DeployOPChain.s.sol index a5133036220..38a0097deda 100644 --- a/packages/contracts-bedrock/scripts/deploy/DeployOPChain.s.sol +++ b/packages/contracts-bedrock/scripts/deploy/DeployOPChain.s.sol @@ -115,6 +115,8 @@ contract DeployOPChain is Script { { (bool permissionless, GameType respectedGameType) = _initialDeployGameSelection(_input.disputeGameType, isSuperRoot); + bool enableCannon = permissionless && _input.disputeGameType.raw() == GameTypes.CANNON.raw(); + bool enableCannonKona = permissionless && _input.disputeGameType.raw() == GameTypes.CANNON_KONA.raw(); // Shared permissioned game config for legacy permissioned games. IOPContractsManagerUtils.PermissionedDisputeGameConfig memory pdgConfig = IOPContractsManagerUtils @@ -132,7 +134,7 @@ contract DeployOPChain is Script { new IOPContractsManagerUtils.DisputeGameConfig[](6); // Config 0: CANNON - disputeGameConfigs[0] = permissionless + disputeGameConfigs[0] = enableCannon ? IOPContractsManagerUtils.DisputeGameConfig({ enabled: true, initBond: DEFAULT_INIT_BOND, @@ -164,7 +166,7 @@ contract DeployOPChain is Script { }); // Config 2: CANNON_KONA - disputeGameConfigs[2] = permissionless + disputeGameConfigs[2] = enableCannonKona ? IOPContractsManagerUtils.DisputeGameConfig({ enabled: true, initBond: DEFAULT_INIT_BOND, diff --git a/packages/contracts-bedrock/test/opcm/DeployOPChain.t.sol b/packages/contracts-bedrock/test/opcm/DeployOPChain.t.sol index 5acac63fc80..a3842bcaf0a 100644 --- a/packages/contracts-bedrock/test/opcm/DeployOPChain.t.sol +++ b/packages/contracts-bedrock/test/opcm/DeployOPChain.t.sol @@ -229,7 +229,7 @@ contract DeployOPChain_Test is DeployOPChain_TestBase { _checkDeploymentAssertions(doo); } - /// @notice A CANNON initial deployment enables both permissionless fault games and seeds the + /// @notice A CANNON initial deployment enables only CANNON and seeds the /// anchor root and respected game type from the input. function test_run_cannonGameType_succeeds() public { skipIfDevFeatureEnabled(DevFeatures.SUPER_ROOT_GAMES_MIGRATION); @@ -240,8 +240,8 @@ contract DeployOPChain_Test is DeployOPChain_TestBase { _checkPermissionlessDeployment(doo, GameTypes.CANNON); } - /// @notice A CANNON_KONA initial deployment behaves like CANNON but sets CANNON_KONA as the - /// respected game type. + /// @notice A CANNON_KONA initial deployment enables only CANNON_KONA and seeds the anchor root + /// and respected game type from the input. function test_run_cannonKonaGameType_succeeds() public { skipIfDevFeatureEnabled(DevFeatures.SUPER_ROOT_GAMES_MIGRATION); deployOPChainInput.disputeGameType = GameTypes.CANNON_KONA; @@ -260,35 +260,40 @@ contract DeployOPChain_Test is DeployOPChain_TestBase { deployOPChain.run(deployOPChainInput); } - /// @notice Asserts a permissionless deployment enabled both fault games with the default bond and - /// non-zero impls, and seeded the ASR with the input anchor root and respected game type. + /// @notice Asserts a permissionless deployment enabled only the selected fault game with the + /// default bond and seeded the ASR with the input anchor root and respected game type. /// @param doo The output of the deployment. - /// @param _respectedType The expected respected game type. - function _checkPermissionlessDeployment(DeployOPChain.Output memory doo, GameType _respectedType) internal view { + /// @param _enabledType The selected and expected respected game type. + function _checkPermissionlessDeployment(DeployOPChain.Output memory doo, GameType _enabledType) internal view { uint256 defaultBond = deployOPChain.DEFAULT_INIT_BOND(); + GameType disabledPermissionlessType = + _enabledType.raw() == GameTypes.CANNON.raw() ? GameTypes.CANNON_KONA : GameTypes.CANNON; - assertEq(doo.disputeGameFactoryProxy.initBonds(GameTypes.CANNON), defaultBond, "CANNON init bond"); - assertNotEq(address(doo.disputeGameFactoryProxy.gameImpls(GameTypes.CANNON)), address(0), "CANNON impl"); - assertEq(doo.disputeGameFactoryProxy.initBonds(GameTypes.CANNON_KONA), defaultBond, "CANNON_KONA init bond"); - assertNotEq( - address(doo.disputeGameFactoryProxy.gameImpls(GameTypes.CANNON_KONA)), address(0), "CANNON_KONA impl" + IOPContractsManagerContainer.Implementations memory impls = IOPContractsManagerV2(opcmAddr).implementations(); + assertEq(doo.disputeGameFactoryProxy.initBonds(_enabledType), defaultBond, "selected init bond"); + assertEq( + address(doo.disputeGameFactoryProxy.gameImpls(_enabledType)), impls.faultDisputeGameImpl, "selected impl" ); - - // gameImpls is the shared faultDisputeGameImpl for both games, so only the per-game stored - // args distinguish their prestates. + assertEq(doo.disputeGameFactoryProxy.initBonds(disabledPermissionlessType), 0, "unselected init bond"); assertEq( - LibGameArgs.decode(doo.disputeGameFactoryProxy.gameArgs(GameTypes.CANNON)).absolutePrestate, - deployOPChainInput.disputeAbsolutePrestate.raw(), - "CANNON prestate wiring" + address(doo.disputeGameFactoryProxy.gameImpls(disabledPermissionlessType)), address(0), "unselected impl" ); + assertEq(doo.disputeGameFactoryProxy.gameArgs(disabledPermissionlessType).length, 0, "unselected args"); + assertEq(doo.disputeGameFactoryProxy.initBonds(GameTypes.PERMISSIONED_CANNON), 0, "permissioned init bond"); + assertEq( + address(doo.disputeGameFactoryProxy.gameImpls(GameTypes.PERMISSIONED_CANNON)), + address(0), + "permissioned impl" + ); + assertEq( - LibGameArgs.decode(doo.disputeGameFactoryProxy.gameArgs(GameTypes.CANNON_KONA)).absolutePrestate, + LibGameArgs.decode(doo.disputeGameFactoryProxy.gameArgs(_enabledType)).absolutePrestate, deployOPChainInput.disputeAbsolutePrestate.raw(), - "CANNON_KONA prestate wiring" + "selected prestate wiring" ); IAnchorStateRegistry asr = doo.anchorStateRegistryProxy; - assertEq(asr.respectedGameType().raw(), _respectedType.raw(), "respected game type"); + assertEq(asr.respectedGameType().raw(), _enabledType.raw(), "respected game type"); Proposal memory anchor = asr.getStartingAnchorRoot(); assertEq(anchor.root.raw(), deployOPChainInput.startingAnchorRoot.raw(), "anchor root"); assertEq(anchor.l2SequenceNumber, 0, "anchor seq"); From 5fa493605f0167060126606f20c8a24b3e4ebd90 Mon Sep 17 00:00:00 2001 From: lumoswiz Date: Wed, 1 Jul 2026 11:26:57 +0200 Subject: [PATCH 10/33] refactor: keep OPCM initial-game validation structural --- .../src/L1/opcm/OPContractsManagerV2.sol | 10 ++++-- .../test/L1/opcm/OPContractsManagerV2.t.sol | 36 +++++++++++++++++-- 2 files changed, 41 insertions(+), 5 deletions(-) diff --git a/packages/contracts-bedrock/src/L1/opcm/OPContractsManagerV2.sol b/packages/contracts-bedrock/src/L1/opcm/OPContractsManagerV2.sol index 718382bb360..aa50ac85ff8 100644 --- a/packages/contracts-bedrock/src/L1/opcm/OPContractsManagerV2.sol +++ b/packages/contracts-bedrock/src/L1/opcm/OPContractsManagerV2.sol @@ -713,6 +713,8 @@ contract OPContractsManagerV2 is ISemver, OPContractsManagerUtilsCaller { revert OPContractsManagerV2_InvalidGameConfigs(); } + bool permissionlessInitialGameEnabled = false; + // 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. @@ -761,6 +763,10 @@ contract OPContractsManagerV2 is ISemver, OPContractsManagerUtilsCaller { } if (_cfg.disputeGameConfigs[i].enabled && (isCannonGame || isCannonKonaGame)) { + if (_isInitialDeployment) { + permissionlessInitialGameEnabled = true; + } + IOPContractsManagerUtils.FaultDisputeGameConfig memory faultGameConfig = abi.decode(_cfg.disputeGameConfigs[i].gameArgs, (IOPContractsManagerUtils.FaultDisputeGameConfig)); if (faultGameConfig.absolutePrestate.raw() == bytes32(0)) { @@ -786,14 +792,12 @@ contract OPContractsManagerV2 is ISemver, OPContractsManagerUtilsCaller { if (_isInitialDeployment) { bytes32 startingAnchorRoot = _cfg.startingAnchorRoot.root.raw(); - bool permissionlessInitialRespectedGameType = _cfg.startingRespectedGameType.raw() == GameTypes.CANNON.raw() - || _cfg.startingRespectedGameType.raw() == GameTypes.CANNON_KONA.raw(); if (startingAnchorRoot == bytes32(0)) { revert OPContractsManagerV2_InvalidGameConfigs(); } - if (permissionlessInitialRespectedGameType && startingAnchorRoot == PLACEHOLDER_STARTING_ANCHOR_ROOT) { + if (permissionlessInitialGameEnabled && startingAnchorRoot == PLACEHOLDER_STARTING_ANCHOR_ROOT) { revert OPContractsManagerV2_InvalidGameConfigs(); } } diff --git a/packages/contracts-bedrock/test/L1/opcm/OPContractsManagerV2.t.sol b/packages/contracts-bedrock/test/L1/opcm/OPContractsManagerV2.t.sol index 2d1b3f71b2a..6bb8363e729 100644 --- a/packages/contracts-bedrock/test/L1/opcm/OPContractsManagerV2.t.sol +++ b/packages/contracts-bedrock/test/L1/opcm/OPContractsManagerV2.t.sol @@ -1756,7 +1756,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"); @@ -1966,11 +1965,15 @@ contract OPContractsManagerV2_Deploy_Test is OPContractsManagerV2_TestInit { deployConfig.disputeGameConfigs[0].initBond = DEFAULT_DISPUTE_GAME_INIT_BOND; deployConfig.disputeGameConfigs[0].gameArgs = abi.encode(IOPContractsManagerUtils.FaultDisputeGameConfig({ absolutePrestate: cannonPrestate })); + deployConfig.startingRespectedGameType = GameTypes.CANNON; IOPContractsManagerV2.ChainContracts memory cts = opcmV2.deploy(deployConfig); assertNotEq( address(cts.disputeGameFactory.gameImpls(GameTypes.CANNON)), address(0), "CANNON impl should be set" ); + assertEq( + cts.anchorStateRegistry.respectedGameType().raw(), GameTypes.CANNON.raw(), "respected game type mismatch" + ); } /// @notice Deploy reverts when CANNON is enabled with a zero prestate. @@ -1993,6 +1996,7 @@ contract OPContractsManagerV2_Deploy_Test is OPContractsManagerV2_TestInit { 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( @@ -2000,6 +2004,35 @@ contract OPContractsManagerV2_Deploy_Test is OPContractsManagerV2_TestInit { address(0), "CANNON_KONA impl should be set" ); + assertEq( + cts.anchorStateRegistry.respectedGameType().raw(), + GameTypes.CANNON_KONA.raw(), + "respected game type mismatch" + ); + } + + /// @notice Multiple permissionless games may be enabled at initial deployment when each config + /// is structurally valid. + function test_deploy_multiplePermissionlessGamesEnabled_succeeds() 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; + + IOPContractsManagerV2.ChainContracts memory cts = opcmV2.deploy(deployConfig); + assertNotEq( + address(cts.disputeGameFactory.gameImpls(GameTypes.CANNON)), address(0), "CANNON impl should be set" + ); + assertNotEq( + address(cts.disputeGameFactory.gameImpls(GameTypes.CANNON_KONA)), + address(0), + "CANNON_KONA impl should be set" + ); } /// @notice Deploy reverts when CANNON_KONA is enabled with a zero prestate. @@ -2046,7 +2079,6 @@ contract OPContractsManagerV2_Deploy_Test is OPContractsManagerV2_TestInit { deployConfig.disputeGameConfigs[0].initBond = DEFAULT_DISPUTE_GAME_INIT_BOND; deployConfig.disputeGameConfigs[0].gameArgs = abi.encode(IOPContractsManagerUtils.FaultDisputeGameConfig({ absolutePrestate: cannonPrestate })); - deployConfig.startingRespectedGameType = GameTypes.CANNON; deployConfig.startingAnchorRoot = Proposal({ root: Hash.wrap(bytes32(hex"dead")), l2SequenceNumber: 0 }); // nosemgrep: sol-style-use-abi-encodecall From 2b173db898c7898e3c5ebc9afe6e49d3d53a5cd4 Mon Sep 17 00:00:00 2001 From: lumoswiz Date: Wed, 1 Jul 2026 12:18:31 +0200 Subject: [PATCH 11/33] test: update remaining selected-game fixtures and comments --- .../test/opcm/DeployOPChain.t.sol | 28 +++++++++++-------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/packages/contracts-bedrock/test/opcm/DeployOPChain.t.sol b/packages/contracts-bedrock/test/opcm/DeployOPChain.t.sol index a3842bcaf0a..27c5824ec60 100644 --- a/packages/contracts-bedrock/test/opcm/DeployOPChain.t.sol +++ b/packages/contracts-bedrock/test/opcm/DeployOPChain.t.sol @@ -237,7 +237,7 @@ contract DeployOPChain_Test is DeployOPChain_TestBase { deployOPChainInput.startingAnchorRoot = permissionlessAnchorRoot; DeployOPChain.Output memory doo = deployOPChain.run(deployOPChainInput); - _checkPermissionlessDeployment(doo, GameTypes.CANNON); + _checkSelectedPermissionlessDeployment(doo, GameTypes.CANNON); } /// @notice A CANNON_KONA initial deployment enables only CANNON_KONA and seeds the anchor root @@ -248,7 +248,7 @@ contract DeployOPChain_Test is DeployOPChain_TestBase { deployOPChainInput.startingAnchorRoot = permissionlessAnchorRoot; DeployOPChain.Output memory doo = deployOPChain.run(deployOPChainInput); - _checkPermissionlessDeployment(doo, GameTypes.CANNON_KONA); + _checkSelectedPermissionlessDeployment(doo, GameTypes.CANNON_KONA); } /// @notice Permissionless game types are rejected when super roots are enabled. @@ -263,16 +263,22 @@ contract DeployOPChain_Test is DeployOPChain_TestBase { /// @notice Asserts a permissionless deployment enabled only the selected fault game with the /// default bond and seeded the ASR with the input anchor root and respected game type. /// @param doo The output of the deployment. - /// @param _enabledType The selected and expected respected game type. - function _checkPermissionlessDeployment(DeployOPChain.Output memory doo, GameType _enabledType) internal view { + /// @param _selectedType The selected and expected respected game type. + function _checkSelectedPermissionlessDeployment( + DeployOPChain.Output memory doo, + GameType _selectedType + ) + internal + view + { uint256 defaultBond = deployOPChain.DEFAULT_INIT_BOND(); GameType disabledPermissionlessType = - _enabledType.raw() == GameTypes.CANNON.raw() ? GameTypes.CANNON_KONA : GameTypes.CANNON; + _selectedType.raw() == GameTypes.CANNON.raw() ? GameTypes.CANNON_KONA : GameTypes.CANNON; IOPContractsManagerContainer.Implementations memory impls = IOPContractsManagerV2(opcmAddr).implementations(); - assertEq(doo.disputeGameFactoryProxy.initBonds(_enabledType), defaultBond, "selected init bond"); + assertEq(doo.disputeGameFactoryProxy.initBonds(_selectedType), defaultBond, "selected init bond"); assertEq( - address(doo.disputeGameFactoryProxy.gameImpls(_enabledType)), impls.faultDisputeGameImpl, "selected impl" + address(doo.disputeGameFactoryProxy.gameImpls(_selectedType)), impls.faultDisputeGameImpl, "selected impl" ); assertEq(doo.disputeGameFactoryProxy.initBonds(disabledPermissionlessType), 0, "unselected init bond"); assertEq( @@ -287,13 +293,13 @@ contract DeployOPChain_Test is DeployOPChain_TestBase { ); assertEq( - LibGameArgs.decode(doo.disputeGameFactoryProxy.gameArgs(_enabledType)).absolutePrestate, + LibGameArgs.decode(doo.disputeGameFactoryProxy.gameArgs(_selectedType)).absolutePrestate, deployOPChainInput.disputeAbsolutePrestate.raw(), "selected prestate wiring" ); IAnchorStateRegistry asr = doo.anchorStateRegistryProxy; - assertEq(asr.respectedGameType().raw(), _enabledType.raw(), "respected game type"); + assertEq(asr.respectedGameType().raw(), _selectedType.raw(), "respected game type"); Proposal memory anchor = asr.getStartingAnchorRoot(); assertEq(anchor.root.raw(), deployOPChainInput.startingAnchorRoot.raw(), "anchor root"); assertEq(anchor.l2SequenceNumber, 0, "anchor seq"); @@ -359,7 +365,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( @@ -369,7 +375,7 @@ 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)), From c940d5f66d5d62994bec2ecd0ddc57bf55db3d2f Mon Sep 17 00:00:00 2001 From: lumoswiz Date: Wed, 1 Jul 2026 12:31:29 +0200 Subject: [PATCH 12/33] chore: refresh OPContractsManagerV2 semver lock --- packages/contracts-bedrock/snapshots/semver-lock.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/contracts-bedrock/snapshots/semver-lock.json b/packages/contracts-bedrock/snapshots/semver-lock.json index 05843514e01..858da91507d 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": "0xa2a91101d962601f475cdd2cb78c01e1b1c8096b16496c8d113601836ff9fcb4", - "sourceCodeHash": "0x0f7699e6beeab595b6f445a9571b45a7b9fbab0f054374c96cb97aac6e81718e" + "initCodeHash": "0xfebb811f8f59b56cc82caf0c1491bf9f8da3523a237e454e9b5b82964b2c78fe", + "sourceCodeHash": "0x25644b370ff227d4946669cadc6b59e02232e4da2f318149487547bf282094da" }, "src/L2/BaseFeeVault.sol:BaseFeeVault": { "initCodeHash": "0x18650c65fa6f23fefc6f0c5fff605f12be99fce0bfbcbc0644403138e4c8bb12", From 852e90768f41bbad2635a0bf46030890e6687896 Mon Sep 17 00:00:00 2001 From: lumoswiz Date: Wed, 1 Jul 2026 15:43:17 +0200 Subject: [PATCH 13/33] chore: remove stale DeployOPChain anchor helper --- .../scripts/deploy/DeployOPChain.s.sol | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/packages/contracts-bedrock/scripts/deploy/DeployOPChain.s.sol b/packages/contracts-bedrock/scripts/deploy/DeployOPChain.s.sol index 38a0097deda..19dbc97992e 100644 --- a/packages/contracts-bedrock/scripts/deploy/DeployOPChain.s.sol +++ b/packages/contracts-bedrock/scripts/deploy/DeployOPChain.s.sol @@ -530,20 +530,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()); - } } From 0b4e2925b8c3e5b288cadc60cd6a91cd501f72b9 Mon Sep 17 00:00:00 2001 From: lumoswiz Date: Thu, 2 Jul 2026 09:44:27 +0200 Subject: [PATCH 14/33] chore: document initial deploy game selection --- packages/contracts-bedrock/scripts/deploy/DeployOPChain.s.sol | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/contracts-bedrock/scripts/deploy/DeployOPChain.s.sol b/packages/contracts-bedrock/scripts/deploy/DeployOPChain.s.sol index 19dbc97992e..344afb0a3a9 100644 --- a/packages/contracts-bedrock/scripts/deploy/DeployOPChain.s.sol +++ b/packages/contracts-bedrock/scripts/deploy/DeployOPChain.s.sol @@ -313,6 +313,7 @@ contract DeployOPChain is Script { } /// @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 From b661b203b14a7063b982d54f5f2f4c74387f48de Mon Sep 17 00:00:00 2001 From: IamFlux <175354924+0xiamflux@users.noreply.github.com> Date: Tue, 7 Jul 2026 10:35:06 -0600 Subject: [PATCH 15/33] refactor: extract boolean flags for rest of games --- .../src/L1/opcm/OPContractsManagerV2.sol | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/packages/contracts-bedrock/src/L1/opcm/OPContractsManagerV2.sol b/packages/contracts-bedrock/src/L1/opcm/OPContractsManagerV2.sol index aa50ac85ff8..1fedf868d63 100644 --- a/packages/contracts-bedrock/src/L1/opcm/OPContractsManagerV2.sol +++ b/packages/contracts-bedrock/src/L1/opcm/OPContractsManagerV2.sol @@ -721,7 +721,10 @@ contract OPContractsManagerV2 is ISemver, OPContractsManagerUtilsCaller { for (uint256 i = 0; i < _cfg.disputeGameConfigs.length; i++) { 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(); @@ -732,14 +735,14 @@ contract OPContractsManagerV2 is ISemver, OPContractsManagerUtilsCaller { revert OPContractsManagerV2_InvalidGameConfigs(); } - if (rawGameType == 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 ( - rawGameType != GameTypes.SUPER_PERMISSIONED.raw() && _cfg.disputeGameConfigs[i].enabled + !isSuperPermissionedGame && _cfg.disputeGameConfigs[i].enabled && _cfg.disputeGameConfigs[i].initBond == 0 ) { revert OPContractsManagerV2_InvalidGameConfigs(); @@ -747,8 +750,8 @@ contract OPContractsManagerV2 is ISemver, OPContractsManagerUtilsCaller { // During initial deployment, only CANNON, PERMISSIONED_CANNON, CANNON_KONA, and // SUPER_PERMISSIONED may be enabled. - bool enableableAtInitialDeployment = isCannonGame || rawGameType == GameTypes.PERMISSIONED_CANNON.raw() - || isCannonKonaGame || rawGameType == GameTypes.SUPER_PERMISSIONED.raw(); + bool enableableAtInitialDeployment = + isCannonGame || isPermissionedCannonGame || isCannonKonaGame || isSuperPermissionedGame; if (_isInitialDeployment && !enableableAtInitialDeployment && _cfg.disputeGameConfigs[i].enabled) { revert OPContractsManagerV2_InvalidGameConfigs(); @@ -756,7 +759,7 @@ contract OPContractsManagerV2 is ISemver, OPContractsManagerUtilsCaller { // ZK_DISPUTE_GAME can only be enabled when the dev flag is on (upgrade path). if ( - rawGameType == GameTypes.ZK_DISPUTE_GAME.raw() && _cfg.disputeGameConfigs[i].enabled + isZkDisputeGame && _cfg.disputeGameConfigs[i].enabled && !isDevFeatureEnabled(DevFeatures.ZK_DISPUTE_GAME) ) { revert OPContractsManagerV2_InvalidGameConfigs(); From d9c1e6848f95f703e93f3e704aa01ddfa9532ad0 Mon Sep 17 00:00:00 2001 From: IamFlux <175354924+0xiamflux@users.noreply.github.com> Date: Tue, 7 Jul 2026 10:49:27 -0600 Subject: [PATCH 16/33] chore: add clarifying comment on prestate validity --- packages/contracts-bedrock/src/L1/opcm/OPContractsManagerV2.sol | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/contracts-bedrock/src/L1/opcm/OPContractsManagerV2.sol b/packages/contracts-bedrock/src/L1/opcm/OPContractsManagerV2.sol index 1fedf868d63..f1bd10a591a 100644 --- a/packages/contracts-bedrock/src/L1/opcm/OPContractsManagerV2.sol +++ b/packages/contracts-bedrock/src/L1/opcm/OPContractsManagerV2.sol @@ -765,6 +765,7 @@ contract OPContractsManagerV2 is ISemver, OPContractsManagerUtilsCaller { revert OPContractsManagerV2_InvalidGameConfigs(); } + // If a permissionless game is being enabled the prestate must be not empty, otherwise revert with error. if (_cfg.disputeGameConfigs[i].enabled && (isCannonGame || isCannonKonaGame)) { if (_isInitialDeployment) { permissionlessInitialGameEnabled = true; From f8e4932e530a6771f7d5c698dbc71eeb8b0d74a8 Mon Sep 17 00:00:00 2001 From: IamFlux <175354924+0xiamflux@users.noreply.github.com> Date: Tue, 7 Jul 2026 11:37:02 -0600 Subject: [PATCH 17/33] refactor: extract _isSuperRootEnabled to internal function --- .../scripts/deploy/DeployOPChain.s.sol | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/packages/contracts-bedrock/scripts/deploy/DeployOPChain.s.sol b/packages/contracts-bedrock/scripts/deploy/DeployOPChain.s.sol index 344afb0a3a9..6fb9effeaba 100644 --- a/packages/contracts-bedrock/scripts/deploy/DeployOPChain.s.sol +++ b/packages/contracts-bedrock/scripts/deploy/DeployOPChain.s.sol @@ -77,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); @@ -328,6 +328,13 @@ contract DeployOPChain is Script { : (_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); + } + // -------- Validations -------- /// @notice Checks if the input is valid. @@ -349,9 +356,7 @@ contract DeployOPChain is Script { require(_i.opcm != address(0), "DeployOPChainInput: opcm not set"); DeployUtils.assertValidContractAddress(_i.opcm); - bool superRoot = DevFeatures.isDevFeatureEnabled( - IOPContractsManagerV2(_i.opcm).devFeatureBitmap(), DevFeatures.SUPER_ROOT_GAMES_MIGRATION - ); + bool superRoot = _isSuperRootEnabled(IOPContractsManagerV2(_i.opcm)); bool permissionless = _isPermissionlessDeploy(_i.disputeGameType, superRoot); require(_i.disputeMaxGameDepth != 0, "DeployOPChainInput: disputeMaxGameDepth not set"); From 13f4d5cabf77946ccab8e627678a39f19a0f9603 Mon Sep 17 00:00:00 2001 From: IamFlux <175354924+0xiamflux@users.noreply.github.com> Date: Tue, 7 Jul 2026 11:49:05 -0600 Subject: [PATCH 18/33] refactor: move out placeholder to constants file --- .../src/L1/opcm/OPContractsManagerV2.sol | 8 +++----- packages/contracts-bedrock/src/libraries/Constants.sol | 3 +++ 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/contracts-bedrock/src/L1/opcm/OPContractsManagerV2.sol b/packages/contracts-bedrock/src/L1/opcm/OPContractsManagerV2.sol index f1bd10a591a..670b941b18f 100644 --- a/packages/contracts-bedrock/src/L1/opcm/OPContractsManagerV2.sol +++ b/packages/contracts-bedrock/src/L1/opcm/OPContractsManagerV2.sol @@ -54,10 +54,6 @@ import { IOPContractsManagerUtils } from "interfaces/L1/opcm/IOPContractsManager /// design. Look at _apply, squint, and imagine that it can output an upgrade plan rather than /// actually executing the upgrade, and then you'll see how it can be improved. contract OPContractsManagerV2 is ISemver, OPContractsManagerUtilsCaller { - // TODO(#20912): Remove once deploy pipelines provide real anchor roots. - /// @notice Placeholder anchor root historically used for permissioned initial deployments. - bytes32 internal constant PLACEHOLDER_STARTING_ANCHOR_ROOT = bytes32(hex"dead"); - /// @notice Contracts that represent the Superchain system. struct SuperchainContracts { ISuperchainConfig superchainConfig; @@ -801,7 +797,9 @@ contract OPContractsManagerV2 is ISemver, OPContractsManagerUtilsCaller { revert OPContractsManagerV2_InvalidGameConfigs(); } - if (permissionlessInitialGameEnabled && startingAnchorRoot == PLACEHOLDER_STARTING_ANCHOR_ROOT) { + // TODO(#20912): Remove once deploy pipelines provide real anchor roots. + // A permissionless initial deployment must not use the placeholder anchor root. + if (permissionlessInitialGameEnabled && startingAnchorRoot == Constants.PLACEHOLDER_STARTING_ANCHOR_ROOT) { revert OPContractsManagerV2_InvalidGameConfigs(); } } 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) { From 5f17dce85c0d03fe623482b2a6b0d1a1854f7fe8 Mon Sep 17 00:00:00 2001 From: IamFlux <175354924+0xiamflux@users.noreply.github.com> Date: Tue, 7 Jul 2026 12:23:03 -0600 Subject: [PATCH 19/33] refactor: removes _isPermissionlessDeploy --- .../scripts/deploy/DeployOPChain.s.sol | 33 ++++++++++--------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/packages/contracts-bedrock/scripts/deploy/DeployOPChain.s.sol b/packages/contracts-bedrock/scripts/deploy/DeployOPChain.s.sol index 6fb9effeaba..fd03e5e41f4 100644 --- a/packages/contracts-bedrock/scripts/deploy/DeployOPChain.s.sol +++ b/packages/contracts-bedrock/scripts/deploy/DeployOPChain.s.sol @@ -299,19 +299,6 @@ contract DeployOPChain is Script { require(cfg_.maxResourceLimit > 0, "DeployOPChain: gasLimit too small for any deposit budget"); } - /// @notice Returns whether the deployment should enable permissionless dispute games. - function _isPermissionlessDeploy(GameType _disputeGameType, bool _isSuperRoot) internal pure returns (bool) { - bool supportedGameType = _disputeGameType.raw() == GameTypes.PERMISSIONED_CANNON.raw() - || _disputeGameType.raw() == GameTypes.CANNON.raw() || _disputeGameType.raw() == GameTypes.CANNON_KONA.raw(); - require(supportedGameType, "DeployOPChain: unsupported dispute game type"); - require( - !_isSuperRoot || _disputeGameType.raw() == GameTypes.PERMISSIONED_CANNON.raw(), - "DeployOPChain: permissionless game type not supported with super roots" - ); - - return !_isSuperRoot && _disputeGameType.raw() != GameTypes.PERMISSIONED_CANNON.raw(); - } - /// @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( @@ -322,7 +309,23 @@ contract DeployOPChain is Script { pure returns (bool permissionless_, GameType respectedGameType_) { - permissionless_ = _isPermissionlessDeploy(_disputeGameType, _isSuperRoot); + permissionless_ = + _disputeGameType.raw() == GameTypes.CANNON.raw() || + _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); @@ -357,7 +360,7 @@ 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 = _isPermissionlessDeploy(_i.disputeGameType, superRoot); + (bool permissionless,) = _initialDeployGameSelection(_i.disputeGameType, superRoot); require(_i.disputeMaxGameDepth != 0, "DeployOPChainInput: disputeMaxGameDepth not set"); require(_i.disputeSplitDepth != 0, "DeployOPChainInput: disputeSplitDepth not set"); From cb675cacf95f7edf88e16fafe2a022ca53a123e8 Mon Sep 17 00:00:00 2001 From: IamFlux <175354924+0xiamflux@users.noreply.github.com> Date: Tue, 7 Jul 2026 14:00:15 -0600 Subject: [PATCH 20/33] refactor: simplify config checks for initial deployments --- .../src/L1/opcm/OPContractsManagerV2.sol | 30 ++++++++----------- 1 file changed, 12 insertions(+), 18 deletions(-) diff --git a/packages/contracts-bedrock/src/L1/opcm/OPContractsManagerV2.sol b/packages/contracts-bedrock/src/L1/opcm/OPContractsManagerV2.sol index 670b941b18f..1cbf49a2903 100644 --- a/packages/contracts-bedrock/src/L1/opcm/OPContractsManagerV2.sol +++ b/packages/contracts-bedrock/src/L1/opcm/OPContractsManagerV2.sol @@ -709,7 +709,10 @@ contract OPContractsManagerV2 is ISemver, OPContractsManagerUtilsCaller { revert OPContractsManagerV2_InvalidGameConfigs(); } - bool permissionlessInitialGameEnabled = false; + // 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 @@ -761,12 +764,17 @@ contract OPContractsManagerV2 is ISemver, OPContractsManagerUtilsCaller { revert OPContractsManagerV2_InvalidGameConfigs(); } - // If a permissionless game is being enabled the prestate must be not empty, otherwise revert with error. if (_cfg.disputeGameConfigs[i].enabled && (isCannonGame || isCannonKonaGame)) { - if (_isInitialDeployment) { - permissionlessInitialGameEnabled = true; + // 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)) { @@ -789,20 +797,6 @@ contract OPContractsManagerV2 is ISemver, OPContractsManagerUtilsCaller { if (!startingGameTypeFound) { revert OPContractsManagerV2_InvalidGameConfigs(); } - - if (_isInitialDeployment) { - bytes32 startingAnchorRoot = _cfg.startingAnchorRoot.root.raw(); - - if (startingAnchorRoot == bytes32(0)) { - revert OPContractsManagerV2_InvalidGameConfigs(); - } - - // TODO(#20912): Remove once deploy pipelines provide real anchor roots. - // A permissionless initial deployment must not use the placeholder anchor root. - if (permissionlessInitialGameEnabled && startingAnchorRoot == Constants.PLACEHOLDER_STARTING_ANCHOR_ROOT) { - revert OPContractsManagerV2_InvalidGameConfigs(); - } - } } /// @notice Executes the deployment/upgrade action. From 732e97edd8103e38b9771c827dbb480eb5163cd2 Mon Sep 17 00:00:00 2001 From: IamFlux <175354924+0xiamflux@users.noreply.github.com> Date: Tue, 7 Jul 2026 14:04:26 -0600 Subject: [PATCH 21/33] chore: lint fix --- .../contracts-bedrock/scripts/deploy/DeployOPChain.s.sol | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/packages/contracts-bedrock/scripts/deploy/DeployOPChain.s.sol b/packages/contracts-bedrock/scripts/deploy/DeployOPChain.s.sol index fd03e5e41f4..d32113af83e 100644 --- a/packages/contracts-bedrock/scripts/deploy/DeployOPChain.s.sol +++ b/packages/contracts-bedrock/scripts/deploy/DeployOPChain.s.sol @@ -310,20 +310,17 @@ contract DeployOPChain is Script { returns (bool permissionless_, GameType respectedGameType_) { permissionless_ = - _disputeGameType.raw() == GameTypes.CANNON.raw() || - _disputeGameType.raw() == GameTypes.CANNON_KONA.raw(); + _disputeGameType.raw() == GameTypes.CANNON.raw() || _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(), + 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" + !(_isSuperRoot && permissionless_), "DeployOPChain: permissionless game type not supported with super roots" ); respectedGameType_ = permissionless_ From 319b506adb0f573d46e317d7e5d941e12fe7a0b1 Mon Sep 17 00:00:00 2001 From: IamFlux <175354924+0xiamflux@users.noreply.github.com> Date: Tue, 7 Jul 2026 21:07:08 -0600 Subject: [PATCH 22/33] refactor: extracts game config creation to internal function --- .../scripts/deploy/DeployOPChain.s.sol | 86 +++++++++---------- 1 file changed, 42 insertions(+), 44 deletions(-) diff --git a/packages/contracts-bedrock/scripts/deploy/DeployOPChain.s.sol b/packages/contracts-bedrock/scripts/deploy/DeployOPChain.s.sol index d32113af83e..057546ad15f 100644 --- a/packages/contracts-bedrock/scripts/deploy/DeployOPChain.s.sol +++ b/packages/contracts-bedrock/scripts/deploy/DeployOPChain.s.sol @@ -117,6 +117,8 @@ contract DeployOPChain is Script { _initialDeployGameSelection(_input.disputeGameType, isSuperRoot); bool enableCannon = permissionless && _input.disputeGameType.raw() == GameTypes.CANNON.raw(); bool enableCannonKona = permissionless && _input.disputeGameType.raw() == GameTypes.CANNON_KONA.raw(); + // PERMISSIONED_CANNON is the default game in non-super-root, non-permissionless deployments. + bool enablePermissionedCannon = !isSuperRoot && !permissionless; // Shared permissioned game config for legacy permissioned games. IOPContractsManagerUtils.PermissionedDisputeGameConfig memory pdgConfig = IOPContractsManagerUtils @@ -134,53 +136,26 @@ contract DeployOPChain is Script { new IOPContractsManagerUtils.DisputeGameConfig[](6); // Config 0: CANNON - disputeGameConfigs[0] = enableCannon - ? IOPContractsManagerUtils.DisputeGameConfig({ - enabled: true, - initBond: DEFAULT_INIT_BOND, - gameType: GameTypes.CANNON, - gameArgs: abi.encode( - IOPContractsManagerUtils.FaultDisputeGameConfig({ absolutePrestate: _input.disputeAbsolutePrestate }) - ) - }) - : IOPContractsManagerUtils.DisputeGameConfig({ - enabled: false, - initBond: 0, - gameType: GameTypes.CANNON, - gameArgs: bytes("") - }); + disputeGameConfigs[0] = _createGameConfig( + enableCannon, + GameTypes.CANNON, + abi.encode( + IOPContractsManagerUtils.FaultDisputeGameConfig({ absolutePrestate: _input.disputeAbsolutePrestate }) + ) + ); - // Config 1: PERMISSIONED_CANNON — enabled only in non-super-root permissioned mode. - disputeGameConfigs[1] = isSuperRoot || permissionless - ? 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 1: PERMISSIONED_CANNON + disputeGameConfigs[1] = + _createGameConfig(enablePermissionedCannon, GameTypes.PERMISSIONED_CANNON, abi.encode(pdgConfig)); // Config 2: CANNON_KONA - disputeGameConfigs[2] = enableCannonKona - ? IOPContractsManagerUtils.DisputeGameConfig({ - enabled: true, - initBond: DEFAULT_INIT_BOND, - gameType: GameTypes.CANNON_KONA, - gameArgs: abi.encode( - IOPContractsManagerUtils.FaultDisputeGameConfig({ absolutePrestate: _input.disputeAbsolutePrestate }) - ) - }) - : IOPContractsManagerUtils.DisputeGameConfig({ - enabled: false, - initBond: 0, - gameType: GameTypes.CANNON_KONA, - gameArgs: bytes("") - }); + 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 @@ -335,6 +310,29 @@ contract DeployOPChain is Script { 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. From d63c21a16d2972b28e1dd9ee9e3a378ed304583e Mon Sep 17 00:00:00 2001 From: IamFlux <175354924+0xiamflux@users.noreply.github.com> Date: Tue, 7 Jul 2026 21:36:26 -0600 Subject: [PATCH 23/33] refactor: remove if/else branching in ChainAssertions --- .../scripts/deploy/ChainAssertions.sol | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/packages/contracts-bedrock/scripts/deploy/ChainAssertions.sol b/packages/contracts-bedrock/scripts/deploy/ChainAssertions.sol index 1ec36df8bd0..2ae94f6a17a 100644 --- a/packages/contracts-bedrock/scripts/deploy/ChainAssertions.sol +++ b/packages/contracts-bedrock/scripts/deploy/ChainAssertions.sol @@ -423,17 +423,10 @@ library ChainAssertions { // The below check cannot be done in the standard validator because the assertion only applies at deploy time. Proposal memory actualRoot = _anchorStateRegistryProxy.getStartingAnchorRoot(); - if (_isProxy) { - require( - _anchorStateRegistryProxy.respectedGameType().raw() == _expectedRespectedGameType.raw(), "ANCHORP-30" - ); - require(actualRoot.root.raw() == _expectedRoot.raw(), "ANCHORP-40"); - require(actualRoot.l2SequenceNumber == 0, "ANCHORP-50"); - } else { - require(_anchorStateRegistryProxy.respectedGameType().raw() == GameType.wrap(0).raw(), "ANCHORP-30"); - require(actualRoot.root.raw() == bytes32(0), "ANCHORP-40"); - require(actualRoot.l2SequenceNumber == 0, "ANCHORP-50"); - } + + 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. From f363d199009ddc516d2cd919d017c3c444eb57b1 Mon Sep 17 00:00:00 2001 From: IamFlux <175354924+0xiamflux@users.noreply.github.com> Date: Tue, 7 Jul 2026 22:58:09 -0600 Subject: [PATCH 24/33] chore: just pr --- packages/contracts-bedrock/snapshots/semver-lock.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/contracts-bedrock/snapshots/semver-lock.json b/packages/contracts-bedrock/snapshots/semver-lock.json index d213de55e14..cd5b274d5cf 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": "0xfebb811f8f59b56cc82caf0c1491bf9f8da3523a237e454e9b5b82964b2c78fe", - "sourceCodeHash": "0x25644b370ff227d4946669cadc6b59e02232e4da2f318149487547bf282094da" + "initCodeHash": "0x35e6acf023b0388d2a1832483b231894057f10228ff74220f334f7f233d39672", + "sourceCodeHash": "0x65d7b4213931b5026e34a56e2331bb2464a12d42e4ec9b0f034da5473784d9f0" }, "src/L2/BaseFeeVault.sol:BaseFeeVault": { "initCodeHash": "0x18650c65fa6f23fefc6f0c5fff605f12be99fce0bfbcbc0644403138e4c8bb12", From 8ee953b1cd46b1c3b5b21e92da22bcf6afae815e Mon Sep 17 00:00:00 2001 From: lumoswiz Date: Wed, 8 Jul 2026 14:04:34 +0200 Subject: [PATCH 25/33] test: fix stale legacy cannon leftovers from develop merge --- .../contracts-bedrock/test/L1/opcm/OPContractsManagerV2.t.sol | 2 +- packages/contracts-bedrock/test/setup/ForkL1Live.s.sol | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/contracts-bedrock/test/L1/opcm/OPContractsManagerV2.t.sol b/packages/contracts-bedrock/test/L1/opcm/OPContractsManagerV2.t.sol index 2fb8320c2b2..23ecc42083e 100644 --- a/packages/contracts-bedrock/test/L1/opcm/OPContractsManagerV2.t.sol +++ b/packages/contracts-bedrock/test/L1/opcm/OPContractsManagerV2.t.sol @@ -2125,7 +2125,7 @@ contract OPContractsManagerV2_Deploy_Test is OPContractsManagerV2_TestInit { function test_deploy_permissionedPlaceholderStartingAnchorRoot_succeeds() public { deployConfig.startingAnchorRoot = Proposal({ root: Hash.wrap(bytes32(hex"dead")), l2SequenceNumber: 0 }); bool superRoot = isDevFeatureEnabled(DevFeatures.SUPER_ROOT_GAMES_MIGRATION); - string memory expectedErrors = superRoot ? "SCKDG-SHAPE,SCKDG-10" : "CKDG-NOSHAPE,PLDG-10,CKDG-10"; + 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(), bytes32(hex"dead"), "starting anchor root mismatch"); diff --git a/packages/contracts-bedrock/test/setup/ForkL1Live.s.sol b/packages/contracts-bedrock/test/setup/ForkL1Live.s.sol index 6936647dc9c..93cc80b0532 100644 --- a/packages/contracts-bedrock/test/setup/ForkL1Live.s.sol +++ b/packages/contracts-bedrock/test/setup/ForkL1Live.s.sol @@ -410,7 +410,6 @@ contract ForkL1Live is Deployer, StdAssertions, FeatureFlags { } else { // Standard upgrade path: CANNON disabled, remaining legacy types enabled, super types disabled. // Order must match validGameTypes in OPContractsManagerV2._assertValidFullConfig(). - uint256 cannonInitBond = _permissionlessGameInitBondForUpgrade(disputeGameFactory, GameTypes.CANNON); uint256 cannonKonaInitBond = _permissionlessGameInitBondForUpgrade(disputeGameFactory, GameTypes.CANNON_KONA); From 828f6d31c17bfa5fae7722024a03e92753b2cae8 Mon Sep 17 00:00:00 2001 From: lumoswiz Date: Wed, 8 Jul 2026 16:35:26 +0200 Subject: [PATCH 26/33] refactor: restrict permissionless initial deploy selection to CANNON_KONA --- .../scripts/deploy/DeployOPChain.s.sol | 19 ++++++++----------- .../test/opcm/DeployOPChain.t.sol | 14 ++++++-------- 2 files changed, 14 insertions(+), 19 deletions(-) diff --git a/packages/contracts-bedrock/scripts/deploy/DeployOPChain.s.sol b/packages/contracts-bedrock/scripts/deploy/DeployOPChain.s.sol index 057546ad15f..8d9b32855c2 100644 --- a/packages/contracts-bedrock/scripts/deploy/DeployOPChain.s.sol +++ b/packages/contracts-bedrock/scripts/deploy/DeployOPChain.s.sol @@ -115,7 +115,6 @@ contract DeployOPChain is Script { { (bool permissionless, GameType respectedGameType) = _initialDeployGameSelection(_input.disputeGameType, isSuperRoot); - bool enableCannon = permissionless && _input.disputeGameType.raw() == GameTypes.CANNON.raw(); bool enableCannonKona = permissionless && _input.disputeGameType.raw() == GameTypes.CANNON_KONA.raw(); // PERMISSIONED_CANNON is the default game in non-super-root, non-permissionless deployments. bool enablePermissionedCannon = !isSuperRoot && !permissionless; @@ -135,14 +134,13 @@ contract DeployOPChain is Script { IOPContractsManagerUtils.DisputeGameConfig[] memory disputeGameConfigs = new IOPContractsManagerUtils.DisputeGameConfig[](6); - // Config 0: CANNON - disputeGameConfigs[0] = _createGameConfig( - enableCannon, - GameTypes.CANNON, - abi.encode( - IOPContractsManagerUtils.FaultDisputeGameConfig({ absolutePrestate: _input.disputeAbsolutePrestate }) - ) - ); + // 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, + gameType: GameTypes.CANNON, + gameArgs: bytes("") + }); // Config 1: PERMISSIONED_CANNON disputeGameConfigs[1] = @@ -284,8 +282,7 @@ contract DeployOPChain is Script { pure returns (bool permissionless_, GameType respectedGameType_) { - permissionless_ = - _disputeGameType.raw() == GameTypes.CANNON.raw() || _disputeGameType.raw() == GameTypes.CANNON_KONA.raw(); + permissionless_ = _disputeGameType.raw() == GameTypes.CANNON_KONA.raw(); // PERMISSIONED_CANNON is the only **permissioned** type supported for an initial deploy. require( diff --git a/packages/contracts-bedrock/test/opcm/DeployOPChain.t.sol b/packages/contracts-bedrock/test/opcm/DeployOPChain.t.sol index 27c5824ec60..5dfcf69d78a 100644 --- a/packages/contracts-bedrock/test/opcm/DeployOPChain.t.sol +++ b/packages/contracts-bedrock/test/opcm/DeployOPChain.t.sol @@ -229,15 +229,13 @@ contract DeployOPChain_Test is DeployOPChain_TestBase { _checkDeploymentAssertions(doo); } - /// @notice A CANNON initial deployment enables only CANNON and seeds the - /// anchor root and respected game type from the input. - function test_run_cannonGameType_succeeds() public { - skipIfDevFeatureEnabled(DevFeatures.SUPER_ROOT_GAMES_MIGRATION); + /// @notice Legacy CANNON is rejected as an initial deployment game type. + function test_run_cannonGameType_reverts() public { deployOPChainInput.disputeGameType = GameTypes.CANNON; deployOPChainInput.startingAnchorRoot = permissionlessAnchorRoot; - DeployOPChain.Output memory doo = deployOPChain.run(deployOPChainInput); - _checkSelectedPermissionlessDeployment(doo, GameTypes.CANNON); + vm.expectRevert("DeployOPChain: unsupported dispute game type"); + deployOPChain.run(deployOPChainInput); } /// @notice A CANNON_KONA initial deployment enables only CANNON_KONA and seeds the anchor root @@ -254,7 +252,7 @@ contract DeployOPChain_Test is DeployOPChain_TestBase { /// @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; + deployOPChainInput.disputeGameType = GameTypes.CANNON_KONA; deployOPChainInput.startingAnchorRoot = permissionlessAnchorRoot; vm.expectRevert("DeployOPChain: permissionless game type not supported with super roots"); deployOPChain.run(deployOPChainInput); @@ -505,7 +503,7 @@ contract DeployOPChain_TestFail is DeployOPChain_TestBase { function test_run_permissionlessPlaceholderStartingAnchorRoot_reverts() public { skipIfDevFeatureEnabled(DevFeatures.SUPER_ROOT_GAMES_MIGRATION); - deployOPChainInput.disputeGameType = GameTypes.CANNON; + deployOPChainInput.disputeGameType = GameTypes.CANNON_KONA; // startingAnchorRoot stays at the 0xdead placeholder default. vm.expectRevert("DeployOPChainInput: permissionless startingAnchorRoot cannot be placeholder"); deployOPChain.run(deployOPChainInput); From d7c5bd158e01ccbf625b71637741ea074c9af17a Mon Sep 17 00:00:00 2001 From: lumoswiz Date: Wed, 8 Jul 2026 17:02:46 +0200 Subject: [PATCH 27/33] feat: reject legacy CANNON at initial deployment in OPCMv2 --- .../snapshots/semver-lock.json | 4 +- .../src/L1/opcm/OPContractsManagerV2.sol | 7 +-- .../test/L1/opcm/OPContractsManagerV2.t.sol | 57 ++++++------------- 3 files changed, 22 insertions(+), 46 deletions(-) diff --git a/packages/contracts-bedrock/snapshots/semver-lock.json b/packages/contracts-bedrock/snapshots/semver-lock.json index cd5b274d5cf..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": "0x35e6acf023b0388d2a1832483b231894057f10228ff74220f334f7f233d39672", - "sourceCodeHash": "0x65d7b4213931b5026e34a56e2331bb2464a12d42e4ec9b0f034da5473784d9f0" + "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 1cbf49a2903..10479a30941 100644 --- a/packages/contracts-bedrock/src/L1/opcm/OPContractsManagerV2.sol +++ b/packages/contracts-bedrock/src/L1/opcm/OPContractsManagerV2.sol @@ -747,10 +747,9 @@ contract OPContractsManagerV2 is ISemver, OPContractsManagerUtilsCaller { revert OPContractsManagerV2_InvalidGameConfigs(); } - // During initial deployment, only CANNON, PERMISSIONED_CANNON, CANNON_KONA, and - // SUPER_PERMISSIONED may be enabled. - bool enableableAtInitialDeployment = - isCannonGame || isPermissionedCannonGame || isCannonKonaGame || isSuperPermissionedGame; + // Post-U19 initial deployments may enable only PERMISSIONED_CANNON, + // CANNON_KONA, and SUPER_PERMISSIONED. Legacy CANNON is rejected. + bool enableableAtInitialDeployment = isPermissionedCannonGame || isCannonKonaGame || isSuperPermissionedGame; if (_isInitialDeployment && !enableableAtInitialDeployment && _cfg.disputeGameConfigs[i].enabled) { revert OPContractsManagerV2_InvalidGameConfigs(); diff --git a/packages/contracts-bedrock/test/L1/opcm/OPContractsManagerV2.t.sol b/packages/contracts-bedrock/test/L1/opcm/OPContractsManagerV2.t.sol index 23ecc42083e..489159685b9 100644 --- a/packages/contracts-bedrock/test/L1/opcm/OPContractsManagerV2.t.sol +++ b/packages/contracts-bedrock/test/L1/opcm/OPContractsManagerV2.t.sol @@ -1971,31 +1971,14 @@ contract OPContractsManagerV2_Deploy_Test is OPContractsManagerV2_TestInit { ); } - /// @notice CANNON may be enabled at initial deployment now that its prestate is supplied via the - /// deploy config. - function test_deploy_cannonGameEnabled_succeeds() public { + /// @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 = DEFAULT_DISPUTE_GAME_INIT_BOND; deployConfig.disputeGameConfigs[0].gameArgs = abi.encode(IOPContractsManagerUtils.FaultDisputeGameConfig({ absolutePrestate: cannonPrestate })); deployConfig.startingRespectedGameType = GameTypes.CANNON; - IOPContractsManagerV2.ChainContracts memory cts = opcmV2.deploy(deployConfig); - assertNotEq( - address(cts.disputeGameFactory.gameImpls(GameTypes.CANNON)), address(0), "CANNON impl should be set" - ); - assertEq( - cts.anchorStateRegistry.respectedGameType().raw(), GameTypes.CANNON.raw(), "respected game type mismatch" - ); - } - - /// @notice Deploy reverts when CANNON is enabled with a zero prestate. - function test_deploy_cannonGameEnabledZeroPrestate_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: Claim.wrap(bytes32(0)) })); - // nosemgrep: sol-style-use-abi-encodecall runDeployV2( deployConfig, abi.encodeWithSelector(IOPContractsManagerV2.OPContractsManagerV2_InvalidGameConfigs.selector) @@ -2024,9 +2007,8 @@ contract OPContractsManagerV2_Deploy_Test is OPContractsManagerV2_TestInit { ); } - /// @notice Multiple permissionless games may be enabled at initial deployment when each config - /// is structurally valid. - function test_deploy_multiplePermissionlessGamesEnabled_succeeds() public { + /// @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 = @@ -2035,16 +2017,11 @@ contract OPContractsManagerV2_Deploy_Test is OPContractsManagerV2_TestInit { deployConfig.disputeGameConfigs[2].initBond = DEFAULT_DISPUTE_GAME_INIT_BOND; deployConfig.disputeGameConfigs[2].gameArgs = abi.encode(IOPContractsManagerUtils.FaultDisputeGameConfig({ absolutePrestate: cannonKonaPrestate })); - deployConfig.startingRespectedGameType = GameTypes.CANNON; + deployConfig.startingRespectedGameType = GameTypes.CANNON_KONA; - IOPContractsManagerV2.ChainContracts memory cts = opcmV2.deploy(deployConfig); - assertNotEq( - address(cts.disputeGameFactory.gameImpls(GameTypes.CANNON)), address(0), "CANNON impl should be set" - ); - assertNotEq( - address(cts.disputeGameFactory.gameImpls(GameTypes.CANNON_KONA)), - address(0), - "CANNON_KONA impl should be set" + // nosemgrep: sol-style-use-abi-encodecall + runDeployV2( + deployConfig, abi.encodeWithSelector(IOPContractsManagerV2.OPContractsManagerV2_InvalidGameConfigs.selector) ); } @@ -2073,11 +2050,11 @@ contract OPContractsManagerV2_Deploy_Test is OPContractsManagerV2_TestInit { /// @notice Deploy reverts when an initial permissionless deploy uses a zero starting anchor root. function test_deploy_permissionlessZeroStartingAnchorRoot_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.startingRespectedGameType = GameTypes.CANNON; + 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 @@ -2088,10 +2065,10 @@ contract OPContractsManagerV2_Deploy_Test is OPContractsManagerV2_TestInit { /// @notice Deploy reverts when an initial permissionless deploy uses the permissioned placeholder anchor root. function test_deploy_permissionlessPlaceholderStartingAnchorRoot_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.startingAnchorRoot = Proposal({ root: Hash.wrap(bytes32(hex"dead")), l2SequenceNumber: 0 }); // nosemgrep: sol-style-use-abi-encodecall From 1b4265b51caa08197c2fa50a108b4e699fa120e8 Mon Sep 17 00:00:00 2001 From: lumoswiz Date: Wed, 8 Jul 2026 18:40:43 +0200 Subject: [PATCH 28/33] test: address permissionless deploy review nits --- .../test/L1/opcm/OPContractsManagerV2.t.sol | 18 ++++++---- .../test/opcm/DeployOPChain.t.sol | 36 ++++++++----------- 2 files changed, 26 insertions(+), 28 deletions(-) diff --git a/packages/contracts-bedrock/test/L1/opcm/OPContractsManagerV2.t.sol b/packages/contracts-bedrock/test/L1/opcm/OPContractsManagerV2.t.sol index 489159685b9..03334cb63e6 100644 --- a/packages/contracts-bedrock/test/L1/opcm/OPContractsManagerV2.t.sol +++ b/packages/contracts-bedrock/test/L1/opcm/OPContractsManagerV2.t.sol @@ -1924,9 +1924,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( @@ -2069,7 +2071,8 @@ contract OPContractsManagerV2_Deploy_Test is OPContractsManagerV2_TestInit { 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(bytes32(hex"dead")), l2SequenceNumber: 0 }); + deployConfig.startingAnchorRoot = + Proposal({ root: Hash.wrap(Constants.PLACEHOLDER_STARTING_ANCHOR_ROOT), l2SequenceNumber: 0 }); // nosemgrep: sol-style-use-abi-encodecall runDeployV2( @@ -2100,12 +2103,15 @@ contract OPContractsManagerV2_Deploy_Test is OPContractsManagerV2_TestInit { /// @notice The 0xdead placeholder anchor remains allowed for initial permissioned deployments. function test_deploy_permissionedPlaceholderStartingAnchorRoot_succeeds() public { - deployConfig.startingAnchorRoot = Proposal({ root: Hash.wrap(bytes32(hex"dead")), l2SequenceNumber: 0 }); + 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(), bytes32(hex"dead"), "starting anchor root mismatch"); + 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 diff --git a/packages/contracts-bedrock/test/opcm/DeployOPChain.t.sol b/packages/contracts-bedrock/test/opcm/DeployOPChain.t.sol index 5dfcf69d78a..ff233800681 100644 --- a/packages/contracts-bedrock/test/opcm/DeployOPChain.t.sol +++ b/packages/contracts-bedrock/test/opcm/DeployOPChain.t.sol @@ -232,7 +232,6 @@ contract DeployOPChain_Test is DeployOPChain_TestBase { /// @notice Legacy CANNON is rejected as an initial deployment game type. function test_run_cannonGameType_reverts() public { deployOPChainInput.disputeGameType = GameTypes.CANNON; - deployOPChainInput.startingAnchorRoot = permissionlessAnchorRoot; vm.expectRevert("DeployOPChain: unsupported dispute game type"); deployOPChain.run(deployOPChainInput); @@ -246,7 +245,7 @@ contract DeployOPChain_Test is DeployOPChain_TestBase { deployOPChainInput.startingAnchorRoot = permissionlessAnchorRoot; DeployOPChain.Output memory doo = deployOPChain.run(deployOPChainInput); - _checkSelectedPermissionlessDeployment(doo, GameTypes.CANNON_KONA); + _checkCannonKonaPermissionlessDeployment(doo); } /// @notice Permissionless game types are rejected when super roots are enabled. @@ -258,31 +257,24 @@ contract DeployOPChain_Test is DeployOPChain_TestBase { deployOPChain.run(deployOPChainInput); } - /// @notice Asserts a permissionless deployment enabled only the selected fault game with the + /// @notice Asserts a CANNON_KONA permissionless deployment enabled only CANNON_KONA with the /// default bond and seeded the ASR with the input anchor root and respected game type. /// @param doo The output of the deployment. - /// @param _selectedType The selected and expected respected game type. - function _checkSelectedPermissionlessDeployment( - DeployOPChain.Output memory doo, - GameType _selectedType - ) - internal - view - { - uint256 defaultBond = deployOPChain.DEFAULT_INIT_BOND(); - GameType disabledPermissionlessType = - _selectedType.raw() == GameTypes.CANNON.raw() ? GameTypes.CANNON_KONA : GameTypes.CANNON; - + function _checkCannonKonaPermissionlessDeployment(DeployOPChain.Output memory doo) internal view { IOPContractsManagerContainer.Implementations memory impls = IOPContractsManagerV2(opcmAddr).implementations(); - assertEq(doo.disputeGameFactoryProxy.initBonds(_selectedType), defaultBond, "selected init bond"); assertEq( - address(doo.disputeGameFactoryProxy.gameImpls(_selectedType)), impls.faultDisputeGameImpl, "selected impl" + doo.disputeGameFactoryProxy.initBonds(GameTypes.CANNON_KONA), + deployOPChain.DEFAULT_INIT_BOND(), + "selected init bond" ); - assertEq(doo.disputeGameFactoryProxy.initBonds(disabledPermissionlessType), 0, "unselected init bond"); assertEq( - address(doo.disputeGameFactoryProxy.gameImpls(disabledPermissionlessType)), address(0), "unselected impl" + address(doo.disputeGameFactoryProxy.gameImpls(GameTypes.CANNON_KONA)), + impls.faultDisputeGameImpl, + "selected impl" ); - assertEq(doo.disputeGameFactoryProxy.gameArgs(disabledPermissionlessType).length, 0, "unselected args"); + 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), 0, "permissioned init bond"); assertEq( address(doo.disputeGameFactoryProxy.gameImpls(GameTypes.PERMISSIONED_CANNON)), @@ -291,13 +283,13 @@ contract DeployOPChain_Test is DeployOPChain_TestBase { ); assertEq( - LibGameArgs.decode(doo.disputeGameFactoryProxy.gameArgs(_selectedType)).absolutePrestate, + LibGameArgs.decode(doo.disputeGameFactoryProxy.gameArgs(GameTypes.CANNON_KONA)).absolutePrestate, deployOPChainInput.disputeAbsolutePrestate.raw(), "selected prestate wiring" ); IAnchorStateRegistry asr = doo.anchorStateRegistryProxy; - assertEq(asr.respectedGameType().raw(), _selectedType.raw(), "respected game type"); + 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"); From f977197339fd9557205087323ec0e9da38537591 Mon Sep 17 00:00:00 2001 From: lumoswiz Date: Thu, 9 Jul 2026 13:29:25 +0200 Subject: [PATCH 29/33] fix: register permissioned fallback for permissionless deploys --- .../scripts/deploy/DeployOPChain.s.sol | 5 +- .../test/L1/opcm/OPContractsManagerV2.t.sol | 7 +++ .../test/opcm/DeployOPChain.t.sol | 48 +++++++++++++++---- 3 files changed, 50 insertions(+), 10 deletions(-) diff --git a/packages/contracts-bedrock/scripts/deploy/DeployOPChain.s.sol b/packages/contracts-bedrock/scripts/deploy/DeployOPChain.s.sol index 8d9b32855c2..f5a71600baa 100644 --- a/packages/contracts-bedrock/scripts/deploy/DeployOPChain.s.sol +++ b/packages/contracts-bedrock/scripts/deploy/DeployOPChain.s.sol @@ -116,8 +116,9 @@ contract DeployOPChain is Script { (bool permissionless, GameType respectedGameType) = _initialDeployGameSelection(_input.disputeGameType, isSuperRoot); bool enableCannonKona = permissionless && _input.disputeGameType.raw() == GameTypes.CANNON_KONA.raw(); - // PERMISSIONED_CANNON is the default game in non-super-root, non-permissionless deployments. - bool enablePermissionedCannon = !isSuperRoot && !permissionless; + // 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 diff --git a/packages/contracts-bedrock/test/L1/opcm/OPContractsManagerV2.t.sol b/packages/contracts-bedrock/test/L1/opcm/OPContractsManagerV2.t.sol index 03334cb63e6..5fda376f51b 100644 --- a/packages/contracts-bedrock/test/L1/opcm/OPContractsManagerV2.t.sol +++ b/packages/contracts-bedrock/test/L1/opcm/OPContractsManagerV2.t.sol @@ -2002,6 +2002,13 @@ contract OPContractsManagerV2_Deploy_Test is OPContractsManagerV2_TestInit { 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(), diff --git a/packages/contracts-bedrock/test/opcm/DeployOPChain.t.sol b/packages/contracts-bedrock/test/opcm/DeployOPChain.t.sol index ff233800681..f9c65067f3f 100644 --- a/packages/contracts-bedrock/test/opcm/DeployOPChain.t.sol +++ b/packages/contracts-bedrock/test/opcm/DeployOPChain.t.sol @@ -23,6 +23,7 @@ import { IOPContractsManagerV2 } from "interfaces/L1/opcm/IOPContractsManagerV2. import { IOPContractsManagerContainer } from "interfaces/L1/opcm/IOPContractsManagerContainer.sol"; import { IResourceMetering } from "interfaces/L1/IResourceMetering.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"; @@ -237,8 +238,8 @@ contract DeployOPChain_Test is DeployOPChain_TestBase { deployOPChain.run(deployOPChainInput); } - /// @notice A CANNON_KONA initial deployment enables only CANNON_KONA and seeds the anchor root - /// and respected game type from the input. + /// @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; @@ -248,6 +249,27 @@ contract DeployOPChain_Test is DeployOPChain_TestBase { _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); @@ -257,9 +279,10 @@ contract DeployOPChain_Test is DeployOPChain_TestBase { deployOPChain.run(deployOPChainInput); } - /// @notice Asserts a CANNON_KONA permissionless deployment enabled only CANNON_KONA with the - /// default bond and seeded the ASR with the input anchor root and respected game type. - /// @param doo The output of the deployment. + /// @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( @@ -275,11 +298,15 @@ contract DeployOPChain_Test is DeployOPChain_TestBase { 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), 0, "permissioned init bond"); + assertEq( + doo.disputeGameFactoryProxy.initBonds(GameTypes.PERMISSIONED_CANNON), + deployOPChain.DEFAULT_INIT_BOND(), + "fallback init bond" + ); assertEq( address(doo.disputeGameFactoryProxy.gameImpls(GameTypes.PERMISSIONED_CANNON)), - address(0), - "permissioned impl" + impls.permissionedDisputeGameImpl, + "fallback impl" ); assertEq( @@ -287,6 +314,11 @@ contract DeployOPChain_Test is DeployOPChain_TestBase { 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"); From 48e9b64963ffb55296cde316dc08e2e6c0560f32 Mon Sep 17 00:00:00 2001 From: lumoswiz Date: Thu, 9 Jul 2026 19:23:16 +0200 Subject: [PATCH 30/33] test: unify permissionless game init bond helper in DisputeGames --- .../checks/test-validation/exclusions.toml | 1 + .../test/L1/opcm/OPContractsManagerV2.t.sol | 17 +++--- .../test/setup/DisputeGames.sol | 22 ++++++++ .../test/setup/DisputeGames.t.sol | 54 +++++++++++++++++++ .../test/setup/ForkL1Live.s.sol | 26 ++------- 5 files changed, 86 insertions(+), 34 deletions(-) create mode 100644 packages/contracts-bedrock/test/setup/DisputeGames.t.sol 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/test/L1/opcm/OPContractsManagerV2.t.sol b/packages/contracts-bedrock/test/L1/opcm/OPContractsManagerV2.t.sol index 5e6a71e8bc1..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: _permissionlessGameInitBondForUpgrade(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 }) @@ -338,15 +340,6 @@ contract OPContractsManagerV2_Upgrade_TestInit is OPContractsManagerV2_TestInit ); } - /// @notice Returns the permissionless game init bond to use in upgrade inputs. - /// @dev Uses the live bond when present, otherwise falls back to a valid nonzero default. - /// @param _gameType Game type to check. - /// @return The init bond to use for the upgrade config. - function _permissionlessGameInitBondForUpgrade(GameType _gameType) internal view returns (uint256) { - uint256 initBond = disputeGameFactory.initBonds(_gameType); - return initBond == 0 ? DEFAULT_DISPUTE_GAME_INIT_BOND : initBond; - } - /// @notice Helper function that runs an OPCM V2 upgrade, asserts that the upgrade was successful, /// and runs post-upgrade smoke tests. /// @param _opcm The OPCM contract to reference for shared components. @@ -754,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 = _permissionlessGameInitBondForUpgrade(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 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 3fc180b6c40..d3cff3e52e7 100644 --- a/packages/contracts-bedrock/test/setup/ForkL1Live.s.sol +++ b/packages/contracts-bedrock/test/setup/ForkL1Live.s.sol @@ -233,27 +233,6 @@ contract ForkL1Live is Deployer, StdAssertions, FeatureFlags { || raw == GameTypes.SUPER_CANNON_KONA.raw(); } - /// @notice Returns the permissionless game init bond to use in upgrade inputs. Uses the live - /// bond when present, otherwise falls back to a valid nonzero default. A fork can have - /// a stale nonzero bond for an unregistered game, so the impl check comes first. - /// @param _disputeGameFactory Dispute game factory used to inspect game config. - /// @param _gameType Game type to check. - /// @return The init bond to use for the upgrade config. - function _permissionlessGameInitBondForUpgrade( - IDisputeGameFactory _disputeGameFactory, - GameType _gameType - ) - internal - view - returns (uint256) - { - if (address(_disputeGameFactory.gameImpls(_gameType)) == address(0)) { - return DEFAULT_PERMISSIONLESS_INIT_BOND; - } - uint256 initBond = _disputeGameFactory.initBonds(_gameType); - return initBond == 0 ? DEFAULT_PERMISSIONLESS_INIT_BOND : initBond; - } - /// @notice Calls to the Deploy.s.sol contract etched by Setup.sol to a deterministic address, sets up the /// environment, and deploys new implementations. function _deployNewImplementations() internal { @@ -388,8 +367,9 @@ 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 = - _permissionlessGameInitBondForUpgrade(disputeGameFactory, GameTypes.CANNON_KONA); + uint256 cannonKonaInitBond = DisputeGames.permissionlessGameInitBondForUpgrade( + disputeGameFactory, GameTypes.CANNON_KONA, DEFAULT_PERMISSIONLESS_INIT_BOND + ); disputeGameConfigs = new IOPContractsManagerUtils.DisputeGameConfig[](6); disputeGameConfigs[0] = IOPContractsManagerUtils.DisputeGameConfig({ From a50ef76e3910c50cdb2c688efa6ef278bcb01ff0 Mon Sep 17 00:00:00 2001 From: lumoswiz Date: Fri, 10 Jul 2026 16:28:26 +0200 Subject: [PATCH 31/33] fix: separate permissioned Cannon fallback prestate --- op-chain-ops/interopgen/deploy.go | 13 +++++- op-chain-ops/interopgen/deploy_test.go | 34 ++++++++++++++ op-deployer/pkg/deployer/opcm/opchain.go | 1 + op-deployer/pkg/deployer/opcm/scripts_test.go | 1 + op-deployer/pkg/deployer/pipeline/opchain.go | 1 + .../pkg/deployer/pipeline/opchain_test.go | 2 + .../scripts/deploy/DeployOPChain.s.sol | 10 ++++- .../scripts/libraries/Types.sol | 1 + .../test/opcm/DeployOPChain.t.sol | 45 +++++++++++++++++-- 9 files changed, 102 insertions(+), 6 deletions(-) diff --git a/op-chain-ops/interopgen/deploy.go b/op-chain-ops/interopgen/deploy.go index f7cec37204e..04e0910a6d1 100644 --- a/op-chain-ops/interopgen/deploy.go +++ b/op-chain-ops/interopgen/deploy.go @@ -18,6 +18,7 @@ import ( "github.com/ethereum-optimism/optimism/op-chain-ops/genesis" "github.com/ethereum-optimism/optimism/op-chain-ops/genesis/beacondeposit" "github.com/ethereum-optimism/optimism/op-chain-ops/script" + gameTypes "github.com/ethereum-optimism/optimism/op-challenger/game/types" "github.com/ethereum-optimism/optimism/op-core/devfeatures" "github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/manage" "github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/opcm" @@ -228,6 +229,7 @@ func DeployL2ToL1(l1Host *script.Host, superCfg *SuperchainConfig, superDeployme if err != nil { return nil, fmt.Errorf("failed to load DeployOPChain script: %w", err) } + selectedAbsolutePrestate, cannonAbsolutePrestate := initialDisputeAbsolutePrestates(cfg) output, err := deployOPChainScript.Run(opcm.DeployOPChainInput{ OpChainProxyAdminOwner: superCfg.ProxyAdminOwner, @@ -243,8 +245,9 @@ func DeployL2ToL1(l1Host *script.Host, superCfg *SuperchainConfig, superDeployme SaltMixer: cfg.SaltMixer, GasLimit: cfg.GasLimit, DisputeGameType: cfg.DisputeGameType, - DisputeAbsolutePrestate: cfg.DisputeAbsolutePrestate, + DisputeAbsolutePrestate: selectedAbsolutePrestate, StartingAnchorRoot: opcm.DefaultStartingAnchorRoot.Root, + CannonAbsolutePrestate: cannonAbsolutePrestate, DisputeMaxGameDepth: new(big.Int).SetUint64(cfg.DisputeMaxGameDepth), DisputeSplitDepth: new(big.Int).SetUint64(cfg.DisputeSplitDepth), DisputeClockExtension: cfg.DisputeClockExtension, @@ -265,6 +268,14 @@ func DeployL2ToL1(l1Host *script.Host, superCfg *SuperchainConfig, superDeployme }, nil } +func initialDisputeAbsolutePrestates(cfg *L2Config) (common.Hash, common.Hash) { + selectedAbsolutePrestate := cfg.DisputeAbsolutePrestate + if cfg.DisputeGameType == uint32(gameTypes.CannonKonaGameType) { + selectedAbsolutePrestate = cfg.DisputeKonaAbsolutePrestate + } + return selectedAbsolutePrestate, cfg.DisputeAbsolutePrestate +} + func MigrateInterop( l1Host *script.Host, l1GenesisTimestamp uint64, superCfg *SuperchainConfig, superDeployment *SuperchainDeployment, l2Cfgs map[string]*L2Config, l2Deployments map[string]*L2Deployment, ) (*InteropDeployment, error) { diff --git a/op-chain-ops/interopgen/deploy_test.go b/op-chain-ops/interopgen/deploy_test.go index 9fd95805ae6..ac4c1ad9115 100644 --- a/op-chain-ops/interopgen/deploy_test.go +++ b/op-chain-ops/interopgen/deploy_test.go @@ -3,12 +3,46 @@ package interopgen import ( "testing" + gameTypes "github.com/ethereum-optimism/optimism/op-challenger/game/types" "github.com/ethereum-optimism/optimism/op-core/devfeatures" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/stretchr/testify/require" ) +func TestInitialDisputeAbsolutePrestates(t *testing.T) { + cannonPrestate := common.HexToHash("0x01") + cannonKonaPrestate := common.HexToHash("0x02") + tests := []struct { + name string + gameType gameTypes.GameType + selectedPrestate common.Hash + }{ + { + name: "permissioned cannon selects cannon prestate", + gameType: gameTypes.PermissionedGameType, + selectedPrestate: cannonPrestate, + }, + { + name: "cannon kona selects kona prestate", + gameType: gameTypes.CannonKonaGameType, + selectedPrestate: cannonKonaPrestate, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + cfg := &L2Config{ + DisputeGameType: uint32(test.gameType), + DisputeAbsolutePrestate: cannonPrestate, + DisputeKonaAbsolutePrestate: cannonKonaPrestate, + } + selectedPrestate, fallbackPrestate := initialDisputeAbsolutePrestates(cfg) + require.Equal(t, test.selectedPrestate, selectedPrestate) + require.Equal(t, cannonPrestate, fallbackPrestate) + }) + } +} + func TestInteropAtGenesis(t *testing.T) { zero := hexutil.Uint64(0) nonzero := hexutil.Uint64(24) diff --git a/op-deployer/pkg/deployer/opcm/opchain.go b/op-deployer/pkg/deployer/opcm/opchain.go index c7b85bd21a0..5485dd51091 100644 --- a/op-deployer/pkg/deployer/opcm/opchain.go +++ b/op-deployer/pkg/deployer/opcm/opchain.go @@ -37,6 +37,7 @@ type DeployOPChainInput struct { DisputeGameType uint32 DisputeAbsolutePrestate common.Hash StartingAnchorRoot common.Hash + CannonAbsolutePrestate common.Hash DisputeMaxGameDepth *big.Int DisputeSplitDepth *big.Int DisputeClockExtension uint64 diff --git a/op-deployer/pkg/deployer/opcm/scripts_test.go b/op-deployer/pkg/deployer/opcm/scripts_test.go index dc563e5f4f1..a3206c0985a 100644 --- a/op-deployer/pkg/deployer/opcm/scripts_test.go +++ b/op-deployer/pkg/deployer/opcm/scripts_test.go @@ -24,5 +24,6 @@ func TestNewScripts(t *testing.T) { require.NotNil(t, scripts.DeployAltDA) require.NotNil(t, scripts.DeployDisputeGame) require.NotNil(t, scripts.DeployMIPS) + require.NotNil(t, scripts.DeployOPChain) }) } diff --git a/op-deployer/pkg/deployer/pipeline/opchain.go b/op-deployer/pkg/deployer/pipeline/opchain.go index c959c8cfc0a..4fba7cef8f5 100644 --- a/op-deployer/pkg/deployer/pipeline/opchain.go +++ b/op-deployer/pkg/deployer/pipeline/opchain.go @@ -151,6 +151,7 @@ func makeDCI(intent *state.Intent, thisIntent *state.ChainIntent, chainID common DisputeGameType: proofParams.DisputeGameType, DisputeAbsolutePrestate: proofParams.DisputeAbsolutePrestate, StartingAnchorRoot: opcm.DefaultStartingAnchorRoot.Root, + CannonAbsolutePrestate: proofParams.DisputeAbsolutePrestate, 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/op-deployer/pkg/deployer/pipeline/opchain_test.go b/op-deployer/pkg/deployer/pipeline/opchain_test.go index 9c63f62d65c..3ce62443f08 100644 --- a/op-deployer/pkg/deployer/pipeline/opchain_test.go +++ b/op-deployer/pkg/deployer/pipeline/opchain_test.go @@ -14,6 +14,7 @@ import ( "github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/broadcaster" "github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/forge" "github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/opcm" + "github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/standard" "github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/state" "github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/testutil" "github.com/ethereum-optimism/optimism/op-deployer/pkg/env" @@ -114,6 +115,7 @@ func Test_makeDCI_OpcmAddress(t *testing.T) { if got.Opcm != tt.expectedOpcm { t.Errorf("makeDCI() Opcm = %v, want %v", got.Opcm, tt.expectedOpcm) } + require.Equal(t, standard.DisputeAbsolutePrestate, got.CannonAbsolutePrestate) }) } } diff --git a/packages/contracts-bedrock/scripts/deploy/DeployOPChain.s.sol b/packages/contracts-bedrock/scripts/deploy/DeployOPChain.s.sol index f5a71600baa..a6756a13287 100644 --- a/packages/contracts-bedrock/scripts/deploy/DeployOPChain.s.sol +++ b/packages/contracts-bedrock/scripts/deploy/DeployOPChain.s.sol @@ -28,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, Proposal } from "src/dispute/lib/Types.sol"; +import { Claim, GameType, GameTypes, Proposal } from "src/dispute/lib/Types.sol"; import { DevFeatures } from "src/libraries/DevFeatures.sol"; contract DeployOPChain is Script { @@ -119,11 +119,13 @@ contract DeployOPChain is Script { // 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; + Claim permissionedCannonPrestate = + enableCannonKona ? _input.cannonAbsolutePrestate : _input.disputeAbsolutePrestate; // Shared permissioned game config for legacy permissioned games. IOPContractsManagerUtils.PermissionedDisputeGameConfig memory pdgConfig = IOPContractsManagerUtils .PermissionedDisputeGameConfig({ - absolutePrestate: _input.disputeAbsolutePrestate, + absolutePrestate: permissionedCannonPrestate, proposer: _input.proposer, challenger: _input.challenger }); @@ -361,6 +363,10 @@ contract DeployOPChain is Script { require(_i.disputeAbsolutePrestate.raw() != bytes32(0), "DeployOPChainInput: disputeAbsolutePrestate not set"); require(_i.startingAnchorRoot.raw() != bytes32(0), "DeployOPChainInput: startingAnchorRoot not set"); + if (_i.disputeGameType.raw() == GameTypes.CANNON_KONA.raw()) { + require(_i.cannonAbsolutePrestate.raw() != bytes32(0), "DeployOPChainInput: cannonAbsolutePrestate not set"); + } + if (permissionless) { require( _i.startingAnchorRoot.raw() != ScriptConstants.DEFAULT_OUTPUT_ROOT().root.raw(), diff --git a/packages/contracts-bedrock/scripts/libraries/Types.sol b/packages/contracts-bedrock/scripts/libraries/Types.sol index c42ac54be62..7f86f26290b 100644 --- a/packages/contracts-bedrock/scripts/libraries/Types.sol +++ b/packages/contracts-bedrock/scripts/libraries/Types.sol @@ -41,6 +41,7 @@ library Types { GameType disputeGameType; Claim disputeAbsolutePrestate; Hash startingAnchorRoot; + Claim cannonAbsolutePrestate; uint256 disputeMaxGameDepth; uint256 disputeSplitDepth; Duration disputeClockExtension; diff --git a/packages/contracts-bedrock/test/opcm/DeployOPChain.t.sol b/packages/contracts-bedrock/test/opcm/DeployOPChain.t.sol index f9c65067f3f..6d8ced351ae 100644 --- a/packages/contracts-bedrock/test/opcm/DeployOPChain.t.sol +++ b/packages/contracts-bedrock/test/opcm/DeployOPChain.t.sol @@ -20,6 +20,7 @@ import { LibGameArgs } from "src/dispute/lib/LibGameArgs.sol"; // Interfaces import { IOPContractsManagerV2 } from "interfaces/L1/opcm/IOPContractsManagerV2.sol"; +import { IOPContractsManagerStandardValidator } from "interfaces/L1/IOPContractsManagerStandardValidator.sol"; import { IOPContractsManagerContainer } from "interfaces/L1/opcm/IOPContractsManagerContainer.sol"; import { IResourceMetering } from "interfaces/L1/IResourceMetering.sol"; import { Claim, Duration, GameType, GameTypes, Hash, Proposal } from "src/dispute/lib/Types.sol"; @@ -42,8 +43,8 @@ contract DeployOPChain_TestBase is Test, FeatureFlags { // DeployImplementations default inputs. // - superchainConfigProxy is set during `setUp` since it is an output of DeploySuperchain. uint256 withdrawalDelaySeconds = 100; - uint256 minProposalSizeBytes = 200; - uint256 challengePeriodSeconds = 300; + uint256 minProposalSizeBytes = 126_000; + uint256 challengePeriodSeconds = 86_400; uint256 proofMaturityDelaySeconds = 400; uint256 disputeGameFinalityDelaySeconds = 500; @@ -63,6 +64,8 @@ contract DeployOPChain_TestBase is Test, FeatureFlags { GameType disputeGameType = GameTypes.PERMISSIONED_CANNON; Claim disputeAbsolutePrestate = Claim.wrap(0x038512e02c4c3f7bdaec27d00edf55b7155e0905301e1a88083e4e0a6764d54c); Hash startingAnchorRoot = Hash.wrap(0xdead000000000000000000000000000000000000000000000000000000000000); + Claim cannonAbsolutePrestate = Claim.wrap(0x038512e02c4c3f7bdaec27d00edf55b7155e0905301e1a88083e4e0a6764d54c); + Claim cannonKonaAbsolutePrestate = Claim.wrap(0x035ef680a6fa34c50d8d8169075b5d133ecd7b38fe2b2a83cc76fc81ae5d7c52); // Non-placeholder anchor root for the permissionless deploy tests. Hash permissionlessAnchorRoot = Hash.wrap(0x02f4397b2de6fce03b3f9982378c2b4c4deff9c92c662dcc6f9643267aeb5e47); uint256 disputeMaxGameDepth = 73; @@ -133,6 +136,7 @@ contract DeployOPChain_TestBase is Test, FeatureFlags { disputeGameType: disputeGameType, disputeAbsolutePrestate: disputeAbsolutePrestate, startingAnchorRoot: startingAnchorRoot, + cannonAbsolutePrestate: cannonAbsolutePrestate, disputeMaxGameDepth: disputeMaxGameDepth, disputeSplitDepth: disputeSplitDepth, disputeClockExtension: disputeClockExtension, @@ -243,10 +247,12 @@ contract DeployOPChain_Test is DeployOPChain_TestBase { function test_run_cannonKonaGameType_succeeds() public { skipIfDevFeatureEnabled(DevFeatures.SUPER_ROOT_GAMES_MIGRATION); deployOPChainInput.disputeGameType = GameTypes.CANNON_KONA; + deployOPChainInput.disputeAbsolutePrestate = cannonKonaAbsolutePrestate; deployOPChainInput.startingAnchorRoot = permissionlessAnchorRoot; DeployOPChain.Output memory doo = deployOPChain.run(deployOPChainInput); _checkCannonKonaPermissionlessDeployment(doo); + _validateCannonKonaPermissionlessDeployment(doo); } /// @notice Verifies the guardian can switch a CANNON_KONA deploy to PERMISSIONED_CANNON @@ -254,6 +260,7 @@ contract DeployOPChain_Test is DeployOPChain_TestBase { function test_run_cannonKonaGameTypeFallback_succeeds() public { skipIfDevFeatureEnabled(DevFeatures.SUPER_ROOT_GAMES_MIGRATION); deployOPChainInput.disputeGameType = GameTypes.CANNON_KONA; + deployOPChainInput.disputeAbsolutePrestate = cannonKonaAbsolutePrestate; deployOPChainInput.startingAnchorRoot = permissionlessAnchorRoot; DeployOPChain.Output memory doo = deployOPChain.run(deployOPChainInput); @@ -274,6 +281,7 @@ contract DeployOPChain_Test is DeployOPChain_TestBase { function test_run_permissionlessGameTypeWithSuperRoot_reverts() public { skipIfDevFeatureDisabled(DevFeatures.SUPER_ROOT_GAMES_MIGRATION); deployOPChainInput.disputeGameType = GameTypes.CANNON_KONA; + deployOPChainInput.disputeAbsolutePrestate = cannonKonaAbsolutePrestate; deployOPChainInput.startingAnchorRoot = permissionlessAnchorRoot; vm.expectRevert("DeployOPChain: permissionless game type not supported with super roots"); deployOPChain.run(deployOPChainInput); @@ -316,7 +324,7 @@ contract DeployOPChain_Test is DeployOPChain_TestBase { ); LibGameArgs.GameArgs memory pdgArgs = LibGameArgs.decode(doo.disputeGameFactoryProxy.gameArgs(GameTypes.PERMISSIONED_CANNON)); - assertEq(pdgArgs.absolutePrestate, deployOPChainInput.disputeAbsolutePrestate.raw(), "fallback prestate"); + assertEq(pdgArgs.absolutePrestate, deployOPChainInput.cannonAbsolutePrestate.raw(), "fallback prestate"); assertEq(pdgArgs.proposer, proposer, "fallback proposer"); assertEq(pdgArgs.challenger, challenger, "fallback challenger"); @@ -327,6 +335,26 @@ contract DeployOPChain_Test is DeployOPChain_TestBase { assertEq(anchor.l2SequenceNumber, 0, "anchor seq"); } + /// @notice Validates the full standard shape of a CANNON_KONA deployment with its permissioned fallback. + /// @param doo The deployment output. + function _validateCannonKonaPermissionlessDeployment(DeployOPChain.Output memory doo) internal view { + IOPContractsManagerStandardValidator validator = IOPContractsManagerV2(opcmAddr).opcmStandardValidator(); + validator.validateWithOverrides( + IOPContractsManagerStandardValidator.ValidationInputDev({ + sysCfg: doo.systemConfigProxy, + cannonPrestate: cannonAbsolutePrestate.raw(), + cannonKonaPrestate: cannonKonaAbsolutePrestate.raw(), + l2ChainID: l2ChainId, + proposer: proposer + }), + false, + IOPContractsManagerStandardValidator.ValidationOverrides({ + l1PAOMultisig: opChainProxyAdminOwner, + challenger: challenger + }) + ); + } + /// @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 { @@ -519,6 +547,16 @@ contract DeployOPChain_TestFail is DeployOPChain_TestBase { deployOPChain.run(deployOPChainInput); } + function test_run_cannonKonaZeroCannonAbsolutePrestate_reverts() public { + skipIfDevFeatureEnabled(DevFeatures.SUPER_ROOT_GAMES_MIGRATION); + deployOPChainInput.disputeGameType = GameTypes.CANNON_KONA; + deployOPChainInput.disputeAbsolutePrestate = cannonKonaAbsolutePrestate; + deployOPChainInput.startingAnchorRoot = permissionlessAnchorRoot; + deployOPChainInput.cannonAbsolutePrestate = Claim.wrap(bytes32(0)); + vm.expectRevert("DeployOPChainInput: cannonAbsolutePrestate not set"); + deployOPChain.run(deployOPChainInput); + } + function test_run_zeroStartingAnchorRoot_reverts() public { deployOPChainInput.startingAnchorRoot = Hash.wrap(bytes32(0)); vm.expectRevert("DeployOPChainInput: startingAnchorRoot not set"); @@ -528,6 +566,7 @@ contract DeployOPChain_TestFail is DeployOPChain_TestBase { function test_run_permissionlessPlaceholderStartingAnchorRoot_reverts() public { skipIfDevFeatureEnabled(DevFeatures.SUPER_ROOT_GAMES_MIGRATION); deployOPChainInput.disputeGameType = GameTypes.CANNON_KONA; + deployOPChainInput.disputeAbsolutePrestate = cannonKonaAbsolutePrestate; // startingAnchorRoot stays at the 0xdead placeholder default. vm.expectRevert("DeployOPChainInput: permissionless startingAnchorRoot cannot be placeholder"); deployOPChain.run(deployOPChainInput); From 4325e88a6ba40ba0ad3d22570ed86633e02f4089 Mon Sep 17 00:00:00 2001 From: OneTony Date: Fri, 10 Jul 2026 18:34:19 +0300 Subject: [PATCH 32/33] fix: report registered CANNON_KONA impl in DeployOPChain output --- .../scripts/deploy/DeployOPChain.s.sol | 32 +++++++++++++------ .../test/opcm/DeployOPChain.t.sol | 15 +++++++++ 2 files changed, 38 insertions(+), 9 deletions(-) diff --git a/packages/contracts-bedrock/scripts/deploy/DeployOPChain.s.sol b/packages/contracts-bedrock/scripts/deploy/DeployOPChain.s.sol index a6756a13287..53a4dc26699 100644 --- a/packages/contracts-bedrock/scripts/deploy/DeployOPChain.s.sol +++ b/packages/contracts-bedrock/scripts/deploy/DeployOPChain.s.sol @@ -98,9 +98,12 @@ contract DeployOPChain is Script { vm.label(address(output_.disputeGameFactoryProxy), "disputeGameFactoryProxy"); vm.label(address(output_.anchorStateRegistryProxy), "anchorStateRegistryProxy"); vm.label(address(output_.delayedWETHPermissionedGameProxy), "delayedWETHPermissionedGameProxy"); - // TODO: Eventually switch from Permissioned to Permissionless. - // vm.label(address(output_.faultDisputeGame), "faultDisputeGame"); - // vm.label(address(output_.delayedWETHPermissionlessGameProxy), "delayedWETHPermissionlessGameProxy"); + // TODO: OPCMV2 uses one shared DelayedWETH for all game types; both Output WETH fields + // alias it, so only one label is set above. Revisit the WETH label and field naming in + // a follow-up PR. + if (address(output_.faultDisputeGame) != address(0)) { + vm.label(address(output_.faultDisputeGame), "faultDisputeGame"); + } } // -------- Features -------- @@ -218,6 +221,8 @@ contract DeployOPChain is Script { { GameType permGameType = isSuperRoot ? GameTypes.SUPER_PERMISSIONED : GameTypes.PERMISSIONED_CANNON; address permissionedDgImpl = address(_chainContracts.disputeGameFactory.gameImpls(permGameType)); + // TODO(#21695): Read SUPER_CANNON_KONA once super-root permissionless deploys land. + address faultDgImpl = address(_chainContracts.disputeGameFactory.gameImpls(GameTypes.CANNON_KONA)); output_ = Output({ opChainProxyAdmin: _chainContracts.proxyAdmin, @@ -231,8 +236,7 @@ contract DeployOPChain is Script { ethLockboxProxy: _chainContracts.ethLockbox, disputeGameFactoryProxy: _chainContracts.disputeGameFactory, anchorStateRegistryProxy: _chainContracts.anchorStateRegistry, - // Explicitly set to address(0) maintaining consistency with OPCM v1 behavior. - faultDisputeGame: IFaultDisputeGame(address(0)), + faultDisputeGame: IFaultDisputeGame(faultDgImpl), permissionedDisputeGame: IPermissionedDisputeGame(permissionedDgImpl), delayedWETHPermissionedGameProxy: _chainContracts.delayedWETH, delayedWETHPermissionlessGameProxy: IDelayedWETH(payable(_chainContracts.delayedWETH)) @@ -427,11 +431,21 @@ contract DeployOPChain is Script { IOPContractsManagerContainer.Implementations memory implementations = opcmV2.implementations(); (bool permissionless, GameType respectedGameType) = _initialDeployGameSelection(_i.disputeGameType, isSuperRoot); - address expectedDGImpl = permissionless - ? implementations.faultDisputeGameImpl - : (isSuperRoot ? implementations.superPermissionedDisputeGameImpl : implementations.permissionedDisputeGameImpl); + address expectedPermissionedDGImpl = + isSuperRoot ? implementations.superPermissionedDisputeGameImpl : implementations.permissionedDisputeGameImpl; + // TODO(#21695): Use superFaultDisputeGameImpl for super-root permissionless deploys. + address expectedRespectedDGImpl = + permissionless ? implementations.faultDisputeGameImpl : expectedPermissionedDGImpl; ChainAssertions.checkDisputeGameFactory( - _o.disputeGameFactoryProxy, _i.opChainProxyAdminOwner, expectedDGImpl, true, respectedGameType + _o.disputeGameFactoryProxy, _i.opChainProxyAdminOwner, expectedRespectedDGImpl, true, respectedGameType + ); + require( + address(_o.faultDisputeGame) == (permissionless ? expectedRespectedDGImpl : address(0)), + "DeployOPChain: faultDisputeGame output mismatch" + ); + require( + address(_o.permissionedDisputeGame) == expectedPermissionedDGImpl, + "DeployOPChain: permissionedDisputeGame output mismatch" ); ChainAssertions.checkAnchorStateRegistryProxy( _o.anchorStateRegistryProxy, true, respectedGameType, _i.startingAnchorRoot diff --git a/packages/contracts-bedrock/test/opcm/DeployOPChain.t.sol b/packages/contracts-bedrock/test/opcm/DeployOPChain.t.sol index 6d8ced351ae..5194ec97493 100644 --- a/packages/contracts-bedrock/test/opcm/DeployOPChain.t.sol +++ b/packages/contracts-bedrock/test/opcm/DeployOPChain.t.sol @@ -277,6 +277,19 @@ contract DeployOPChain_Test is DeployOPChain_TestBase { assertTrue(asr.isGameRespected(game), "fallback game must be respected"); } + /// @notice checkOutput rejects a permissionless deployment output missing the permissioned fallback. + function test_checkOutput_missingPermissionedFallback_reverts() public { + skipIfDevFeatureEnabled(DevFeatures.SUPER_ROOT_GAMES_MIGRATION); + deployOPChainInput.disputeGameType = GameTypes.CANNON_KONA; + deployOPChainInput.disputeAbsolutePrestate = cannonKonaAbsolutePrestate; + deployOPChainInput.startingAnchorRoot = permissionlessAnchorRoot; + DeployOPChain.Output memory doo = deployOPChain.run(deployOPChainInput); + + doo.permissionedDisputeGame = IPermissionedDisputeGame(address(0)); + vm.expectRevert("DeployOPChain: permissionedDisputeGame output mismatch"); + deployOPChain.checkOutput(deployOPChainInput, doo); + } + /// @notice Permissionless game types are rejected when super roots are enabled. function test_run_permissionlessGameTypeWithSuperRoot_reverts() public { skipIfDevFeatureDisabled(DevFeatures.SUPER_ROOT_GAMES_MIGRATION); @@ -303,6 +316,7 @@ contract DeployOPChain_Test is DeployOPChain_TestBase { impls.faultDisputeGameImpl, "selected impl" ); + assertEq(address(doo.faultDisputeGame), impls.faultDisputeGameImpl, "output faultDisputeGame"); 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"); @@ -316,6 +330,7 @@ contract DeployOPChain_Test is DeployOPChain_TestBase { impls.permissionedDisputeGameImpl, "fallback impl" ); + assertEq(address(doo.permissionedDisputeGame), impls.permissionedDisputeGameImpl, "output fallback"); assertEq( LibGameArgs.decode(doo.disputeGameFactoryProxy.gameArgs(GameTypes.CANNON_KONA)).absolutePrestate, From 409e50871a63c9e4861d005714a6c2e0cc5316e6 Mon Sep 17 00:00:00 2001 From: OneTony Date: Fri, 10 Jul 2026 19:04:02 +0300 Subject: [PATCH 33/33] fix: reject equal Cannon/Kona prestates in DeployOPChain input --- op-deployer/pkg/deployer/pipeline/opchain.go | 4 +++- .../scripts/deploy/DeployOPChain.s.sol | 10 ++++++++ .../test/opcm/DeployOPChain.t.sol | 23 ++++++++++++++++--- 3 files changed, 33 insertions(+), 4 deletions(-) diff --git a/op-deployer/pkg/deployer/pipeline/opchain.go b/op-deployer/pkg/deployer/pipeline/opchain.go index 4fba7cef8f5..427fafa3f35 100644 --- a/op-deployer/pkg/deployer/pipeline/opchain.go +++ b/op-deployer/pkg/deployer/pipeline/opchain.go @@ -134,7 +134,9 @@ 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. + // TODO(#20912): Populate StartingAnchorRoot and DisputeAbsolutePrestate from pipeline state for permissionless + // deploys. This also needs a second prestate field in ChainProofParams: CannonAbsolutePrestate below reuses the + // single existing field, and DeployOPChain.checkInput rejects equal prestates for CANNON_KONA deploys. return opcm.DeployOPChainInput{ OpChainProxyAdminOwner: thisIntent.Roles.L1ProxyAdminOwner, SystemConfigOwner: thisIntent.Roles.SystemConfigOwner, diff --git a/packages/contracts-bedrock/scripts/deploy/DeployOPChain.s.sol b/packages/contracts-bedrock/scripts/deploy/DeployOPChain.s.sol index 53a4dc26699..baae1483a94 100644 --- a/packages/contracts-bedrock/scripts/deploy/DeployOPChain.s.sol +++ b/packages/contracts-bedrock/scripts/deploy/DeployOPChain.s.sol @@ -122,6 +122,10 @@ contract DeployOPChain is Script { // 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; + // disputeAbsolutePrestate always carries the SELECTED game's prestate. In CANNON_KONA mode + // that is the Kona prestate, so the permissioned fallback takes its Cannon prestate from + // the dedicated field; in permissioned mode the selected game IS the permissioned Cannon + // game, so the selected prestate is already the right one. Claim permissionedCannonPrestate = enableCannonKona ? _input.cannonAbsolutePrestate : _input.disputeAbsolutePrestate; @@ -369,6 +373,12 @@ contract DeployOPChain is Script { if (_i.disputeGameType.raw() == GameTypes.CANNON_KONA.raw()) { require(_i.cannonAbsolutePrestate.raw() != bytes32(0), "DeployOPChainInput: cannonAbsolutePrestate not set"); + // The two prestates commit to different fault-proof programs (op-program vs Kona), + // so equal values always indicate a misconfigured producer. + require( + _i.cannonAbsolutePrestate.raw() != _i.disputeAbsolutePrestate.raw(), + "DeployOPChainInput: cannonAbsolutePrestate must differ from disputeAbsolutePrestate" + ); } if (permissionless) { diff --git a/packages/contracts-bedrock/test/opcm/DeployOPChain.t.sol b/packages/contracts-bedrock/test/opcm/DeployOPChain.t.sol index 5194ec97493..b97edfd0358 100644 --- a/packages/contracts-bedrock/test/opcm/DeployOPChain.t.sol +++ b/packages/contracts-bedrock/test/opcm/DeployOPChain.t.sol @@ -62,11 +62,16 @@ contract DeployOPChain_TestBase is Test, FeatureFlags { string saltMixer = "saltMixer"; uint64 gasLimit = 60_000_000; GameType disputeGameType = GameTypes.PERMISSIONED_CANNON; + // Prestates are real release hashes from the superchain registry's standard-prestates.toml; + // the tests only need them to be non-zero and distinct from each other. + // cannon32 v1.3.1 (op-program). Claim disputeAbsolutePrestate = Claim.wrap(0x038512e02c4c3f7bdaec27d00edf55b7155e0905301e1a88083e4e0a6764d54c); - Hash startingAnchorRoot = Hash.wrap(0xdead000000000000000000000000000000000000000000000000000000000000); - Claim cannonAbsolutePrestate = Claim.wrap(0x038512e02c4c3f7bdaec27d00edf55b7155e0905301e1a88083e4e0a6764d54c); + Hash startingAnchorRoot = Hash.wrap(Constants.PLACEHOLDER_STARTING_ANCHOR_ROOT); + // cannon64 v1.6.1 (op-program). + Claim cannonAbsolutePrestate = Claim.wrap(0x03eb07101fbdeaf3f04d9fb76526362c1eea2824e4c6e970bdb19675b72e4fc8); + // cannon64-kona-interop v1.2.13 (Kona). Claim cannonKonaAbsolutePrestate = Claim.wrap(0x035ef680a6fa34c50d8d8169075b5d133ecd7b38fe2b2a83cc76fc81ae5d7c52); - // Non-placeholder anchor root for the permissionless deploy tests. + // Arbitrary non-placeholder anchor root for the permissionless deploy tests. Hash permissionlessAnchorRoot = Hash.wrap(0x02f4397b2de6fce03b3f9982378c2b4c4deff9c92c662dcc6f9643267aeb5e47); uint256 disputeMaxGameDepth = 73; uint256 disputeSplitDepth = 30; @@ -572,6 +577,18 @@ contract DeployOPChain_TestFail is DeployOPChain_TestBase { deployOPChain.run(deployOPChainInput); } + /// @notice The Cannon fallback prestate and the selected Kona prestate can never legitimately + /// be equal (they commit to different fault-proof programs). + function test_run_cannonKonaEqualPrestates_reverts() public { + skipIfDevFeatureEnabled(DevFeatures.SUPER_ROOT_GAMES_MIGRATION); + deployOPChainInput.disputeGameType = GameTypes.CANNON_KONA; + deployOPChainInput.disputeAbsolutePrestate = cannonKonaAbsolutePrestate; + deployOPChainInput.startingAnchorRoot = permissionlessAnchorRoot; + deployOPChainInput.cannonAbsolutePrestate = cannonKonaAbsolutePrestate; + vm.expectRevert("DeployOPChainInput: cannonAbsolutePrestate must differ from disputeAbsolutePrestate"); + deployOPChain.run(deployOPChainInput); + } + function test_run_zeroStartingAnchorRoot_reverts() public { deployOPChainInput.startingAnchorRoot = Hash.wrap(bytes32(0)); vm.expectRevert("DeployOPChainInput: startingAnchorRoot not set");