diff --git a/foundry.toml b/foundry.toml index 0f20320384..89f4ead342 100644 --- a/foundry.toml +++ b/foundry.toml @@ -20,7 +20,7 @@ remappings = [ '@solady/=lib/optimism/packages/contracts-bedrock/lib/solady/src/', '@lib-keccak/=lib/optimism/packages/contracts-bedrock/lib/lib-keccak/contracts/lib/', 'ds-test/=lib/optimism/packages/contracts-bedrock/lib/forge-std/lib/ds-test/src', - 'forge-std/=lib/forge-std/src/', + 'forge-std/=lib/forge-std/src/' ] [profile.ci] diff --git a/src/template/L1PortalExecuteL2Call.sol b/src/template/L1PortalExecuteL2Call.sol new file mode 100644 index 0000000000..cfedd993b4 --- /dev/null +++ b/src/template/L1PortalExecuteL2Call.sol @@ -0,0 +1,118 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.15; + +import {VmSafe} from "forge-std/Vm.sol"; +import {stdToml} from "forge-std/StdToml.sol"; + +import {MultisigTaskPrinter} from "src/libraries/MultisigTaskPrinter.sol"; +import {Action} from "src/libraries/MultisigTypes.sol"; +import {L2TaskBase} from "src/tasks/types/L2TaskBase.sol"; +import {SuperchainAddressRegistry} from "src/SuperchainAddressRegistry.sol"; + +/// @notice Interface for the OptimismPortal2 contract on L1. +interface IOptimismPortal2 { + function depositTransaction(address _to, uint256 _value, uint64 _gasLimit, bool _isCreation, bytes memory _data) + external + payable; +} + +/// @notice Template to execute an L2 call via the L1 Optimism Portal from a nested L1 Safe. +/// Sends an L2 transaction using OptimismPortal.depositTransaction with config-driven params. +/// Supports: op-contracts/v4.6.0 +contract L1PortalExecuteL2Call is L2TaskBase { + using stdToml for string; + + // -------- Config inputs -------- + /// @notice The address of the L2 target contract. + address public l2Target; + /// @notice The calldata to be executed on l2Target. + bytes public l2Data; + /// @notice The L2 gas limit. + uint64 public gasLimit; + /// @notice Whether to create a contract on L2. + bool public isCreation; + + /// @notice Default Safe name. Can be overridden via `safeAddressString` in config.toml. + function safeAddressString() public pure override returns (string memory) { + return "ProxyAdminOwner"; + } + + /// @notice The contracts expected to have storage writes during execution. + /// Allowlist the OptimismPortal since it will mutate state (queue/event) on deposit. + function _taskStorageWrites() internal pure override returns (string[] memory) { + string[] memory _storageWrites = new string[](1); + _storageWrites[0] = "OptimismPortalProxy"; + return _storageWrites; + } + + /// @notice The contracts expected to have balance changes during execution. + function _taskBalanceChanges() internal pure override returns (string[] memory) {} + + /// @notice Parse config and initialize template variables. + /// Expected TOML keys: + /// - l2Target: address (L2 target address) + /// - l2Data: hex string (e.g. 0x1234...) + /// - gasLimit: uint (will be cast to uint64) + /// - isCreation: bool (optional, default false) + function _templateSetup(string memory _taskConfigFilePath, address) internal override { + string memory _toml = vm.readFile(_taskConfigFilePath); + + l2Target = _toml.readAddress(".l2Target"); + require(l2Target != address(0), "l2Target must be set"); + + // Read hex string and parse to bytes. + l2Data = _toml.readBytes(".l2Data"); + require(l2Data.length > 0, "l2Data must be set"); + + uint256 _gasLimitTmp = _toml.readUint(".gasLimit"); + require(_gasLimitTmp > 0 && _gasLimitTmp <= type(uint64).max, "invalid gasLimit"); + gasLimit = uint64(_gasLimitTmp); + + // Optional fields + isCreation = false; + try vm.parseTomlBool(_toml, ".isCreation") returns (bool _b) { + isCreation = _b; + } catch {} + + // early revert in case of attempted contract creation with a non-zero target + require(isCreation && l2Target == address(0) || !isCreation, "contract creation requires zero target address"); + } + + /// @notice Build the portal deposit action. WARNING: State changes here are reverted after capture. + function _build(address) internal override { + SuperchainAddressRegistry.ChainInfo[] memory chains = superchainAddrRegistry.getChains(); + for (uint256 _i = 0; _i < chains.length; _i++) { + IOptimismPortal2(superchainAddrRegistry.getAddress("OptimismPortalProxy", chains[_i].chainId)) + .depositTransaction(l2Target, 0, gasLimit, isCreation, l2Data); + } + } + + /// @notice Validate that exactly one action to the portal with the expected calldata was captured. + function _validate(VmSafe.AccountAccess[] memory, Action[] memory _actions, address) internal view override { + bytes memory _expected = + abi.encodeCall(IOptimismPortal2.depositTransaction, (l2Target, 0, gasLimit, isCreation, l2Data)); + + bool _found; + uint256 _matches; + SuperchainAddressRegistry.ChainInfo[] memory chains = superchainAddrRegistry.getChains(); + for (uint256 _i = 0; _i < chains.length; _i++) { + for (uint256 _j = 0; _j < _actions.length; _j++) { + if ( + _actions[_j].target == superchainAddrRegistry.getAddress("OptimismPortalProxy", chains[_i].chainId) + && _actions[_j].value == 0 + ) { + if (keccak256(_actions[_j].arguments) == keccak256(_expected)) { + _found = true; + _matches++; + } + } + } + } + + require(_found && _matches == chains.length, "expected one portal deposit action for each chain"); + MultisigTaskPrinter.printTitle("Validated portal deposit action"); + } + + /// @notice No code exceptions required for this template. + function _getCodeExceptions() internal view override returns (address[] memory) {} +} diff --git a/test/tasks/Regression.t.sol b/test/tasks/Regression.t.sol index 70455e273c..b16ffbd3c6 100644 --- a/test/tasks/Regression.t.sol +++ b/test/tasks/Regression.t.sol @@ -37,6 +37,7 @@ import {BlacklistGamesV400} from "src/template/BlacklistGamesV400.sol"; import {OPCMUpgradeV220toV410} from "src/template/OPCMUpgradeV220toV410.sol"; import {OPCMUpgradeV410} from "src/template/OPCMUpgradeV410.sol"; import {OPCMUpgradeSuperchainConfigV410} from "src/template/OPCMUpgradeSuperchainConfigV410.sol"; +import {L1PortalExecuteL2Call} from "src/template/L1PortalExecuteL2Call.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 @@ -869,6 +870,34 @@ contract RegressionTest is Test { ); } + /// @notice Expected call data and data to sign generated by manually running the L1PortalExecuteL2CallUpgradeGovernor template at block 23197819 on mainnet. + /// Simulate from task directory (test/tasks/example/eth/014-noop-call-optimismportal/config.toml) with: + /// just --dotenv-path $(pwd)/.env --justfile ../../../../../src/improvements/justfile simulate (foundation|council) + function testRegressionCallDataMatches_L1PortalExecuteL2CallUpgradeGovernor() public { + string memory taskConfigFilePath = "test/tasks/example/eth/014-noop-call-optimismportal/config.toml"; + string memory expectedCallData = + "0x174dea71000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000020000000000000000000000000beb5fc579115071764c7423a4f12edde41f106ed0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000104e9e05c42000000000000000000000000cdf27f107725988f2261ce2256bdfcde8b382b100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007a120000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000243659cfe6000000000000000000000000ecbf4ed9f47302f00f0f039a691e7db83bdd26240000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"; + MultisigTask multisigTask = new L1PortalExecuteL2Call(); + address rootSafe = address(0x5a0Aae59D09fccBdDb6C6CcEB07B7279367C3d2A); // L1PAO + address securityCouncilChildMultisig = address(0xc2819DC788505Aac350142A7A707BF9D03E3Bd03); + address[] memory allSafes = MultisigTaskTestHelper.getAllSafes(rootSafe, securityCouncilChildMultisig); + + (Action[] memory actions, uint256[] memory allOriginalNonces) = + _setupAndSimulate(taskConfigFilePath, 23197819, "mainnet", multisigTask, allSafes); + + _assertCallDataMatches(multisigTask, actions, allSafes, allOriginalNonces, expectedCallData); + + string[] memory expectedDataToSign = new string[](2); + // Foundation + expectedDataToSign[0] = + "0x1901a4a9c312badf3fcaa05eafe5dc9bee8bd9316c78ee8b0bebe3115bb21b73267229ea72d29d343d55ff76a6ce84cc8514d45683b4339b10bef5e956955bfe65c9"; + // Security Council + expectedDataToSign[1] = + "0x1901df53d510b56e539b90b369ef08fce3631020fbf921e3136ea5f8747c20bce9672b811a78d33f39e928848432a404247a2ab7c4a596b8586797a2e86b284b3b8b"; + + _assertDataToSignNestedMultisig(multisigTask, actions, expectedDataToSign, MULTICALL3_ADDRESS, rootSafe); + } + /// @notice Internal function to set up the fork and run the simulate method. Requires a gas limit to be passed to it. function _setupAndSimulate( string memory taskConfigFilePath, diff --git a/test/tasks/example/eth/014-noop-call-optimismportal/.env b/test/tasks/example/eth/014-noop-call-optimismportal/.env new file mode 100644 index 0000000000..d4a7b76058 --- /dev/null +++ b/test/tasks/example/eth/014-noop-call-optimismportal/.env @@ -0,0 +1,3 @@ +TENDERLY_GAS=10000000 +NESTED_SAFE_NAME_DEPTH_1=council +FORK_BLOCK_NUMBER=23197819 \ No newline at end of file diff --git a/test/tasks/example/eth/014-noop-call-optimismportal/config.toml b/test/tasks/example/eth/014-noop-call-optimismportal/config.toml new file mode 100644 index 0000000000..318a8809d0 --- /dev/null +++ b/test/tasks/example/eth/014-noop-call-optimismportal/config.toml @@ -0,0 +1,9 @@ +templateName = "L1PortalExecuteL2Call" + +l2chains = [{name = "OP Mainnet", chainId = 10}] + +# L2 call params +l2Target = "0xcDF27F107725988f2261Ce2256bDfCdE8B382B10" # OptimismGovernor Proxy +l2Data = "0x3659cfe6000000000000000000000000ecbf4ed9f47302f00f0f039a691e7db83bdd2624" # upgradeTo(currentImpl) -> 0xecbf4ed9f47302f00f0f039a691e7db83bdd2624 +gasLimit = 500000 +isCreation = false