diff --git a/op-deployer/cmd/op-deployer/main.go b/op-deployer/cmd/op-deployer/main.go index 2560d6a0c025c..633f27636620f 100644 --- a/op-deployer/cmd/op-deployer/main.go +++ b/op-deployer/cmd/op-deployer/main.go @@ -11,6 +11,7 @@ import ( "github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer" "github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/bootstrap" "github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/inspect" + "github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/manage" "github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/version" opservice "github.com/ethereum-optimism/optimism/op-service" @@ -73,6 +74,11 @@ func main() { Flags: cliapp.ProtectFlags(deployer.VerifyFlags), Action: verify.VerifyCLI, }, + { + Name: "manage", + Usage: "manages the chain", + Subcommands: manage.Commands, + }, } app.Writer = os.Stdout app.ErrWriter = os.Stderr diff --git a/op-deployer/pkg/deployer/manage/add_game_type.go b/op-deployer/pkg/deployer/manage/add_game_type.go new file mode 100644 index 0000000000000..ced4a536cdf39 --- /dev/null +++ b/op-deployer/pkg/deployer/manage/add_game_type.go @@ -0,0 +1,143 @@ +package manage + +import ( + "context" + "encoding/json" + "fmt" + "os" + + "github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer" + "github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/artifacts" + "github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/broadcaster" + "github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/opcm" + "github.com/ethereum-optimism/optimism/op-deployer/pkg/env" + "github.com/ethereum-optimism/optimism/op-service/ctxinterrupt" + oplog "github.com/ethereum-optimism/optimism/op-service/log" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/rpc" + "github.com/urfave/cli/v2" +) + +type AddGameTypeConfig struct { + L1RPCUrl string + Logger log.Logger + ArtifactsLocator *artifacts.Locator + Input opcm.AddGameTypeInput + CacheDir string +} + +func (c *AddGameTypeConfig) Check() error { + if c.L1RPCUrl == "" { + return fmt.Errorf("l1RPCUrl must be specified") + } + + if c.Logger == nil { + return fmt.Errorf("logger must be specified") + } + + if c.ArtifactsLocator == nil { + return fmt.Errorf("artifacts locator must be specified") + } + + return nil +} + +func AddGameTypeCLI(cliCtx *cli.Context) error { + logCfg := oplog.ReadCLIConfig(cliCtx) + l := oplog.NewLogger(oplog.AppOut(cliCtx), logCfg) + oplog.SetGlobalLogHandler(l.Handler()) + + l1RPCUrl := cliCtx.String(deployer.L1RPCURLFlagName) + configFile := cliCtx.String(ConfigFlag.Name) + artifactsLocatorStr := cliCtx.String(deployer.ArtifactsLocatorFlag.Name) + cacheDir := cliCtx.String(deployer.CacheDirFlag.Name) + + artifactsLocator := new(artifacts.Locator) + if err := artifactsLocator.UnmarshalText([]byte(artifactsLocatorStr)); err != nil { + return fmt.Errorf("failed to parse artifacts locator: %w", err) + } + + // Read the input configuration from file + configData, err := os.ReadFile(configFile) + if err != nil { + return fmt.Errorf("failed to read config file: %w", err) + } + + var input opcm.AddGameTypeInput + if err := json.Unmarshal(configData, &input); err != nil { + return fmt.Errorf("failed to parse config file: %w", err) + } + + ctx := ctxinterrupt.WithCancelOnInterrupt(cliCtx.Context) + + _, calldata, err := AddGameType(ctx, AddGameTypeConfig{ + L1RPCUrl: l1RPCUrl, + Logger: l, + ArtifactsLocator: artifactsLocator, + Input: input, + CacheDir: cacheDir, + }) + if err != nil { + return fmt.Errorf("failed to add game type: %w", err) + } + + enc := json.NewEncoder(os.Stdout) + enc.SetIndent("", " ") + if err := enc.Encode(calldata); err != nil { + return fmt.Errorf("failed to encode calldata: %w", err) + } + + return nil +} + +func AddGameType(ctx context.Context, cfg AddGameTypeConfig) (opcm.AddGameTypeOutput, []broadcaster.CalldataDump, error) { + var output opcm.AddGameTypeOutput + if err := cfg.Check(); err != nil { + return output, nil, fmt.Errorf("invalid config for AddGameType: %w", err) + } + + lgr := cfg.Logger + + artifactsFS, err := artifacts.Download(ctx, cfg.ArtifactsLocator, artifacts.BarProgressor(), cfg.CacheDir) + if err != nil { + return output, nil, fmt.Errorf("failed to download artifacts: %w", err) + } + + bcaster := new(broadcaster.CalldataBroadcaster) + + l1RPC, err := rpc.Dial(cfg.L1RPCUrl) + if err != nil { + return output, nil, fmt.Errorf("failed to connect to L1 RPC: %w", err) + } + + l1Host, err := env.DefaultForkedScriptHost( + ctx, + bcaster, + lgr, + common.Address{'D'}, + artifactsFS, + l1RPC, + ) + if err != nil { + return output, nil, fmt.Errorf("failed to create script host: %w", err) + } + + script, err := opcm.NewAddGameTypeScript(l1Host) + if err != nil { + return output, nil, fmt.Errorf("failed to create L2 genesis script: %w", err) + } + + output, err = script.Run(cfg.Input) + if err != nil { + return output, nil, fmt.Errorf("error adding game type: %w", err) + } + + // Get the calldata + calldata, err := bcaster.Dump() + if err != nil { + return output, nil, fmt.Errorf("failed to get calldata: %w", err) + } + + return output, calldata, nil +} diff --git a/op-deployer/pkg/deployer/manage/add_game_type_test.go b/op-deployer/pkg/deployer/manage/add_game_type_test.go new file mode 100644 index 0000000000000..4bf8e47b0ef37 --- /dev/null +++ b/op-deployer/pkg/deployer/manage/add_game_type_test.go @@ -0,0 +1,72 @@ +package manage + +import ( + "context" + "log/slog" + "math/big" + "os" + "testing" + "time" + + "github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/standard" + "github.com/ethereum/go-ethereum/superchain" + + "github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/opcm" + "github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/testutil" + "github.com/ethereum-optimism/optimism/op-service/testlog" + "github.com/ethereum-optimism/optimism/op-service/testutils" + "github.com/ethereum-optimism/superchain-registry/validation" + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/require" +) + +func TestAddGameType(t *testing.T) { + rpcURL := os.Getenv("SEPOLIA_RPC_URL") + require.NotEmpty(t, rpcURL, "must specify RPC url via SEPOLIA_RPC_URL env var") + + afacts, _ := testutil.LocalArtifacts(t) + v200SepoliaAddrs := validation.StandardVersionsSepolia[standard.ContractsV200Tag] + testCacheDir := testutils.IsolatedTestDirWithAutoCleanup(t) + + supChain, err := superchain.GetChain(11155420) + require.NoError(t, err) + supChainConfig, err := supChain.Config() + require.NoError(t, err) + + cfg := AddGameTypeConfig{ + L1RPCUrl: rpcURL, + Logger: testlog.Logger(t, slog.LevelInfo), + ArtifactsLocator: afacts, + Input: opcm.AddGameTypeInput{ + SaltMixer: "foo", + // The values below were pulled from the Superchain Registry for OP Sepolia. + SystemConfigProxy: *supChainConfig.Addresses.SystemConfigProxy, + OPChainProxyAdmin: *supChainConfig.Addresses.ProxyAdmin, + DelayedWETHProxy: *supChainConfig.Addresses.DelayedWETHProxy, + DisputeGameType: 999, + DisputeAbsolutePrestate: common.HexToHash("0x1234"), + DisputeMaxGameDepth: big.NewInt(73), + DisputeSplitDepth: big.NewInt(30), + DisputeClockExtension: 10800, + DisputeMaxClockDuration: 302400, + InitialBond: big.NewInt(0), + VM: common.Address(*v200SepoliaAddrs.Mips.Address), + Permissioned: false, + Prank: *supChainConfig.Roles.ProxyAdminOwner, + OPCMImpl: common.Address(*v200SepoliaAddrs.OPContractsManager.Address), + }, + CacheDir: testCacheDir, + } + + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + output, broadcasts, err := AddGameType(ctx, cfg) + require.NoError(t, err) + + require.Equal(t, 1, len(broadcasts)) + // Selector for addGameType + require.EqualValues(t, []byte{0x16, 0x61, 0xa2, 0xe9}, broadcasts[0].Data[0:4]) + + require.NotEqual(t, common.Address{}, output.DelayedWETHProxy) + require.NotEqual(t, common.Address{}, output.FaultDisputeGameProxy) +} diff --git a/op-deployer/pkg/deployer/manage/flags.go b/op-deployer/pkg/deployer/manage/flags.go new file mode 100644 index 0000000000000..bd6115d6923bc --- /dev/null +++ b/op-deployer/pkg/deployer/manage/flags.go @@ -0,0 +1,27 @@ +package manage + +import ( + "github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer" + oplog "github.com/ethereum-optimism/optimism/op-service/log" + "github.com/urfave/cli/v2" +) + +var ( + ConfigFlag = &cli.StringFlag{ + Name: "config", + Usage: "path to the config file", + } +) + +var Commands = cli.Commands{ + &cli.Command{ + Name: "add-game-type", + Usage: "adds a new game type to the chain", + Flags: append([]cli.Flag{ + deployer.L1RPCURLFlag, + deployer.ArtifactsLocatorFlag, + ConfigFlag, + }, oplog.CLIFlags(deployer.EnvVarPrefix)...), + Action: AddGameTypeCLI, + }, +} diff --git a/op-deployer/pkg/deployer/opcm/add_game_type.go b/op-deployer/pkg/deployer/opcm/add_game_type.go new file mode 100644 index 0000000000000..96d25d7a172bb --- /dev/null +++ b/op-deployer/pkg/deployer/opcm/add_game_type.go @@ -0,0 +1,124 @@ +package opcm + +import ( + "encoding/json" + "math/big" + + "github.com/ethereum-optimism/optimism/op-chain-ops/script" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" +) + +type AddGameTypeInput struct { + Prank common.Address + OPCMImpl common.Address `abi:"opcmImpl"` + SystemConfigProxy common.Address + OPChainProxyAdmin common.Address `abi:"opChainProxyAdmin"` + DelayedWETHProxy common.Address + DisputeGameType uint32 + DisputeAbsolutePrestate common.Hash + DisputeMaxGameDepth *big.Int + DisputeSplitDepth *big.Int + DisputeClockExtension uint64 + DisputeMaxClockDuration uint64 + InitialBond *big.Int + VM common.Address `abi:"vm"` + Permissioned bool + SaltMixer string +} + +type addGameTypeInputJSON struct { + Prank common.Address `json:"prank"` + OPCMImpl common.Address `json:"opcmimpl"` + SystemConfigProxy common.Address `json:"systemConfigProxy"` + OPChainProxyAdmin common.Address `json:"opChainProxyAdmin"` + DelayedWETHProxy common.Address `json:"delayedWETHProxy"` + DisputeGameType uint32 `json:"disputeGameType"` + DisputeAbsolutePrestate common.Hash `json:"disputeAbsolutePrestate"` + DisputeMaxGameDepth *hexutil.Big `json:"disputeMaxGameDepth"` + DisputeSplitDepth *hexutil.Big `json:"disputeSplitDepth"` + DisputeClockExtension uint64 `json:"disputeClockExtension"` + DisputeMaxClockDuration uint64 `json:"disputeMaxClockDuration"` + InitialBond *hexutil.Big `json:"initialBond"` + VM common.Address `json:"vm"` + Permissioned bool `json:"permissioned"` + SaltMixer string `json:"saltMixer"` +} + +func (a *AddGameTypeInput) UnmarshalJSON(b []byte) error { + var alias addGameTypeInputJSON + if err := json.Unmarshal(b, &alias); err != nil { + return err + } + + a.Prank = alias.Prank + a.OPCMImpl = alias.OPCMImpl + a.SystemConfigProxy = alias.SystemConfigProxy + a.OPChainProxyAdmin = alias.OPChainProxyAdmin + a.DelayedWETHProxy = alias.DelayedWETHProxy + a.DisputeGameType = alias.DisputeGameType + a.DisputeAbsolutePrestate = alias.DisputeAbsolutePrestate + + if alias.DisputeMaxGameDepth != nil { + a.DisputeMaxGameDepth = (*big.Int)(alias.DisputeMaxGameDepth) + } + + if alias.DisputeSplitDepth != nil { + a.DisputeSplitDepth = (*big.Int)(alias.DisputeSplitDepth) + } + + a.DisputeClockExtension = alias.DisputeClockExtension + a.DisputeMaxClockDuration = alias.DisputeMaxClockDuration + + if alias.InitialBond != nil { + a.InitialBond = (*big.Int)(alias.InitialBond) + } + + a.VM = alias.VM + a.Permissioned = alias.Permissioned + a.SaltMixer = alias.SaltMixer + + return nil +} + +func (a AddGameTypeInput) MarshalJSON() ([]byte, error) { + alias := addGameTypeInputJSON{ + Prank: a.Prank, + OPCMImpl: a.OPCMImpl, + SystemConfigProxy: a.SystemConfigProxy, + OPChainProxyAdmin: a.OPChainProxyAdmin, + DelayedWETHProxy: a.DelayedWETHProxy, + DisputeGameType: a.DisputeGameType, + DisputeAbsolutePrestate: a.DisputeAbsolutePrestate, + DisputeClockExtension: a.DisputeClockExtension, + DisputeMaxClockDuration: a.DisputeMaxClockDuration, + VM: a.VM, + Permissioned: a.Permissioned, + SaltMixer: a.SaltMixer, + } + + if a.DisputeMaxGameDepth != nil { + alias.DisputeMaxGameDepth = (*hexutil.Big)(a.DisputeMaxGameDepth) + } + + if a.DisputeSplitDepth != nil { + alias.DisputeSplitDepth = (*hexutil.Big)(a.DisputeSplitDepth) + } + + if a.InitialBond != nil { + alias.InitialBond = (*hexutil.Big)(a.InitialBond) + } + + return json.Marshal(alias) +} + +type AddGameTypeOutput struct { + DelayedWETHProxy common.Address `json:"delayedWETHProxy"` + FaultDisputeGameProxy common.Address `json:"faultDisputeGameProxy"` +} + +type AddGameTypeScript script.DeployScriptWithOutput[AddGameTypeInput, AddGameTypeOutput] + +func NewAddGameTypeScript(host *script.Host) (AddGameTypeScript, error) { + return script.NewDeployScriptWithOutputFromFile[AddGameTypeInput, AddGameTypeOutput](host, "AddGameType.s.sol", "AddGameType") +} diff --git a/op-deployer/pkg/deployer/opcm/add_game_type_test.go b/op-deployer/pkg/deployer/opcm/add_game_type_test.go new file mode 100644 index 0000000000000..c22d90274e6c6 --- /dev/null +++ b/op-deployer/pkg/deployer/opcm/add_game_type_test.go @@ -0,0 +1,110 @@ +package opcm + +import ( + "encoding/json" + "math/big" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/require" +) + +func compareBigInt(a, b *big.Int) bool { + if a == nil && b == nil { + return true + } + if a == nil || b == nil { + return false + } + return a.Cmp(b) == 0 +} + +// compareAddGameTypeInputs compares two AddGameTypeInput structs with special handling for *big.Int fields. We can't +// use require.Equal directly because zero *big.Int structs can either be nil or a zero value, which trips up the +// equality checker. +func compareAddGameTypeInputs(t *testing.T, expected, actual AddGameTypeInput) { + require.Equal(t, expected.Prank, actual.Prank) + require.Equal(t, expected.OPCMImpl, actual.OPCMImpl) + require.Equal(t, expected.SystemConfigProxy, actual.SystemConfigProxy) + require.Equal(t, expected.OPChainProxyAdmin, actual.OPChainProxyAdmin) + require.Equal(t, expected.DelayedWETHProxy, actual.DelayedWETHProxy) + require.Equal(t, expected.DisputeGameType, actual.DisputeGameType) + require.Equal(t, expected.DisputeAbsolutePrestate, actual.DisputeAbsolutePrestate) + require.Equal(t, expected.DisputeClockExtension, actual.DisputeClockExtension) + require.Equal(t, expected.DisputeMaxClockDuration, actual.DisputeMaxClockDuration) + require.Equal(t, expected.VM, actual.VM) + require.Equal(t, expected.Permissioned, actual.Permissioned) + require.Equal(t, expected.SaltMixer, actual.SaltMixer) + + // Special handling for *big.Int fields + require.True(t, compareBigInt(expected.DisputeMaxGameDepth, actual.DisputeMaxGameDepth)) + require.True(t, compareBigInt(expected.DisputeSplitDepth, actual.DisputeSplitDepth)) + require.True(t, compareBigInt(expected.InitialBond, actual.InitialBond)) +} + +func TestAddGameTypeInput_MarshalUnmarshalJSON(t *testing.T) { + // Define test cases + testCases := []struct { + name string + input AddGameTypeInput + }{ + { + name: "basic", + input: AddGameTypeInput{ + Prank: common.HexToAddress("0x1111111111111111111111111111111111111111"), + OPCMImpl: common.HexToAddress("0x2222222222222222222222222222222222222222"), + SystemConfigProxy: common.HexToAddress("0x3333333333333333333333333333333333333333"), + OPChainProxyAdmin: common.HexToAddress("0x4444444444444444444444444444444444444444"), + DelayedWETHProxy: common.HexToAddress("0x5555555555555555555555555555555555555555"), + DisputeGameType: 1, + DisputeAbsolutePrestate: common.HexToHash("0x6666666666666666666666666666666666666666666666666666666666666666"), + DisputeMaxGameDepth: big.NewInt(100), + DisputeSplitDepth: big.NewInt(10), + DisputeClockExtension: 1000, + DisputeMaxClockDuration: 2000, + InitialBond: big.NewInt(5000000000000000000), // 5 ETH + VM: common.HexToAddress("0x7777777777777777777777777777777777777777"), + Permissioned: true, + SaltMixer: "salt_mixer_value", + }, + }, + { + name: "nil big.Int fields", + input: AddGameTypeInput{ + Prank: common.HexToAddress("0x1111111111111111111111111111111111111111"), + OPCMImpl: common.HexToAddress("0x2222222222222222222222222222222222222222"), + SystemConfigProxy: common.HexToAddress("0x3333333333333333333333333333333333333333"), + OPChainProxyAdmin: common.HexToAddress("0x4444444444444444444444444444444444444444"), + DelayedWETHProxy: common.HexToAddress("0x5555555555555555555555555555555555555555"), + DisputeGameType: 1, + DisputeAbsolutePrestate: common.HexToHash("0x6666666666666666666666666666666666666666666666666666666666666666"), + DisputeMaxGameDepth: nil, // nil big.Int + DisputeSplitDepth: nil, // nil big.Int + DisputeClockExtension: 1000, + DisputeMaxClockDuration: 2000, + InitialBond: nil, // nil big.Int + VM: common.HexToAddress("0x7777777777777777777777777777777777777777"), + Permissioned: false, + SaltMixer: "salt_mixer_value", + }, + }, + } + + // Run test cases + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + data, err := json.Marshal(tc.input) + require.NoError(t, err) + + var unmarshaled AddGameTypeInput + err = json.Unmarshal(data, &unmarshaled) + require.NoError(t, err) + + compareAddGameTypeInputs(t, tc.input, unmarshaled) + + newData, err := json.Marshal(unmarshaled) + require.NoError(t, err) + require.JSONEq(t, string(data), string(newData)) + }) + } +} diff --git a/packages/contracts-bedrock/scripts/deploy/AddGameType.s.sol b/packages/contracts-bedrock/scripts/deploy/AddGameType.s.sol new file mode 100644 index 0000000000000..5d65d1111cdcc --- /dev/null +++ b/packages/contracts-bedrock/scripts/deploy/AddGameType.s.sol @@ -0,0 +1,111 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.15; + +// Forge +import { Script } from "forge-std/Script.sol"; + +// Scripts +import { DeployUtils } from "scripts/libraries/DeployUtils.sol"; + +// Interfaces +import { OPContractsManager } from "src/L1/OPContractsManager.sol"; +import { ISystemConfig } from "interfaces/L1/ISystemConfig.sol"; +import { IProxyAdmin } from "interfaces/universal/IProxyAdmin.sol"; +import { IDelayedWETH } from "interfaces/dispute/IDelayedWETH.sol"; +import { IBigStepper } from "interfaces/dispute/IBigStepper.sol"; +import { GameType, Duration, Claim } from "src/dispute/lib/Types.sol"; +import { IFaultDisputeGame } from "interfaces/dispute/IFaultDisputeGame.sol"; + +/// @title AddGameType +contract AddGameType is Script { + struct Input { + // Address that will be used for the DummyCaller contract + address prank; + // OPCM contract address + OPContractsManager opcmImpl; + // SystemConfig contract address + ISystemConfig systemConfigProxy; + // ProxyAdmin contract address + IProxyAdmin opChainProxyAdmin; + // DelayedWETH contract address (optional) + IDelayedWETH delayedWETHProxy; + // Game type to add + GameType disputeGameType; + // Absolute prestate for the game + Claim disputeAbsolutePrestate; + // Maximum game depth + uint256 disputeMaxGameDepth; + // Split depth for the game + uint256 disputeSplitDepth; + // Clock extension duration + Duration disputeClockExtension; + // Maximum clock duration + Duration disputeMaxClockDuration; + // Initial bond amount + uint256 initialBond; + // VM contract address + IBigStepper vm; + // Whether this is a permissioned game + bool permissioned; + // Salt mixer for deterministic addresses + string saltMixer; + } + + struct Output { + IDelayedWETH delayedWETHProxy; + IFaultDisputeGame faultDisputeGameProxy; + } + + function run(Input memory _agi) public returns (Output memory) { + // Create the game input + OPContractsManager.AddGameInput[] memory gameConfigs = new OPContractsManager.AddGameInput[](1); + gameConfigs[0] = OPContractsManager.AddGameInput({ + saltMixer: _agi.saltMixer, + systemConfig: _agi.systemConfigProxy, + proxyAdmin: _agi.opChainProxyAdmin, + delayedWETH: _agi.delayedWETHProxy, + disputeGameType: _agi.disputeGameType, + disputeAbsolutePrestate: _agi.disputeAbsolutePrestate, + disputeMaxGameDepth: _agi.disputeMaxGameDepth, + disputeSplitDepth: _agi.disputeSplitDepth, + disputeClockExtension: _agi.disputeClockExtension, + disputeMaxClockDuration: _agi.disputeMaxClockDuration, + initialBond: _agi.initialBond, + vm: _agi.vm, + permissioned: _agi.permissioned + }); + + // Etch DummyCaller contract + address prank = _agi.prank; + bytes memory code = vm.getDeployedCode("AddGameType.s.sol:DummyCaller"); + vm.etch(prank, code); + vm.store(prank, bytes32(0), bytes32(uint256(uint160(address(_agi.opcmImpl))))); + vm.label(prank, "DummyCaller"); + + // Call into the DummyCaller to perform the delegatecall + vm.broadcast(msg.sender); + (bool success, bytes memory result) = DummyCaller(prank).addGameType(gameConfigs); + require(success, "AddGameType: addGameType failed"); + + // Decode the result and set it in the output + OPContractsManager.AddGameOutput[] memory outputs = abi.decode(result, (OPContractsManager.AddGameOutput[])); + require(outputs.length == 1, "AddGameType: unexpected number of outputs"); + return Output({ delayedWETHProxy: outputs[0].delayedWETH, faultDisputeGameProxy: outputs[0].faultDisputeGame }); + } + + function checkOutput(Output memory _ago) internal view { + DeployUtils.assertValidContractAddress(address(_ago.delayedWETHProxy)); + DeployUtils.assertValidContractAddress(address(_ago.faultDisputeGameProxy)); + } +} + +/// @title DummyCaller +contract DummyCaller { + address internal _opcmAddr; + + function addGameType(OPContractsManager.AddGameInput[] memory _gameConfigs) external returns (bool, bytes memory) { + bytes memory data = abi.encodeCall(DummyCaller.addGameType, _gameConfigs); + (bool success, bytes memory result) = _opcmAddr.delegatecall(data); + return (success, result); + } +}