diff --git a/foundry.toml b/foundry.toml index 5ceefe9fd1..e870d239d5 100644 --- a/foundry.toml +++ b/foundry.toml @@ -14,6 +14,7 @@ remappings = [ '@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts', '@base-contracts/=lib/base-contracts/', '@eth-optimism-bedrock/=lib/optimism/packages/contracts-bedrock/', + '@op/=lib/optimism/packages/contracts-bedrock/', '@rari-capital/solmate/=lib/solmate', '@eth-optimism-superchain-registry/=lib/superchain-registry/', '@solady/=lib/optimism/packages/contracts-bedrock/lib/solady/src/', diff --git a/src/improvements/template/AddGameTypeTemplate.sol b/src/improvements/template/AddGameTypeTemplate.sol new file mode 100644 index 0000000000..a8fcab0392 --- /dev/null +++ b/src/improvements/template/AddGameTypeTemplate.sol @@ -0,0 +1,144 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.15; + +import {VmSafe} from "forge-std/Vm.sol"; +import {stdToml} from "forge-std/StdToml.sol"; + +import {OPCMTaskBase} from "src/improvements/tasks/types/OPCMTaskBase.sol"; +import {SuperchainAddressRegistry} from "src/improvements/SuperchainAddressRegistry.sol"; + +import {IDeputyGuardianModule, IOptimismPortal2} from "@op/interfaces/safe/IDeputyGuardianModule.sol"; +import {GameType, Claim, Duration} from "@op/src/dispute/lib/Types.sol"; +import { + IOPContractsManager, + IDisputeGameFactory, + IFaultDisputeGame, + IBigStepper, + IProxyAdmin, + IDelayedWETH, + ISystemConfig +} from "@op/interfaces/L1/IOPContractsManager.sol"; + +/// @title AddGameTypeTemplate +/// @notice This template is used to add a game type to the DisputeGameFactory contract. +contract AddGameTypeTemplate is OPCMTaskBase { + using stdToml for string; + + /// @notice Struct that extends the original AddGameInput struct and includes the chain id. + /// Notably the fields here are also in alphabetical order, this is required because of + /// the way that Foundry parses TOML data. This MUST be kept in alphabetical order. If + /// you are adding a new field, you MUST make sure it's in order. Seriously. + struct AddGameInputWithChainId { + uint256 chainId; + IDelayedWETH delayedWETH; + Claim disputeAbsolutePrestate; + Duration disputeClockExtension; + GameType disputeGameType; + Duration disputeMaxClockDuration; + uint256 disputeMaxGameDepth; + uint256 disputeSplitDepth; + uint256 initialBond; + bool permissioned; + IProxyAdmin proxyAdmin; + string saltMixer; + ISystemConfig systemConfig; + IBigStepper vm; + } + + /// @notice Mapping of chain ID to configuration for the task. + mapping(uint256 => AddGameInputWithChainId) private cfg; + + /// @notice Returns string identifiers for addresses that are expected to have their storage written to. + function _taskStorageWrites() internal pure override returns (string[] memory) { + string[] memory storageWrites = new string[](3); + storageWrites[0] = "ProxyAdminOwner"; + storageWrites[1] = "OPCM"; + storageWrites[2] = "DisputeGameFactoryProxy"; + return storageWrites; + } + + /// @notice Sets up the template with implementation configurations from a TOML file. + function _templateSetup(string memory taskConfigFilePath) internal override { + super._templateSetup(taskConfigFilePath); + string memory tomlContent = vm.readFile(taskConfigFilePath); + + // Load configuration. + AddGameInputWithChainId[] memory configs = + abi.decode(tomlContent.parseRaw(".configs"), (AddGameInputWithChainId[])); + for (uint256 i = 0; i < configs.length; i++) { + cfg[configs[i].chainId] = configs[i]; + } + + // Load OPCM address. + OPCM = tomlContent.readAddress(".addresses.OPCM"); + vm.label(OPCM, "OPCM"); + } + + /// @notice Write the calls that you want to execute for the task. + function _build() internal override { + // Iterate over the chains pull out the configs. + SuperchainAddressRegistry.ChainInfo[] memory chains = superchainAddrRegistry.getChains(); + IOPContractsManager.AddGameInput[] memory configs = new IOPContractsManager.AddGameInput[](chains.length); + for (uint256 i = 0; i < chains.length; i++) { + uint256 chainId = chains[i].chainId; + configs[i] = _toAddGameInput(cfg[chainId]); + } + + // Delegatecall the OPCM.addGameType() function. + (bool success,) = OPCM.delegatecall(abi.encodeCall(IOPContractsManager.addGameType, configs)); + require(success, "AddGameType: failed to add game type"); + } + + /// @notice This method performs all validations and assertions that verify the calls executed as expected. + function _validate(VmSafe.AccountAccess[] memory, Action[] memory) internal view override { + // Iterate over the chains and validate the respected game type. + SuperchainAddressRegistry.ChainInfo[] memory chains = superchainAddrRegistry.getChains(); + for (uint256 i = 0; i < chains.length; i++) { + uint256 chainId = chains[i].chainId; + address factoryAddress = superchainAddrRegistry.getAddress("DisputeGameFactoryProxy", chainId); + IDisputeGameFactory factory = IDisputeGameFactory(factoryAddress); + IFaultDisputeGame game = IFaultDisputeGame(address(factory.gameImpls(cfg[chainId].disputeGameType))); + + // Assert that everything is as expected. + assertEq(address(game.weth()), address(cfg[chainId].delayedWETH)); + assertEq(game.gameType().raw(), cfg[chainId].disputeGameType.raw()); + assertEq(game.absolutePrestate().raw(), cfg[chainId].disputeAbsolutePrestate.raw()); + assertEq(game.maxGameDepth(), cfg[chainId].disputeMaxGameDepth); + assertEq(game.splitDepth(), cfg[chainId].disputeSplitDepth); + assertEq(game.clockExtension().raw(), cfg[chainId].disputeClockExtension.raw()); + assertEq(game.maxClockDuration().raw(), cfg[chainId].disputeMaxClockDuration.raw()); + + // Assert that the bond is set correctly. + assertEq(factory.initBonds(cfg[chainId].disputeGameType), cfg[chainId].initialBond); + } + } + + /// @notice Override to return a list of addresses that should not be checked for code length. + function getCodeExceptions() internal pure override returns (address[] memory) { + address[] memory codeExceptions = new address[](0); + return codeExceptions; + } + + /// @notice Converts the AddGameInputWithChainId struct to the AddGameInput struct. + function _toAddGameInput(AddGameInputWithChainId memory _input) + internal + pure + returns (IOPContractsManager.AddGameInput memory) + { + return IOPContractsManager.AddGameInput({ + saltMixer: _input.saltMixer, + systemConfig: _input.systemConfig, + proxyAdmin: _input.proxyAdmin, + delayedWETH: _input.delayedWETH, + disputeGameType: _input.disputeGameType, + disputeAbsolutePrestate: _input.disputeAbsolutePrestate, + disputeMaxGameDepth: _input.disputeMaxGameDepth, + disputeSplitDepth: _input.disputeSplitDepth, + disputeClockExtension: _input.disputeClockExtension, + disputeMaxClockDuration: _input.disputeMaxClockDuration, + initialBond: _input.initialBond, + vm: _input.vm, + permissioned: _input.permissioned + }); + } +} diff --git a/test/tasks/Regression.t.sol b/test/tasks/Regression.t.sol index a9152400df..31dd7ebff5 100644 --- a/test/tasks/Regression.t.sol +++ b/test/tasks/Regression.t.sol @@ -23,6 +23,7 @@ import {TransferL2PAOFromL1} from "src/improvements/template/TransferL2PAOFromL1 import {DisableModule} from "src/improvements/template/DisableModule.sol"; import {Action} from "src/libraries/MultisigTypes.sol"; import {GnosisSafeApproveHash} from "src/improvements/template/GnosisSafeApproveHash.sol"; +import {AddGameTypeTemplate} from "src/improvements/template/AddGameTypeTemplate.sol"; /// @notice Ensures that simulating the task consistently produces the same call data and data to sign. /// This guarantees determinism—if a bug is introduced in the task logic, the call data or data to sign @@ -364,6 +365,34 @@ contract RegressionTest is Test { _assertDataToSignNestedMultisig(multisigTask, actions, expectedDataToSign); } + /// @notice Expected call data and data to sign generated by manually running the AddGameType template at block 8383837 on Sepolia. + /// Simulate from task directory (test/tasks/example/sep/012-add-game-type) with: + /// SIMULATE_WITHOUT_LEDGER=1 just --dotenv-path $(pwd)/.env --justfile ../../../../../src/improvements/single.just simulate + function testRegressionCalldataMatches_AddGameType() public { + string memory taskConfigFilePath = "test/tasks/example/sep/012-add-game-type/config.toml"; + + // Call data generated by manually running the AddGameType template at block 8383837 on Sepolia. + string memory expectedCallData = + "0x82ad56cb000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000020000000000000000000000000fbceed4de885645fbded164910e10f52febfab350000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000002441661a2e900000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000001a0000000000000000000000000034edd2a225f7f429a63e0f1d2084b9e0a93b538000000000000000000000000189abaaaa82dfc015a588a7dbad6f13b1d3485bc000000000000000000000000cdfdc692a53b4ae9f81e0aebd26107da4a71db84000000000000000000000000000000000000000000000000000000000000000003682932cec7ce0a3874b19675a6bbc923054a7b321efc7d3835187b172494b60000000000000000000000000000000000000000000000000000000000000049000000000000000000000000000000000000000000000000000000000000001e0000000000000000000000000000000000000000000000000000000000002a300000000000000000000000000000000000000000000000000000000000049d40000000000000000000000000000000000000000000000000011c37937e080000000000000000000000000000f027f4a985560fb13324e943edf55ad6f1d15dc1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000147468697320697320612073616c74206d6978657200000000000000000000000000000000000000000000000000000000000000000000000000000000"; + + MultisigTask multisigTask = new AddGameTypeTemplate(); + address foundationChildMultisig = 0xDEe57160aAfCF04c34C887B5962D0a69676d3C8B; + MultisigTask.Action[] memory actions = + _setupAndSimulateRun(taskConfigFilePath, 8383837, "sepolia", multisigTask, foundationChildMultisig); + + _assertCallData(multisigTask, actions, expectedCallData); + + // Data to sign generated by manually running the AddGameType template at block 8383837 on Sepolia. + string[] memory expectedDataToSign = new string[](2); + // Foundation + expectedDataToSign[0] = + "0x190137e1f5dd3b92a004a23589b741196c8a214629d4ea3a690ec8e41ae45c689cbb195e8071f7d2fcb4120c11aec197007f1b41900d82f62741d1a58c134f350549"; + // Security council + expectedDataToSign[1] = + "0x1901be081970e9fc104bd1ea27e375cd21ec7bb1eec56bfe43347c3e36c5d27b853304e459124b117bdf49183bbb2c639b7fde05b9ec9d15f554c8f1837ade2ad463"; + _assertDataToSignNestedMultisig(multisigTask, actions, expectedDataToSign); + } + /// @notice Expected call data and data to sign generated by manually running the TransferL2PAOFromL1 template at block 22447773 on mainnet. /// Simulate from task directory (test/tasks/example/eth/008-transfer-l2pao) with: /// SIMULATE_WITHOUT_LEDGER=1 just --dotenv-path $(pwd)/.env --justfile ../../../../../src/improvements/nested.just simulate diff --git a/test/tasks/example/sep/012-add-game-type/.env b/test/tasks/example/sep/012-add-game-type/.env new file mode 100644 index 0000000000..1bfe2fa7fa --- /dev/null +++ b/test/tasks/example/sep/012-add-game-type/.env @@ -0,0 +1 @@ +FORK_BLOCK_NUMBER=8383837 diff --git a/test/tasks/example/sep/012-add-game-type/config.toml b/test/tasks/example/sep/012-add-game-type/config.toml new file mode 100644 index 0000000000..a7b02a7857 --- /dev/null +++ b/test/tasks/example/sep/012-add-game-type/config.toml @@ -0,0 +1,24 @@ +templateName = "AddGameTypeTemplate" + +l2chains = [ + {name = "OP Sepolia Testnet", chainId = 11155420}, +] + +[[configs]] +chainId = 11155420 +saltMixer = "this is a salt mixer" +systemConfig = "0x034edD2A225f7f429A63E0f1D2084B9E0A93b538" +proxyAdmin = "0x189aBAAaa82DfC015A588A7dbaD6F13b1D3485Bc" +delayedWETH = "0xcdFdC692a53B4aE9F81E0aEBd26107Da4a71dB84" +disputeGameType = 0 +disputeAbsolutePrestate = "0x03682932cec7ce0a3874b19675a6bbc923054a7b321efc7d3835187b172494b6" +disputeMaxGameDepth = 73 +disputeSplitDepth = 30 +disputeClockExtension = 10800 +disputeMaxClockDuration = 302400 +initialBond = 80000000000000000 +vm = "0xF027F4A985560fb13324e943edf55ad6F1d15Dc1" +permissioned = false + +[addresses] +OPCM = "0xfbceed4de885645fbded164910e10f52febfab35" diff --git a/test/tasks/mock/configs/AddGameTypeTemplate.toml b/test/tasks/mock/configs/AddGameTypeTemplate.toml new file mode 100644 index 0000000000..a7b02a7857 --- /dev/null +++ b/test/tasks/mock/configs/AddGameTypeTemplate.toml @@ -0,0 +1,24 @@ +templateName = "AddGameTypeTemplate" + +l2chains = [ + {name = "OP Sepolia Testnet", chainId = 11155420}, +] + +[[configs]] +chainId = 11155420 +saltMixer = "this is a salt mixer" +systemConfig = "0x034edD2A225f7f429A63E0f1D2084B9E0A93b538" +proxyAdmin = "0x189aBAAaa82DfC015A588A7dbaD6F13b1D3485Bc" +delayedWETH = "0xcdFdC692a53B4aE9F81E0aEBd26107Da4a71dB84" +disputeGameType = 0 +disputeAbsolutePrestate = "0x03682932cec7ce0a3874b19675a6bbc923054a7b321efc7d3835187b172494b6" +disputeMaxGameDepth = 73 +disputeSplitDepth = 30 +disputeClockExtension = 10800 +disputeMaxClockDuration = 302400 +initialBond = 80000000000000000 +vm = "0xF027F4A985560fb13324e943edf55ad6F1d15Dc1" +permissioned = false + +[addresses] +OPCM = "0xfbceed4de885645fbded164910e10f52febfab35"