From e0160aeda3d7630646cd3aaac539b8fdc348c1d9 Mon Sep 17 00:00:00 2001 From: 0xarktos Date: Fri, 3 Jul 2026 11:10:46 +0300 Subject: [PATCH 1/5] fix: consitent bit format in decodeZK and LibGameArgs ZK game args roundtrip test --- .../contracts-bedrock/src/dispute/lib/LibGameArgs.sol | 2 +- .../test/L1/opcm/OPContractsManagerUtils.t.sol | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/packages/contracts-bedrock/src/dispute/lib/LibGameArgs.sol b/packages/contracts-bedrock/src/dispute/lib/LibGameArgs.sol index cc2d9fe42c1..e3623085846 100644 --- a/packages/contracts-bedrock/src/dispute/lib/LibGameArgs.sol +++ b/packages/contracts-bedrock/src/dispute/lib/LibGameArgs.sol @@ -168,7 +168,7 @@ library LibGameArgs { assembly { // skip length prefix - let base := add(_args, 0x20) + let base := add(_args, 32) absolutePrestate := mload(base) verifier := shr(96, mload(add(base, 32))) maxChallengeDuration := shr(192, mload(add(base, 52))) diff --git a/packages/contracts-bedrock/test/L1/opcm/OPContractsManagerUtils.t.sol b/packages/contracts-bedrock/test/L1/opcm/OPContractsManagerUtils.t.sol index 76bcb1b1b4a..ca33d078a01 100644 --- a/packages/contracts-bedrock/test/L1/opcm/OPContractsManagerUtils.t.sol +++ b/packages/contracts-bedrock/test/L1/opcm/OPContractsManagerUtils.t.sol @@ -25,6 +25,7 @@ import { ISemver } from "interfaces/universal/ISemver.sol"; import { IStorageSetter } from "interfaces/universal/IStorageSetter.sol"; import { Claim, Duration } from "src/dispute/lib/LibUDT.sol"; import { GameTypes } from "src/dispute/lib/Types.sol"; +import { LibGameArgs } from "src/dispute/lib/LibGameArgs.sol"; import { IAnchorStateRegistry } from "interfaces/dispute/IAnchorStateRegistry.sol"; import { IDelayedWETH } from "interfaces/dispute/IDelayedWETH.sol"; import { IZKVerifier } from "interfaces/dispute/zk/IZKVerifier.sol"; @@ -975,6 +976,16 @@ contract OPContractsManagerUtils_MakeGameArgs_Test is OPContractsManagerUtils_Te address(delayedWETH) ); assertEq(keccak256(result), keccak256(expected), "ZK game args CWIA layout mismatch"); + + // Decode the encoded args back through LibGameArgs and assert every field round-trips. + LibGameArgs.ZKGameArgs memory decoded = LibGameArgs.decodeZK(result); + assertEq(decoded.absolutePrestate, absolutePrestate.raw(), "absolutePrestate mismatch"); + assertEq(decoded.verifier, address(verifier), "verifier mismatch"); + assertEq(decoded.maxChallengeDuration, maxChallengeDuration.raw(), "maxChallengeDuration mismatch"); + assertEq(decoded.maxProveDuration, maxProveDuration.raw(), "maxProveDuration mismatch"); + assertEq(decoded.challengerBond, challengerBond, "challengerBond mismatch"); + assertEq(decoded.anchorStateRegistry, address(anchorStateRegistry), "anchorStateRegistry mismatch"); + assertEq(decoded.weth, address(delayedWETH), "weth mismatch"); } /// @notice Tests that makeGameArgs reverts for an unsupported game type. From b376f4b3e9b07ae4916b92fbbc6cf42b48feed7a Mon Sep 17 00:00:00 2001 From: 0xarktos Date: Fri, 3 Jul 2026 11:11:25 +0300 Subject: [PATCH 2/5] docs: natspec improvements and clarifications for super zk game --- .../snapshots/semver-lock.json | 4 +-- .../src/dispute/zk/ZKDisputeGame.sol | 28 +++++++++---------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/packages/contracts-bedrock/snapshots/semver-lock.json b/packages/contracts-bedrock/snapshots/semver-lock.json index 2a95ab65726..3d541a73960 100644 --- a/packages/contracts-bedrock/snapshots/semver-lock.json +++ b/packages/contracts-bedrock/snapshots/semver-lock.json @@ -180,8 +180,8 @@ "sourceCodeHash": "0x2cc9c53d544e5715e38a50ad266551fe7b0f05488e7a66f3f70bca618fbd7fc7" }, "src/dispute/zk/ZKDisputeGame.sol:ZKDisputeGame": { - "initCodeHash": "0x66e573b7b42ba3b9addc46340bb3ca3ac21ae08496c13c14689dc48ab740a882", - "sourceCodeHash": "0xf193a08ff3404656cfe7ab74fd31d9acafe127fe5717f25635e06b003c6d8094" + "initCodeHash": "0x215801f4d42de7076f376c4cb67d0a19431c793635b42b18687ce53508fb9c9b", + "sourceCodeHash": "0xfd830061ff6bddb8c3f8fe34bfd8714a4bb6785f44864e29b723ab56273533ea" }, "src/legacy/DeployerWhitelist.sol:DeployerWhitelist": { "initCodeHash": "0x2e0ef4c341367eb59cc6c25190c64eff441d3fe130189da91d4d126f6bdbc9b5", diff --git a/packages/contracts-bedrock/src/dispute/zk/ZKDisputeGame.sol b/packages/contracts-bedrock/src/dispute/zk/ZKDisputeGame.sol index c6546b0d0f0..a82c7166670 100644 --- a/packages/contracts-bedrock/src/dispute/zk/ZKDisputeGame.sol +++ b/packages/contracts-bedrock/src/dispute/zk/ZKDisputeGame.sol @@ -48,8 +48,8 @@ import { IAnchorStateRegistry } from "interfaces/dispute/IAnchorStateRegistry.so import { IDelayedWETH } from "interfaces/dispute/IDelayedWETH.sol"; /// @title ZKDisputeGame -/// @notice A ZK proof-based dispute game using the MCP (Modular Clone Proxy) pattern -/// with Clone-With-Immutable-Args (CWIA). Spec-compliant, permissionless +/// @notice A super-roots compatible ZK proof-based dispute game using the MCP (Modular Clone Proxy) pattern +/// with Clone-With-Immutable-Args (CWIA). Spec-compliant, interop-ready, permissionless /// design that uses a generic IZKVerifier and DelayedWETH for bond custody. /// @dev Derived from https://github.com/succinctlabs/op-succinct (at commit c13844a9bbc330cca69eef2538d8f8ec123e1653) contract ZKDisputeGame is Clone, ISemver, IDisputeGame { @@ -84,8 +84,8 @@ contract ZKDisputeGame is Clone, ISemver, IDisputeGame { /// @custom:field claim The root claim being disputed. struct ClaimData { uint32 parentIndex; // 4 bytes - ProposalStatus status; // 1 byte |-- slot 1 (25 bytes) - address challenger; // 20 bytes + ProposalStatus status; // 1 byte + address challenger; // 20 bytes |-- slot 1 (25 bytes) address prover; // 20 bytes Timestamp deadline; // 8 bytes |-- slot 2 (28 bytes) Claim claim; // 32 bytes |-- slot 3 @@ -111,8 +111,8 @@ contract ZKDisputeGame is Clone, ISemver, IDisputeGame { //////////////////////////////////////////////////////////////// /// @notice Semantic version. - /// @custom:semver 2.0.0 - string public constant version = "2.0.0"; + /// @custom:semver 2.0.1 + string public constant version = "2.0.1"; /// @notice The starting timestamp of the game. Timestamp public createdAt; @@ -209,49 +209,49 @@ contract ZKDisputeGame is Clone, ISemver, IDisputeGame { //////////////////////////////////////////////////////////////// /// @notice Returns the absolute prestate commitment (ZK circuit identity) for the super-root ZK program. - /// @dev `clones-with-immutable-args` argument #6 + /// @dev `clones-with-immutable-args` argument #5 /// @return absolutePrestate_ The absolute prestate vkey of the multi-chain super-root ZK circuit. function absolutePrestate() public pure returns (bytes32 absolutePrestate_) { absolutePrestate_ = _getArgBytes32(_preExtraDataByteCount() + _extraDataByteCount()); } /// @notice Returns the ZK verifier contract. - /// @dev `clones-with-immutable-args` argument #7 + /// @dev `clones-with-immutable-args` argument #6 /// @return verifier_ The ZK verifier contract used to validate proofs. function verifier() public pure returns (IZKVerifier verifier_) { verifier_ = IZKVerifier(_getArgAddress(_preExtraDataByteCount() + _extraDataByteCount() + 32)); } /// @notice Returns the max challenge duration. - /// @dev `clones-with-immutable-args` argument #8 + /// @dev `clones-with-immutable-args` argument #7 /// @return maxChallengeDuration_ The maximum time a proposal can remain unchallenged. function maxChallengeDuration() public pure returns (Duration maxChallengeDuration_) { maxChallengeDuration_ = Duration.wrap(_getArgUint64(_preExtraDataByteCount() + _extraDataByteCount() + 52)); } /// @notice Returns the max prove duration. - /// @dev `clones-with-immutable-args` argument #9 + /// @dev `clones-with-immutable-args` argument #8 /// @return maxProveDuration_ The maximum time a challenged proposal can remain unproven. function maxProveDuration() public pure returns (Duration maxProveDuration_) { maxProveDuration_ = Duration.wrap(_getArgUint64(_preExtraDataByteCount() + _extraDataByteCount() + 60)); } /// @notice Returns the challenger bond amount. - /// @dev `clones-with-immutable-args` argument #10 + /// @dev `clones-with-immutable-args` argument #9 /// @return challengerBond_ The required bond, in wei, for a challenger to challenge the proposal. function challengerBond() public pure returns (uint256 challengerBond_) { challengerBond_ = _getArgUint256(_preExtraDataByteCount() + _extraDataByteCount() + 68); } /// @notice Returns the anchor state registry contract. - /// @dev `clones-with-immutable-args` argument #11 + /// @dev `clones-with-immutable-args` argument #10 /// @return registry_ The anchor state registry contract. function anchorStateRegistry() public pure returns (IAnchorStateRegistry registry_) { registry_ = IAnchorStateRegistry(_getArgAddress(_preExtraDataByteCount() + _extraDataByteCount() + 100)); } /// @notice Returns the DelayedWETH contract used for bond custody. - /// @dev `clones-with-immutable-args` argument #12 + /// @dev `clones-with-immutable-args` argument #11 /// @return weth_ The DelayedWETH contract used for bond custody. function weth() public pure returns (IDelayedWETH weth_) { weth_ = IDelayedWETH(payable(_getArgAddress(_preExtraDataByteCount() + _extraDataByteCount() + 120))); @@ -596,7 +596,7 @@ contract ZKDisputeGame is Clone, ISemver, IDisputeGame { status = GameStatus.DEFENDER_WINS; normalModeCredit[gameCreator()] = totalBonds; } else if (claimData.status == ProposalStatus.Challenged) { - // Claim is challenged, challenger wins, challenger wins everything + // Claim is challenged, prove deadline expired, challenger wins, challenger wins everything status = GameStatus.CHALLENGER_WINS; normalModeCredit[claimData.challenger] = totalBonds; } else if (claimData.status == ProposalStatus.UnchallengedAndValidProofProvided) { From b91332b331cd8028495bf435c458636b96a83343 Mon Sep 17 00:00:00 2001 From: 0xarktos Date: Fri, 3 Jul 2026 11:12:11 +0300 Subject: [PATCH 3/5] test: libClone vulnerability regression test for CWIA game creation --- .../test/dispute/DisputeGameFactory.t.sol | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/packages/contracts-bedrock/test/dispute/DisputeGameFactory.t.sol b/packages/contracts-bedrock/test/dispute/DisputeGameFactory.t.sol index 14022a679a9..0cb8e747d78 100644 --- a/packages/contracts-bedrock/test/dispute/DisputeGameFactory.t.sol +++ b/packages/contracts-bedrock/test/dispute/DisputeGameFactory.t.sol @@ -9,6 +9,7 @@ import { ForgeArtifacts, StorageSlot } from "scripts/libraries/ForgeArtifacts.so import { DeployUtils } from "scripts/libraries/DeployUtils.sol"; // Libraries +import { LibClone } from "@solady/utils/LibClone.sol"; import { DevFeatures } from "src/libraries/DevFeatures.sol"; import "src/dispute/lib/Types.sol"; import "src/dispute/lib/Errors.sol"; @@ -1062,6 +1063,33 @@ contract DisputeGameFactory_Create_ZkDisputeGame_Test is DisputeGameFactory_ZkDi vm.prank(proposer); disputeGameFactory.create{ value: 1 ether }(GameTypes.ZK_DISPUTE_GAME, rc, ed); } + + /// @notice Regression guard for the LibClone immutable-args length-overflow finding from the Solady audit + /// here: https://cantina.xyz/portfolio/018cf146-6e36-49a9-82b3-9ae39904f95a + /// CWIA clones must revert and not silently deploy a corrupted, codeless proxy that + /// permanently traps the bond when the appended args exceed the 2-byte length field. + function test_create_oversizedExtraData_reverts() public { + // Register both game implementations on the factory. + setupZKDisputeGame(defaultZKParams); + setupSuperFaultDisputeGame(Claim.wrap(bytes32(0))); + + // Create a ton of extra data, far past the 2-byte CWIA length limit. + bytes memory oversizedExtraData = new bytes(0xe0000); + Claim rootClaim = Claim.wrap(keccak256("rootClaim")); + vm.deal(address(this), 10 ether); + + // ZK dispute game: the clone must revert instead of deploying a codeless proxy. + vm.expectRevert(LibClone.DeploymentFailed.selector); + disputeGameFactory.create{ value: defaultZKParams.challengerBond }( + GameTypes.ZK_DISPUTE_GAME, rootClaim, oversizedExtraData + ); + + // Super fault dispute game: same guarantee. + vm.expectRevert(LibClone.DeploymentFailed.selector); + disputeGameFactory.create{ value: DEFAULT_DISPUTE_GAME_INIT_BOND }( + GameTypes.SUPER_CANNON, rootClaim, oversizedExtraData + ); + } } /// @title DisputeGameFactory_SetImplementation_ZkDisputeGame_Test From d4ed3366db6e10122ed392cc6a545b402773be08 Mon Sep 17 00:00:00 2001 From: 0xarktos Date: Fri, 3 Jul 2026 13:25:10 +0300 Subject: [PATCH 4/5] chore: reference issue number in zk todos --- .../src/L1/OPContractsManagerStandardValidator.sol | 2 +- .../contracts-bedrock/src/L1/opcm/StandardValidatorUtils.sol | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/contracts-bedrock/src/L1/OPContractsManagerStandardValidator.sol b/packages/contracts-bedrock/src/L1/OPContractsManagerStandardValidator.sol index d93c3990170..3391577e7c1 100644 --- a/packages/contracts-bedrock/src/L1/OPContractsManagerStandardValidator.sol +++ b/packages/contracts-bedrock/src/L1/OPContractsManagerStandardValidator.sol @@ -1106,7 +1106,7 @@ contract OPContractsManagerStandardValidator is ISemver { // Note: Even if the devFeatureBitmap is on for ZK_DISPUTE_GAME, we treat the deployment pipeline and // as extension, the factory as the source of truth for deciding whether to validate the ZK game. // ZK is the only per-chain opt-in game type; mandatory game types fail loud in getGameImplementation() - // TODO: Once ZK is mandatory (not per-chain opt-in) post interop migration, remove this early return so chains + // TODO(#21529): Once ZK is mandatory (not per-chain opt-in) post interop migration, remove this early return so chains // without ZKDisputeGame registered fail validation. Companion to the ZKDG-NOSHAPE TODO in // StandardValidatorUtils.sol. IDisputeGameFactory _factory = IDisputeGameFactory(_sysCfg.disputeGameFactory()); diff --git a/packages/contracts-bedrock/src/L1/opcm/StandardValidatorUtils.sol b/packages/contracts-bedrock/src/L1/opcm/StandardValidatorUtils.sol index 214499cbe15..f27f65f47ad 100644 --- a/packages/contracts-bedrock/src/L1/opcm/StandardValidatorUtils.sol +++ b/packages/contracts-bedrock/src/L1/opcm/StandardValidatorUtils.sol @@ -190,7 +190,7 @@ contract StandardValidatorUtils { internalRequire(address(dgf.gameImpls(GameTypes.SUPER_PERMISSIONED)) == address(0), "SPDG-NOSHAPE", _errors); _errors = internalRequire(address(dgf.gameImpls(GameTypes.SUPER_CANNON_KONA)) == address(0), "SCKDG-NOSHAPE", _errors); - // TODO: add ZKDG-NOSHAPE check here once the super-root ZKDisputeGame is ready to be used + // TODO(#21529): add ZKDG-NOSHAPE check here once the super-root ZKDisputeGame is ready to be used // after interop migration. After that, ZK_DISPUTE_GAME must never be registered on non-super-root chains. _errors = internalRequire( address(dgf.gameImpls(GameTypes.PERMISSIONED_CANNON)) != address(0), "PDDG-NOSHAPE", _errors From 000b2781502bfa7dc94306025617519618e7194d Mon Sep 17 00:00:00 2001 From: 0xarktos Date: Tue, 7 Jul 2026 11:26:56 +0300 Subject: [PATCH 5/5] chore: bump validator version due to natspec change --- packages/contracts-bedrock/snapshots/semver-lock.json | 4 ++-- .../src/L1/OPContractsManagerStandardValidator.sol | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/contracts-bedrock/snapshots/semver-lock.json b/packages/contracts-bedrock/snapshots/semver-lock.json index cb25ec35ac0..aaba55ac8c1 100644 --- a/packages/contracts-bedrock/snapshots/semver-lock.json +++ b/packages/contracts-bedrock/snapshots/semver-lock.json @@ -20,8 +20,8 @@ "sourceCodeHash": "0x0ef8461e8bc55314e61e60924465574db17bdf30bbbfe9739b52cac623bba2db" }, "src/L1/OPContractsManagerStandardValidator.sol:OPContractsManagerStandardValidator": { - "initCodeHash": "0x9a704de7a4137faa533a63249c02268512af5ed4e188d2e1057e3d75f3a550de", - "sourceCodeHash": "0xeceae49028d9f20fd3606cfe4fecfd076e24773c2c455df00292608f25f4a934" + "initCodeHash": "0x6c04d1e2333f52dd27b42bbd889bf3ba6d5edc0670af4514fb1a646566dcc5fb", + "sourceCodeHash": "0x8a2409bb3f08ead8a9619a7f54a995aca224f17fc96121cac0e0f2f8531d3cf6" }, "src/L1/OptimismPortal2.sol:OptimismPortal2": { "initCodeHash": "0x02f8cfb0213d614c34c12904f0d06b4f8fad14f7d127b3b780f9d5cc5ebba72a", diff --git a/packages/contracts-bedrock/src/L1/OPContractsManagerStandardValidator.sol b/packages/contracts-bedrock/src/L1/OPContractsManagerStandardValidator.sol index 90fdd841b97..df31a4c7731 100644 --- a/packages/contracts-bedrock/src/L1/OPContractsManagerStandardValidator.sol +++ b/packages/contracts-bedrock/src/L1/OPContractsManagerStandardValidator.sol @@ -46,8 +46,8 @@ import { IBigStepper } from "interfaces/dispute/IBigStepper.sol"; /// before and after an upgrade. contract OPContractsManagerStandardValidator is ISemver { /// @notice The semantic version of the OPContractsManagerStandardValidator contract. - /// @custom:semver 2.10.4 - string public constant version = "2.10.4"; + /// @custom:semver 2.10.5 + string public constant version = "2.10.5"; /// @notice The SuperchainConfig contract. ISuperchainConfig public superchainConfig; @@ -1096,7 +1096,7 @@ contract OPContractsManagerStandardValidator is ISemver { // Note: Even if the devFeatureBitmap is on for ZK_DISPUTE_GAME, we treat the deployment pipeline and // as extension, the factory as the source of truth for deciding whether to validate the ZK game. // ZK is the only per-chain opt-in game type; mandatory game types fail loud in getGameImplementation() - // TODO(#21529): Once ZK is mandatory (not per-chain opt-in) post interop migration, remove this early return so chains + // TODO(#21529): Once ZK is mandatory (not per-chain opt-in), remove this early return so chains // without ZKDisputeGame registered fail validation. Companion to the ZKDG-NOSHAPE TODO in // StandardValidatorUtils.sol. IDisputeGameFactory _factory = IDisputeGameFactory(_sysCfg.disputeGameFactory());