diff --git a/src/ci.just b/src/ci.just index ba5041ea6e..c73b6312c8 100644 --- a/src/ci.just +++ b/src/ci.just @@ -53,6 +53,15 @@ simulate-all-templates: task_name=$(basename "$task") task_path="$root_dir/test/tasks/example/$network/$task_name" + # Skip tasks that exceed the 15M gas limit + # 023-u13-to-u16a: OPCMUpgradeV220toV410 performs 4 sequential upgrades (U13 through U16a) + # consuming ~21.7M gas, which exceeds the 15M safety limit for Fusaka EIP-7825 compatibility. + # This template is not actively used and can be skipped in CI. + if [ "$task_name" = "023-u13-to-u16a" ]; then + echo "Skipping $task_name (exceeds 15M gas limit)" + continue + fi + # Launch each simulation in background. "${root_dir}/src/script/simulate-task.sh" "$task_path" "$nested_safe_name_depth_1" "$nested_safe_name_depth_2" "$network" & pids+=( "$!" ) current_batch=$((current_batch + 1)) @@ -65,7 +74,7 @@ simulate-all-templates: wait "$pid" simulation_count=$((simulation_count + 1)) done - + # Reset for next batch pids=() current_batch=0 diff --git a/src/doc/simulate-l2-ownership-transfer.md b/src/doc/simulate-l2-ownership-transfer.md index 7de7429fb9..c5d5028e86 100644 --- a/src/doc/simulate-l2-ownership-transfer.md +++ b/src/doc/simulate-l2-ownership-transfer.md @@ -1,6 +1,7 @@ # Simulating an L2 Deposit Transaction Before Execution -The following steps describe how to simulate an L2 deposit transaction prior to L1 task execution. The [TransferL2PAOFromL1.sol](../template/TransferL2PAOFromL1.sol) template executes an L1 transaction, which is later forwarded to the L2 by the op-node. To gain additional confidence that the L2 deposit transaction works as expected, we manually simulate it and record the results in the task’s VALIDATION.md file. +The following steps describe how to simulate an L2 deposit transaction prior to L1 task execution. The [TransferL2PAOFromL1.sol](../template/TransferL2PAOFromL1.sol) transfers the ownership of a L2PAO from a current aliased L1 address to a new aliased L1 address. +This template executes an L1 transaction, which is later forwarded to the L2 by the op-node. To gain additional confidence that the L2 deposit transaction works as expected, we manually simulate it and record the results in the task’s VALIDATION.md file. ## Steps @@ -32,7 +33,7 @@ The following steps describe how to simulate an L2 deposit transaction prior to ``` We must put `0xf2fde38b0000000000000000000000006b1bae59d09fccbddb6c6cceb07b7279367c4e3b` in the `Enter raw input data` field. ![Enter Raw Input Data](./images/tenderly-raw-input-data.png) -8. Double-check that the address returned from the `cast calldata-decode` step matches the aliased L1 ProxyAdmin owner (L1PAO) for the target chain. In this case, the L1PAO is `0x5a0Aae59D09fccBdDb6C6CcEB07B7279367C3d2A`. Confirm this by manually unaliasing the address using [chisel](https://book.getfoundry.sh/chisel/). +8. Double-check that the address returned from the `cast calldata-decode` step matches the aliased new L1 ProxyAdmin owner (L1PAO) for the target chain. In this case, the L1PAO is `0x5a0Aae59D09fccBdDb6C6CcEB07B7279367C3d2A`. Confirm this by manually unaliasing the address using [chisel](https://book.getfoundry.sh/chisel/). ```bash > uint160 constant offset = uint160(0x1111000000000000000000000000000000001111) > function undoL1ToL2Alias(address l2Address) internal pure returns (address l1Address) { @@ -43,7 +44,7 @@ The following steps describe how to simulate an L2 deposit transaction prior to > undoL1ToL2Alias(0x6B1BAE59D09fCcbdDB6C6cceb07B7279367C4E3b) # returns: 0x5a0Aae59D09fccBdDb6C6CcEB07B7279367C3d2A ``` -9. Next we need to fill out the `Transaction Parameters` section on the right of the UI. Specifically, fill out the `From` address and `Gas` fields. The `From` address should be the aliased L1PAO address obtained in the previous step (i.e. `0x6B1BAE59D09fCcbdDB6C6cceb07B7279367C4E3b`). The `Gas` field should be set to `200000`. You can get this number by further parsing the opaque data and extracting the gas limit. +9. Next we need to fill out the `Transaction Parameters` section on the right of the UI. Specifically, fill out the `From` address and `Gas` fields. The `From` address should be the aliased old L1PAO address obtained as the `from` field in the `TransactionDeposited` event (i.e. `0x6B1BAE59D09fCcbdDB6C6cceb07B7279367C4E3b`). The `Gas` field should be set to `200000`. You can get this number by further parsing the opaque data and extracting the gas limit. ```bash cast --to-dec 0x30d40 # returns: 200000 diff --git a/src/tasks/MultisigTask.sol b/src/tasks/MultisigTask.sol index acbca1e98a..148118e0ca 100644 --- a/src/tasks/MultisigTask.sol +++ b/src/tasks/MultisigTask.sol @@ -30,6 +30,9 @@ abstract contract MultisigTask is Test, Script, StateOverrideManager, TaskManage using AccountAccessParser for VmSafe.AccountAccess; using StdStyle for string; + /// @notice Maximum gas limit for transactions, 15M gas is ~90% of the limit + uint256 internal constant MAX_GAS_LIMIT = 15_000_000; + /// @notice AddressesRegistry contract AddressRegistry public addrRegistry; @@ -519,6 +522,24 @@ abstract contract MultisigTask is Test, Script, StateOverrideManager, TaskManage (bool success, bytes memory returnData) = multisig.call{gas: gas}(callData); + // Check that the transaction did not exceed the maximum gas limit. + // We must check gas consumed BEFORE refunds, because the EVM requires enough gas upfront, + // therefore we check gasTotalUsed + gasRefunded + // Note: gasRefunded is int64, but should always be >= 0 in practice, unsure why this is + // an int64 in forge. + VmSafe.Gas memory gasInfo = vm.lastCallGas(); + require(gasInfo.gasRefunded >= 0, "MultisigTask: negative gas refund is invalid"); + uint256 gasRefunded = uint256(uint64(gasInfo.gasRefunded)); + uint256 gasConsumedBeforeRefund = uint256(gasInfo.gasTotalUsed) + gasRefunded; + if (gasConsumedBeforeRefund > MAX_GAS_LIMIT) { + console.log("Gas consumed before refund:", gasConsumedBeforeRefund); + console.log("Gas refunded:", gasRefunded); + console.log("Gas final used:", gasInfo.gasTotalUsed); + console.log("Gas limit:", MAX_GAS_LIMIT); + console.log("Fusaka EIP-7825 cap: 16,777,216 gas"); + revert("MultisigTask: transaction exceeds 15M gas limit"); + } + if (!success) { MultisigTaskPrinter.printErrorExecutingMultisigTransaction(returnData); revert("MultisigTask: execute failed"); diff --git a/src/tasks/sep/049-rev-share-betanet/README.md b/src/tasks/sep/049-rev-share-betanet/README.md index e7eb3bf391..b1908e79cf 100644 --- a/src/tasks/sep/049-rev-share-betanet/README.md +++ b/src/tasks/sep/049-rev-share-betanet/README.md @@ -1,6 +1,6 @@ # 049-rev-share-betanet: RevShare Upgrade and Setup for Betanet -Status: [DRAFT, NOT READY TO SIGN]() +Status: [READY TO SIGN] ## Objective diff --git a/src/tasks/sep/050-rev-share-ink-soneium/README.md b/src/tasks/sep/050-rev-share-ink-soneium/README.md index dcc97a5b21..6c4ca3a48b 100644 --- a/src/tasks/sep/050-rev-share-ink-soneium/README.md +++ b/src/tasks/sep/050-rev-share-ink-soneium/README.md @@ -1,6 +1,6 @@ # 050-rev-share-ink-soneium: RevShare Upgrade and Setup for Ink Sepolia and Soneium Minato -Status: [DRAFT, NOT READY TO SIGN]() +Status: [READY TO SIGN] ## Objective diff --git a/src/template/OPCMUpgradeV600.sol b/src/template/OPCMUpgradeV600.sol new file mode 100644 index 0000000000..4feaef5971 --- /dev/null +++ b/src/template/OPCMUpgradeV600.sol @@ -0,0 +1,223 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.15; + +import {Claim} from "@eth-optimism-bedrock/src/dispute/lib/Types.sol"; +import {VmSafe} from "forge-std/Vm.sol"; +import {stdToml} from "forge-std/StdToml.sol"; +import {LibString} from "solady/utils/LibString.sol"; + +import {OPCMTaskBase} from "src/tasks/types/OPCMTaskBase.sol"; +import {SuperchainAddressRegistry} from "src/SuperchainAddressRegistry.sol"; +import {Action} from "src/libraries/MultisigTypes.sol"; + +/// @notice A template contract for configuring OPCMTaskBase templates. +/// Supports: op-contracts/v6.0.0 +contract OPCMUpgradeV600 is OPCMTaskBase { + using stdToml for string; + using LibString for string; + + /// @notice Struct to store inputs data for each L2 chain. + struct OPCMUpgrade { + Claim cannonPrestate; + Claim cannonKonaPrestate; + uint256 chainId; + string expectedValidationErrors; + } + + /// @notice Mapping of L2 chain IDs to their respective OPCMUpgrade structs. + mapping(uint256 => OPCMUpgrade) public upgrades; + + /// @notice The Standard Validator returned by OPCM + IOPContractsManagerStandardValidator public STANDARD_VALIDATOR; + + /// @notice OPCM we delegatecall into (must be v6.0.0). + address public OPCM; + + /// @notice Names in the SuperchainAddressRegistry that are expected to be written during this task. + function _taskStorageWrites() internal pure virtual override returns (string[] memory) { + string[] memory storageWrites = new string[](9); + storageWrites[0] = "DisputeGameFactoryProxy"; + storageWrites[1] = "SystemConfigProxy"; + storageWrites[2] = "OptimismPortalProxy"; + storageWrites[3] = "OptimismMintableERC20FactoryProxy"; + storageWrites[4] = "AddressManager"; + storageWrites[5] = "ProxyAdminOwner"; + storageWrites[6] = "AnchorStateRegistryProxy"; + storageWrites[7] = "L1StandardBridgeProxy"; + storageWrites[8] = "L1ERC721BridgeProxy"; + return storageWrites; + } + + /// @notice Returns an array of strings that refer to contract names in the address registry. + /// Contracts with these names are expected to have their balance changes during the task. + /// By default returns an empty array. Override this function if your task expects balance changes. + function _taskBalanceChanges() internal view virtual override returns (string[] memory) {} + + /// @notice Sets up the template with implementation configurations from a TOML file. + function _templateSetup(string memory taskConfigFilePath, address rootSafe) internal override { + super._templateSetup(taskConfigFilePath, rootSafe); + string memory tomlContent = vm.readFile(taskConfigFilePath); + + // Load upgrades from TOML + OPCMUpgrade[] memory _upgrades = abi.decode(tomlContent.parseRaw(".opcmUpgrades"), (OPCMUpgrade[])); + for (uint256 i = 0; i < _upgrades.length; i++) { + upgrades[_upgrades[i].chainId] = _upgrades[i]; + } + + // OPCM from TOML; must be v6.0.0 + OPCM = tomlContent.readAddress(".addresses.OPCM"); + OPCM_TARGETS.push(OPCM); + require(IOPContractsManagerV600(OPCM).version().eq("6.0.0"), "Incorrect OPCM"); + vm.label(OPCM, "OPCM"); + + // Fetch the validator directly from OPCM so it doesn't need to be configured in TOML + address validatorAddr = address(IOPCM(OPCM).opcmStandardValidator()); + require(validatorAddr != address(0), "OPCM returned zero validator"); + require(validatorAddr.code.length > 0, "Validator has no code"); + STANDARD_VALIDATOR = IOPContractsManagerStandardValidator(validatorAddr); + vm.label(address(STANDARD_VALIDATOR), "OPCMStandardValidator"); + } + + /// @notice Builds the actions for executing the operations. + function _build(address) internal override { + SuperchainAddressRegistry.ChainInfo[] memory chains = superchainAddrRegistry.getChains(); + IOPContractsManagerV600.OpChainConfig[] memory opChainConfigs = + new IOPContractsManagerV600.OpChainConfig[](chains.length); + + for (uint256 i = 0; i < chains.length; i++) { + uint256 chainId = chains[i].chainId; + require(upgrades[chainId].chainId != 0, "OPCMUpgradeV600: Config not found for chain"); + + require( + Claim.unwrap(upgrades[chainId].cannonPrestate) != bytes32(0), "OPCMUpgradeV600: cannonPrestate is zero" + ); + require( + Claim.unwrap(upgrades[chainId].cannonKonaPrestate) != bytes32(0), + "OPCMUpgradeV600: cannonKonaPrestate is zero" + ); + + opChainConfigs[i] = IOPContractsManagerV600.OpChainConfig({ + systemConfigProxy: ISystemConfig(superchainAddrRegistry.getAddress("SystemConfigProxy", chainId)), + cannonPrestate: upgrades[chainId].cannonPrestate, + cannonKonaPrestate: upgrades[chainId].cannonKonaPrestate + }); + } + + // Delegatecall the OPCM.upgrade() function + (bool ok,) = OPCM_TARGETS[0].delegatecall( + abi.encodeWithSelector(IOPContractsManagerV600.upgrade.selector, opChainConfigs) + ); + require(ok, "OPCMUpgradeV600: Delegatecall failed in _build."); + } + + /// @notice This method performs all validations and assertions that verify the calls executed as expected. + function _validate(VmSafe.AccountAccess[] memory, Action[] memory, address) internal view override { + SuperchainAddressRegistry.ChainInfo[] memory chains = superchainAddrRegistry.getChains(); + + // Cache standard validator's expected values (same for all chains) + address standardL1PAO = STANDARD_VALIDATOR.l1PAOMultisig(); + address standardChallenger = STANDARD_VALIDATOR.challenger(); + + for (uint256 i = 0; i < chains.length; i++) { + uint256 chainId = chains[i].chainId; + + IOPContractsManagerStandardValidator.ValidationInputDev memory input = IOPContractsManagerStandardValidator + .ValidationInputDev({ + sysCfg: ISystemConfig(superchainAddrRegistry.getAddress("SystemConfigProxy", chainId)), + cannonPrestate: Claim.unwrap(upgrades[chainId].cannonPrestate), + cannonKonaPrestate: Claim.unwrap(upgrades[chainId].cannonKonaPrestate), + l2ChainID: chainId, + proposer: superchainAddrRegistry.getAddress("Proposer", chainId) + }); + + // Compute overrides: non-zero only if chain differs from standard + address l1PAOOverride = superchainAddrRegistry.getAddress("ProxyAdminOwner", chainId); + address challengerOverride = superchainAddrRegistry.getAddress("Challenger", chainId); + + l1PAOOverride = l1PAOOverride != standardL1PAO ? l1PAOOverride : address(0); + challengerOverride = challengerOverride != standardChallenger ? challengerOverride : address(0); + + string memory errors; + if (l1PAOOverride != address(0) || challengerOverride != address(0)) { + errors = STANDARD_VALIDATOR.validateWithOverrides({ + _input: input, + _allowFailure: true, + _overrides: IOPContractsManagerStandardValidator.ValidationOverrides({ + l1PAOMultisig: l1PAOOverride, + challenger: challengerOverride + }) + }); + } else { + errors = STANDARD_VALIDATOR.validate({_input: input, _allowFailure: true}); + } + + string memory expErrors = upgrades[chainId].expectedValidationErrors; + require(errors.eq(expErrors), string.concat("Unexpected errors: ", errors, "; expected: ", expErrors)); + } + } + + /// @notice Override to return a list of addresses that should not be checked for code length. + function _getCodeExceptions() internal view virtual override returns (address[] memory) {} +} + +/* ---------- Interfaces ---------- */ +/// @notice OPCM Interface. +interface IOPContractsManagerV600 { + struct OpChainConfig { + ISystemConfig systemConfigProxy; + Claim cannonPrestate; + Claim cannonKonaPrestate; + } + + function version() external view returns (string memory); + + function upgrade(OpChainConfig[] memory _opChainConfigs) external; + + function opcmStandardValidator() external view returns (IOPContractsManagerStandardValidator); +} + +/// @notice Interface to retrieve the standard validator from OPCM. +interface IOPCM { + function opcmStandardValidator() external view returns (IOPContractsManagerStandardValidator); +} + +/// @notice Validator interface for validateWithOverrides usage. +interface IOPContractsManagerStandardValidator { + struct ValidationInputDev { + ISystemConfig sysCfg; + bytes32 cannonPrestate; + bytes32 cannonKonaPrestate; + uint256 l2ChainID; + address proposer; + } + + struct ValidationOverrides { + address l1PAOMultisig; + address challenger; + } + + function validate(ValidationInputDev memory _input, bool _allowFailure) external view returns (string memory); + function l1PAOMultisig() external view returns (address); + function challenger() external view returns (address); + function validateWithOverrides( + ValidationInputDev memory _input, + bool _allowFailure, + ValidationOverrides memory _overrides + ) external view returns (string memory); + + function version() external view returns (string memory); +} + +interface ISystemConfig { + struct Addresses { + address l1CrossDomainMessenger; + address l1ERC721Bridge; + address l1StandardBridge; + address optimismPortal; + address optimismMintableERC20Factory; + address delayedWETH; + address opcm; + } + + function getAddresses() external view returns (Addresses memory); +} diff --git a/test/tasks/MultisigTask.t.sol b/test/tasks/MultisigTask.t.sol index 325c250516..c23bd7606b 100644 --- a/test/tasks/MultisigTask.t.sol +++ b/test/tasks/MultisigTask.t.sol @@ -6,7 +6,6 @@ import {VmSafe} from "forge-std/Vm.sol"; import {Test} from "forge-std/Test.sol"; import {stdStorage, StdStorage} from "forge-std/StdStorage.sol"; import {IGnosisSafe, Enum} from "@base-contracts/script/universal/IGnosisSafe.sol"; -import {LibString} from "@solady/utils/LibString.sol"; import {Vm} from "forge-std/Vm.sol"; import {Solarray} from "lib/optimism/packages/contracts-bedrock/scripts/libraries/Solarray.sol"; @@ -15,6 +14,7 @@ import {SuperchainAddressRegistry} from "src/SuperchainAddressRegistry.sol"; import {Action, TaskPayload} from "src/libraries/MultisigTypes.sol"; import {MockMultisigTask} from "test/tasks/mock/MockMultisigTask.sol"; import {MockTarget} from "test/tasks/mock/MockTarget.sol"; +import {HighGasMultisigTask} from "test/tasks/mock/HighGasMultisigTask.sol"; contract MultisigTaskUnitTest is Test { using stdStorage for StdStorage; @@ -408,6 +408,21 @@ contract MultisigTaskUnitTest is Test { assertNestedCalldata(result[0], root, abi.encodeCall(IGnosisSafe(root).approveHash, (hash))); } + /// @notice Tests that MultisigTask reverts when a transaction exceeds the 15M gas limit + function test_simulate_revertsOnHighGasUsage_fails() public { + string memory highGasToml = + "l2chains = [{name = \"OP Mainnet\", chainId = 10}]\n" "\n" "templateName = \"HighGasMultisigTask\"\n" "\n"; + + string memory taskConfigFilePath = + MultisigTaskTestHelper.createTempTomlFile(highGasToml, TESTING_DIRECTORY, "highgas"); + + HighGasMultisigTask highGasTask = new HighGasMultisigTask(); + + vm.expectRevert("MultisigTask: transaction exceeds 15M gas limit"); + highGasTask.simulate(taskConfigFilePath); + MultisigTaskTestHelper.removeFile(taskConfigFilePath); + } + /// @notice Asserts that the root safe calldata is correct. function assertRootCalldata(bytes memory data, address target, uint256 value, bytes memory callData) internal diff --git a/test/tasks/Regression.t.sol b/test/tasks/Regression.t.sol index fcce553564..4a55357601 100644 --- a/test/tasks/Regression.t.sol +++ b/test/tasks/Regression.t.sol @@ -11,6 +11,7 @@ import {OPCMUpgradeV200} from "src/template/OPCMUpgradeV200.sol"; import {OPCMUpgradeV300} from "src/template/OPCMUpgradeV300.sol"; import {OPCMUpgradeV400} from "src/template/OPCMUpgradeV400.sol"; import {OPCMUpgradeV500} from "src/template/OPCMUpgradeV500.sol"; +import {OPCMUpgradeV600} from "src/template/OPCMUpgradeV600.sol"; import {OPCMUpdatePrestateV300} from "src/template/OPCMUpdatePrestateV300.sol"; import {OPCMUpdatePrestateV410} from "src/template/OPCMUpdatePrestateV410.sol"; import {SetRespectedGameTypeTemplate} from "src/template/SetRespectedGameTypeTemplate.sol"; @@ -871,6 +872,32 @@ contract RegressionTest is Test { _assertDataToSignNestedMultisig(multisigTask, actions, expectedDataToSign, MULTICALL3_ADDRESS, rootSafe); } + /// @notice Expected call data generated by manually running the OPCMUpgradeV600Template at block 9874342 on sepolia. + /// Simulate from task directory (test/tasks/example/sep/033-opcm-upgrade-v600/config.toml) with: + /// SIMULATE_WITHOUT_LEDGER=1 just --dotenv-path "$(pwd)"/.env --justfile ../../../../../src/justfile simulate + function testRegressionCallDataMatches_OPCMUpgradeV600Template() public { + string memory taskConfigFilePath = "test/tasks/example/sep/033-opcm-upgrade-v600/config.toml"; + string memory expectedCallData = + "0x82ad56cb000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000020000000000000000000000000f0a2e224519e876979ea6b2cd15ef5cc3d6703bd0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a4cbeda5a700000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000001000000000000000000000000034edd2a225f7f429a63e0f1d2084b9e0a93b53803845751c66672c0b09e68ba7c3024a7543a1a22edaa90d7c2c90ebc8cecee6803f833cc2a644a9f7bba9718c13f622b867c513f3c43f3eb5a0cad17784bd40800000000000000000000000000000000000000000000000000000000"; + MultisigTask multisigTask = new OPCMUpgradeV600(); + address rootSafe = address(0x1Eb2fFc903729a0F03966B917003800b145F56E2); + address nestedSafe = address(0xDEe57160aAfCF04c34C887B5962D0a69676d3C8B); // sepolia + address[] memory allSafes = MultisigTaskTestHelper.getAllSafes(rootSafe, nestedSafe); + (Action[] memory actions, uint256[] memory allOriginalNonces) = + _setupAndSimulate(taskConfigFilePath, 9874342, "sepolia", multisigTask, allSafes); + + _assertCallDataMatches(multisigTask, actions, allSafes, allOriginalNonces, expectedCallData); + + string[] memory expectedDataToSign = new string[](2); + // Foundation + expectedDataToSign[0] = + "0x190137e1f5dd3b92a004a23589b741196c8a214629d4ea3a690ec8e41ae45c689cbbdebb61a012c61cc79de4251db4172889f9c2bea3593fdb3cf79b7016d90b88d8"; + // Security Council + expectedDataToSign[1] = + "0x1901be081970e9fc104bd1ea27e375cd21ec7bb1eec56bfe43347c3e36c5d27b85338af46bfe0ef8d4c4dcd740b727f664c01b5d0ee612f67383466fdebaff9ae1fb"; + _assertDataToSignNestedMultisig(multisigTask, actions, expectedDataToSign, MULTICALL3_ADDRESS, rootSafe); + } + /// @notice Expected call data generated by manually running the OPCMUpgradeSuperchainConfigV410Template at block 9167980 on sepolia. /// Simulate from task directory (test/tasks/example/sep/025-opcm-upgrade-superchainconfig-v410/config.toml) with: /// SIMULATE_WITHOUT_LEDGER=1 just --dotenv-path "$(pwd)"/.env --justfile ../../../../../src/justfile simulate diff --git a/test/tasks/example/sep/033-opcm-upgrade-v600/.env b/test/tasks/example/sep/033-opcm-upgrade-v600/.env new file mode 100644 index 0000000000..a178015776 --- /dev/null +++ b/test/tasks/example/sep/033-opcm-upgrade-v600/.env @@ -0,0 +1,3 @@ +TENDERLY_GAS=15000000 +FORK_BLOCK_NUMBER=9874342 +NESTED_SAFE_NAME_DEPTH_1=foundation diff --git a/test/tasks/example/sep/033-opcm-upgrade-v600/config.toml b/test/tasks/example/sep/033-opcm-upgrade-v600/config.toml new file mode 100644 index 0000000000..d88a2275db --- /dev/null +++ b/test/tasks/example/sep/033-opcm-upgrade-v600/config.toml @@ -0,0 +1,15 @@ +l2chains = [ + {name = "OP Sepolia Testnet", chainId = 11155420} +] + +templateName = "OPCMUpgradeV600" + +[[opcmUpgrades]] +chainId = 11155420 +# Dummy prestates from betanet: https://www.notion.so/oplabs/Betanet-2a9f153ee16280859261e3000d866ee9?source=copy_link#2c5f153ee162805abcc3e949ab6c837e +cannonPrestate = "0x03f833cc2a644a9f7bba9718c13f622b867c513f3c43f3eb5a0cad17784bd408" +cannonKonaPrestate = "0x03845751c66672c0b09e68ba7c3024a7543a1a22edaa90d7c2c90ebc8cecee68" +expectedValidationErrors = "" # The template provides PAO and Challenger overrides to the validator from the SCR. These simply indicate overrides were used. + +[addresses] +OPCM = "0xf0a2e224519e876979ea6b2cd15ef5cc3d6703bd" # v600 OPCM source: https://www.notion.so/oplabs/Upgrade-18-Hub-2a3f153ee1628173bfe4f732657e4f01?source=copy_link#2cef153ee162806482fceb958964651e diff --git a/test/tasks/mock/HighGasMultisigTask.sol b/test/tasks/mock/HighGasMultisigTask.sol new file mode 100644 index 0000000000..565afc752e --- /dev/null +++ b/test/tasks/mock/HighGasMultisigTask.sol @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.15; + +import {VmSafe} from "forge-std/Vm.sol"; + +import {L2TaskBase} from "src/tasks/types/L2TaskBase.sol"; +import {Action} from "src/libraries/MultisigTypes.sol"; +import {MockTarget} from "test/tasks/mock/MockTarget.sol"; + +/// @notice Mock task that consumes a lot of gas to test that MultisigTask correctly rejects +/// transactions that consume gas too close to the Fusaka EIP-7825 cap of 16,777,216 gas. +contract HighGasMultisigTask is L2TaskBase { + /// @notice reference to the mock target contract + MockTarget public mockTarget; + + /// @notice Returns the safe address string identifier + /// @return The string "ProxyAdminOwner" + function safeAddressString() public pure override returns (string memory) { + return "ProxyAdminOwner"; + } + + /// @notice Returns the storage write permissions required for this task + /// @return Array of storage write permissions + function _taskStorageWrites() internal pure override returns (string[] memory) { + string[] memory storageWrites = new string[](1); + storageWrites[0] = "ProxyAdminOwner"; + return storageWrites; + } + + function _templateSetup(string memory, address rootSafe) internal override { + super._templateSetup("", rootSafe); + // Initialize mockTarget so it's available when _build() runs + mockTarget = new MockTarget(); + } + + /// @notice Build function that creates an action which will consume >15M gas on-chain + function _build(address) internal override { + // Call mockTarget.consumeGas() which will expand memory during execution to use a lot of gas + mockTarget.consumeGas(); + } + + /// @notice Validates that the task executed + function _validate(VmSafe.AccountAccess[] memory, Action[] memory, address) internal view override {} + + /// @notice No code exceptions for this template. + function _getCodeExceptions() internal view virtual override returns (address[] memory) {} +} diff --git a/test/tasks/mock/MockTarget.sol b/test/tasks/mock/MockTarget.sol index 306d0c8b7e..962c534d4b 100644 --- a/test/tasks/mock/MockTarget.sol +++ b/test/tasks/mock/MockTarget.sol @@ -19,7 +19,30 @@ contract MockTarget is Test { vm.store(task, startSnapshotSlot, bytes32(id)); } - function foobar() public { - // This function is when creating dummy/noop actions for testing. + /// @notice Function that consumes a lot of gas through memory expansion. + /// Used to test that MultisigTask correctly rejects transactions that consume gas + /// too close to the Fusaka EIP-7825 cap of 16,777,216 gas. + function consumeGas() public { + // Expand memory to consume ~17M gas. Memory expansion cost grows quadratically, so we + // need to allocate enough memory to exceed the 15M threshold in MultisigTask.sol. + bytes memory largeData1 = new bytes(200000); // 200KB + bytes memory largeData2 = new bytes(400000); // 400KB + bytes memory largeData3 = new bytes(800000); // 800KB + bytes memory largeData4 = new bytes(1600000); // 1.6MB + + // Write to storage to ensure this call is captured as an action, if this function was + // view or pure it would get filtered out by MultisigTask.sol. + uint256 sum; + assembly { + // Touch each array to prevent these from being optimized away. + let val1 := mload(add(largeData1, 32)) + let val2 := mload(add(largeData2, 32)) + let val3 := mload(add(largeData3, 32)) + let val4 := mload(add(largeData4, 32)) + sum := add(add(add(val1, val2), val3), val4) + } + + // Write to storage to make this a state-changing function + task = address(uint160(sum)); } }