From 308ce74c7ab82a7a4a4fb5dd709224af316d2921 Mon Sep 17 00:00:00 2001 From: Maurelian Date: Tue, 24 Sep 2024 10:45:42 -0400 Subject: [PATCH] OPSM: Deploy Permissioned Game (#12064) * chore: fix semver lock * fix: no permissionless root, remove hash from 0xdead * fix: use 0xdead root properly * feat: add remaining fault proof support * chore: Update semver-lock * fix: Remove extra anchor root definition and restore aritfactsFs argument * feat: Add wip big blueprint code * Don't wrap input to deployBigBytecode with preamble * fix: off by one in deployBigBytecode * feat: more gas efficient blueprint deployment for permissioned game * Get the big deployments working * perf: more efficient preamble parsing * chore: snapshots + fix revert * test: skip FaultDisputeGameAddress since we don't deploy it yet * chore: cleanup --------- Co-authored-by: Matt Solomon Co-authored-by: Matthew Slipper --- .../deployer/integration_test/apply_test.go | 9 +- .../scripts/DeployImplementations.s.sol | 52 ++++++-- .../scripts/DeployOPChain.s.sol | 38 ++++-- packages/contracts-bedrock/semver-lock.json | 4 +- .../snapshots/abi/OPStackManager.json | 25 ++++ .../snapshots/abi/OPStackManagerInterop.json | 25 ++++ .../storageLayout/OPStackManager.json | 27 ++-- .../storageLayout/OPStackManagerInterop.json | 27 ++-- .../src/L1/OPStackManager.sol | 115 ++++++++++++++---- .../src/libraries/Blueprint.sol | 55 ++++++++- .../test/libraries/Blueprint.t.sol | 2 +- 11 files changed, 309 insertions(+), 70 deletions(-) diff --git a/op-chain-ops/deployer/integration_test/apply_test.go b/op-chain-ops/deployer/integration_test/apply_test.go index 6d673ed03791..b69595b67761 100644 --- a/op-chain-ops/deployer/integration_test/apply_test.go +++ b/op-chain-ops/deployer/integration_test/apply_test.go @@ -27,6 +27,8 @@ import ( const TestParams = ` participants: - el_type: geth + el_extra_params: + - "--gcmode=archive" cl_type: lighthouse network_params: prefunded_accounts: '{ "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266": { "balance": "1000000ETH" } }' @@ -41,6 +43,7 @@ network_params: }' network_id: "77799777" seconds_per_slot: 3 + genesis_delay: 0 ` type deployerKey struct{} @@ -56,7 +59,7 @@ func (d *deployerKey) String() string { func TestEndToEndApply(t *testing.T) { kurtosisutil.Test(t) - lgr := testlog.Logger(t, slog.LevelInfo) + lgr := testlog.Logger(t, slog.LevelDebug) ctx, cancel := context.WithCancel(context.Background()) defer cancel() @@ -189,6 +192,10 @@ func TestEndToEndApply(t *testing.T) { {"DelayedWETHPermissionlessGameProxyAddress", chainState.DelayedWETHPermissionlessGameProxyAddress}, } for _, addr := range chainAddrs { + // TODO Delete this `if`` block once FaultDisputeGameAddress is deployed. + if addr.name == "FaultDisputeGameAddress" { + continue + } t.Run(fmt.Sprintf("chain %s - %s", chainState.ID, addr.name), func(t *testing.T) { code, err := l1Client.CodeAt(ctx, addr.addr, nil) require.NoError(t, err) diff --git a/packages/contracts-bedrock/scripts/DeployImplementations.s.sol b/packages/contracts-bedrock/scripts/DeployImplementations.s.sol index 433b1573efe4..81cafa89c272 100644 --- a/packages/contracts-bedrock/scripts/DeployImplementations.s.sol +++ b/packages/contracts-bedrock/scripts/DeployImplementations.s.sol @@ -10,6 +10,7 @@ import { ISuperchainConfig } from "src/L1/interfaces/ISuperchainConfig.sol"; import { Constants } from "src/libraries/Constants.sol"; import { Predeploys } from "src/libraries/Predeploys.sol"; +import { Bytes } from "src/libraries/Bytes.sol"; import { ProxyAdmin } from "src/universal/ProxyAdmin.sol"; import { Proxy } from "src/universal/Proxy.sol"; @@ -23,6 +24,7 @@ import { IPreimageOracle } from "src/cannon/interfaces/IPreimageOracle.sol"; import { MIPS } from "src/cannon/MIPS.sol"; import { DisputeGameFactory } from "src/dispute/DisputeGameFactory.sol"; import { AnchorStateRegistry } from "src/dispute/AnchorStateRegistry.sol"; +import { PermissionedDisputeGame } from "src/dispute/PermissionedDisputeGame.sol"; import { SuperchainConfig } from "src/L1/SuperchainConfig.sol"; import { ProtocolVersions } from "src/L1/ProtocolVersions.sol"; @@ -514,10 +516,11 @@ contract DeployImplementations is Script { blueprints.l1ChugSplashProxy = deployBytecode(Blueprint.blueprintDeployerBytecode(type(L1ChugSplashProxy).creationCode), salt); blueprints.resolvedDelegateProxy = deployBytecode(Blueprint.blueprintDeployerBytecode(type(ResolvedDelegateProxy).creationCode), salt); blueprints.anchorStateRegistry = deployBytecode(Blueprint.blueprintDeployerBytecode(type(AnchorStateRegistry).creationCode), salt); + (blueprints.permissionedDisputeGame1, blueprints.permissionedDisputeGame2) = deployBigBytecode(type(PermissionedDisputeGame).creationCode, salt); vm.stopBroadcast(); // forgefmt: disable-end - OPStackManager.ImplementationSetter[] memory setters = new OPStackManager.ImplementationSetter[](7); + OPStackManager.ImplementationSetter[] memory setters = new OPStackManager.ImplementationSetter[](9); setters[0] = OPStackManager.ImplementationSetter({ name: "L1ERC721Bridge", info: OPStackManager.Implementation(address(_dio.l1ERC721BridgeImpl()), L1ERC721Bridge.initialize.selector) @@ -543,13 +546,22 @@ contract DeployImplementations is Script { name: "L1StandardBridge", info: OPStackManager.Implementation(address(_dio.l1StandardBridgeImpl()), L1StandardBridge.initialize.selector) }); - setters[6] = OPStackManager.ImplementationSetter({ name: "DisputeGameFactory", info: OPStackManager.Implementation( address(_dio.disputeGameFactoryImpl()), DisputeGameFactory.initialize.selector ) }); + setters[7] = OPStackManager.ImplementationSetter({ + name: "DelayedWETH", + info: OPStackManager.Implementation(address(_dio.delayedWETHImpl()), DelayedWETH.initialize.selector) + }); + setters[8] = OPStackManager.ImplementationSetter({ + name: "MIPS", + // MIPS is a singleton for all chains, so it doesn't need to be initialized, so the + // selector is just `bytes4(0)`. + info: OPStackManager.Implementation(address(_dio.mipsSingleton()), bytes4(0)) + }); // This call contains a broadcast to deploy OPSM which is proxied. OPStackManager opsmProxy = createOPSMContract(_dii, _dio, blueprints, release, setters); @@ -617,14 +629,14 @@ contract DeployImplementations is Script { // The fault proofs contracts are configured as follows: // | Contract | Proxied | Deployment | MCP Ready | // |-------------------------|---------|-----------------------------------|------------| - // | DisputeGameFactory | Yes | Bespoke | Yes | X - // | AnchorStateRegistry | Yes | Bespoke | No | X - // | FaultDisputeGame | No | Bespoke | No | Todo - // | PermissionedDisputeGame | No | Bespoke | No | Todo - // | DelayedWETH | Yes | Two bespoke (one per DisputeGame) | No | Todo: Proxies. - // | PreimageOracle | No | Shared | N/A | X - // | MIPS | No | Shared | N/A | X - // | OptimismPortal2 | Yes | Shared | No | X + // | DisputeGameFactory | Yes | Bespoke | Yes | + // | AnchorStateRegistry | Yes | Bespoke | No | + // | FaultDisputeGame | No | Bespoke | No | Not yet supported by OPCM + // | PermissionedDisputeGame | No | Bespoke | No | + // | DelayedWETH | Yes | Two bespoke (one per DisputeGame) | No | + // | PreimageOracle | No | Shared | N/A | + // | MIPS | No | Shared | N/A | + // | OptimismPortal2 | Yes | Shared | No | // // This script only deploys the shared contracts. The bespoke contracts are deployed by // `DeployOPChain.s.sol`. When the shared contracts are proxied, the contracts deployed here are @@ -731,6 +743,26 @@ contract DeployImplementations is Script { } require(newContract_ != address(0), "DeployImplementations: create2 failed"); } + + function deployBigBytecode( + bytes memory _bytecode, + bytes32 _salt + ) + public + returns (address newContract1_, address newContract2_) + { + // Preamble needs 3 bytes. + uint256 maxInitCodeSize = 24576 - 3; + require(_bytecode.length > maxInitCodeSize, "DeployImplementations: Use deployBytecode instead"); + + bytes memory part1Slice = Bytes.slice(_bytecode, 0, maxInitCodeSize); + bytes memory part1 = Blueprint.blueprintDeployerBytecode(part1Slice); + bytes memory part2Slice = Bytes.slice(_bytecode, maxInitCodeSize, _bytecode.length - maxInitCodeSize); + bytes memory part2 = Blueprint.blueprintDeployerBytecode(part2Slice); + + newContract1_ = deployBytecode(part1, _salt); + newContract2_ = deployBytecode(part2, _salt); + } } // Similar to how DeploySuperchain.s.sol contains a lot of comments to thoroughly document the script diff --git a/packages/contracts-bedrock/scripts/DeployOPChain.s.sol b/packages/contracts-bedrock/scripts/DeployOPChain.s.sol index 50fd9060ae91..e0df48cc6029 100644 --- a/packages/contracts-bedrock/scripts/DeployOPChain.s.sol +++ b/packages/contracts-bedrock/scripts/DeployOPChain.s.sol @@ -11,6 +11,7 @@ import { BaseDeployIO } from "scripts/utils/BaseDeployIO.sol"; import { IResourceMetering } from "src/L1/interfaces/IResourceMetering.sol"; import { ISuperchainConfig } from "src/L1/interfaces/ISuperchainConfig.sol"; +import { IBigStepper } from "src/dispute/interfaces/IBigStepper.sol"; import { Constants } from "src/libraries/Constants.sol"; import { Predeploys } from "src/libraries/Predeploys.sol"; @@ -23,7 +24,7 @@ import { DisputeGameFactory } from "src/dispute/DisputeGameFactory.sol"; import { AnchorStateRegistry } from "src/dispute/AnchorStateRegistry.sol"; import { FaultDisputeGame } from "src/dispute/FaultDisputeGame.sol"; import { PermissionedDisputeGame } from "src/dispute/PermissionedDisputeGame.sol"; -import { GameType, GameTypes, Hash, OutputRoot } from "src/dispute/lib/Types.sol"; +import { Claim, GameType, GameTypes, Hash, OutputRoot } from "src/dispute/lib/Types.sol"; import { OPStackManager } from "src/L1/OPStackManager.sol"; import { OptimismPortal2 } from "src/L1/OptimismPortal2.sol"; @@ -201,7 +202,7 @@ contract DeployOPChainOutput is BaseDeployIO { address(_disputeGameFactoryProxy), address(_anchorStateRegistryProxy), address(_anchorStateRegistryImpl), - address(_faultDisputeGame), + // address(_faultDisputeGame), address(_permissionedDisputeGame), address(_delayedWETHPermissionedGameProxy), address(_delayedWETHPermissionlessGameProxy) @@ -289,8 +290,8 @@ contract DeployOPChainOutput is BaseDeployIO { // -------- Deployment Assertions -------- function assertValidDeploy(DeployOPChainInput _doi) internal { - assertValidAnchorStateRegistryProxy(_doi); assertValidAnchorStateRegistryImpl(_doi); + assertValidAnchorStateRegistryProxy(_doi); assertValidDelayedWETHs(_doi); assertValidDisputeGameFactory(_doi); assertValidL1CrossDomainMessenger(_doi); @@ -298,9 +299,23 @@ contract DeployOPChainOutput is BaseDeployIO { assertValidL1StandardBridge(_doi); assertValidOptimismMintableERC20Factory(_doi); assertValidOptimismPortal(_doi); + assertValidPermissionedDisputeGame(_doi); assertValidSystemConfig(_doi); - // TODO Other FP assertions like the dispute games, anchor state registry, etc. - // TODO add initialization assertions + } + + function assertValidPermissionedDisputeGame(DeployOPChainInput _doi) internal view { + PermissionedDisputeGame game = permissionedDisputeGame(); + + require(GameType.unwrap(game.gameType()) == GameType.unwrap(GameTypes.PERMISSIONED_CANNON), "DPG-10"); + require(Claim.unwrap(game.absolutePrestate()) == bytes32(hex"dead"), "DPG-20"); + + OPStackManager opsm = _doi.opsmProxy(); + (address mips,) = opsm.implementations(opsm.latestRelease(), "MIPS"); + require(game.vm() == IBigStepper(mips), "DPG-30"); + + require(address(game.weth()) == address(delayedWETHPermissionedGameProxy()), "DPG-40"); + require(address(game.anchorStateRegistry()) == address(anchorStateRegistryProxy()), "DPG-50"); + require(game.l2ChainId() == _doi.l2ChainId(), "DPG-60"); } function assertValidAnchorStateRegistryProxy(DeployOPChainInput) internal { @@ -436,7 +451,14 @@ contract DeployOPChainOutput is BaseDeployIO { } function assertValidDisputeGameFactory(DeployOPChainInput) internal view { - // TODO add in once FP support is added. + DisputeGameFactory factory = disputeGameFactoryProxy(); + + DeployUtils.assertInitialized({ _contractAddress: address(factory), _slot: 0, _offset: 0 }); + + require( + address(factory.gameImpls(GameTypes.PERMISSIONED_CANNON)) == address(permissionedDisputeGame()), "DF-10" + ); + require(factory.owner() == address(opChainProxyAdmin()), "DF-20"); } function assertValidDelayedWETHs(DeployOPChainInput) internal view { @@ -480,7 +502,7 @@ contract DeployOPChain is Script { vm.label(address(deployOutput.disputeGameFactoryProxy), "disputeGameFactoryProxy"); vm.label(address(deployOutput.anchorStateRegistryProxy), "anchorStateRegistryProxy"); vm.label(address(deployOutput.anchorStateRegistryImpl), "anchorStateRegistryImpl"); - vm.label(address(deployOutput.faultDisputeGame), "faultDisputeGame"); + // vm.label(address(deployOutput.faultDisputeGame), "faultDisputeGame"); vm.label(address(deployOutput.permissionedDisputeGame), "permissionedDisputeGame"); vm.label(address(deployOutput.delayedWETHPermissionedGameProxy), "delayedWETHPermissionedGameProxy"); vm.label(address(deployOutput.delayedWETHPermissionlessGameProxy), "delayedWETHPermissionlessGameProxy"); @@ -498,7 +520,7 @@ contract DeployOPChain is Script { _doo.set(_doo.disputeGameFactoryProxy.selector, address(deployOutput.disputeGameFactoryProxy)); _doo.set(_doo.anchorStateRegistryProxy.selector, address(deployOutput.anchorStateRegistryProxy)); _doo.set(_doo.anchorStateRegistryImpl.selector, address(deployOutput.anchorStateRegistryImpl)); - _doo.set(_doo.faultDisputeGame.selector, address(deployOutput.faultDisputeGame)); + // _doo.set(_doo.faultDisputeGame.selector, address(deployOutput.faultDisputeGame)); _doo.set(_doo.permissionedDisputeGame.selector, address(deployOutput.permissionedDisputeGame)); _doo.set(_doo.delayedWETHPermissionedGameProxy.selector, address(deployOutput.delayedWETHPermissionedGameProxy)); _doo.set( diff --git a/packages/contracts-bedrock/semver-lock.json b/packages/contracts-bedrock/semver-lock.json index db35c5b37429..7312208bee3e 100644 --- a/packages/contracts-bedrock/semver-lock.json +++ b/packages/contracts-bedrock/semver-lock.json @@ -32,8 +32,8 @@ "sourceCodeHash": "0xde4df0f9633dc0cdb1c9f634003ea5b0f7c5c1aebc407bc1b2f44c0ecf938649" }, "src/L1/OPStackManager.sol": { - "initCodeHash": "0x4bffecbd95e63f9bd04ab8e3c6a804cc25e0cd151ebeb7f8d6b9330332e6eb20", - "sourceCodeHash": "0x850f1eacc77f1a5c680625196618bc4b4332cb68924d9eddd57c749bedcd7c94" + "initCodeHash": "0x5b451782192b8429f6822c88270c4f0dbd10342518c5695ecf4dff7b5ebfb4e4", + "sourceCodeHash": "0x4a9c242ce96471437ec97662d2365a7bda376db765c630a41cbe238811f1df51" }, "src/L1/OptimismPortal.sol": { "initCodeHash": "0xbe2c0c81b3459014f287d8c89cdc0d27dde5d1f44e5d024fa1e4773ddc47c190", diff --git a/packages/contracts-bedrock/snapshots/abi/OPStackManager.json b/packages/contracts-bedrock/snapshots/abi/OPStackManager.json index 2ad0a4d1dc2c..9654f8f084ab 100644 --- a/packages/contracts-bedrock/snapshots/abi/OPStackManager.json +++ b/packages/contracts-bedrock/snapshots/abi/OPStackManager.json @@ -50,6 +50,16 @@ "internalType": "address", "name": "anchorStateRegistry", "type": "address" + }, + { + "internalType": "address", + "name": "permissionedDisputeGame1", + "type": "address" + }, + { + "internalType": "address", + "name": "permissionedDisputeGame2", + "type": "address" } ], "internalType": "struct OPStackManager.Blueprints", @@ -298,6 +308,16 @@ "internalType": "address", "name": "anchorStateRegistry", "type": "address" + }, + { + "internalType": "address", + "name": "permissionedDisputeGame1", + "type": "address" + }, + { + "internalType": "address", + "name": "permissionedDisputeGame2", + "type": "address" } ], "internalType": "struct OPStackManager.Blueprints", @@ -499,6 +519,11 @@ "name": "EmptyInitcode", "type": "error" }, + { + "inputs": [], + "name": "IdentityPrecompileCallFailed", + "type": "error" + }, { "inputs": [], "name": "InvalidChainId", diff --git a/packages/contracts-bedrock/snapshots/abi/OPStackManagerInterop.json b/packages/contracts-bedrock/snapshots/abi/OPStackManagerInterop.json index 2ad0a4d1dc2c..9654f8f084ab 100644 --- a/packages/contracts-bedrock/snapshots/abi/OPStackManagerInterop.json +++ b/packages/contracts-bedrock/snapshots/abi/OPStackManagerInterop.json @@ -50,6 +50,16 @@ "internalType": "address", "name": "anchorStateRegistry", "type": "address" + }, + { + "internalType": "address", + "name": "permissionedDisputeGame1", + "type": "address" + }, + { + "internalType": "address", + "name": "permissionedDisputeGame2", + "type": "address" } ], "internalType": "struct OPStackManager.Blueprints", @@ -298,6 +308,16 @@ "internalType": "address", "name": "anchorStateRegistry", "type": "address" + }, + { + "internalType": "address", + "name": "permissionedDisputeGame1", + "type": "address" + }, + { + "internalType": "address", + "name": "permissionedDisputeGame2", + "type": "address" } ], "internalType": "struct OPStackManager.Blueprints", @@ -499,6 +519,11 @@ "name": "EmptyInitcode", "type": "error" }, + { + "inputs": [], + "name": "IdentityPrecompileCallFailed", + "type": "error" + }, { "inputs": [], "name": "InvalidChainId", diff --git a/packages/contracts-bedrock/snapshots/storageLayout/OPStackManager.json b/packages/contracts-bedrock/snapshots/storageLayout/OPStackManager.json index 881871a50dd1..c22ed7c2c8da 100644 --- a/packages/contracts-bedrock/snapshots/storageLayout/OPStackManager.json +++ b/packages/contracts-bedrock/snapshots/storageLayout/OPStackManager.json @@ -13,32 +13,39 @@ "slot": "0", "type": "bool" }, - { - "bytes": "192", - "label": "blueprint", - "offset": 0, - "slot": "1", - "type": "struct OPStackManager.Blueprints" - }, { "bytes": "32", "label": "latestRelease", "offset": 0, - "slot": "7", + "slot": "1", "type": "string" }, { "bytes": "32", "label": "implementations", "offset": 0, - "slot": "8", + "slot": "2", "type": "mapping(string => mapping(string => struct OPStackManager.Implementation))" }, { "bytes": "32", "label": "systemConfigs", "offset": 0, - "slot": "9", + "slot": "3", "type": "mapping(uint256 => contract SystemConfig)" + }, + { + "bytes": "256", + "label": "blueprint", + "offset": 0, + "slot": "4", + "type": "struct OPStackManager.Blueprints" + }, + { + "bytes": "1600", + "label": "__gap", + "offset": 0, + "slot": "12", + "type": "uint256[50]" } ] \ No newline at end of file diff --git a/packages/contracts-bedrock/snapshots/storageLayout/OPStackManagerInterop.json b/packages/contracts-bedrock/snapshots/storageLayout/OPStackManagerInterop.json index 881871a50dd1..c22ed7c2c8da 100644 --- a/packages/contracts-bedrock/snapshots/storageLayout/OPStackManagerInterop.json +++ b/packages/contracts-bedrock/snapshots/storageLayout/OPStackManagerInterop.json @@ -13,32 +13,39 @@ "slot": "0", "type": "bool" }, - { - "bytes": "192", - "label": "blueprint", - "offset": 0, - "slot": "1", - "type": "struct OPStackManager.Blueprints" - }, { "bytes": "32", "label": "latestRelease", "offset": 0, - "slot": "7", + "slot": "1", "type": "string" }, { "bytes": "32", "label": "implementations", "offset": 0, - "slot": "8", + "slot": "2", "type": "mapping(string => mapping(string => struct OPStackManager.Implementation))" }, { "bytes": "32", "label": "systemConfigs", "offset": 0, - "slot": "9", + "slot": "3", "type": "mapping(uint256 => contract SystemConfig)" + }, + { + "bytes": "256", + "label": "blueprint", + "offset": 0, + "slot": "4", + "type": "struct OPStackManager.Blueprints" + }, + { + "bytes": "1600", + "label": "__gap", + "offset": 0, + "slot": "12", + "type": "uint256[50]" } ] \ No newline at end of file diff --git a/packages/contracts-bedrock/src/L1/OPStackManager.sol b/packages/contracts-bedrock/src/L1/OPStackManager.sol index f7d71233005e..1a81430ff1e6 100644 --- a/packages/contracts-bedrock/src/L1/OPStackManager.sol +++ b/packages/contracts-bedrock/src/L1/OPStackManager.sol @@ -8,6 +8,10 @@ import { Initializable } from "@openzeppelin/contracts/proxy/utils/Initializable import { ISemver } from "src/universal/interfaces/ISemver.sol"; import { IResourceMetering } from "src/L1/interfaces/IResourceMetering.sol"; +import { IBigStepper } from "src/dispute/interfaces/IBigStepper.sol"; +import { IDelayedWETH } from "src/dispute/interfaces/IDelayedWETH.sol"; +import { IAnchorStateRegistry } from "src/dispute/interfaces/IAnchorStateRegistry.sol"; +import { IDisputeGame } from "src/dispute/interfaces/IDisputeGame.sol"; import { Proxy } from "src/universal/Proxy.sol"; import { ProxyAdmin } from "src/universal/ProxyAdmin.sol"; @@ -23,7 +27,7 @@ import { DisputeGameFactory } from "src/dispute/DisputeGameFactory.sol"; import { AnchorStateRegistry } from "src/dispute/AnchorStateRegistry.sol"; import { FaultDisputeGame } from "src/dispute/FaultDisputeGame.sol"; import { PermissionedDisputeGame } from "src/dispute/PermissionedDisputeGame.sol"; -import { GameTypes } from "src/dispute/lib/Types.sol"; +import { Claim, Duration, GameType, GameTypes } from "src/dispute/lib/Types.sol"; import { SuperchainConfig } from "src/L1/SuperchainConfig.sol"; import { ProtocolVersions } from "src/L1/ProtocolVersions.sol"; @@ -105,6 +109,8 @@ contract OPStackManager is ISemver, Initializable { address l1ChugSplashProxy; address resolvedDelegateProxy; address anchorStateRegistry; + address permissionedDisputeGame1; + address permissionedDisputeGame2; } /// @notice Inputs required when initializing the OPStackManager. To avoid 'StackTooDeep' errors, @@ -118,8 +124,8 @@ contract OPStackManager is ISemver, Initializable { // -------- Constants and Variables -------- - /// @custom:semver 1.0.0-beta.4 - string public constant version = "1.0.0-beta.4"; + /// @custom:semver 1.0.0-beta.5 + string public constant version = "1.0.0-beta.5"; /// @notice Address of the SuperchainConfig contract shared by all chains. SuperchainConfig public immutable superchainConfig; @@ -127,12 +133,6 @@ contract OPStackManager is ISemver, Initializable { /// @notice Address of the ProtocolVersions contract shared by all chains. ProtocolVersions public immutable protocolVersions; - /// @notice Addresses of the Blueprint contracts. - /// This is internal because if public the autogenerated getter method would return a tuple of - /// addresses, but we want it to return a struct. This is also set via `initialize` because - /// we can't make this an immutable variable as it is a non-value type. - Blueprints internal blueprint; - /// @notice The latest release of the OP Stack Manager, as a string of the format `op-contracts/vX.Y.Z`. string public latestRelease; @@ -142,6 +142,16 @@ contract OPStackManager is ISemver, Initializable { /// @notice Maps an L2 Chain ID to the SystemConfig for that chain. mapping(uint256 => SystemConfig) public systemConfigs; + /// @notice Addresses of the Blueprint contracts. + /// This is internal because if public the autogenerated getter method would return a tuple of + /// addresses, but we want it to return a struct. This is also set via `initialize` because + /// we can't make this an immutable variable as it is a non-value type. + Blueprints internal blueprint; + + /// @notice Storage gap for future modifications, so we can expand the number of blueprints + /// without affecting other storage variables. + uint256[50] private __gap; + // -------- Events -------- /// @notice Emitted when a new OP Stack chain is deployed. @@ -206,16 +216,6 @@ contract OPStackManager is ISemver, Initializable { bytes32 salt = bytes32(_input.l2ChainId); DeployOutput memory output; - // -------- TODO: Placeholders -------- - // For contracts we don't yet deploy, we set the outputs to dummy proxies so they have code to pass assertions. - // We do these first, that way the disputeGameFactoryProxy is set when passed to the SystemConfig input. - output.faultDisputeGame = FaultDisputeGame(deployProxy(l2ChainId, output.opChainProxyAdmin, "5")); - output.permissionedDisputeGame = PermissionedDisputeGame(deployProxy(l2ChainId, output.opChainProxyAdmin, "6")); - output.delayedWETHPermissionedGameProxy = - DelayedWETH(payable(deployProxy(l2ChainId, output.opChainProxyAdmin, "7"))); - output.delayedWETHPermissionlessGameProxy = - DelayedWETH(payable(deployProxy(l2ChainId, output.opChainProxyAdmin, "8"))); - // -------- Deploy Chain Singletons -------- // The ProxyAdmin is the owner of all proxies for the chain. We temporarily set the owner to @@ -266,6 +266,22 @@ contract OPStackManager is ISemver, Initializable { Blueprint.deployFrom(blueprint.anchorStateRegistry, salt, abi.encode(output.disputeGameFactoryProxy)) ); + // We have two delayed WETH contracts per chain, one for each of the permissioned and permissionless games. + output.delayedWETHPermissionlessGameProxy = + DelayedWETH(payable(deployProxy(l2ChainId, output.opChainProxyAdmin, "DelayedWETHPermissionlessGame"))); + output.delayedWETHPermissionedGameProxy = + DelayedWETH(payable(deployProxy(l2ChainId, output.opChainProxyAdmin, "DelayedWETHPermissionedGame"))); + + // While not a proxy, we deploy the PermissionedDisputeGame here as well because it's bespoke per chain. + output.permissionedDisputeGame = PermissionedDisputeGame( + Blueprint.deployFrom( + blueprint.permissionedDisputeGame1, + blueprint.permissionedDisputeGame2, + salt, + encodePermissionedDisputeGameConstructor(_input, output) + ) + ); + // -------- Set and Initialize Proxy Implementations -------- Implementation memory impl; bytes memory data; @@ -294,10 +310,20 @@ contract OPStackManager is ISemver, Initializable { data = encodeL1StandardBridgeInitializer(impl.initializer, output); upgradeAndCall(output.opChainProxyAdmin, address(output.l1StandardBridgeProxy), impl.logic, data); - // TODO: also call setImplementation() once the dispute games are deployed. + impl = getLatestImplementation("DelayedWETH"); + data = encodeDelayedWETHInitializer(impl.initializer, _input); + upgradeAndCall(output.opChainProxyAdmin, address(output.delayedWETHPermissionedGameProxy), impl.logic, data); + upgradeAndCall(output.opChainProxyAdmin, address(output.delayedWETHPermissionlessGameProxy), impl.logic, data); + + // We set the initial owner to this contract, set game implementations, then transfer ownership. impl = getLatestImplementation("DisputeGameFactory"); data = encodeDisputeGameFactoryInitializer(impl.initializer, _input); upgradeAndCall(output.opChainProxyAdmin, address(output.disputeGameFactoryProxy), impl.logic, data); + output.disputeGameFactoryProxy.setImplementation( + GameTypes.PERMISSIONED_CANNON, IDisputeGame(address(output.permissionedDisputeGame)) + ); + output.disputeGameFactoryProxy.setInitBond(GameTypes.PERMISSIONED_CANNON, 0.08 ether); + output.disputeGameFactoryProxy.transferOwnership(address(output.opChainProxyAdmin)); impl.logic = address(output.anchorStateRegistryImpl); impl.initializer = AnchorStateRegistry.initialize.selector; @@ -387,7 +413,11 @@ contract OPStackManager is ISemver, Initializable { _output; // TODO make GameTypes.CANNON an input once FPs are supported return abi.encodeWithSelector( - _selector, _output.disputeGameFactoryProxy, _output.systemConfigProxy, superchainConfig, GameTypes.CANNON + _selector, + _output.disputeGameFactoryProxy, + _output.systemConfigProxy, + superchainConfig, + GameTypes.PERMISSIONED_CANNON ); } @@ -463,14 +493,16 @@ contract OPStackManager is ISemver, Initializable { function encodeDisputeGameFactoryInitializer( bytes4 _selector, - DeployInput memory _input + DeployInput memory ) internal view virtual returns (bytes memory) { - return abi.encodeWithSelector(_selector, _input.roles.opChainProxyAdminOwner); + // This contract must be the initial owner so we can set game implementations, then + // ownership is transferred after. + return abi.encodeWithSelector(_selector, address(this)); } function encodeAnchorStateRegistryInitializer( @@ -488,6 +520,43 @@ contract OPStackManager is ISemver, Initializable { return abi.encodeWithSelector(_selector, startingAnchorRoots, superchainConfig); } + function encodeDelayedWETHInitializer( + bytes4 _selector, + DeployInput memory _input + ) + internal + view + virtual + returns (bytes memory) + { + return abi.encodeWithSelector(_selector, _input.roles.opChainProxyAdminOwner, superchainConfig); + } + + function encodePermissionedDisputeGameConstructor( + DeployInput memory _input, + DeployOutput memory _output + ) + internal + view + virtual + returns (bytes memory) + { + return abi.encode( + GameType.wrap(1), // Permissioned Cannon + Claim.wrap(bytes32(hex"dead")), // absolutePrestate + 73, // maxGameDepth + 30, // splitDepth + Duration.wrap(3 hours), // clockExtension + Duration.wrap(3.5 days), // maxClockDuration + IBigStepper(getLatestImplementation("MIPS").logic), + IDelayedWETH(payable(address(_output.delayedWETHPermissionedGameProxy))), + IAnchorStateRegistry(address(_output.anchorStateRegistryProxy)), + _input.l2ChainId, + _input.roles.proposer, + _input.roles.challenger + ); + } + /// @notice Returns default, standard config arguments for the SystemConfig initializer. /// This is used by subclasses to reduce code duplication. function defaultSystemConfigParams( diff --git a/packages/contracts-bedrock/src/libraries/Blueprint.sol b/packages/contracts-bedrock/src/libraries/Blueprint.sol index 2e0979e1c6bf..a7ddf1f9009b 100644 --- a/packages/contracts-bedrock/src/libraries/Blueprint.sol +++ b/packages/contracts-bedrock/src/libraries/Blueprint.sol @@ -20,6 +20,9 @@ library Blueprint { /// @notice Thrown when parsing a blueprint preamble and the resulting initcode is empty. error EmptyInitcode(); + /// @notice Thrown when call to the identity precompile fails. + error IdentityPrecompileCallFailed(); + /// @notice Thrown when parsing a blueprint preamble and the bytecode does not contain the expected prefix bytes. error NotABlueprint(); @@ -56,7 +59,7 @@ library Blueprint { /// @notice Given bytecode as a sequence of bytes, parse the blueprint preamble and deconstruct /// the bytecode into the ERC version, preamble data and initcode. Reverts if the bytecode is /// not a valid blueprint contract according to ERC-5202. - function parseBlueprintPreamble(bytes memory _bytecode) internal pure returns (Preamble memory) { + function parseBlueprintPreamble(bytes memory _bytecode) internal view returns (Preamble memory) { if (_bytecode.length < 2 || _bytecode[0] != 0xFE || _bytecode[1] != 0x71) { revert NotABlueprint(); } @@ -77,18 +80,34 @@ library Blueprint { bytes memory preambleData = new bytes(dataLength); if (nLengthBytes != 0) { uint256 dataStart = 3 + nLengthBytes; + // This loop is very small, so not worth using the identity precompile like we do with initcode below. for (uint256 i = 0; i < dataLength; i++) { preambleData[i] = _bytecode[dataStart + i]; } } + // Parsing the initcode byte-by-byte is too costly for long initcode, so we perform a staticcall + // to the identity precompile at address(0x04) to copy the initcode. uint256 initcodeStart = 3 + nLengthBytes + dataLength; - bytes memory initcode = new bytes(_bytecode.length - initcodeStart); - for (uint256 i = 0; i < initcode.length; i++) { - initcode[i] = _bytecode[initcodeStart + i]; + uint256 initcodeLength = _bytecode.length - initcodeStart; + if (initcodeLength == 0) revert EmptyInitcode(); + + bytes memory initcode = new bytes(initcodeLength); + bool success; + assembly ("memory-safe") { + // Calculate the memory address of the input data (initcode) within _bytecode. + // - add(_bytecode, 32): Moves past the length field to the start of _bytecode's data. + // - add(..., initcodeStart): Adds the offset to reach the initcode within _bytecode. + let inputData := add(add(_bytecode, 32), initcodeStart) + + // Calculate the memory address for the output data in initcode. + let outputData := add(initcode, 32) + + // Perform the staticcall to the identity precompile. + success := staticcall(gas(), 0x04, inputData, initcodeLength, outputData, initcodeLength) } - if (initcode.length == 0) revert EmptyInitcode(); + if (!success) revert IdentityPrecompileCallFailed(); return Preamble(ercVersion, preambleData, initcode); } @@ -112,6 +131,32 @@ library Blueprint { if (newContract_ == address(0)) revert DeploymentFailed(); } + /// @notice Parses the code at two target addresses as individual blueprints, concatentates them and then deploys + /// the resulting initcode with the given `_data` appended, i.e. `_data` is the ABI-encoded constructor arguments. + function deployFrom( + address _target1, + address _target2, + bytes32 _salt, + bytes memory _data + ) + internal + returns (address newContract_) + { + Preamble memory preamble1 = parseBlueprintPreamble(address(_target1).code); + if (preamble1.ercVersion != 0) revert UnsupportedERCVersion(preamble1.ercVersion); + if (preamble1.preambleData.length != 0) revert UnexpectedPreambleData(preamble1.preambleData); + + Preamble memory preamble2 = parseBlueprintPreamble(address(_target2).code); + if (preamble2.ercVersion != 0) revert UnsupportedERCVersion(preamble2.ercVersion); + if (preamble2.preambleData.length != 0) revert UnexpectedPreambleData(preamble2.preambleData); + + bytes memory initcode = bytes.concat(preamble1.initcode, preamble2.initcode, _data); + assembly ("memory-safe") { + newContract_ := create2(0, add(initcode, 0x20), mload(initcode), _salt) + } + if (newContract_ == address(0)) revert DeploymentFailed(); + } + /// @notice Convert a bytes array to a uint256. function bytesToUint(bytes memory _b) internal pure returns (uint256) { if (_b.length > 32) revert BytesArrayTooLong(); diff --git a/packages/contracts-bedrock/test/libraries/Blueprint.t.sol b/packages/contracts-bedrock/test/libraries/Blueprint.t.sol index 94a30ab99fb0..c94616a88e4f 100644 --- a/packages/contracts-bedrock/test/libraries/Blueprint.t.sol +++ b/packages/contracts-bedrock/test/libraries/Blueprint.t.sol @@ -22,7 +22,7 @@ contract BlueprintHarness { return Blueprint.blueprintDeployerBytecode(_initcode); } - function parseBlueprintPreamble(bytes memory _bytecode) public pure returns (Blueprint.Preamble memory) { + function parseBlueprintPreamble(bytes memory _bytecode) public view returns (Blueprint.Preamble memory) { return Blueprint.parseBlueprintPreamble(_bytecode); }