From 4b7c6f4a9e8e953f73e5f0efd84aede7746827ca Mon Sep 17 00:00:00 2001 From: Adrian Sutton Date: Tue, 17 Feb 2026 09:36:22 +1000 Subject: [PATCH 1/6] fix(contracts): handle v1/v2 OPCM structures when loading DelayedWETH artifacts Upgrade tests were failing because ForkLive assumed v1 structure (DelayedWETH on game impl) but after OPCM v2 upgrades it's stored in gameArgs. This caused artifact loading to fail when tests ran past upgrades without useUpgradedFork. Co-Authored-By: Claude Sonnet 4.5 --- .../test/setup/ForkLive.s.sol | 31 +++++++++++++------ 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/packages/contracts-bedrock/test/setup/ForkLive.s.sol b/packages/contracts-bedrock/test/setup/ForkLive.s.sol index c59a3feafb6..0f92b2cc310 100644 --- a/packages/contracts-bedrock/test/setup/ForkLive.s.sol +++ b/packages/contracts-bedrock/test/setup/ForkLive.s.sol @@ -179,15 +179,28 @@ contract ForkLive is Deployer, StdAssertions, FeatureFlags { IDisputeGameFactory(artifacts.mustGetAddress("DisputeGameFactoryProxy")); // The PermissionedDisputeGame and PermissionedDelayedWETHProxy are not listed in the registry for OP, so we - // look it up onchain - IFaultDisputeGame permissionedDisputeGame = - IFaultDisputeGame(address(disputeGameFactory.gameImpls(GameTypes.PERMISSIONED_CANNON))); - artifacts.save("PermissionedDisputeGame", address(permissionedDisputeGame)); - artifacts.save("PermissionedDelayedWETHProxy", address(permissionedDisputeGame.weth())); - - // The SR seems out-of-date, so pull the DelayedWETH addresses from the PermissionedDisputeGame. - artifacts.save("DelayedWETHProxy", address(permissionedDisputeGame.weth())); - artifacts.save("DelayedWETHImpl", EIP1967Helper.getImplementation(address(permissionedDisputeGame.weth()))); + // look it up onchain. + // Try gameArgs first (v2 structure), fall back to gameImpl.weth() (v1 structure). + address permissionedGameImpl = address(disputeGameFactory.gameImpls(GameTypes.PERMISSIONED_CANNON)); + artifacts.save("PermissionedDisputeGame", permissionedGameImpl); + + IDelayedWETH delayedWeth; + try disputeGameFactory.gameArgs(GameTypes.PERMISSIONED_CANNON) returns (bytes memory gameArgsData) { + if (gameArgsData.length > 0) { + // V2 structure: DelayedWETH is in gameArgs + delayedWeth = IDelayedWETH(payable(LibGameArgs.decode(gameArgsData).weth)); + } else { + // V1 structure: DelayedWETH is on game impl + delayedWeth = IFaultDisputeGame(permissionedGameImpl).weth(); + } + } catch { + // V1 structure (gameArgs() doesn't exist): DelayedWETH is on game impl + delayedWeth = IFaultDisputeGame(permissionedGameImpl).weth(); + } + + artifacts.save("PermissionedDelayedWETHProxy", address(delayedWeth)); + artifacts.save("DelayedWETHProxy", address(delayedWeth)); + artifacts.save("DelayedWETHImpl", EIP1967Helper.getImplementation(address(delayedWeth))); } /// @notice Calls to the Deploy.s.sol contract etched by Setup.sol to a deterministic address, sets up the From fe74f55054aa63d1a65a2788c7f867a63e550404 Mon Sep 17 00:00:00 2001 From: Adrian Sutton Date: Tue, 17 Feb 2026 09:42:37 +1000 Subject: [PATCH 2/6] fix(contracts): add underscore suffix to named return argument Fixes semgrep rule sol-style-return-arg-fmt which requires named return arguments to have an underscore suffix. Co-Authored-By: Claude Sonnet 4.5 --- packages/contracts-bedrock/test/setup/ForkLive.s.sol | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/contracts-bedrock/test/setup/ForkLive.s.sol b/packages/contracts-bedrock/test/setup/ForkLive.s.sol index 0f92b2cc310..ab60fcf0dbe 100644 --- a/packages/contracts-bedrock/test/setup/ForkLive.s.sol +++ b/packages/contracts-bedrock/test/setup/ForkLive.s.sol @@ -185,10 +185,10 @@ contract ForkLive is Deployer, StdAssertions, FeatureFlags { artifacts.save("PermissionedDisputeGame", permissionedGameImpl); IDelayedWETH delayedWeth; - try disputeGameFactory.gameArgs(GameTypes.PERMISSIONED_CANNON) returns (bytes memory gameArgsData) { - if (gameArgsData.length > 0) { + try disputeGameFactory.gameArgs(GameTypes.PERMISSIONED_CANNON) returns (bytes memory gameArgsData_) { + if (gameArgsData_.length > 0) { // V2 structure: DelayedWETH is in gameArgs - delayedWeth = IDelayedWETH(payable(LibGameArgs.decode(gameArgsData).weth)); + delayedWeth = IDelayedWETH(payable(LibGameArgs.decode(gameArgsData_).weth)); } else { // V1 structure: DelayedWETH is on game impl delayedWeth = IFaultDisputeGame(permissionedGameImpl).weth(); From 5eaaec266ac53b40b7362acb28f0db4680689fc2 Mon Sep 17 00:00:00 2001 From: Adrian Sutton Date: Tue, 17 Feb 2026 10:03:32 +1000 Subject: [PATCH 3/6] fix(contracts): improve v1/v2 fallback logic for DelayedWETH loading Simplify the fallback logic so that: - If gameArgs returns data (length > 0), it must decode successfully - Only fall back to v1 (game impl .weth()) if gameArgs doesn't exist or returns empty bytes This ensures proper error propagation when gameArgs data is invalid. Co-Authored-By: Claude Sonnet 4.5 --- .../contracts-bedrock/test/setup/ForkLive.s.sol | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/packages/contracts-bedrock/test/setup/ForkLive.s.sol b/packages/contracts-bedrock/test/setup/ForkLive.s.sol index ab60fcf0dbe..433ad80a7c2 100644 --- a/packages/contracts-bedrock/test/setup/ForkLive.s.sol +++ b/packages/contracts-bedrock/test/setup/ForkLive.s.sol @@ -184,17 +184,26 @@ contract ForkLive is Deployer, StdAssertions, FeatureFlags { address permissionedGameImpl = address(disputeGameFactory.gameImpls(GameTypes.PERMISSIONED_CANNON)); artifacts.save("PermissionedDisputeGame", permissionedGameImpl); + // Try to get DelayedWETH from gameArgs (v2), fallback to game impl (v1) IDelayedWETH delayedWeth; + bool useV1 = false; + + // Try v2 structure first (gameArgs) try disputeGameFactory.gameArgs(GameTypes.PERMISSIONED_CANNON) returns (bytes memory gameArgsData_) { if (gameArgsData_.length > 0) { - // V2 structure: DelayedWETH is in gameArgs + // V2 structure: gameArgs exists and has data, decode it (must succeed) delayedWeth = IDelayedWETH(payable(LibGameArgs.decode(gameArgsData_).weth)); } else { - // V1 structure: DelayedWETH is on game impl - delayedWeth = IFaultDisputeGame(permissionedGameImpl).weth(); + // V1 structure: gameArgs exists but returns empty + useV1 = true; } } catch { - // V1 structure (gameArgs() doesn't exist): DelayedWETH is on game impl + // V1 structure: gameArgs() doesn't exist + useV1 = true; + } + + // Fallback to v1 structure (get weth from game impl) + if (useV1) { delayedWeth = IFaultDisputeGame(permissionedGameImpl).weth(); } From fcc6528cca5bbfa1c08c1fadcf39b151bc7ffba1 Mon Sep 17 00:00:00 2001 From: Adrian Sutton Date: Tue, 17 Feb 2026 11:05:16 +1000 Subject: [PATCH 4/6] Use helper method. --- .../test/L1/OPContractsManager.t.sol | 12 +++---- .../test/setup/DisputeGames.sol | 30 ++++++++++++++++ .../test/setup/ForkLive.s.sol | 36 ++++++------------- 3 files changed, 44 insertions(+), 34 deletions(-) diff --git a/packages/contracts-bedrock/test/L1/OPContractsManager.t.sol b/packages/contracts-bedrock/test/L1/OPContractsManager.t.sol index f103db0053e..eea48a117b6 100644 --- a/packages/contracts-bedrock/test/L1/OPContractsManager.t.sol +++ b/packages/contracts-bedrock/test/L1/OPContractsManager.t.sol @@ -167,18 +167,14 @@ contract OPContractsManager_Upgrade_Harness is CommonTest { IDelayedWETH(payable(artifacts.mustGetAddress("PermissionedDelayedWETHProxy"))); permissionedDisputeGame = IPermissionedDisputeGame(address(artifacts.mustGetAddress("PermissionedDisputeGame"))); IDisputeGameFactory dgf = IDisputeGameFactory(address(artifacts.mustGetAddress("DisputeGameFactoryProxy"))); - faultDisputeGame = IFaultDisputeGame(address(dgf.gameImpls(GameTypes.CANNON))); - delayedWeth = faultDisputeGame.weth(); // Grab the pre-upgrade state. Use getGameImplPrestate to handle both v1 and v2 // dispute games (v1 stores prestate on game impl, v2 stores it in gameArgs). preUpgradeState = PreUpgradeState({ - cannonAbsolutePrestate: DisputeGames.getGameImplPrestate(disputeGameFactory, GameTypes.CANNON), - permissionedAbsolutePrestate: DisputeGames.getGameImplPrestate( - disputeGameFactory, GameTypes.PERMISSIONED_CANNON - ), - cannonKonaAbsolutePrestate: DisputeGames.getGameImplPrestate(disputeGameFactory, GameTypes.CANNON_KONA), - permissionlessWethProxy: delayedWeth, + cannonAbsolutePrestate: DisputeGames.getGameImplPrestate(dgf, GameTypes.CANNON), + permissionedAbsolutePrestate: DisputeGames.getGameImplPrestate(dgf, GameTypes.PERMISSIONED_CANNON), + cannonKonaAbsolutePrestate: DisputeGames.getGameImplPrestate(dgf, GameTypes.CANNON_KONA), + permissionlessWethProxy: DisputeGames.getGameImplDelayedWeth(dgf, GameTypes.CANNON), permissionedCannonWethProxy: delayedWETHPermissionedGameProxy }); diff --git a/packages/contracts-bedrock/test/setup/DisputeGames.sol b/packages/contracts-bedrock/test/setup/DisputeGames.sol index 6268a5bc92f..e71b17ca4b7 100644 --- a/packages/contracts-bedrock/test/setup/DisputeGames.sol +++ b/packages/contracts-bedrock/test/setup/DisputeGames.sol @@ -13,6 +13,7 @@ import { LibGameArgs } from "src/dispute/lib/LibGameArgs.sol"; // Interfaces import "../../interfaces/dispute/IDisputeGame.sol"; +import "../../interfaces/dispute/IDelayedWETH.sol"; import "../../interfaces/dispute/IDisputeGameFactory.sol"; import { IFaultDisputeGame } from "../../interfaces/dispute/IFaultDisputeGame.sol"; import { IPermissionedDisputeGame } from "../../interfaces/dispute/IPermissionedDisputeGame.sol"; @@ -154,6 +155,35 @@ library DisputeGames { } } + /// @notice Gets the DelayedWETH for a game type, handling both v1 and v2 dispute games. + /// V1 games store the prestate on the game implementation, v2 games store it in gameArgs. + /// Returns address(0) if no implementation exists for the game type. + /// @param _dgf The dispute game factory. + /// @param _gameType The game type to get the DelayedWETH for. + /// @return delayedWeth_ The delayedWETH address. + function getGameImplDelayedWeth( + IDisputeGameFactory _dgf, + GameType _gameType + ) + internal + view + returns (IDelayedWETH delayedWeth_) + { + // Return zero if no implementation exists for this game type + address gameImpl = address(_dgf.gameImpls(_gameType)); + if (gameImpl == address(0)) { + return IDelayedWETH(payable(address(0))); + } + + (bool gameArgsExist, bytes memory gameArgsData) = _getGameArgs(_dgf, _gameType); + if (gameArgsExist) { + LibGameArgs.GameArgs memory gameArgs = LibGameArgs.decode(gameArgsData); + delayedWeth_ = IDelayedWETH(payable(gameArgs.weth)); + } else { + delayedWeth_ = IFaultDisputeGame(gameImpl).weth(); + } + } + 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/ForkLive.s.sol b/packages/contracts-bedrock/test/setup/ForkLive.s.sol index 433ad80a7c2..a54852cb147 100644 --- a/packages/contracts-bedrock/test/setup/ForkLive.s.sol +++ b/packages/contracts-bedrock/test/setup/ForkLive.s.sol @@ -32,7 +32,6 @@ import { ISystemConfig } from "interfaces/L1/ISystemConfig.sol"; import { IProxyAdmin } from "interfaces/universal/IProxyAdmin.sol"; import { IOPContractsManager } from "interfaces/L1/IOPContractsManager.sol"; import { IAnchorStateRegistry } from "interfaces/dispute/IAnchorStateRegistry.sol"; -import { IFaultDisputeGame } from "interfaces/dispute/IFaultDisputeGame.sol"; import { IETHLockbox } from "interfaces/L1/IETHLockbox.sol"; import { IOptimismPortal2 } from "interfaces/L1/IOptimismPortal2.sol"; import { IOPContractsManagerUpgrader } from "interfaces/L1/IOPContractsManager.sol"; @@ -180,36 +179,21 @@ contract ForkLive is Deployer, StdAssertions, FeatureFlags { // The PermissionedDisputeGame and PermissionedDelayedWETHProxy are not listed in the registry for OP, so we // look it up onchain. - // Try gameArgs first (v2 structure), fall back to gameImpl.weth() (v1 structure). address permissionedGameImpl = address(disputeGameFactory.gameImpls(GameTypes.PERMISSIONED_CANNON)); artifacts.save("PermissionedDisputeGame", permissionedGameImpl); - // Try to get DelayedWETH from gameArgs (v2), fallback to game impl (v1) - IDelayedWETH delayedWeth; - bool useV1 = false; - - // Try v2 structure first (gameArgs) - try disputeGameFactory.gameArgs(GameTypes.PERMISSIONED_CANNON) returns (bytes memory gameArgsData_) { - if (gameArgsData_.length > 0) { - // V2 structure: gameArgs exists and has data, decode it (must succeed) - delayedWeth = IDelayedWETH(payable(LibGameArgs.decode(gameArgsData_).weth)); - } else { - // V1 structure: gameArgs exists but returns empty - useV1 = true; - } - } catch { - // V1 structure: gameArgs() doesn't exist - useV1 = true; - } + // Get DelayedWETH for PERMISSIONED games + IDelayedWETH permissionedDelayedWeth = + DisputeGames.getGameImplDelayedWeth(disputeGameFactory, GameTypes.PERMISSIONED_CANNON); + artifacts.save("PermissionedDelayedWETHProxy", address(permissionedDelayedWeth)); - // Fallback to v1 structure (get weth from game impl) - if (useV1) { - delayedWeth = IFaultDisputeGame(permissionedGameImpl).weth(); - } + // Get DelayedWETH for PERMISSIONLESS games (CANNON) + IDelayedWETH permissionlessDelayedWeth = + DisputeGames.getGameImplDelayedWeth(disputeGameFactory, GameTypes.PERMISSIONED_CANNON); - artifacts.save("PermissionedDelayedWETHProxy", address(delayedWeth)); - artifacts.save("DelayedWETHProxy", address(delayedWeth)); - artifacts.save("DelayedWETHImpl", EIP1967Helper.getImplementation(address(delayedWeth))); + // 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 Calls to the Deploy.s.sol contract etched by Setup.sol to a deterministic address, sets up the From 7a0551ac19295183226b240c739efa60feac806d Mon Sep 17 00:00:00 2001 From: Adrian Sutton Date: Tue, 17 Feb 2026 11:30:07 +1000 Subject: [PATCH 5/6] Fetch the pre-upgrade permissioned delayed weth from on chain. --- packages/contracts-bedrock/test/L1/OPContractsManager.t.sol | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/contracts-bedrock/test/L1/OPContractsManager.t.sol b/packages/contracts-bedrock/test/L1/OPContractsManager.t.sol index eea48a117b6..d5a79d78e81 100644 --- a/packages/contracts-bedrock/test/L1/OPContractsManager.t.sol +++ b/packages/contracts-bedrock/test/L1/OPContractsManager.t.sol @@ -163,9 +163,6 @@ contract OPContractsManager_Upgrade_Harness is CommonTest { // Artifacts encoded as an address. l2ChainId = uint256(uint160(address(artifacts.mustGetAddress("L2ChainId")))); - delayedWETHPermissionedGameProxy = - IDelayedWETH(payable(artifacts.mustGetAddress("PermissionedDelayedWETHProxy"))); - permissionedDisputeGame = IPermissionedDisputeGame(address(artifacts.mustGetAddress("PermissionedDisputeGame"))); IDisputeGameFactory dgf = IDisputeGameFactory(address(artifacts.mustGetAddress("DisputeGameFactoryProxy"))); // Grab the pre-upgrade state. Use getGameImplPrestate to handle both v1 and v2 @@ -175,7 +172,7 @@ contract OPContractsManager_Upgrade_Harness is CommonTest { permissionedAbsolutePrestate: DisputeGames.getGameImplPrestate(dgf, GameTypes.PERMISSIONED_CANNON), cannonKonaAbsolutePrestate: DisputeGames.getGameImplPrestate(dgf, GameTypes.CANNON_KONA), permissionlessWethProxy: DisputeGames.getGameImplDelayedWeth(dgf, GameTypes.CANNON), - permissionedCannonWethProxy: delayedWETHPermissionedGameProxy + permissionedCannonWethProxy: DisputeGames.getGameImplDelayedWeth(dgf, GameTypes.PERMISSIONED_CANNON) }); // Since this superchainConfig is already at the expected reinitializer version... From 47fd02a68235cb7779d4a64dcb504fc7fd4d9e23 Mon Sep 17 00:00:00 2001 From: Adrian Sutton Date: Tue, 17 Feb 2026 11:45:05 +1000 Subject: [PATCH 6/6] Load preupgrade state from on chain. --- .../test/L1/OPContractsManager.t.sol | 18 ++++++++++++++---- .../test/setup/ForkLive.s.sol | 2 +- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/packages/contracts-bedrock/test/L1/OPContractsManager.t.sol b/packages/contracts-bedrock/test/L1/OPContractsManager.t.sol index d5a79d78e81..e175104ef75 100644 --- a/packages/contracts-bedrock/test/L1/OPContractsManager.t.sol +++ b/packages/contracts-bedrock/test/L1/OPContractsManager.t.sol @@ -390,11 +390,19 @@ contract OPContractsManager_Upgrade_Harness is CommonTest { vm.assertEq(blockhash(block.number - 1), game.l1Head().raw()); if (gt.raw() == GameTypes.PERMISSIONED_CANNON.raw()) { - vm.assertEq(address(preUpgradeState.permissionedCannonWethProxy), address(game.weth())); + vm.assertEq( + address(preUpgradeState.permissionedCannonWethProxy), + address(game.weth()), + "Incorrect permissioned WETH" + ); vm.assertEq(_challenger, game.challenger()); vm.assertEq(_proposer, game.proposer()); } else { - vm.assertEq(address(preUpgradeState.permissionlessWethProxy), address(game.weth())); + vm.assertEq( + address(preUpgradeState.permissionlessWethProxy), + address(game.weth()), + "Incorrect permissionless WETH" + ); } } @@ -1421,8 +1429,10 @@ contract OPContractsManager_Upgrade_Test is OPContractsManager_Upgrade_Harness { disputeGameFactory, GameTypes.PERMISSIONED_CANNON ), cannonKonaAbsolutePrestate: DisputeGames.getGameImplPrestate(disputeGameFactory, GameTypes.CANNON_KONA), - permissionlessWethProxy: delayedWeth, - permissionedCannonWethProxy: delayedWETHPermissionedGameProxy + permissionlessWethProxy: DisputeGames.getGameImplDelayedWeth(disputeGameFactory, GameTypes.CANNON), + permissionedCannonWethProxy: DisputeGames.getGameImplDelayedWeth( + disputeGameFactory, GameTypes.PERMISSIONED_CANNON + ) }); } diff --git a/packages/contracts-bedrock/test/setup/ForkLive.s.sol b/packages/contracts-bedrock/test/setup/ForkLive.s.sol index a54852cb147..be371195255 100644 --- a/packages/contracts-bedrock/test/setup/ForkLive.s.sol +++ b/packages/contracts-bedrock/test/setup/ForkLive.s.sol @@ -189,7 +189,7 @@ contract ForkLive is Deployer, StdAssertions, FeatureFlags { // Get DelayedWETH for PERMISSIONLESS games (CANNON) IDelayedWETH permissionlessDelayedWeth = - DisputeGames.getGameImplDelayedWeth(disputeGameFactory, GameTypes.PERMISSIONED_CANNON); + DisputeGames.getGameImplDelayedWeth(disputeGameFactory, GameTypes.CANNON); // The SR seems out-of-date, so pull the DelayedWETH addresses from the games. artifacts.save("DelayedWETHProxy", address(permissionlessDelayedWeth));