From 6b0280d6bace82f5a8538f7e3bcf7a80d0b839c0 Mon Sep 17 00:00:00 2001 From: IamFlux <175354924+0xiamflux@users.noreply.github.com> Date: Wed, 1 Oct 2025 12:29:34 -0400 Subject: [PATCH 01/18] feat: L2 call via portal template (#1231) * chore: add remapping for optimism lib * feat: L1PortalExecuteL2Call task implementation * feat: rehearsals specs for L1PortalExecuteL2Call task * test: add regression test for new template L1PortalExecuteL2Call * feat: create new rehearsal ceremony to make an upgrade to L2 Governor * chore: update comment on Optimism Portal address Co-authored-by: 0xOneTony <112496816+0xOneTony@users.noreply.github.com> Signed-off-by: IamFlux <175354924+0xiamflux@users.noreply.github.com> * refactor: remove nested try-catch * chore: update fmt * chore: complies with ci * chore: remove unecessary remapping * chore: remove rehearsal docs for no-op upgrade * chore: fix compiler warnings * chore: fix just simulate sep command failure * chore: remove newline in sc rehearsals README * refactor: add OptimismPortal interface in template to avoid coupling * chore: update comment natspec on template * feat: revert before reaching portal with a descriptive message when isCreation=true * refactor: prefe readBytes over manually parsing * fix: use encodeCall over encodeWithSelector * chore: undo diff for canceled task * chore: fix README newlines * chore: add fmt fix to template * fix: lib import path in template * refactor: make value passed to portal 0 * fix: library path * refactor: use L2TaskBase instead of SimpleTaskBase * chore: add version tag of OptimismPortal * refactor: renaming loop variables * fix: portal from value change allowlist, remove value natspec ref --------- Signed-off-by: IamFlux <175354924+0xiamflux@users.noreply.github.com> Co-authored-by: Lumi (Wonderland) Co-authored-by: 0xOneTony <112496816+0xOneTony@users.noreply.github.com> --- foundry.toml | 2 +- src/template/L1PortalExecuteL2Call.sol | 118 ++++++++++++++++++ test/tasks/Regression.t.sol | 29 +++++ .../eth/014-noop-call-optimismportal/.env | 3 + .../014-noop-call-optimismportal/config.toml | 9 ++ 5 files changed, 160 insertions(+), 1 deletion(-) create mode 100644 src/template/L1PortalExecuteL2Call.sol create mode 100644 test/tasks/example/eth/014-noop-call-optimismportal/.env create mode 100644 test/tasks/example/eth/014-noop-call-optimismportal/config.toml 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 From 06b2ceadab2bcc4df0584a3f1a13dce6a22eb8a4 Mon Sep 17 00:00:00 2001 From: Maurelian Date: Fri, 3 Oct 2025 11:23:07 -0400 Subject: [PATCH 02/18] Pin FORK_BLOCK_NUMBER for eth/013 example task (#1240) * Pin FORK_BLOCK_NUMBER for eth/013 example task * Update status of eth/022 * Set FORK_BLOCK_NUMBER for eth/023 * fix setting executed status from 022 to 023 * Set FORK_BLOCK_NUMBER for eth/022 * chore: uni as executed * fix: env loading --------- Co-authored-by: JosepBove --- src/justfile | 54 +++++++++++++++---- src/tasks/TaskManager.sol | 27 ++++++---- .../eth/022-U16a-opcm-upgrade-v410-base/.env | 1 + .../022-U16a-opcm-upgrade-v410-base/README.md | 30 +++++------ .../.env | 1 + .../README.md | 4 +- .../README.md | 2 +- test/tasks/TaskManager.t.sol | 17 +++--- test/tasks/example/eth/013-U16-base/.env | 1 + 9 files changed, 95 insertions(+), 42 deletions(-) diff --git a/src/justfile b/src/justfile index 20695cf623..cb900f8f43 100644 --- a/src/justfile +++ b/src/justfile @@ -70,28 +70,53 @@ simulate-stack NETWORK="" TASK="" CHILD_SAFE_NAME_DEPTH_1="" CHILD_SAFE_NAME_DEP [ -z "{{NETWORK}}" ] && { echo -e "\n\033[31mError: No network specified\033[0m"; show_usage; exit 1; } ETH_RPC_URL=$("$root_dir"/src/script/get-rpc-url.sh "{{NETWORK}}") + + # Load FORK_BLOCK_NUMBER from the task's .env file if available + if [ -n "{{TASK}}" ]; then + task_env_file="${root_dir}/src/tasks/{{NETWORK}}/{{TASK}}/.env" + if [ -f "$task_env_file" ]; then + set -a + source "$task_env_file" + set +a + fi + else + # For full stack simulations, find the first .env file that contains FORK_BLOCK_NUMBER + # Search through .env files in numerical order by task number + for env_file in $(find "${root_dir}/src/tasks/{{NETWORK}}" -name ".env" -type f | sort -V); do + if grep -q "FORK_BLOCK_NUMBER" "$env_file" 2>/dev/null; then + set -a + source "$env_file" + set +a + break + fi + done + fi + + # Get fork block arguments - will be used for all tasks in the stack + fork_block_args=$(just --justfile "$root_just_file" _get-fork-block-args) + echo -e "\n⏳ Stacked Task simulation in progress..." if [ -z "{{TASK}}" ]; then echo -e "⏳ You are simulating all tasks for network: {{NETWORK}}\n" just list-stack {{NETWORK}} - forge script ${root_dir}/src/tasks/StackedSimulator.sol:StackedSimulator --sig "simulateStack(string)" {{NETWORK}} --ffi --fork-url $ETH_RPC_URL --fork-retries {{forge_fork_retries}} --fork-retry-backoff {{forge_fork_retry_backoff}} + forge script ${root_dir}/src/tasks/StackedSimulator.sol:StackedSimulator --sig "simulateStack(string)" {{NETWORK}} --ffi --fork-url $ETH_RPC_URL --fork-retries {{forge_fork_retries}} --fork-retry-backoff {{forge_fork_retry_backoff}} ${fork_block_args} elif [ -z "{{CHILD_SAFE_NAME_DEPTH_1}}" ] && [ -z "{{CHILD_SAFE_NAME_DEPTH_2}}" ]; then echo -e "⏳ You are simulating the task: {{TASK}} for network: '{{NETWORK}}' on the root safe\n" just list-stack {{NETWORK}} {{TASK}} - forge script ${root_dir}/src/tasks/StackedSimulator.sol:StackedSimulator --sig "simulateStack(string,string)" "{{NETWORK}}" "{{TASK}}" --ffi --fork-url $ETH_RPC_URL --fork-retries {{forge_fork_retries}} --fork-retry-backoff {{forge_fork_retry_backoff}} + forge script ${root_dir}/src/tasks/StackedSimulator.sol:StackedSimulator --sig "simulateStack(string,string)" "{{NETWORK}}" "{{TASK}}" --ffi --fork-url $ETH_RPC_URL --fork-retries {{forge_fork_retries}} --fork-retry-backoff {{forge_fork_retry_backoff}} ${fork_block_args} else task_dir_path="${root_dir}/src/tasks/{{NETWORK}}/{{TASK}}" child_safe_depth_1=$(just --justfile "$root_just_file" _fetch-safe "$task_dir_path" "{{CHILD_SAFE_NAME_DEPTH_1}}") child_safe_depth_2=$(just --justfile "$root_just_file" _fetch-safe "$task_dir_path" "{{CHILD_SAFE_NAME_DEPTH_2}}") just list-stack {{NETWORK}} {{TASK}} - + if [ "$child_safe_depth_2" != "$ZERO_ADDRESS" ]; then echo -e "⏳ You are simulating the task: {{TASK}} for network: '{{NETWORK}}' on the nested safe: $child_safe_depth_2\n" - forge script ${root_dir}/src/tasks/StackedSimulator.sol:StackedSimulator --sig "simulateStack(string,string,address,address)" "{{NETWORK}}" "{{TASK}}" "$child_safe_depth_2" "$child_safe_depth_1" --ffi --fork-url $ETH_RPC_URL --fork-retries {{forge_fork_retries}} --fork-retry-backoff {{forge_fork_retry_backoff}} + forge script ${root_dir}/src/tasks/StackedSimulator.sol:StackedSimulator --sig "simulateStack(string,string,address,address)" "{{NETWORK}}" "{{TASK}}" "$child_safe_depth_2" "$child_safe_depth_1" --ffi --fork-url $ETH_RPC_URL --fork-retries {{forge_fork_retries}} --fork-retry-backoff {{forge_fork_retry_backoff}} ${fork_block_args} else echo -e "⏳ You are simulating the task: {{TASK}} for network: '{{NETWORK}}' on the nested safe: $child_safe_depth_1\n" - forge script ${root_dir}/src/tasks/StackedSimulator.sol:StackedSimulator --sig "simulateStack(string,string,address)" "{{NETWORK}}" "{{TASK}}" "$child_safe_depth_1" --ffi --fork-url $ETH_RPC_URL --fork-retries {{forge_fork_retries}} --fork-retry-backoff {{forge_fork_retry_backoff}} + forge script ${root_dir}/src/tasks/StackedSimulator.sol:StackedSimulator --sig "simulateStack(string,string,address)" "{{NETWORK}}" "{{TASK}}" "$child_safe_depth_1" --ffi --fork-url $ETH_RPC_URL --fork-retries {{forge_fork_retries}} --fork-retry-backoff {{forge_fork_retry_backoff}} ${fork_block_args} fi fi echo -e "\n⚠️ Please note: Some Tenderly links may not work when running a stacked simulation. Check out Tenderly's Virtual TestNets as a workaround.⚠️" @@ -119,6 +144,17 @@ sign-stack NETWORK="" TASK="" CHILD_SAFE_NAME_DEPTH_1="" CHILD_SAFE_NAME_DEPTH_2 child_safe_depth_1=$(just --justfile "$root_just_file" _fetch-safe "$task_dir_path" "{{CHILD_SAFE_NAME_DEPTH_1}}") child_safe_depth_2=$(just --justfile "$root_just_file" _fetch-safe "$task_dir_path" "{{CHILD_SAFE_NAME_DEPTH_2}}") + # Load FORK_BLOCK_NUMBER from the task's .env file if available + task_env_file="$task_dir_path/.env" + if [ -f "$task_env_file" ]; then + set -a + source "$task_env_file" + set +a + fi + + # Get fork block arguments + fork_block_args=$(just --justfile "$root_just_file" _get-fork-block-args) + HD_PATH=${HD_PATH:-0} USE_KEYSTORE=${USE_KEYSTORE:-} signer_info=$(just --justfile "$root_just_file" _get-signer-args "$HD_PATH" "$USE_KEYSTORE") @@ -126,20 +162,20 @@ sign-stack NETWORK="" TASK="" CHILD_SAFE_NAME_DEPTH_1="" CHILD_SAFE_NAME_DEPTH_2 export SIGNING_MODE_IN_PROGRESS=true export STACKED_SIGNING_MODE=true - + if [ "$child_safe_depth_2" != "$ZERO_ADDRESS" ]; then echo -e "⏳ You are signing the task: {{TASK}} for network: '{{NETWORK}}' on the nested safe: $child_safe_depth_2\n" ${root_dir}/bin/eip712sign ${signer_args} -- \ - forge script ${root_dir}/src/tasks/StackedSimulator.sol:StackedSimulator --sig "simulateStack(string,string,address,address)" "{{NETWORK}}" "{{TASK}}" "$child_safe_depth_2" "$child_safe_depth_1" --ffi --fork-url $ETH_RPC_URL --fork-retries {{forge_fork_retries}} --fork-retry-backoff {{forge_fork_retry_backoff}} + forge script ${root_dir}/src/tasks/StackedSimulator.sol:StackedSimulator --sig "simulateStack(string,string,address,address)" "{{NETWORK}}" "{{TASK}}" "$child_safe_depth_2" "$child_safe_depth_1" --ffi --fork-url $ETH_RPC_URL --fork-retries {{forge_fork_retries}} --fork-retry-backoff {{forge_fork_retry_backoff}} ${fork_block_args} elif [ -z "{{CHILD_SAFE_NAME_DEPTH_1}}" ] && [ -z "{{CHILD_SAFE_NAME_DEPTH_2}}" ]; then echo -e "⏳ You are signing the task: {{TASK}} for network: '{{NETWORK}}' on the root safe\n" just list-stack {{NETWORK}} {{TASK}} ${root_dir}/bin/eip712sign ${signer_args} -- \ - forge script ${root_dir}/src/tasks/StackedSimulator.sol:StackedSimulator --sig "simulateStack(string,string)" "{{NETWORK}}" "{{TASK}}" --ffi --fork-url $ETH_RPC_URL --fork-retries {{forge_fork_retries}} --fork-retry-backoff {{forge_fork_retry_backoff}} + forge script ${root_dir}/src/tasks/StackedSimulator.sol:StackedSimulator --sig "simulateStack(string,string)" "{{NETWORK}}" "{{TASK}}" --ffi --fork-url $ETH_RPC_URL --fork-retries {{forge_fork_retries}} --fork-retry-backoff {{forge_fork_retry_backoff}} ${fork_block_args} else echo -e "⏳ You are signing the task: {{TASK}} for network: '{{NETWORK}}' on the nested safe: $child_safe_depth_1\n" ${root_dir}/bin/eip712sign ${signer_args} -- \ - forge script ${root_dir}/src/tasks/StackedSimulator.sol:StackedSimulator --sig "simulateStack(string,string,address)" "{{NETWORK}}" "{{TASK}}" "$child_safe_depth_1" --ffi --fork-url $ETH_RPC_URL --fork-retries {{forge_fork_retries}} --fork-retry-backoff {{forge_fork_retry_backoff}} + forge script ${root_dir}/src/tasks/StackedSimulator.sol:StackedSimulator --sig "simulateStack(string,string,address)" "{{NETWORK}}" "{{TASK}}" "$child_safe_depth_1" --ffi --fork-url $ETH_RPC_URL --fork-retries {{forge_fork_retries}} --fork-retry-backoff {{forge_fork_retry_backoff}} ${fork_block_args} fi echo -e "\n⚠️ Please note: Some Tenderly links may not work when running a stacked simulation. Check out Tenderly's Virtual TestNets as a workaround.⚠️" diff --git a/src/tasks/TaskManager.sol b/src/tasks/TaskManager.sol index c260dab888..bcc31c3660 100644 --- a/src/tasks/TaskManager.sol +++ b/src/tasks/TaskManager.sol @@ -105,7 +105,7 @@ contract TaskManager is Script { string memory formattedRootSafe = vm.toString(config.rootSafe).green().bold(); - setTenderlyGasEnv(config.basePath); + setEnv(config.basePath); string[] memory parts = vm.split(config.basePath, "/"); string memory taskName = parts[parts.length - 1]; @@ -234,25 +234,34 @@ contract TaskManager is Script { ); } - /// @notice Sets the TENDERLY_GAS environment variable for the task if it exists. - function setTenderlyGasEnv(string memory basePath) public { + /// @notice Sets environment variables from the task's .env file if it exists. + function setEnv(string memory basePath) public { string memory envFile = string.concat(basePath, "/", ".env"); if (vm.isFile(envFile)) { string memory envContent = vm.readFile(envFile); string[] memory lines = vm.split(envContent, "\n"); for (uint256 i = 0; i < lines.length; i++) { - if (lines[i].contains("TENDERLY_GAS=")) { + // Skip empty lines and comments + if (bytes(lines[i]).length == 0 || lines[i].startsWith("#")) { + continue; + } + + // Parse key=value pairs + if (lines[i].contains("=")) { string[] memory keyValue = vm.split(lines[i], "="); - if (keyValue.length == 2) { - vm.setEnv("TENDERLY_GAS", keyValue[1]); - return; + if (keyValue.length >= 2) { + // Trim whitespace from key and value, then set the environment variable + string memory key = vm.trim(keyValue[0]); + string memory value = vm.trim(keyValue[1]); + vm.setEnv(key, value); } } } + } else { + // If no .env file exists, set TENDERLY_GAS to empty for backwards compatibility + vm.setEnv("TENDERLY_GAS", ""); } - // If no TENDERLY_GAS is found, set it as empty. - vm.setEnv("TENDERLY_GAS", ""); } /// @notice Useful function to tell if a task is nested or not based on the task config. diff --git a/src/tasks/eth/022-U16a-opcm-upgrade-v410-base/.env b/src/tasks/eth/022-U16a-opcm-upgrade-v410-base/.env index adf17d369c..205f6ffe64 100644 --- a/src/tasks/eth/022-U16a-opcm-upgrade-v410-base/.env +++ b/src/tasks/eth/022-U16a-opcm-upgrade-v410-base/.env @@ -1 +1,2 @@ TENDERLY_GAS=15000000 +FORK_BLOCK_NUMBER=23491327 diff --git a/src/tasks/eth/022-U16a-opcm-upgrade-v410-base/README.md b/src/tasks/eth/022-U16a-opcm-upgrade-v410-base/README.md index d32a8453fd..a4792f1560 100644 --- a/src/tasks/eth/022-U16a-opcm-upgrade-v410-base/README.md +++ b/src/tasks/eth/022-U16a-opcm-upgrade-v410-base/README.md @@ -25,7 +25,7 @@ cd superchain-ops git pull ``` -## Step 3. Install & Configure Mise +## Step 3. Install & Configure Mise Install Mise by executing the following commands. When the Bash script runs, follow the instructions in the log output to activate mise in your shell. @@ -57,12 +57,12 @@ Base has a doubly-nested safe architecture which is supported by superchain-ops. ```bash # # ┌─────────────────────────────────────────────┐ ┌─────────────────────────────────────────────┐ ┌─────────────────────────────────────────────┐ -# │ Base Council │ │ Base Operations │ │ Foundation Operations (FOS) │ +# │ Base Council │ │ Base Operations │ │ Foundation Operations (FOS) │ # │ (7 of 10) │ │ (3 of 6) │ │ (5 of 7) │ # │ 0x20AcF55A3DCfe07fC4cecaCFa1628F788EC8A4Dd │ │ 0x9C4a57Feb77e294Fd7BF5EBE9AB01CAA0a90A110 │ │ 0x9BA6e03D8B90dE867373Db8cF1A58d2F7F006b3A │ # └─────────────────────┬───────────────────────┘ └─────────────────────┬───────────────────────┘ └─────────────────────┬───────────────────────┘ # │ │ │ -# └─────────────────┬───────────────────────────────────┘ │ +# └─────────────────┬───────────────────────────────────┘ │ # ▼ │ # ┌─────────────────────────────────────────────┐ │ # │ Base Nested │ │ @@ -87,18 +87,18 @@ First, simulate the upgrade transaction using the command below corresponding to cd tasks/eth/022-U16a-opcm-upgrade-v410-base # Base Council: 0x20AcF55A3DCfe07fC4cecaCFa1628F788EC8A4Dd -# ┌────────────────────┐ +# ┌────────────────────┐ # │ Child Safe Depth 2 │ # │ 'base-council' │ -# └────────────────────┘ -# │ +# └────────────────────┘ +# │ # └─────────────────┬ -# ▼ +# ▼ # ┌────────────────────┐ # │ Child Safe Depth 1 │ # │ 'base-nested' │ # └────────────────────┘ -# │ +# │ # └──────────┬ # ▼ # ┌─────────────────┐ @@ -111,18 +111,18 @@ SIMULATE_WITHOUT_LEDGER=1 just --dotenv-path $(pwd)/.env simulate base-nested ba # Normalized Hash: 0x1040a2a57a0fc30a1ff18d3c0e35898dbf98c89dc172945b99a0f3b65508c659 # Base Operations: 0x9C4a57Feb77e294Fd7BF5EBE9AB01CAA0a90A110 -# ┌────────────────────┐ +# ┌────────────────────┐ # │ Child Safe Depth 2 │ # │ 'base-operations' │ -# └────────────────────┘ -# │ +# └────────────────────┘ +# │ # └─────────────────┬ -# ▼ +# ▼ # ┌────────────────────┐ # │ Child Safe Depth 1 │ # │ 'base-nested' │ # └────────────────────┘ -# │ +# │ # └──────────┬ # ▼ # ┌─────────────────┐ @@ -140,7 +140,7 @@ SIMULATE_WITHOUT_LEDGER=1 just --dotenv-path $(pwd)/.env simulate base-nested ba # │ Child Safe Depth 1 │ # │ 'FOS' │ # └────────────────────┘ -# │ +# │ # └──────────┬ # ▼ # ┌─────────────────┐ @@ -217,7 +217,7 @@ Now, perform the signing for whichever of the safes you are a member of: ```bash cd src/tasks/eth/022-U16a-opcm-upgrade-v410-base -just --dotenv-path $(pwd)/.env sign base-nested base-council +just --dotenv-path $(pwd)/.env sign base-nested base-council # Expected Hashes # Domain Hash: 0x1fbfdc61ceb715f63cb17c56922b88c3a980f1d83873df2b9325a579753e8aa3 # Message Hash: 0x520aeeb85997f9db884ae07d1da74b5251550f49ab662b9ada3fa34572ece772 diff --git a/src/tasks/eth/023-U16a-opcm-upgrade-v410-op-soneium-ink/.env b/src/tasks/eth/023-U16a-opcm-upgrade-v410-op-soneium-ink/.env index 16c7320407..ec1f573a10 100644 --- a/src/tasks/eth/023-U16a-opcm-upgrade-v410-op-soneium-ink/.env +++ b/src/tasks/eth/023-U16a-opcm-upgrade-v410-op-soneium-ink/.env @@ -1 +1,2 @@ TENDERLY_GAS=32000000 +FORK_BLOCK_NUMBER=23491327 diff --git a/src/tasks/eth/023-U16a-opcm-upgrade-v410-op-soneium-ink/README.md b/src/tasks/eth/023-U16a-opcm-upgrade-v410-op-soneium-ink/README.md index dd29134478..12100f664d 100644 --- a/src/tasks/eth/023-U16a-opcm-upgrade-v410-op-soneium-ink/README.md +++ b/src/tasks/eth/023-U16a-opcm-upgrade-v410-op-soneium-ink/README.md @@ -1,6 +1,6 @@ # 023-U16a-opcm-upgrade-v410-op-soneium-ink: Upgrade 16a: OP, Soneium, Ink on Mainnet -Status: [READY TO SIGN]() +Status: [EXECUTED](https://etherscan.io/tx/0x744cf7d28324729cace731ac736872da669439a0d380f1f52b8d31af203f7329) ## Objective @@ -44,4 +44,4 @@ just --dotenv-path $(pwd)/.env sign foundation # Security Council - Simulate and Sign SIMULATE_WITHOUT_LEDGER=1 just --dotenv-path $(pwd)/.env simulate council just --dotenv-path $(pwd)/.env sign council -``` \ No newline at end of file +``` diff --git a/src/tasks/eth/024-U16a-opcm-upgrade-v410-unichain/README.md b/src/tasks/eth/024-U16a-opcm-upgrade-v410-unichain/README.md index a13b104991..d1ec5bb71f 100644 --- a/src/tasks/eth/024-U16a-opcm-upgrade-v410-unichain/README.md +++ b/src/tasks/eth/024-U16a-opcm-upgrade-v410-unichain/README.md @@ -1,6 +1,6 @@ # 024-U16a-opcm-upgrade-v410-unichain: Upgrade 16a: Unichain -Status: [READY TO SIGN]() +Status: [EXECUTED](https://etherscan.io/tx/0xc98240172fa3950ec9c62f32cc09d0eb0ed21684b02f1b2677357674cda64ca5) ## Objective diff --git a/test/tasks/TaskManager.t.sol b/test/tasks/TaskManager.t.sol index ff1ee8afbf..0e9ba7538e 100644 --- a/test/tasks/TaskManager.t.sol +++ b/test/tasks/TaskManager.t.sol @@ -26,23 +26,28 @@ contract TaskManagerUnitTest is StateOverrideManager, Test { function testSetTenderlyGasEnv() public { TaskManager tm = new TaskManager(); - tm.setTenderlyGasEnv("./src/tasks/sep/000-opcm-upgrade-v200/"); + tm.setEnv("./src/tasks/sep/000-opcm-upgrade-v200/"); assertEq(vm.envString("TENDERLY_GAS"), "30000000"); - tm.setTenderlyGasEnv("./src/tasks/sep/001-opcm-upgrade-v200/"); + tm.setEnv("./src/tasks/sep/001-opcm-upgrade-v200/"); assertEq(vm.envString("TENDERLY_GAS"), "16000000"); - tm.setTenderlyGasEnv("./src/tasks/sep/002-unichain-superchain-config-fix/"); + tm.setEnv("./src/tasks/sep/002-unichain-superchain-config-fix/"); assertEq(vm.envString("TENDERLY_GAS"), ""); - tm.setTenderlyGasEnv("./src/tasks/sep/003-opcm-upgrade-v200/"); + tm.setEnv("./src/tasks/sep/003-opcm-upgrade-v200/"); assertEq(vm.envString("TENDERLY_GAS"), "16000000"); - tm.setTenderlyGasEnv("./src/tasks/eth/000-opcm-upgrade-v200/"); + tm.setEnv("./src/tasks/eth/000-opcm-upgrade-v200/"); assertEq(vm.envString("TENDERLY_GAS"), "30000000"); - tm.setTenderlyGasEnv("./src/tasks/eth/002-opcm-upgrade-v200/"); + tm.setEnv("./src/tasks/eth/002-opcm-upgrade-v200/"); assertEq(vm.envString("TENDERLY_GAS"), "16000000"); + + // Test loading multiple environment variables including FORK_BLOCK_NUMBER + tm.setEnv("./src/tasks/eth/022-U16a-opcm-upgrade-v410-base/"); + assertEq(vm.envString("TENDERLY_GAS"), "15000000"); + assertEq(vm.envString("FORK_BLOCK_NUMBER"), "23491327"); } function createStateDiff(address who, bytes32 slot, bytes32 oldValue, bytes32 newValue) diff --git a/test/tasks/example/eth/013-U16-base/.env b/test/tasks/example/eth/013-U16-base/.env index b4b63fd552..3504c5992a 100644 --- a/test/tasks/example/eth/013-U16-base/.env +++ b/test/tasks/example/eth/013-U16-base/.env @@ -1,3 +1,4 @@ TENDERLY_GAS=27000000 NESTED_SAFE_NAME_DEPTH_1=base-nested NESTED_SAFE_NAME_DEPTH_2=base-council +FORK_BLOCK_NUMBER=23491327 From f3eb9b3079f9b1115e8d7bef16990bf2e51552ac Mon Sep 17 00:00:00 2001 From: JosepBove Date: Fri, 3 Oct 2025 17:40:41 +0200 Subject: [PATCH 03/18] Change base commands (#1241) * chore: command change * fix: typo --------- Co-authored-by: Matt Solomon --- src/tasks/eth/022-U16a-opcm-upgrade-v410-base/README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/tasks/eth/022-U16a-opcm-upgrade-v410-base/README.md b/src/tasks/eth/022-U16a-opcm-upgrade-v410-base/README.md index a4792f1560..cfb02dd2fa 100644 --- a/src/tasks/eth/022-U16a-opcm-upgrade-v410-base/README.md +++ b/src/tasks/eth/022-U16a-opcm-upgrade-v410-base/README.md @@ -104,7 +104,7 @@ cd tasks/eth/022-U16a-opcm-upgrade-v410-base # ┌─────────────────┐ # │ ProxyAdminOwner │ # └─────────────────┘ -SIMULATE_WITHOUT_LEDGER=1 just --dotenv-path $(pwd)/.env simulate base-nested base-council +SIMULATE_WITHOUT_LEDGER=1 SKIP_DECODE_AND_PRINT=1 just --dotenv-path $(pwd)/.env simulate base-nested base-council # Expected Hashes # Domain Hash: 0x1fbfdc61ceb715f63cb17c56922b88c3a980f1d83873df2b9325a579753e8aa3 # Message Hash: 0x520aeeb85997f9db884ae07d1da74b5251550f49ab662b9ada3fa34572ece772 @@ -128,7 +128,7 @@ SIMULATE_WITHOUT_LEDGER=1 just --dotenv-path $(pwd)/.env simulate base-nested ba # ┌─────────────────┐ # │ ProxyAdminOwner │ # └─────────────────┘ -SIMULATE_WITHOUT_LEDGER=1 just --dotenv-path $(pwd)/.env simulate base-nested base-operations +SIMULATE_WITHOUT_LEDGER=1 SKIP_DECODE_AND_PRINT=1 just --dotenv-path $(pwd)/.env simulate base-nested base-operations # Expected Hashes # Domain Hash: 0xfb308368b8deca582e84a807d31c1bfcec6fda754061e2801b4d6be5cb52a8ac # Message Hash: 0x5ae6e3b8fe66bd6cbe5fae6374222b43a874c13ca850745926ecc430cafdb21a @@ -146,7 +146,7 @@ SIMULATE_WITHOUT_LEDGER=1 just --dotenv-path $(pwd)/.env simulate base-nested ba # ┌─────────────────┐ # │ ProxyAdminOwner │ # └─────────────────┘ -SIMULATE_WITHOUT_LEDGER=1 just --dotenv-path $(pwd)/.env simulate foundation-operations +SIMULATE_WITHOUT_LEDGER=1 SKIP_DECODE_AND_PRINT=1 just --dotenv-path $(pwd)/.env simulate foundation-operations # Expected Hashes # Domain Hash: 0x4e6a6554de0308f5ece8ff736beed8a1b876d16f5c27cac8e466d7de0c703890 # Message Hash: 0x2b7f17c0100e6766aaac289acba0122860a51bdd64810626948b0f986f88efa5 From b54a6260340887f4f71418ceb98a2bf4b65b7d2b Mon Sep 17 00:00:00 2001 From: Matt Solomon Date: Tue, 7 Oct 2025 12:20:47 -0700 Subject: [PATCH 04/18] doc: update Base U16a task status (#1242) --- src/tasks/eth/022-U16a-opcm-upgrade-v410-base/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tasks/eth/022-U16a-opcm-upgrade-v410-base/README.md b/src/tasks/eth/022-U16a-opcm-upgrade-v410-base/README.md index cfb02dd2fa..82ba798608 100644 --- a/src/tasks/eth/022-U16a-opcm-upgrade-v410-base/README.md +++ b/src/tasks/eth/022-U16a-opcm-upgrade-v410-base/README.md @@ -1,6 +1,6 @@ # 022-U16a-opcm-upgrade-v410-base: Upgrades Base Mainnet to `op-contracts/v4.1.0` (i.e. U16a) -Status: [READY TO SIGN]() +Status: [EXECUTED](https://etherscan.io/tx/0x98ef4f3ecc10996b184385bd4ca5b877dce0a3b527d88f4ee0cfe296cf004a38) ## Objective From 22bd7ed79c3d5c5a9c4f8a213c4c8bc8faab505e Mon Sep 17 00:00:00 2001 From: Wazabie <48911235+Wazabie@users.noreply.github.com> Date: Wed, 8 Oct 2025 22:01:04 +0200 Subject: [PATCH 05/18] Fusaka Prestate Update Template, example task and OP + Ink Sepolia Task (#1237) * Adding initial files * Add Ink * Update test/tasks/Regression.t.sol Co-authored-by: mbaxter * Update template, sample and task * Update env gas * Update OPCMUpdatePrestateV410.sol * Update OPCMUpdatePrestateV410.sol * Update prestates * Update VALIDATION.md * Update VALIDATION.md * Update prestates * Update OPCMUpdatePrestateV410.sol * Update OPCMUpdatePrestateV410.sol * Update OPCMUpdatePrestateV410.sol * Update OPCMUpdatePrestateV410.sol * Update OPCMUpdatePrestateV410.sol * Update config.toml * Update config.toml * Update config.toml --------- Co-authored-by: mbaxter --- .../sep/034-op-ink-sep-fusaka-prestate/.env | 1 + .../034-op-ink-sep-fusaka-prestate/README.md | 21 +++ .../VALIDATION.md | 42 +++++ .../config.toml | 33 ++++ src/template/OPCMUpdatePrestateV410.sol | 165 ++++++++++++++++++ test/tasks/Regression.t.sol | 30 ++++ .../sep/028-opcm-update-prestate-v410/.env | 3 + .../028-opcm-update-prestate-v410/config.toml | 24 +++ 8 files changed, 319 insertions(+) create mode 100644 src/tasks/sep/034-op-ink-sep-fusaka-prestate/.env create mode 100644 src/tasks/sep/034-op-ink-sep-fusaka-prestate/README.md create mode 100644 src/tasks/sep/034-op-ink-sep-fusaka-prestate/VALIDATION.md create mode 100644 src/tasks/sep/034-op-ink-sep-fusaka-prestate/config.toml create mode 100644 src/template/OPCMUpdatePrestateV410.sol create mode 100644 test/tasks/example/sep/028-opcm-update-prestate-v410/.env create mode 100644 test/tasks/example/sep/028-opcm-update-prestate-v410/config.toml diff --git a/src/tasks/sep/034-op-ink-sep-fusaka-prestate/.env b/src/tasks/sep/034-op-ink-sep-fusaka-prestate/.env new file mode 100644 index 0000000000..7c96bef7da --- /dev/null +++ b/src/tasks/sep/034-op-ink-sep-fusaka-prestate/.env @@ -0,0 +1 @@ +TENDERLY_GAS=30000000 diff --git a/src/tasks/sep/034-op-ink-sep-fusaka-prestate/README.md b/src/tasks/sep/034-op-ink-sep-fusaka-prestate/README.md new file mode 100644 index 0000000000..98874bbf3d --- /dev/null +++ b/src/tasks/sep/034-op-ink-sep-fusaka-prestate/README.md @@ -0,0 +1,21 @@ +# 034-op-ink-sep-fusaka-prestate + +Status: [DRAFT, NOT READY TO SIGN] + +## Objective + +This task uses `op-contract/v4.1.0` OPContractsManager to update the prestate of OP Sepolia and Ink Sepolia to the Fusaka compatible prestate. + +## Simulation & Signing + +Simulation commands for each safe: +```bash +cd src/tasks/sep/034-op-ink-sep-fusaka-prestate +SIMULATE_WITHOUT_LEDGER=1 SKIP_DECODE_AND_PRINT=1 just --dotenv-path $(pwd)/.env simulate +``` + +Signing commands for each safe: +```bash +cd src/tasks/sep/034-op-ink-sep-fusaka-prestate +SKIP_DECODE_AND_PRINT=1 just --dotenv-path $(pwd)/.env sign +``` diff --git a/src/tasks/sep/034-op-ink-sep-fusaka-prestate/VALIDATION.md b/src/tasks/sep/034-op-ink-sep-fusaka-prestate/VALIDATION.md new file mode 100644 index 0000000000..6228ec8d76 --- /dev/null +++ b/src/tasks/sep/034-op-ink-sep-fusaka-prestate/VALIDATION.md @@ -0,0 +1,42 @@ +# Validation + +This document can be used to validate the inputs and result of the execution of the upgrade transaction which you are +signing. + +The steps are: + +1. [Validate the Domain and Message Hashes](#expected-domain-and-message-hashes) +2. [Verifying the state changes via the normalized state diff hash](#normalized-state-diff-hash-attestation) +3. [Verifying the transaction input](#understanding-task-calldata) +4. [Verifying the state changes](#task-state-changes) + +## Expected Domain and Message Hashes + +First, we need to validate the domain and message hashes. These values should match both the values on your ledger and +the values printed to the terminal when you run the task. + +> [!CAUTION] +> +> Before signing, ensure the below hashes match what is on your ledger. +> +> ### Security Council Safe (`0xf64bc17485f0B4Ea5F06A96514182FC4cB561977`) +> +> - Domain Hash: `0xbe081970e9fc104bd1ea27e375cd21ec7bb1eec56bfe43347c3e36c5d27b8533` +> - Message Hash: `0xa311fd9776ab0338d1680920fd903a3eeb8127b6157a7d8adb9c9250c51a3552` +> +> ### Foundation Safe (`0xDEe57160aAfCF04c34C887B5962D0a69676d3C8B`) +> +> - Domain Hash: `0x37e1f5dd3b92a004a23589b741196c8a214629d4ea3a690ec8e41ae45c689cbb` +> - Message Hash: `0x0100ad527dc0a5bfde1487c1e459f345375c236cdcdd25855dbe4ce4a1ea023e` +## Normalized State Diff Hash Attestation + +The normalized state diff hash **MUST** match the hash produced by the state changes attested to in the state diff audit report. As a signer, you are responsible for verifying that this hash is correct. Please compare the hash below with the one in the audit report. If no audit report is available for this task, you must still ensure that the normalized state diff hash matches the output in your terminal. + +**Normalized hash:** `0x46c2ce3b0ad59afaa4b38c2240580cd823c763fc52dccb3b4e3631b7e500409e` + +## Task Calldata + +Calldata: +``` +0x82ad56cb0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000200000000000000000000000003bb6437aba031afbf9cb3538fa064161e2bf2d780000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000001049a72745b00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000002000000000000000000000000034edd2a225f7f429a63e0f1d2084b9e0a93b538000000000000000000000000189abaaaa82dfc015a588a7dbad6f13b1d3485bc0339db503776757491b9f3038bf6f1d37b7988a2f75e823fe2656c1352ef2f9100000000000000000000000005c993e60179f28bf649a2bb5b00b5f4283bd525000000000000000000000000d7db319a49362b2328cf417a934300cccb442c8d0339db503776757491b9f3038bf6f1d37b7988a2f75e823fe2656c1352ef2f9100000000000000000000000000000000000000000000000000000000 +``` \ No newline at end of file diff --git a/src/tasks/sep/034-op-ink-sep-fusaka-prestate/config.toml b/src/tasks/sep/034-op-ink-sep-fusaka-prestate/config.toml new file mode 100644 index 0000000000..4cd40df065 --- /dev/null +++ b/src/tasks/sep/034-op-ink-sep-fusaka-prestate/config.toml @@ -0,0 +1,33 @@ +templateName = "OPCMUpdatePrestateV410" + +[[l2chains]] +chainId = 11155420 +name = "OP Sepolia Testnet" + +[[l2chains]] +chainId = 763373 +name = "Ink Sepolia Testnet" + +[[opcmUpgrades]] +chainId = 11155420 +absolutePrestate = "0x0339db503776757491b9f3038bf6f1d37b7988a2f75e823fe2656c1352ef2f91" # Fusaka Absolute Prestate published here https://github.com/ethereum-optimism/superchain-registry/blob/df5d4feb51f5d698d043a72ac2c6dfa9ce2a4afa/validation/standard/standard-prestates.toml#L6C7-L6C73 +expectedValidationErrors = "OVERRIDES-L1PAOMULTISIG,OVERRIDES-CHALLENGER" # The template provides PAO and Challenger overrides to the validator from the SCR. These simply indicate overrides were used. + +[[opcmUpgrades]] +chainId = 763373 +absolutePrestate = "0x0339db503776757491b9f3038bf6f1d37b7988a2f75e823fe2656c1352ef2f91" # Fusaka Absolute Prestate published here https://github.com/ethereum-optimism/superchain-registry/blob/df5d4feb51f5d698d043a72ac2c6dfa9ce2a4afa/validation/standard/standard-prestates.toml#L6C7-L6C73 +expectedValidationErrors = "OVERRIDES-L1PAOMULTISIG,OVERRIDES-CHALLENGER" # The template provides PAO and Challenger overrides to the validator from the SCR. These simply indicate overrides were used. + +[addresses] +OPCM = "0x3bb6437aba031afbf9cb3538fa064161e2bf2d78" # version 3.2.0 https://github.com/ethereum-optimism/superchain-registry/blob/40526b1288534f6b84b7aae21d13c0b5f5b12f47/validation/standard/standard-versions-sepolia.toml#L23 + +[stateOverrides] +0x1Eb2fFc903729a0F03966B917003800b145F56E2 = [ # L1PAO + {key = "0x0000000000000000000000000000000000000000000000000000000000000005", value = 37} +] +0xf64bc17485f0B4Ea5F06A96514182FC4cB561977 = [ # SC + {key = "0x0000000000000000000000000000000000000000000000000000000000000005", value = 48} +] +0xDEe57160aAfCF04c34C887B5962D0a69676d3C8B = [ # FUS + {key = "0x0000000000000000000000000000000000000000000000000000000000000005", value = 51} +] diff --git a/src/template/OPCMUpdatePrestateV410.sol b/src/template/OPCMUpdatePrestateV410.sol new file mode 100644 index 0000000000..723bf4082c --- /dev/null +++ b/src/template/OPCMUpdatePrestateV410.sol @@ -0,0 +1,165 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.15; + +import {ISystemConfig, IProxyAdmin} from "@eth-optimism-bedrock/interfaces/L1/IOPContractsManager.sol"; +import {IOPContractsManager} from "lib/optimism/packages/contracts-bedrock/interfaces/L1/IOPContractsManager.sol"; +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 {console2 as console} from "forge-std/console2.sol"; +import {LibString} from "solady/utils/LibString.sol"; + +import {SuperchainAddressRegistry} from "src/SuperchainAddressRegistry.sol"; +import {OPCMTaskBase} from "src/tasks/types/OPCMTaskBase.sol"; +import {Action} from "src/libraries/MultisigTypes.sol"; + +/// @notice This template provides OPCM-based absolute prestate updates. +/// Supports: op-contracts/v4.1.0 +contract OPCMUpdatePrestateV410 is OPCMTaskBase { + using stdToml for string; + using LibString for string; + + /// @notice Struct to store inputs for OPCM.updatePrestate() function per l2 chain + struct OPCMUpgrade { + Claim absolutePrestate; + uint256 chainId; + string expectedValidationErrors; + } + + /// @notice Mapping of l2 chain IDs to their respective prestates + mapping(uint256 => OPCMUpgrade) public upgrades; + + /// @notice The Standard Validator returned by OPCM + IOPContractsManagerStandardValidator public STANDARD_VALIDATOR; + + /// @notice Returns the storage write permissions required for this task + function _taskStorageWrites() internal pure virtual override returns (string[] memory) { + string[] memory storageWrites = new string[](1); + storageWrites[0] = "DisputeGameFactoryProxy"; + return storageWrites; + } + + /// @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); + + OPCMUpgrade[] memory _upgrades = abi.decode(tomlContent.parseRaw(".opcmUpgrades"), (OPCMUpgrade[])); + for (uint256 i = 0; i < _upgrades.length; i++) { + console.log("Adding upgrade - chainID: %s, absolutePrestate:", _upgrades[i].chainId); + console.logBytes32(Claim.unwrap(_upgrades[i].absolutePrestate)); + console.log("Expected errors: %s", _upgrades[i].expectedValidationErrors); + upgrades[_upgrades[i].chainId] = _upgrades[i]; + } + + address OPCM = tomlContent.readAddress(".addresses.OPCM"); + OPCM_TARGETS.push(OPCM); + require(IOPContractsManager(OPCM).version().eq("3.2.0"), "Incorrect OPCM - expected version 3.2.0"); + 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 Before implementing the `_build` function, template developers must consider the following: + /// 1. Which Multicall contract does this template use — `Multicall3` or `Multicall3Delegatecall`? + /// 2. Based on the contract, should the target be called using `call` or `delegatecall`? + /// 3. Ensure that the call to the target uses the appropriate method (`call` or `delegatecall`) accordingly. + /// Guidelines: + /// - `Multicall3Delegatecall`: + /// If the template inherits from `OPCMTaskBase`, it uses the `Multicall3Delegatecall` contract. + /// In this case, calls to the target **must** use `delegatecall`, e.g.: + /// `(bool success,) = OPCM.delegatecall(abi.encodeWithSelector(IOPCMPrestateUpdate.upgrade.selector, opChainConfigs));` + function _build(address) internal override { + SuperchainAddressRegistry.ChainInfo[] memory chains = superchainAddrRegistry.getChains(); + IOPContractsManager.OpChainConfig[] memory opChainConfigs = + new IOPContractsManager.OpChainConfig[](chains.length); + + for (uint256 i = 0; i < chains.length; i++) { + uint256 chainId = chains[i].chainId; + opChainConfigs[i] = IOPContractsManager.OpChainConfig({ + systemConfigProxy: ISystemConfig(superchainAddrRegistry.getAddress("SystemConfigProxy", chainId)), + proxyAdmin: IProxyAdmin(superchainAddrRegistry.getAddress("ProxyAdmin", chainId)), + absolutePrestate: upgrades[chainId].absolutePrestate + }); + } + + (bool success,) = OPCM_TARGETS[0].delegatecall( + abi.encodeWithSelector(IOPCMPrestateUpdate.updatePrestate.selector, opChainConfigs) + ); + require(success, "OPCM.updatePrestate() failed"); + } + + /// @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(); + + for (uint256 i = 0; i < chains.length; i++) { + uint256 chainId = chains[i].chainId; + bytes32 expAbsolutePrestate = Claim.unwrap(upgrades[chainId].absolutePrestate); + string memory expErrors = upgrades[chainId].expectedValidationErrors; + address proxyAdmin = superchainAddrRegistry.getAddress("ProxyAdmin", chainId); + address sysCfg = superchainAddrRegistry.getAddress("SystemConfigProxy", chainId); + + IOPContractsManagerStandardValidator.ValidationInput memory input = IOPContractsManagerStandardValidator + .ValidationInput({ + proxyAdmin: IProxyAdmin(proxyAdmin), + sysCfg: ISystemConfig(sysCfg), + absolutePrestate: expAbsolutePrestate, + l2ChainID: chainId + }); + + IOPContractsManagerStandardValidator.ValidationOverrides memory overrides_ = + IOPContractsManagerStandardValidator.ValidationOverrides({ + l1PAOMultisig: superchainAddrRegistry.getAddress("ProxyAdminOwner", chainId), + challenger: superchainAddrRegistry.getAddress("Challenger", chainId) + }); + + string memory errors = + STANDARD_VALIDATOR.validateWithOverrides({_input: input, _allowFailure: true, _overrides: overrides_}); + + 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) {} +} + +interface IOPCMPrestateUpdate { + function updatePrestate(IOPContractsManager.OpChainConfig[] memory _prestateUpdateInputs) external; +} + +/// @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 ValidationInput { + IProxyAdmin proxyAdmin; + ISystemConfig sysCfg; + bytes32 absolutePrestate; + uint256 l2ChainID; + } + + struct ValidationOverrides { + address l1PAOMultisig; + address challenger; + } + + function validate(ValidationInput memory _input, bool _allowFailure) external view returns (string memory); + + function validateWithOverrides( + ValidationInput memory _input, + bool _allowFailure, + ValidationOverrides memory _overrides + ) external view returns (string memory); + + function version() external view returns (string memory); +} diff --git a/test/tasks/Regression.t.sol b/test/tasks/Regression.t.sol index b16ffbd3c6..f700e709c3 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 {OPCMUpdatePrestateV300} from "src/template/OPCMUpdatePrestateV300.sol"; +import {OPCMUpdatePrestateV410} from "src/template/OPCMUpdatePrestateV410.sol"; import {SetRespectedGameTypeTemplate} from "src/template/SetRespectedGameTypeTemplate.sol"; import {UpdateRetirementTimestampV200} from "src/template/UpdateRetirementTimestampV200.sol"; import {UpdateRetirementTimestampV400} from "src/template/UpdateRetirementTimestampV400.sol"; @@ -418,6 +419,35 @@ contract RegressionTest is Test { _assertDataToSignNestedMultisig(multisigTask, actions, expectedDataToSign, MULTICALL3_ADDRESS, rootSafe); } + /// @notice Expected call data and data to sign generated by manually running the OPCMUpdatePrestateV410 template at block 9327881 on sepolia + /// Simulate from task directory (test/tasks/example/sep/028-opcm-update-prestate-v410) with: + /// SIMULATE_WITHOUT_LEDGER=1 just --dotenv-path "$(pwd)/.env" --justfile ../../../../../src/justfile simulate + function testRegressionCallDataMatches_OPCMUpdatePrestateV410() public { + string memory taskConfigFilePath = "test/tasks/example/sep/028-opcm-update-prestate-v410/config.toml"; + // Call data generated by manually running the OPCMUpdatePrestateV410 template at block 9327881 on sepolia. + string memory expectedCallData = + "0x82ad56cb0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000200000000000000000000000003bb6437aba031afbf9cb3538fa064161e2bf2d780000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a49a72745b00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000001000000000000000000000000034edd2a225f7f429a63e0f1d2084b9e0a93b538000000000000000000000000189abaaaa82dfc015a588a7dbad6f13b1d3485bcdead00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"; + MultisigTask multisigTask = new OPCMUpdatePrestateV410(); + address rootSafe = address(0x1Eb2fFc903729a0F03966B917003800b145F56E2); + address foundationChildMultisig = 0xDEe57160aAfCF04c34C887B5962D0a69676d3C8B; + address[] memory allSafes = MultisigTaskTestHelper.getAllSafes(rootSafe, foundationChildMultisig); + + (Action[] memory actions, uint256[] memory allOriginalNonces) = + _setupAndSimulate(taskConfigFilePath, 9327881, "sepolia", multisigTask, allSafes); + + _assertCallDataMatches(multisigTask, actions, allSafes, allOriginalNonces, expectedCallData); + + // Data to sign generated by manually running the OPCMUpdatePrestateV410 template at block 9327881 on sepolia. + string[] memory expectedDataToSign = new string[](2); + // Foundation + expectedDataToSign[0] = + "0x190137e1f5dd3b92a004a23589b741196c8a214629d4ea3a690ec8e41ae45c689cbb08ad152c64b380ea271693325a628e4a7aa2d70ff311f22cc0ceff34c34d9877"; + // Security council + expectedDataToSign[1] = + "0x1901be081970e9fc104bd1ea27e375cd21ec7bb1eec56bfe43347c3e36c5d27b8533766facb22395ca23edfd1485fd285bfeb3c25354a43ebd02ef6f8d072c69aa14"; + _assertDataToSignNestedMultisig(multisigTask, actions, expectedDataToSign, MULTICALL3_ADDRESS, rootSafe); + } + /// @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/nested.just simulate diff --git a/test/tasks/example/sep/028-opcm-update-prestate-v410/.env b/test/tasks/example/sep/028-opcm-update-prestate-v410/.env new file mode 100644 index 0000000000..4387ef676b --- /dev/null +++ b/test/tasks/example/sep/028-opcm-update-prestate-v410/.env @@ -0,0 +1,3 @@ +TENDERLY_GAS=15000000 +FORK_BLOCK_NUMBER=9327881 +NESTED_SAFE_NAME_DEPTH_1=foundation diff --git a/test/tasks/example/sep/028-opcm-update-prestate-v410/config.toml b/test/tasks/example/sep/028-opcm-update-prestate-v410/config.toml new file mode 100644 index 0000000000..5fe50931a4 --- /dev/null +++ b/test/tasks/example/sep/028-opcm-update-prestate-v410/config.toml @@ -0,0 +1,24 @@ +templateName = "OPCMUpdatePrestateV410" + +[[l2chains]] +chainId = 11155420 +name = "OP Sepolia Testnet" + +[[opcmUpgrades]] +chainId = 11155420 +absolutePrestate = "0xdead000000000000000000000000000000000000000000000000000000000000" # Dummy test prestate +expectedValidationErrors = "OVERRIDES-L1PAOMULTISIG,OVERRIDES-CHALLENGER" # The template provides PAO and Challenger overrides to the validator from the SCR. These simply indicate overrides were used. + +[addresses] +OPCM = "0x3bb6437aba031afbf9cb3538fa064161e2bf2d78" # version 3.2.0 https://github.com/ethereum-optimism/superchain-registry/blob/40526b1288534f6b84b7aae21d13c0b5f5b12f47/validation/standard/standard-versions-sepolia.toml#L23 + +[stateOverrides] +0x1Eb2fFc903729a0F03966B917003800b145F56E2 = [ # L1PAO + {key = "0x0000000000000000000000000000000000000000000000000000000000000005", value = 37} +] +0xf64bc17485f0B4Ea5F06A96514182FC4cB561977 = [ # SC + {key = "0x0000000000000000000000000000000000000000000000000000000000000005", value = 48} +] +0xDEe57160aAfCF04c34C887B5962D0a69676d3C8B = [ # FUS + {key = "0x0000000000000000000000000000000000000000000000000000000000000005", value = 51} +] From 422600fa95c75b9e011110273350300c0328db13 Mon Sep 17 00:00:00 2001 From: smartcontracts <14298799+smartcontracts@users.noreply.github.com> Date: Thu, 9 Oct 2025 16:32:22 -0400 Subject: [PATCH 06/18] feat: mark 025, 025, 027 as executed (#1243) --- src/tasks/eth/025-zora-main-u13-to-u16a/README.md | 2 +- src/tasks/eth/026-metal-main-u13-to-u16a/README.md | 2 +- src/tasks/eth/027-mode-main-u13-to-u16a/README.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/tasks/eth/025-zora-main-u13-to-u16a/README.md b/src/tasks/eth/025-zora-main-u13-to-u16a/README.md index 27256cf2aa..f352351423 100644 --- a/src/tasks/eth/025-zora-main-u13-to-u16a/README.md +++ b/src/tasks/eth/025-zora-main-u13-to-u16a/README.md @@ -1,6 +1,6 @@ # 025-zora-main-u13-to-u16a -Status: [READY TO SIGN]() +Status: [EXECUTED](https://etherscan.io/tx/0x3c9df2c9f2502ed27df838f21bf474be0544246f8c0c3513a698d81e0c2890ae) ## Objective diff --git a/src/tasks/eth/026-metal-main-u13-to-u16a/README.md b/src/tasks/eth/026-metal-main-u13-to-u16a/README.md index 6bbef5997e..29646675b1 100644 --- a/src/tasks/eth/026-metal-main-u13-to-u16a/README.md +++ b/src/tasks/eth/026-metal-main-u13-to-u16a/README.md @@ -1,6 +1,6 @@ # 026-metal-main-u13-to-u16a -Status: [READY TO SIGN]() +Status: [EXECUTED](https://etherscan.io/tx/0x9c091d94c0f98efb21730224c89e59e50d344b74e677ee6de2b91d2a16f565d2) ## Objective diff --git a/src/tasks/eth/027-mode-main-u13-to-u16a/README.md b/src/tasks/eth/027-mode-main-u13-to-u16a/README.md index dd81242ade..f020cdc685 100644 --- a/src/tasks/eth/027-mode-main-u13-to-u16a/README.md +++ b/src/tasks/eth/027-mode-main-u13-to-u16a/README.md @@ -1,6 +1,6 @@ # 027-mode-main-u13-to-u16a -Status: [READY TO SIGN]() +Status: [EXECUTED](https://etherscan.io/tx/0x3ac45d51da454abfba887b5ab1dae831a78e068615893fb62d8034437bb17063) ## Objective From 862e119a5c66616bf36a7a40776af1afa0b5aa6e Mon Sep 17 00:00:00 2001 From: Wazabie <48911235+Wazabie@users.noreply.github.com> Date: Fri, 10 Oct 2025 06:52:13 +0200 Subject: [PATCH 07/18] Fusaka Prestate Update Unichain Sepolia Task (#1239) * Adding initial files * Add Ink * Base Adding initial files * Add initial Uni files * Update Base task * Update prestate * Update prestate * Update other tasks aligning with other PRs * Remove base task * Update src/tasks/sep/036-uni-sep-fusaka-prestate/VALIDATION.md Co-authored-by: Matt Solomon * Update src/tasks/sep/036-uni-sep-fusaka-prestate/VALIDATION.md Co-authored-by: Matt Solomon * Update config.toml * update number task * Update README.md * Update VALIDATION.md * Update README.md * Update VALIDATION.md --------- Co-authored-by: Matt Solomon --- .../sep/035-uni-sep-fusaka-prestate/.env | 1 + .../sep/035-uni-sep-fusaka-prestate/README.md | 21 +++++++++++ .../035-uni-sep-fusaka-prestate/VALIDATION.md | 36 +++++++++++++++++++ .../035-uni-sep-fusaka-prestate/config.toml | 19 ++++++++++ 4 files changed, 77 insertions(+) create mode 100644 src/tasks/sep/035-uni-sep-fusaka-prestate/.env create mode 100644 src/tasks/sep/035-uni-sep-fusaka-prestate/README.md create mode 100644 src/tasks/sep/035-uni-sep-fusaka-prestate/VALIDATION.md create mode 100644 src/tasks/sep/035-uni-sep-fusaka-prestate/config.toml diff --git a/src/tasks/sep/035-uni-sep-fusaka-prestate/.env b/src/tasks/sep/035-uni-sep-fusaka-prestate/.env new file mode 100644 index 0000000000..adf17d369c --- /dev/null +++ b/src/tasks/sep/035-uni-sep-fusaka-prestate/.env @@ -0,0 +1 @@ +TENDERLY_GAS=15000000 diff --git a/src/tasks/sep/035-uni-sep-fusaka-prestate/README.md b/src/tasks/sep/035-uni-sep-fusaka-prestate/README.md new file mode 100644 index 0000000000..04b86dc2f4 --- /dev/null +++ b/src/tasks/sep/035-uni-sep-fusaka-prestate/README.md @@ -0,0 +1,21 @@ +# 035-uni-sep-fusaka-prestate + +Status: [READY TO SIGN] + +## Objective + +This task uses `op-contract/v4.1.0` OPContractsManager to update the prestate of Unichain Sepolia to the Fusaka compatible prestate. + +## Simulation & Signing + +Simulation commands for each safe: +```bash +cd src/tasks/sep/035-uni-sep-fusaka-prestate +SIMULATE_WITHOUT_LEDGER=1 SKIP_DECODE_AND_PRINT=1 just --dotenv-path $(pwd)/.env simulate +``` + +Signing commands for each safe: +```bash +cd src/tasks/sep/035-uni-sep-fusaka-prestate +SKIP_DECODE_AND_PRINT=1 just --dotenv-path $(pwd)/.env sign +``` diff --git a/src/tasks/sep/035-uni-sep-fusaka-prestate/VALIDATION.md b/src/tasks/sep/035-uni-sep-fusaka-prestate/VALIDATION.md new file mode 100644 index 0000000000..e441c0a29b --- /dev/null +++ b/src/tasks/sep/035-uni-sep-fusaka-prestate/VALIDATION.md @@ -0,0 +1,36 @@ +# Validation + +This document can be used to validate the inputs and result of the execution of the upgrade transaction which you are +signing. + +The steps are: + +1. [Validate the Domain and Message Hashes](#expected-domain-and-message-hashes) +2. [Verifying the transaction input](#understanding-task-calldata) +3. [Verifying the state changes](#task-state-changes) + +## Expected Domain and Message Hashes + +First, we need to validate the domain and message hashes. These values should match both the values on your ledger and +the values printed to the terminal when you run the task. + +> [!CAUTION] +> +> Before signing, ensure the below hashes match what is on your ledger. +> +> ### L1PAO: `@0xd363339eE47775888Df411A163c586a8BdEA9dbf` +> +> - Domain Hash: `0x2fedecce87979400ff00d5cec4c77da942d43ab3b9db4a5ffc51bb2ef498f30b` +> - Message Hash: `0x9151c19e2511e6ddf5366deaf4592fd30dd41e0f7d3e371ed85190008ca6e566` +## Normalized State Diff Hash Attestation + +Ensure that the normalized state diff hash matches the output in your terminal. + +**Normalized hash:** `0x72702882bcde0413668da3fabcb6450df3c993b1892b9d881fa750e7cb9c5b52` + +## Task Calldata + +Calldata: +``` +0x82ad56cb0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000200000000000000000000000003bb6437aba031afbf9cb3538fa064161e2bf2d780000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a49a72745b00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000001000000000000000000000000aee94b9ab7752d3f7704bde212c0c6a0b701571d0000000000000000000000002bf403e5353a7a082ef6bb3ae2be3b866d8d3ea40339db503776757491b9f3038bf6f1d37b7988a2f75e823fe2656c1352ef2f9100000000000000000000000000000000000000000000000000000000 +``` \ No newline at end of file diff --git a/src/tasks/sep/035-uni-sep-fusaka-prestate/config.toml b/src/tasks/sep/035-uni-sep-fusaka-prestate/config.toml new file mode 100644 index 0000000000..fbbe6ffe00 --- /dev/null +++ b/src/tasks/sep/035-uni-sep-fusaka-prestate/config.toml @@ -0,0 +1,19 @@ +templateName = "OPCMUpdatePrestateV410" + +[[l2chains]] +chainId = 1301 +name = "Unichain Sepolia Testnet" + +[[opcmUpgrades]] +chainId = 1301 +absolutePrestate = "0x0339db503776757491b9f3038bf6f1d37b7988a2f75e823fe2656c1352ef2f91" # Fusaka Absolute Prestate published here https://github.com/ethereum-optimism/superchain-registry/blob/df5d4feb51f5d698d043a72ac2c6dfa9ce2a4afa/validation/standard/standard-prestates.toml#L6C7-L6C73 +expectedValidationErrors = "OVERRIDES-L1PAOMULTISIG,OVERRIDES-CHALLENGER" # The template provides PAO and Challenger overrides to the validator from the SCR. These simply indicate overrides were used. + +[addresses] +OPCM = "0x3bb6437aba031afbf9cb3538fa064161e2bf2d78" # version 3.2.0 https://github.com/ethereum-optimism/superchain-registry/blob/df5d4feb51f5d698d043a72ac2c6dfa9ce2a4afa/validation/standard/standard-versions-sepolia.toml#L23 + +[stateOverrides] +# Unichain Sepolia ProxyAdminOwner: +0xd363339eE47775888Df411A163c586a8BdEA9dbf = [ # L1PAO + {key = "0x0000000000000000000000000000000000000000000000000000000000000005", value = 38} +] From 8ccd90724b5eaf402ff160e4a2eb4aba3d704719 Mon Sep 17 00:00:00 2001 From: Wazabie <48911235+Wazabie@users.noreply.github.com> Date: Fri, 10 Oct 2025 06:52:53 +0200 Subject: [PATCH 08/18] Update OP-Ink Fusaka task status to 'READY TO SIGN' (#1245) * Update task status to 'READY TO SIGN' * Update VALIDATION.md --- src/tasks/sep/034-op-ink-sep-fusaka-prestate/README.md | 2 +- src/tasks/sep/034-op-ink-sep-fusaka-prestate/VALIDATION.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/tasks/sep/034-op-ink-sep-fusaka-prestate/README.md b/src/tasks/sep/034-op-ink-sep-fusaka-prestate/README.md index 98874bbf3d..13bba45d9f 100644 --- a/src/tasks/sep/034-op-ink-sep-fusaka-prestate/README.md +++ b/src/tasks/sep/034-op-ink-sep-fusaka-prestate/README.md @@ -1,6 +1,6 @@ # 034-op-ink-sep-fusaka-prestate -Status: [DRAFT, NOT READY TO SIGN] +Status: [READY TO SIGN] ## Objective diff --git a/src/tasks/sep/034-op-ink-sep-fusaka-prestate/VALIDATION.md b/src/tasks/sep/034-op-ink-sep-fusaka-prestate/VALIDATION.md index 6228ec8d76..00625948ea 100644 --- a/src/tasks/sep/034-op-ink-sep-fusaka-prestate/VALIDATION.md +++ b/src/tasks/sep/034-op-ink-sep-fusaka-prestate/VALIDATION.md @@ -30,7 +30,7 @@ the values printed to the terminal when you run the task. > - Message Hash: `0x0100ad527dc0a5bfde1487c1e459f345375c236cdcdd25855dbe4ce4a1ea023e` ## Normalized State Diff Hash Attestation -The normalized state diff hash **MUST** match the hash produced by the state changes attested to in the state diff audit report. As a signer, you are responsible for verifying that this hash is correct. Please compare the hash below with the one in the audit report. If no audit report is available for this task, you must still ensure that the normalized state diff hash matches the output in your terminal. +Ensure that the normalized state diff hash matches the output in your terminal. **Normalized hash:** `0x46c2ce3b0ad59afaa4b38c2240580cd823c763fc52dccb3b4e3631b7e500409e` From 9defeb9ffe79020d0d77a590362be2ac273ca27a Mon Sep 17 00:00:00 2001 From: Matt Solomon Date: Fri, 10 Oct 2025 06:31:31 -0700 Subject: [PATCH 09/18] chore: removes all code related to the normalized state diff hash (#1244) --- README.md | 31 +- src/libraries/AccountAccessParser.sol | 221 +-------- src/libraries/MultisigTaskPrinter.sol | 11 - src/tasks/MultisigTask.sol | 23 +- src/tasks/TaskManager.sol | 30 +- .../boilerplate/VALIDATION.template.md | 13 +- test/libraries/AccountAccessParser.t.sol | 454 ------------------ test/tasks/MultisigTask.t.sol | 2 +- test/tasks/NestedMultisigTask.t.sol | 10 +- test/tasks/Regression.t.sol | 2 +- test/tasks/SingleMultisigTask.t.sol | 4 +- test/tasks/StateOverrideManager.t.sol | 4 +- test/tasks/TaskManager.t.sol | 41 +- 13 files changed, 37 insertions(+), 809 deletions(-) diff --git a/README.md b/README.md index c30a19633c..040a3e64ae 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ > **⚠️ Important Notice: System Upgrade (August 2025)** -> +> > The Superchain-ops system has undergone a significant upgrade. For access to historical executed tasks and previous system documentation, please refer to this [tag](https://github.com/ethereum-optimism/superchain-ops/tree/legacy-superchain-ops) for the archived tasks repository. > > • Need help? [Create an issue](https://github.com/ethereum-optimism/superchain-ops/issues) on this repo. @@ -86,7 +86,7 @@ The `[stateOverrides]` TOML table is optional, but in most cases we use it to sp ```toml # USE HEX ENCODED STRINGS WHEN POSSIBLE. [stateOverrides] -0x847B5c174615B1B7fDF770882256e2D3E95b9D92 = [ +0x847B5c174615B1B7fDF770882256e2D3E95b9D92 = [ { key = "0x0000000000000000000000000000000000000000000000000000000000000005", value = "0x0000000000000000000000000000000000000000000000000000000000000017" } ] ``` @@ -95,7 +95,7 @@ However, in some cases it's possible to use the decimal value directly: ```toml # IN SOME CASES, YOU CAN USE THE DECIMAL VALUE DIRECTLY. [stateOverrides] -0x847B5c174615B1B7fDF770882256e2D3E95b9D92 = [ +0x847B5c174615B1B7fDF770882256e2D3E95b9D92 = [ { key = "0x0000000000000000000000000000000000000000000000000000000000000005", value = 23 } ] ``` @@ -104,7 +104,7 @@ But **do not** pass the decimal value as a string—this will cause undefined be ```toml # ❌ INCORRECT: DO NOT USE STRINGIFIED DECIMALS. [stateOverrides] -0x847B5c174615B1B7fDF770882256e2D3E95b9D92 = [ +0x847B5c174615B1B7fDF770882256e2D3E95b9D92 = [ { key = "0x0000000000000000000000000000000000000000000000000000000000000005", value = "23" } ] ``` @@ -127,7 +127,7 @@ just --dotenv-path $(pwd)/.env simulate [child-safe-name-depth-1] [child-safe-na - **Nested Safe Operations** (see [NESTED.md](src/NESTED.md)): ```bash just --dotenv-path $(pwd)/.env simulate foundation - just --dotenv-path $(pwd)/.env simulate council + just --dotenv-path $(pwd)/.env simulate council just --dotenv-path $(pwd)/.env simulate chain-governor ``` @@ -149,7 +149,7 @@ just simulate-stack [child-safe-name-depth-1] [child-safe- 7. Fill out the `README.md` and `VALIDATION.md` files. - If your task status is not `EXECUTED` or `CANCELLED`, it is considered non-terminal and will automatically be included in stacked simulations. - - If your task has a `VALIDATION.md` file, you **must** fill out the `Normalized State Diff Hash Attestation` section. This is so that we can detect if the normalized state diff hash changes unexpectedly. You **must** also fill out the `Expected Domain and Message Hashes` section. This is so that we can detect if the domain and message hashes change unexpectedly. Any mismatches will cause the task to revert. + - If your task has a `VALIDATION.md` file, you **must** fill out the `Expected Domain and Message Hashes` section. This is so that we can detect if the domain and message hashes change unexpectedly. Any mismatches will cause the task to revert. ## FAQ @@ -163,7 +163,7 @@ Stacked simulations are supported. To use this feature, you can use the followin just simulate-stack [task] [child-safe-name-depth-1] [child-safe-name-depth-2] ``` -e.g. +e.g. ```bash just simulate-stack eth # Simulate all tasks for ethereum just simulate-stack eth 001-example # Simulate specific task on root safe @@ -282,7 +282,7 @@ By adding an address to [`addresses.toml`](./src/addresses.toml), you ensure it' ### What if I want to upgrade a chain that is not in the superchain-registry? -If the chain you want to upgrade is not in the [superchain-registry](https://github.com/ethereum-optimism/superchain-registry), you can manually provide a fallback JSON file in your task's `config.toml` (as `fallbackAddressesJsonPath`). +If the chain you want to upgrade is not in the [superchain-registry](https://github.com/ethereum-optimism/superchain-registry), you can manually provide a fallback JSON file in your task's `config.toml` (as `fallbackAddressesJsonPath`). ```toml l2chains = [{name = "Unichain", chainId = 1333330}] @@ -298,19 +298,6 @@ When the task runs, it will first attempt to use the superchain-registry. If the > ⚠️ **Note**: You must manually provide all contract addresses required by your task template in the fallback JSON file. -### What is the 'Normalized State Diff Hash'? - -The normalized state diff hash is a single fingerprint of all the onchain state changes your task would make if executed. We “normalize” the diff first (stable ordering and encoding) so the hash only changes when the actual intended state changes do. - -- Why it exists: To make it easy for reviewers and signers to verify they’re approving the exact same change set that was simulated and reviewed. -- Where to see it: It’s printed at the end of simulation/stacked simulation and recorded in each task’s `VALIDATION.md` under “Normalized State Diff Hash Attestation”. -- When it changes: If any task state we consider meaningful changes, the hash will also change. - -What to do if it doesn’t match: -- **Do not proceed or sign**. -- Contact the task authors and update the task only after review. - - ## Available Templates -All available templates can be found in the [template](src/template/) directory. \ No newline at end of file +All available templates can be found in the [template](src/template/) directory. diff --git a/src/libraries/AccountAccessParser.sol b/src/libraries/AccountAccessParser.sol index 4068af7f6c..e0564c5e8c 100644 --- a/src/libraries/AccountAccessParser.sol +++ b/src/libraries/AccountAccessParser.sol @@ -13,7 +13,7 @@ import {Utils} from "src/libraries/Utils.sol"; /// @notice Parses account accesses into decoded transfers and state diffs. /// The core methods intended to be part of the public interface are `decodeAndPrint`, `decode`, -/// `getUniqueWrites`, `getStateDiffFor`, and `normalizedStateDiffHash`. Example usage: +/// `getUniqueWrites`, and `getStateDiffFor`. Example usage: /// /// ```solidity /// contract MyContract { @@ -37,9 +37,6 @@ import {Utils} from "src/libraries/Utils.sol"; /// /// // Get all new contracts created. /// address[] memory newContracts = accountAccesses.getNewContracts(); -/// -/// // Get the normalized state diff hash. -/// bytes32 normalizedStateDiffHash = accountAccesses.normalizedStateDiffHash(parentMultisig, txHash); /// } /// } /// ``` @@ -87,14 +84,6 @@ library AccountAccessParser { address tokenAddress; } - // This struct represents a state change with the account information - struct AccountStateDiff { - address who; - bytes32 slot; - bytes32 firstOld; - bytes32 lastNew; - } - // Leading underscore because some of the raw keys are reserved words in Solidity, and we need // the keys to be ordered alphabetically here for foundry. struct JsonStorageLayout { @@ -139,7 +128,7 @@ library AccountAccessParser { bytes32 internal constant ANCHOR_STATE_REGISTRY_V410_PROPOSAL_ROOT_SLOT = bytes32(uint256(3)); bytes32 internal constant ANCHOR_STATE_REGISTRY_V410_PROPOSAL_L2_SEQUENCE_NUMBER_SLOT = bytes32(uint256(4)); bytes32 internal constant ANCHOR_STATE_REGISTRY_V410_RETIREMENT_TIMESTAMP_SLOT = bytes32(uint256(6)); - + // op-contracts/v3.0.0 - AnchorStateRegistry version: 2.2.2 bytes32 internal constant ANCHOR_STATE_REGISTRY_V300_OUTPUT_ROOT_STARTING_ANCHOR_ROOT_SLOT = bytes32(uint256(4)); bytes32 internal constant ANCHOR_STATE_REGISTRY_V300_OUTPUT_ROOT_L2_BLOCK_NUMBER_SLOT = bytes32(uint256(5)); @@ -267,212 +256,6 @@ library AccountAccessParser { } } - /// @notice Computes a hash of the normalized state diff from account accesses. The spec for - /// method is: - /// 1. The input is an array of `VmSafe.AccountAccess[]` containing all storage writes. - /// 2. It calls `AccountAccessParser.getUniqueWrites` to filter down all storage writes to a state diff - /// 3. With that state diff, we normalize it by removing data from the state diff array that - /// may change between initial simulation and execution. Removal is done by simply removing - /// the entry from the `AccountAccess[]` array. The set of state changes to remove is: - /// 1. If the state change is an EOA nonce increment, remove it. - /// 2. Remove the following state changes from Gnosis Safes: - /// 1. Nonce increments. - /// 2. Setting an approve hash in storage (the hash is dependent on the nonce, which may change) - /// 3. If the storage slot contains a timestamp, normalize that timestamp to be all zeroes. - /// 1. This will have to be informed by knowing the storage layouts, which is ok - /// 2. We should only normalize the specific section of the slot corresponding to the - /// timestamp, since some timestamps are packed into slots with other data. - /// 4. If the slot is on the LivenessGuard, remove it. - /// 5. The hash to return is computed as `keccak256(abi.encode(normalizedArray))`. - /// @return bytes32 hash of the normalized state diff - function normalizedStateDiffHash( - VmSafe.AccountAccess[] memory _accountAccesses, - address _parentMultisig, - bytes32 _txHash - ) internal view noGasMetering returns (bytes32) { - // Get all storage writes as a state diff. - address[] memory uniqueAddresses = getUniqueWrites({accesses: _accountAccesses, _sort: false}); - - // Create a temporary array to store normalized state changes. - AccountStateDiff[] memory normalizedChanges = new AccountStateDiff[](MAX_STATE_CHANGES); - uint256 normalizedCount = 0; - - // Process each account with storage writes. - for (uint256 i = 0; i < uniqueAddresses.length; i++) { - address account = uniqueAddresses[i]; - StateDiff[] memory diffs = getStateDiffFor({accesses: _accountAccesses, who: account, _sort: false}); - - // Process each diff and apply normalization logic. - for (uint256 j = 0; j < diffs.length; j++) { - StateDiff memory diff = diffs[j]; - if (shouldIncludeDiff(account, diff, _parentMultisig, _txHash)) { - diff = normalizeTimestamp(account, diff); // Normalize the timestamp if present. - normalizedChanges[normalizedCount] = AccountStateDiff({ - who: account, - slot: diff.slot, - firstOld: diff.oldValue, - lastNew: diff.newValue - }); - normalizedCount++; - require(normalizedCount < MAX_STATE_CHANGES, "AccountAccessParser: Max state changes reached"); - } - } - } - - // Create the final array with the correct size. - AccountStateDiff[] memory finalArray = new AccountStateDiff[](normalizedCount); - for (uint256 i = 0; i < normalizedCount; i++) { - finalArray[i] = normalizedChanges[i]; - } - - // Return keccak256 hash of the abi-encoded normalized array. - return keccak256(abi.encode(finalArray)); - } - - function shouldIncludeDiff(address account, StateDiff memory diff, address _parentMultisig, bytes32 _txHash) - internal - view - returns (bool) - { - if (isEOANonceIncrement(account, diff)) { - // 1. If the state change is an EOA nonce increment, remove it. - return false; - } else if (isGnosisSafe(account)) { - // 2. Remove Gnosis Safe nonce increment and approve hash changes. - if (isGnosisSafeNonceIncrement(diff) || isGnosisSafeApproveHash(diff, _parentMultisig, _txHash)) { - // 2.1 Nonce increment or 2.2 Setting an approve hash in storage. - return false; - } - } else if (isLivenessGuardTimestamp(account, diff, _parentMultisig)) { - // 4. If the slot is on the LivenessGuard, don't include it. - return false; - } else if (isOptimismPortalResourceMetering(diff)) { - // 5. If the slot is on the OptimismPortalResourceParams, don't include it. - return false; - } else if (isAnchorStateRegistryProposal(account, diff)) { - // 6. If the diff is an AnchorStateRegistry Proposal, don't include it. - return false; - } - return true; - } - - /// @notice Any function in the OptimismPortal that has the 'metered' modifier will have a non-deterministic state change. - function isOptimismPortalResourceMetering(StateDiff memory _diff) internal view returns (bool) { - if (_diff.slot == OPTIMISM_PORTAL_RESOURCE_PARAMS_SLOT) { - // Extract prevBlockNum from the packed value. It's located in the most significant 64 bits. - // ResourceParams is packed as follows: prevBlockNum (64 bits) | prevBoughtGas (64 bits) | prevBaseFee (128 bits) - uint256 prevBlockNum = uint64(uint256(_diff.newValue) >> (128 + 64)); - // If the current block number is equal to the new values prevBlockNum, then we should remove this - // state change because it means we have a nondeterministic change based on block number at simulation time - return block.number == prevBlockNum; - } - return false; - } - - /// @notice Checks if the state diff represents an EOA nonce increment - function isEOANonceIncrement(address _account, StateDiff memory _diff) internal view returns (bool) { - uint256 codeSize = _account.code.length; - return codeSize == 0 && _diff.slot == bytes32(0) && uint256(_diff.newValue) == uint256(_diff.oldValue) + 1; - } - - /// @notice Checks if the state diff represents a Gnosis Safe nonce increment - function isGnosisSafeNonceIncrement(StateDiff memory _diff) internal pure returns (bool) { - // In Gnosis Safe, the nonce is stored at slot 5. See `GnosisSafeStorage.sol` to verify. - return _diff.slot == GNOSIS_SAFE_NONCE_SLOT && uint256(_diff.newValue) == uint256(_diff.oldValue) + 1; - } - - /// @notice Checks if the state diff represents setting an approve hash in a Gnosis Safe - function isGnosisSafeApproveHash(StateDiff memory _diff, address _parentMultisig, bytes32 _txHash) - internal - view - returns (bool) - { - bytes32[] memory hashSlots = calculateApproveHashSlots(IGnosisSafe(_parentMultisig).getOwners(), _txHash); - for (uint256 i = 0; i < hashSlots.length; i++) { - if (_diff.slot == hashSlots[i]) { - require( - (_diff.oldValue == bytes32(0) && _diff.newValue == bytes32(uint256(1))) - // Some Gnosis Safe versions set approvedHashes to zero upon execution e.g. mainnet FoundationOperationsSafe. - || (_diff.oldValue == bytes32(uint256(1)) && _diff.newValue == bytes32(0)), - "AccountAccessParser: Unexpected approve hash state change." - ); - return true; - } - } - return false; - } - - /// @notice Checks if the given slot matches any liveness guard timestamp for the signers on child multisigs. - function isLivenessGuardTimestamp(address _account, StateDiff memory _diff, address _parentMultisig) - internal - view - returns (bool) - { - if (!isLivenessGuard(_account)) { - return false; - } - - return _checkOwnersForLivenessSlot(_parentMultisig, _diff.slot); - } - - /// @notice Checks if the given slot matches any liveness guard timestamp for owners and nested safe owners - function _checkOwnersForLivenessSlot(address _multisig, bytes32 _slot) internal view returns (bool) { - address[] memory owners = IGnosisSafe(_multisig).getOwners(); - - for (uint256 i = 0; i < owners.length; i++) { - // Check if this owner's liveness slot matches - if (_isOwnerLivenessSlot(owners[i], _slot)) { - return true; - } - - // If this owner is a nested safe, check its owners too - if (isGnosisSafe(owners[i])) { - address[] memory nestedOwners = IGnosisSafe(owners[i]).getOwners(); - for (uint256 j = 0; j < nestedOwners.length; j++) { - if (_isOwnerLivenessSlot(nestedOwners[j], _slot)) { - return true; - } - } - } - } - - return false; - } - - /// @notice Checks if the given slot matches the liveness guard timestamp slot for a specific owner - function _isOwnerLivenessSlot(address _owner, bytes32 _slot) internal pure returns (bool) { - bytes32 ownerSlot = keccak256(abi.encode(_owner, LIVENESS_GUARD_LAST_LIVE_SLOT)); - return _slot == ownerSlot; - } - - function isAnchorStateRegistryProposal(address _account, StateDiff memory _diff) internal view returns (bool) { - if (isAnchorStateRegistry(_account)) { - // The proposal is stored in slot 3 and 4. - if (isAnchorStateRegistryV410(_account)) { - return _diff.slot == ANCHOR_STATE_REGISTRY_V410_PROPOSAL_ROOT_SLOT - || _diff.slot == ANCHOR_STATE_REGISTRY_V410_PROPOSAL_L2_SEQUENCE_NUMBER_SLOT; - } else if (isAnchorStateRegistryV300(_account)) { - return _diff.slot == ANCHOR_STATE_REGISTRY_V300_OUTPUT_ROOT_STARTING_ANCHOR_ROOT_SLOT - || _diff.slot == ANCHOR_STATE_REGISTRY_V300_OUTPUT_ROOT_L2_BLOCK_NUMBER_SLOT; - } - } - return false; - } - - /// @notice Normalizes a timestamp in a storage slot by zeroing out only the timestamp portion if present. - function normalizeTimestamp(address _account, StateDiff memory _diff) internal view returns (StateDiff memory) { - if (_diff.slot == ANCHOR_STATE_REGISTRY_V410_RETIREMENT_TIMESTAMP_SLOT) { - if (isAnchorStateRegistry(_account)) { - // The retirementTimestamp is introduced in the AnchorStateRegistry post op-contracts/v3.0.0-rc.2. - // Define a static mask to zero out 64 bits at offset 4 in little-endian format - bytes32 MASK = bytes32(0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0000000000000000FFFFFFFF); - // Apply the static mask to zero out the specified bytes in the new value - _diff.newValue &= MASK; - } - } - return _diff; - } - /// @notice Extracts all unique storage writes (i.e. writes where the value has actually changed) function getUniqueWrites(VmSafe.AccountAccess[] memory accesses, bool _sort) internal diff --git a/src/libraries/MultisigTaskPrinter.sol b/src/libraries/MultisigTaskPrinter.sol index 5d999b5b2f..8637b140e0 100644 --- a/src/libraries/MultisigTaskPrinter.sol +++ b/src/libraries/MultisigTaskPrinter.sol @@ -104,17 +104,6 @@ library MultisigTaskPrinter { // ======= Verification Information ======== // ========================================== - /// @notice Prints normalized state diff hash. - function printNormalizedStateDiffHash(bytes32 normalizedStateDiffHash) internal pure { - printTitle("NORMALIZED STATE DIFF HASH"); - // forgefmt: disable-start - console.log("Ensure the normalized state diff hash from your simulation matches the hash in the VALIDATION.md file for this task."); - console.log(""); - console.log("Normalized hash: %s", vm.toString(normalizedStateDiffHash)); - console.log(""); - // forgefmt: disable-end - } - /// @notice Prints an OP-TxVerify link for transaction verification. function printOPTxVerifyLink( uint256 chainId, diff --git a/src/tasks/MultisigTask.sol b/src/tasks/MultisigTask.sol index 6bcb44d496..acbca1e98a 100644 --- a/src/tasks/MultisigTask.sol +++ b/src/tasks/MultisigTask.sol @@ -79,7 +79,7 @@ abstract contract MultisigTask is Test, Script, StateOverrideManager, TaskManage /// This works by printing the 'data to sign' for the nested safe which is then passed to the eip712sign binary for signing. function simulate(string memory taskConfigFilePath, address[] memory _childSafes) public - returns (VmSafe.AccountAccess[] memory, Action[] memory, bytes32, bytes memory, address) + returns (VmSafe.AccountAccess[] memory, Action[] memory, bytes memory, address) { return _runTask(taskConfigFilePath, "", _childSafes, true); } @@ -89,7 +89,7 @@ abstract contract MultisigTask is Test, Script, StateOverrideManager, TaskManage public returns (VmSafe.AccountAccess[] memory) { - (VmSafe.AccountAccess[] memory accountAccesses,,,,) = + (VmSafe.AccountAccess[] memory accountAccesses,,,) = _runTask(taskConfigFilePath, signatures, _childSafes, false); return accountAccesses; } @@ -574,16 +574,7 @@ abstract contract MultisigTask is Test, Script, StateOverrideManager, TaskManage bytes memory _signatures, address[] memory _childSafes, bool _isSimulate - ) - internal - returns ( - VmSafe.AccountAccess[] memory, - Action[] memory, - bytes32 normalizedHash_, - bytes memory dataToSign_, - address rootSafe - ) - { + ) internal returns (VmSafe.AccountAccess[] memory, Action[] memory, bytes memory dataToSign_, address rootSafe) { (TaskPayload memory payload, Action[] memory actions) = _taskSetup(_taskConfigFilePath, _childSafes); uint256 rootSafeIndex = payload.safes.length - 1; rootSafe = payload.safes[rootSafeIndex]; @@ -591,14 +582,14 @@ abstract contract MultisigTask is Test, Script, StateOverrideManager, TaskManage executeTaskStep(_signatures, payload, rootSafeIndex); validate(accountAccesses, actions, payload); - (normalizedHash_, dataToSign_) = print(accountAccesses, _isSimulate, txHash, payload); + dataToSign_ = print(accountAccesses, _isSimulate, txHash, payload); // Sanity check that the root safe is a nested safe. if (payload.safes.length > 1) { require(isNestedSafe(rootSafe), "MultisigTask: multisig must be a nested safe."); } - return (accountAccesses, actions, normalizedHash_, dataToSign_, rootSafe); + return (accountAccesses, actions, dataToSign_, rootSafe); } /// @notice Using the tasks config.toml file, this function configures the task. @@ -776,7 +767,7 @@ abstract contract MultisigTask is Test, Script, StateOverrideManager, TaskManage bool isSimulate, bytes32 txHash, TaskPayload memory payload - ) public returns (bytes32 normalizedHash_, bytes memory dataToSign_) { + ) public returns (bytes memory dataToSign_) { console.log(""); MultisigTaskPrinter.printWelcomeMessage(); SafeData memory rootSafe = Utils.getSafeData(payload, payload.safes.length - 1); @@ -810,8 +801,6 @@ abstract contract MultisigTask is Test, Script, StateOverrideManager, TaskManage } _printTenderlySimulationData(payload); } - normalizedHash_ = AccountAccessParser.normalizedStateDiffHash(accountAccesses, rootSafe.safe, txHash); - MultisigTaskPrinter.printNormalizedStateDiffHash(normalizedHash_); } /// @notice Helper function to print the final safe information. diff --git a/src/tasks/TaskManager.sol b/src/tasks/TaskManager.sol index bcc31c3660..7f77ab4d25 100644 --- a/src/tasks/TaskManager.sol +++ b/src/tasks/TaskManager.sol @@ -97,7 +97,7 @@ contract TaskManager is Script { function executeTask(TaskConfig memory config, address[] memory _childSafes) public - returns (VmSafe.AccountAccess[] memory accesses_, bytes32 normalizedHash_, bytes memory dataToSign_) + returns (VmSafe.AccountAccess[] memory accesses_, bytes memory dataToSign_) { // Deploy and run the template string memory templatePath = string.concat("out/", config.templateName, ".sol/", config.templateName, ".json"); @@ -110,16 +110,7 @@ contract TaskManager is Script { string[] memory parts = vm.split(config.basePath, "/"); string memory taskName = parts[parts.length - 1]; - (accesses_, normalizedHash_, dataToSign_) = execute(config, task, _childSafes, taskName, formattedRootSafe); - require( - checkNormalizedHash(normalizedHash_, config), - string.concat( - "TaskManager: Normalized hash for task: ", - taskName, - " does not match. Got: ", - vm.toString(normalizedHash_) - ) - ); + (accesses_, dataToSign_) = execute(config, task, _childSafes, taskName, formattedRootSafe); require( checkDataToSign(dataToSign_, config), string.concat( @@ -138,7 +129,7 @@ contract TaskManager is Script { address[] memory _childSafes, string memory _taskName, string memory _formattedParentMultisig - ) private returns (VmSafe.AccountAccess[] memory accesses_, bytes32 normalizedHash_, bytes memory dataToSign_) { + ) private returns (VmSafe.AccountAccess[] memory accesses_, bytes memory dataToSign_) { string memory line = unicode"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"; if (_config.isNested) { @@ -153,7 +144,7 @@ contract TaskManager is Script { console.log(""); address[] memory allSafes = Solarray.extend(_childSafes, Solarray.addresses(address(_config.rootSafe))); Utils.validateSafesOrder(allSafes); - (accesses_,, normalizedHash_, dataToSign_,) = _task.simulate(_config.configPath, _childSafes); + (accesses_,, dataToSign_,) = _task.simulate(_config.configPath, _childSafes); } else { // forgefmt: disable-start console.log(string.concat("SIMULATING SINGLE TASK: ", _taskName, " FOR ROOT SAFE: ", _formattedParentMultisig)); @@ -164,7 +155,7 @@ contract TaskManager is Script { _childSafes.length == 0, "TaskManager: child safes provided but not expected for a single safe task." ); - (accesses_,, normalizedHash_, dataToSign_,) = _task.simulate(_config.configPath, new address[](0)); + (accesses_,, dataToSign_,) = _task.simulate(_config.configPath, new address[](0)); } } @@ -191,17 +182,6 @@ contract TaskManager is Script { return false; } - /// @notice Cross check most recent normalized hash with normalized hash stored in VALIDATION markdown file. - /// @return 'false' when VALIDATION file is empty or contains the wrong hash. 'true' when VALIDATION file does not exist or contains the correct hash. - function checkNormalizedHash(bytes32 _normalizedHash, TaskConfig memory _config) public view returns (bool) { - bytes memory normalizedHashBytes = abi.encodePacked(_normalizedHash); - return checkValidationFile( - normalizedHashBytes, - _config, - "Normalized hash does not match. Please check that you've added it to the VALIDATION markdown file." - ); - } - /// @notice Cross check most recent data to sign with the domain and message hashes stored in VALIDATION markdown file. /// @return 'false' when VALIDATION file is empty or contains the wrong data to sign. 'true' when VALIDATION file does not exist or contains the correct data to sign. function checkDataToSign(bytes memory _dataToSign, TaskConfig memory _config) public view returns (bool) { diff --git a/src/template/boilerplate/VALIDATION.template.md b/src/template/boilerplate/VALIDATION.template.md index 5dc51a1ef7..680cd7115e 100644 --- a/src/template/boilerplate/VALIDATION.template.md +++ b/src/template/boilerplate/VALIDATION.template.md @@ -8,9 +8,8 @@ signing. The steps are: 1. [Expected Domain and Message Hashes](#expected-domain-and-message-hashes) -2. [Normalized State Diff Hash Attestation](#normalized-state-diff-hash-attestation) -3. [Understanding Task Calldata](#understanding-task-calldata) -4. [Task State Changes](#task-state-changes) +2. [Understanding Task Calldata](#understanding-task-calldata) +3. [Task State Changes](#task-state-changes) ## Expected Domain and Message Hashes @@ -26,12 +25,6 @@ the values printed to the terminal when you run the task. > - Domain Hash: `` > - Message Hash: `` -## Normalized State Diff Hash Attestation - -The normalized state diff hash is a single fingerprint of all the onchain state changes your task would make if executed. We “normalize” the diff first (stable ordering and encoding) so the hash only changes when the actual intended state changes do. You **MUST** ensure that the normalized hash produced from your simulation matches the normalized hash in this document. - -**Normalized hash:** `` - ## Understanding Task Calldata The command to encode the calldata is: @@ -59,4 +52,4 @@ Note: The changes listed below do not include threshold, nonce and owner mapping ### Task State Changes -TODO: You can copy the markdown state changes printed in the terminal and paste them here. \ No newline at end of file +TODO: You can copy the markdown state changes printed in the terminal and paste them here. diff --git a/test/libraries/AccountAccessParser.t.sol b/test/libraries/AccountAccessParser.t.sol index 1e2fafff22..ce74111d1b 100644 --- a/test/libraries/AccountAccessParser.t.sol +++ b/test/libraries/AccountAccessParser.t.sol @@ -1094,79 +1094,6 @@ contract AccountAccessParser_decodeAndPrint_Test is Test { assertEq(blobbasefeeScalar, "1014213", "Failed to extract uint32 from bytes32"); } - /// The retirementTimestamp is introduced in the AnchorStateRegistry post op-contracts/v3.0.0-rc.2 - function test_normalizeTimestamp_AnchorStateRegistry_retirementTimestamp() public { - vm.createSelectFork("mainnet", 22319975); - address anchorStateRegistry = address(0x1c68ECfbf9C8B1E6C0677965b3B9Ecf9A104305b); // op mainnet AnchorStateRegistryProxy - // [offset: 8, bytes: 8, value: 0xFFFFFFFFFFFFFFFF, name: retirementTimestamp] - bytes32 newValue1 = bytes32(uint256(0x0000000000000000000000000000000000000000FFFFFFFFFFFFFFFF00000000)); - AccountAccessParser.StateDiff memory diff1 = AccountAccessParser.StateDiff({ - slot: bytes32(uint256(6)), - oldValue: bytes32(uint256(0)), - newValue: newValue1 - }); - AccountAccessParser.normalizeTimestamp(anchorStateRegistry, diff1); - assertEq(diff1.newValue, bytes32(uint256(0))); - - bytes32 newValue2 = bytes32(uint256(0x0000000000000000000000000000000000000000000000000000000000000000)); - AccountAccessParser.StateDiff memory diff2 = AccountAccessParser.StateDiff({ - slot: bytes32(uint256(6)), - oldValue: bytes32(uint256(0)), - newValue: newValue2 - }); - AccountAccessParser.normalizeTimestamp(anchorStateRegistry, diff2); - assertEq(diff2.newValue, bytes32(uint256(0)), "Value changed for op mainnet AnchorStateRegistryProxy"); - - bytes32 newValue3 = bytes32(uint256(0x0000000000000000000000000000000000000000FFFFFF0FFFFFFFFF00000000)); - AccountAccessParser.StateDiff memory diff3 = AccountAccessParser.StateDiff({ - slot: bytes32(uint256(6)), - oldValue: bytes32(uint256(0)), - newValue: newValue3 - }); - AccountAccessParser.normalizeTimestamp(anchorStateRegistry, diff3); - assertEq(diff3.newValue, bytes32(uint256(0)), "Value changed for op mainnet AnchorStateRegistryProxy"); - - bytes32 newValue4 = bytes32(uint256(0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0000000010000000FFFFFFFF)); - AccountAccessParser.StateDiff memory diff4 = AccountAccessParser.StateDiff({ - slot: bytes32(uint256(6)), - oldValue: bytes32(uint256(0)), - newValue: newValue4 - }); - AccountAccessParser.normalizeTimestamp(anchorStateRegistry, diff4); - assertEq( - diff4.newValue, - bytes32(uint256(0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0000000000000000FFFFFFFF)), - "Value changed for op mainnet AnchorStateRegistryProxy" - ); - } - - function test_normalizeTimestamp_noChangeOnWrongContract() public view { - bytes32 original = bytes32(uint256(0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF)); - - AccountAccessParser.StateDiff memory diff = - AccountAccessParser.StateDiff({slot: bytes32(uint256(6)), oldValue: bytes32(0), newValue: original}); - - diff = AccountAccessParser.normalizeTimestamp(address(0x1234), diff); // wrong contract - - // Should remain unchanged because contract doesn't match - assertEq(diff.newValue, original, "Value changed for wrong contract"); - } - - function test_normalizeTimestamp_noChangeOnWrongSlot() public view { - bytes32 original = bytes32(uint256(0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF)); - - AccountAccessParser.StateDiff memory diff = AccountAccessParser.StateDiff({ - slot: bytes32(uint256(999)), // wrong slot - oldValue: bytes32(0), - newValue: original - }); - - diff = AccountAccessParser.normalizeTimestamp(address(0x1c68ECfbf9C8B1E6C0677965b3B9Ecf9A104305b), diff); - - // Should remain unchanged because slot doesn't match - assertEq(diff.newValue, original, "Value changed for wrong slot"); - } - function test_EmptyLayout() public pure { AccountAccessParser.JsonStorageLayout[] memory layout = new AccountAccessParser.JsonStorageLayout[](0); assertEq(AccountAccessParser.isSlotShared(layout, 0), false); @@ -1353,384 +1280,3 @@ contract AccountAccessParser_decodeAndPrint_Test is Test { } // TODO Add integration tests in a follow up PR that actually send transactions and use the recorded state diff. -contract AccountAccessParser_normalizedStateDiffHash_Test is Test { - using AccountAccessParser for VmSafe.AccountAccess[]; - - bytes32 internal constant GNOSIS_SAFE_NONCE_SLOT = bytes32(uint256(5)); - bytes32 internal constant GNOSIS_SAFE_APPROVE_HASHES_SLOT = bytes32(uint256(8)); - bytes32 internal constant LIVENESS_GUARD_LAST_LIVE_SLOT = bytes32(uint256(0)); - bytes32 internal constant OPTIMISM_PORTAL_RESOURCE_PARAMS_SLOT = bytes32(uint256(1)); - - bool constant isWrite = true; - bool constant reverted = true; - - bytes32 constant slot0 = bytes32(uint256(0)); - bytes32 constant slot1 = bytes32(uint256(1)); - bytes32 constant slot2 = bytes32(uint256(2)); - - bytes32 constant val0 = bytes32(uint256(0)); - bytes32 constant val1 = bytes32(uint256(1)); - bytes32 constant val2 = bytes32(uint256(2)); - - address constant EOA_ADDR = address(0x1111); - address constant SAFE_ADDR = address(0x2222); - address constant RANDOM_CONTRACT_ADDR = address(0x3333); - - function setupTests() public { - bytes memory safeCode = hex"01"; - vm.etch(SAFE_ADDR, safeCode); - vm.mockCall(SAFE_ADDR, abi.encodeWithSignature("getThreshold()"), abi.encode(uint256(1))); - - assertTrue(AccountAccessParser.isGnosisSafe(SAFE_ADDR), "SAFE_ADDR should be detected as a Gnosis Safe"); - assertEq(EOA_ADDR.code.length, 0, "EOA_ADDR should have no code"); - } - - function test_normalizedStateDiffHash_EOANonceIncrement() public { - setupTests(); - - // Create a state diff for an EOA nonce increment (should be removed) - VmSafe.StorageAccess[] memory storageAccesses = new VmSafe.StorageAccess[](1); - storageAccesses[0] = storageAccess(EOA_ADDR, slot0, isWrite, val0, val1); // nonce 0 -> 1 - - VmSafe.AccountAccess[] memory accesses = new VmSafe.AccountAccess[](1); - accesses[0] = accountAccess(EOA_ADDR, storageAccesses); - - // Get the normalized hash - bytes32 hash = accesses.normalizedStateDiffHash(address(0), bytes32(0)); - - // Since this is just an EOA nonce increment, the normalized array should be empty - // and the hash should match an empty array - AccountAccessParser.AccountStateDiff[] memory emptyArray = new AccountAccessParser.AccountStateDiff[](0); - bytes32 expectedHash = keccak256(abi.encode(emptyArray)); - - assertEq(hash, expectedHash, "EOA nonce increment should be removed"); - } - - function test_normalizedStateDiffHash_GnosisSafeNonceIncrement() public { - setupTests(); - - // Create a state diff for a Gnosis Safe nonce increment (should be removed) - VmSafe.StorageAccess[] memory storageAccesses = new VmSafe.StorageAccess[](1); - storageAccesses[0] = storageAccess(SAFE_ADDR, GNOSIS_SAFE_NONCE_SLOT, isWrite, val0, val1); // nonce 0 -> 1 - - VmSafe.AccountAccess[] memory accesses = new VmSafe.AccountAccess[](1); - accesses[0] = accountAccess(SAFE_ADDR, storageAccesses); - - // Get the normalized hash - bytes32 hash = accesses.normalizedStateDiffHash(address(0), bytes32(0)); - - // Since this is just a Safe nonce increment, the normalized array should be empty - // and the hash should match an empty array - AccountAccessParser.AccountStateDiff[] memory emptyArray = new AccountAccessParser.AccountStateDiff[](0); - bytes32 expectedHash = keccak256(abi.encode(emptyArray)); - - assertEq(hash, expectedHash, "Gnosis Safe nonce increment should be removed"); - } - - /// This test uses a real transaction that was approved on mainnet. - /// Find more details here: https://github.com/ethereum-optimism/superchain-ops/blob/main/src/tasks/eth/003-opcm-upgrade-v300-op-ink-soneium/VALIDATION.md - function test_normalizedStateDiffHash_GnosisSafeApproveHash() public { - vm.createSelectFork("mainnet", 22319975); - - address multisig = address(0x5a0Aae59D09fccBdDb6C6CcEB07B7279367C3d2A); - bytes32 approveHashSlot = 0xb83cd9f113d329914a61adce818feb77eb750bf02115fdb71f059425216265be; - - // Create a state diff for a Gnosis Safe approve hash (should be removed) - VmSafe.StorageAccess[] memory storageAccesses = new VmSafe.StorageAccess[](1); - storageAccesses[0] = storageAccess(multisig, approveHashSlot, isWrite, val0, val1); // approve hash - - VmSafe.AccountAccess[] memory accesses = new VmSafe.AccountAccess[](1); - accesses[0] = accountAccess(multisig, storageAccesses); - - bytes32 txHash = 0x0d1a3b425e64a0c9bd90f6933632c1cc0042896a1c5831ac8ef290cab8205e83; - bytes32 hash = accesses.normalizedStateDiffHash(multisig, txHash); - - // Since this is just a Safe approve hash, the normalized array should be empty - // and the hash should match an empty array - AccountAccessParser.AccountStateDiff[] memory emptyArray = new AccountAccessParser.AccountStateDiff[](0); - bytes32 expectedHash = keccak256(abi.encode(emptyArray)); - - assertEq(hash, expectedHash, "Gnosis Safe approve hash should be removed"); - } - - function test_normalizedStateDiffHash_OtherChanges() public { - setupTests(); - - // Create a state diff for a regular contract (should be included) - VmSafe.StorageAccess[] memory storageAccesses = new VmSafe.StorageAccess[](1); - storageAccesses[0] = storageAccess(RANDOM_CONTRACT_ADDR, slot1, isWrite, val0, val2); // some random change - - VmSafe.AccountAccess[] memory accesses = new VmSafe.AccountAccess[](1); - accesses[0] = accountAccess(RANDOM_CONTRACT_ADDR, storageAccesses); - - // Get the normalized hash - bytes32 hash = accesses.normalizedStateDiffHash(address(0), bytes32(0)); - - // This should be included in the normalized array - AccountAccessParser.AccountStateDiff[] memory emptyArray = new AccountAccessParser.AccountStateDiff[](0); - assertNotEq(hash, keccak256(abi.encode(emptyArray)), "Regular state change should be included"); - - // Create the expected AccountStateDiff array - AccountAccessParser.AccountStateDiff[] memory expectedArray = new AccountAccessParser.AccountStateDiff[](1); - expectedArray[0] = AccountAccessParser.AccountStateDiff({ - who: RANDOM_CONTRACT_ADDR, - slot: slot1, - firstOld: val0, - lastNew: val2 - }); - - bytes32 expectedHash = keccak256(abi.encode(expectedArray)); - assertEq(hash, expectedHash, "Hash should match the expected AccountStateDiff"); - } - - function test_normalizedStateDiffHash_MixedChanges() public { - vm.createSelectFork("mainnet", 22319975); - setupTests(); - - address multisig = address(0x5a0Aae59D09fccBdDb6C6CcEB07B7279367C3d2A); - bytes32 txHash = keccak256("fake tx hash"); - bytes32 ownerSlot = keccak256(abi.encode(IGnosisSafe(multisig).getOwners()[0], GNOSIS_SAFE_APPROVE_HASHES_SLOT)); - bytes32 fakeApproveHashSlot = keccak256(abi.encode(txHash, ownerSlot)); - // Create the combined account accesses array with all our test cases - VmSafe.AccountAccess[] memory allAccesses = new VmSafe.AccountAccess[](3); - - // 1. EOA nonce increment (should be filtered out) - VmSafe.StorageAccess[] memory eoaStorageAccesses = new VmSafe.StorageAccess[](1); - eoaStorageAccesses[0] = storageAccess(EOA_ADDR, slot0, isWrite, val0, val1); - allAccesses[0] = accountAccess(EOA_ADDR, eoaStorageAccesses); - - // 2. Gnosis Safe changes (should be filtered out) - VmSafe.StorageAccess[] memory safeStorageAccesses = new VmSafe.StorageAccess[](2); - safeStorageAccesses[0] = storageAccess(multisig, GNOSIS_SAFE_NONCE_SLOT, isWrite, val0, val1); // nonce increment - safeStorageAccesses[1] = storageAccess(multisig, fakeApproveHashSlot, isWrite, val0, val1); // approve hash - allAccesses[1] = accountAccess(multisig, safeStorageAccesses); - - // 3. Regular contract change (should be kept) - VmSafe.StorageAccess[] memory regularStorageAccesses = new VmSafe.StorageAccess[](1); - regularStorageAccesses[0] = storageAccess(RANDOM_CONTRACT_ADDR, slot1, isWrite, val0, val2); - allAccesses[2] = accountAccess(RANDOM_CONTRACT_ADDR, regularStorageAccesses); - - // Get the normalized hash - bytes32 hash = allAccesses.normalizedStateDiffHash(multisig, txHash); - - // Manually construct what we expect the normalized state to be using AccountStateDiff - AccountAccessParser.AccountStateDiff[] memory expectedArray = new AccountAccessParser.AccountStateDiff[](1); - expectedArray[0] = AccountAccessParser.AccountStateDiff({ - who: RANDOM_CONTRACT_ADDR, - slot: slot1, - firstOld: val0, - lastNew: val2 - }); - - bytes32 expectedHash = keccak256(abi.encode(expectedArray)); - - // Now let's check that the regular contract write is included and other writes are excluded - AccountAccessParser.AccountStateDiff[] memory emptyArray = new AccountAccessParser.AccountStateDiff[](0); - assertTrue( - hash != keccak256(abi.encode(emptyArray)), - "Hash should not be of an empty array (regular writes should be included)" - ); - - assertEq(hash, expectedHash, "Normalized hash should match expected state with only regular contract changes"); - } - - /// This test uses data from a real liveness guard timestamp update on mainnet. - /// Find more details here: https://github.com/ethereum-optimism/superchain-ops/blob/main/src/tasks/eth/003-opcm-upgrade-v300-op-ink-soneium/VALIDATION.md - function test_normalizedStateDiffHash_LivenessGuardTimestamp() public { - vm.createSelectFork("mainnet", 22319975); - setupTests(); - - address livenessGuard = address(0x24424336F04440b1c28685a38303aC33C9D14a25); - address firstOwnerOnSecurityCouncil = address(0x07dC0893cAfbF810e3E72505041f2865726Fd073); - bytes32 lastLiveSlot = keccak256(abi.encode(firstOwnerOnSecurityCouncil, LIVENESS_GUARD_LAST_LIVE_SLOT)); - - VmSafe.AccountAccess[] memory allAccesses = new VmSafe.AccountAccess[](1); - // Create a state diff for a LivenessGuard timestamp (should be removed) - VmSafe.StorageAccess[] memory storageAccesses = new VmSafe.StorageAccess[](1); - storageAccesses[0] = storageAccess(livenessGuard, lastLiveSlot, isWrite, val0, val1); - allAccesses[0] = accountAccess(livenessGuard, storageAccesses); - - address parentMultisig = address(0x5a0Aae59D09fccBdDb6C6CcEB07B7279367C3d2A); - bytes32 hash = allAccesses.normalizedStateDiffHash(parentMultisig, bytes32(0)); - - // Since this is just a LivenessGuard timestamp update, the normalized array should be empty - // and the hash should match an empty array - AccountAccessParser.AccountStateDiff[] memory emptyArray = new AccountAccessParser.AccountStateDiff[](0); - bytes32 expectedHash = keccak256(abi.encode(emptyArray)); - - assertEq(hash, expectedHash, "LivenessGuard timestamp update should be removed"); - } - - // Version 3.5.0 - op-contracts/v4.1.0-rc.3 - AnchorStateRegistry - function test_normalizedStateDiffHash_AnchorStateRegistryRetirementTimestamp() public { - vm.createSelectFork("mainnet", 22990600); - setupTests(); - - address anchorStateRegistry = address(0x23B2C62946350F4246f9f9D027e071f0264FD113); - bytes32 retirementTimestampSlot = bytes32(uint256(6)); - bytes32 retirementTimestamp = - bytes32(uint256(0x0000000000000000000000000000000000000000FFFFFFFFFFFFFFFF00000000)); - VmSafe.AccountAccess[] memory allAccesses = new VmSafe.AccountAccess[](1); - VmSafe.StorageAccess[] memory storageAccesses = new VmSafe.StorageAccess[](1); - storageAccesses[0] = - storageAccess(anchorStateRegistry, retirementTimestampSlot, isWrite, val0, retirementTimestamp); - allAccesses[0] = accountAccess(anchorStateRegistry, storageAccesses); - - address parentMultisig = address(0x5a0Aae59D09fccBdDb6C6CcEB07B7279367C3d2A); - bytes32 hash = allAccesses.normalizedStateDiffHash(parentMultisig, bytes32(0)); - // This hash is the zero'd out retirement timestamp slot. Stepped through code to prove this. - bytes32 expectedHash = bytes32(0xba500e0a45c2487e48cabd2c832376eabc7d10080926d10c0340829a3a97e622); - assertEq(hash, expectedHash, "AnchorStateRegistry should match the expected hash"); - - bytes32 retirementTimestamp2 = - bytes32(uint256(0x0000000000000000000000000000000000000000AAAAAAAAAAAAAAAA00000000)); - storageAccesses[0] = - storageAccess(anchorStateRegistry, retirementTimestampSlot, isWrite, val0, retirementTimestamp2); - allAccesses[0] = accountAccess(anchorStateRegistry, storageAccesses); - - bytes32 hash2 = allAccesses.normalizedStateDiffHash(parentMultisig, bytes32(0)); - assertEq(hash2, expectedHash, "AnchorStateRegistry should still match the expected hash"); - } - - // Version 3.5.0 - op-contracts/v4.1.0-rc.3 - AnchorStateRegistry - function test_normalizedStateDiffHash_AnchorStateRegistryProposal() public { - vm.createSelectFork("mainnet", 22990600); - setupTests(); - - address anchorStateRegistry = address(0x23B2C62946350F4246f9f9D027e071f0264FD113); - bytes32 proposalRootSlot = bytes32(uint256(3)); - bytes32 proposalRoot = bytes32(uint256(0x08ce0a407e15a1776bd43cd669328edc6825fcab988b8e2052258774251c25d2)); - bytes32 proposalL2SequenceNumberSlot = bytes32(uint256(4)); - bytes32 proposalL2SequenceNumber = - bytes32(uint256(0x0000000000000000000000000000000000000000000000000000000001287b8d)); - - VmSafe.AccountAccess[] memory allAccesses = new VmSafe.AccountAccess[](1); - VmSafe.StorageAccess[] memory storageAccesses = new VmSafe.StorageAccess[](2); - storageAccesses[0] = storageAccess(anchorStateRegistry, proposalRootSlot, isWrite, val0, proposalRoot); - storageAccesses[1] = - storageAccess(anchorStateRegistry, proposalL2SequenceNumberSlot, isWrite, val0, proposalL2SequenceNumber); - allAccesses[0] = accountAccess(anchorStateRegistry, storageAccesses); - - address parentMultisig = address(0x5a0Aae59D09fccBdDb6C6CcEB07B7279367C3d2A); - bytes32 hash = allAccesses.normalizedStateDiffHash(parentMultisig, bytes32(0)); - - AccountAccessParser.AccountStateDiff[] memory emptyArray = new AccountAccessParser.AccountStateDiff[](0); - bytes32 expectedHash = keccak256(abi.encode(emptyArray)); - - assertEq(hash, expectedHash, "AnchorStateRegistry proposal should be removed"); - } - - // Version 2.2.2 - op-contracts/v3.0.0 - AnchorStateRegistry - function test_normalizedStateDiffHash_AnchorStateRegistryOutputRootSlots() public { - vm.createSelectFork("mainnet", 22319975); - setupTests(); - - address anchorStateRegistry = address(0x496286e5eE7758de84Dd17e6d2d97afC2ACE4cc7); - VmSafe.AccountAccess[] memory allAccesses = new VmSafe.AccountAccess[](1); - VmSafe.StorageAccess[] memory storageAccesses = new VmSafe.StorageAccess[](2); - - bytes32 startingAnchorRootSlot = bytes32(uint256(4)); - bytes32 l2BlockNumberSlot = bytes32(uint256(5)); - - bytes32 startingAnchorRoot = - bytes32(uint256(0x08ce0a407e15a1776bd43cd669328edc6825fcab988b8e2052258774251c25d2)); - bytes32 l2BlockNumber = bytes32(uint256(0x0000000000000000000000000000000000000000000000000000000000123456)); - - storageAccesses[0] = - storageAccess(anchorStateRegistry, startingAnchorRootSlot, isWrite, val0, startingAnchorRoot); - storageAccesses[1] = storageAccess(anchorStateRegistry, l2BlockNumberSlot, isWrite, val0, l2BlockNumber); - allAccesses[0] = accountAccess(anchorStateRegistry, storageAccesses); - - address parentMultisig = address(0x5a0Aae59D09fccBdDb6C6CcEB07B7279367C3d2A); - bytes32 hash = allAccesses.normalizedStateDiffHash(parentMultisig, bytes32(0)); - - AccountAccessParser.AccountStateDiff[] memory emptyArray = new AccountAccessParser.AccountStateDiff[](0); - bytes32 expectedHash = keccak256(abi.encode(emptyArray)); - assertEq(hash, expectedHash, "AnchorStateRegistry output root writes should be removed"); - } - - /// It's possible for there to be more storage writes than accesses. - /// This test checks that the function handles this case correctly. - function test_more_storage_writes_than_accesses_passes() public pure { - address who = address(0xabcd); - VmSafe.AccountAccess[] memory accesses = new VmSafe.AccountAccess[](1); - VmSafe.StorageAccess[] memory sa = new VmSafe.StorageAccess[](2); - sa[0] = storageAccess(who, bytes32(uint256(0x1)), isWrite, val0, val1); - sa[1] = storageAccess(who, bytes32(uint256(0x2)), isWrite, val0, val1); - accesses[0] = accountAccess(who, sa); - - AccountAccessParser.StateDiff[] memory diffs = AccountAccessParser.getStateDiffFor(accesses, who, false); - assertEq(diffs.length, sa.length, "The number of diffs should be equal to the number of storage writes"); - } - - function test_normalizedStateDiffHash_OptimismPortalResourceMetering() public { - setupTests(); - vm.createSelectFork("mainnet", 22319975); // Use a realistic block number - VmSafe.AccountAccess[] memory allAccesses = new VmSafe.AccountAccess[](1); - // Create a state diff for OptimismPortal ResourceMetering - address who = address(0xabcd); - VmSafe.StorageAccess[] memory storageAccesses = new VmSafe.StorageAccess[](1); - IResourceMetering.ResourceParams memory resourceParams = IResourceMetering.ResourceParams({ - prevBaseFee: uint128(5), - prevBoughtGas: uint64(6), - prevBlockNum: uint64(block.number) - }); - bytes32 resourceParamsSlot = packResourceParams(resourceParams); - storageAccesses[0] = - storageAccess(who, OPTIMISM_PORTAL_RESOURCE_PARAMS_SLOT, isWrite, bytes32(uint256(0)), resourceParamsSlot); - allAccesses[0] = accountAccess(who, storageAccesses); - bytes32 hash = allAccesses.normalizedStateDiffHash(address(1), bytes32(0)); - - AccountAccessParser.AccountStateDiff[] memory emptyArray = new AccountAccessParser.AccountStateDiff[](0); - bytes32 expectedHash = keccak256(abi.encode(emptyArray)); - assertEq(hash, expectedHash, "OptimismPortal ResourceParams update should be removed"); - } - - /// @notice Packs the resource params into a bytes32. Where prevBlockNum is the most significant 64 bits, prevBoughtGas is the next 64 bits, and prevBaseFee is the least significant 128 bits. - function packResourceParams(IResourceMetering.ResourceParams memory _resourceParams) - internal - pure - returns (bytes32) - { - return (bytes32(uint256(_resourceParams.prevBlockNum)) << (128 + 64)) - | (bytes32(uint256(_resourceParams.prevBoughtGas)) << 128) | (bytes32(uint256(_resourceParams.prevBaseFee))); - } - - /// Helper functions similar to those in AccountAccessParser.t.sol - function accountAccess(address _account, VmSafe.StorageAccess[] memory _storageAccesses) - internal - pure - returns (VmSafe.AccountAccess memory) - { - return VmSafe.AccountAccess({ - chainInfo: VmSafe.ChainInfo({chainId: 1, forkId: 1}), - kind: VmSafe.AccountAccessKind.Call, - account: _account, - accessor: address(0), - initialized: true, - oldBalance: 0, - newBalance: 0, - deployedCode: new bytes(0), - value: 0, - data: new bytes(0), - reverted: false, - storageAccesses: _storageAccesses, - depth: 0 - }); - } - - function storageAccess(address _account, bytes32 _slot, bool _isWrite, bytes32 _previousValue, bytes32 _newValue) - internal - pure - returns (VmSafe.StorageAccess memory) - { - return VmSafe.StorageAccess({ - account: _account, - slot: _slot, - isWrite: _isWrite, - previousValue: _previousValue, - newValue: _newValue, - reverted: false - }); - } -} diff --git a/test/tasks/MultisigTask.t.sol b/test/tasks/MultisigTask.t.sol index c79561bf72..325c250516 100644 --- a/test/tasks/MultisigTask.t.sol +++ b/test/tasks/MultisigTask.t.sol @@ -153,7 +153,7 @@ contract MultisigTaskUnitTest is Test { public returns (VmSafe.AccountAccess[] memory accountAccesses, Action[] memory actions) { - (accountAccesses, actions,,,) = task.simulate(taskConfigFilePath, Solarray.addresses(childMultisig)); + (accountAccesses, actions,,) = task.simulate(taskConfigFilePath, Solarray.addresses(childMultisig)); (address[] memory targets, uint256[] memory values, bytes[] memory calldatas) = task.processTaskActions(actions); diff --git a/test/tasks/NestedMultisigTask.t.sol b/test/tasks/NestedMultisigTask.t.sol index 358c30f32c..3bed164851 100644 --- a/test/tasks/NestedMultisigTask.t.sol +++ b/test/tasks/NestedMultisigTask.t.sol @@ -72,7 +72,7 @@ contract NestedMultisigTaskTest is Test { string memory configFilePath = MultisigTaskTestHelper.createTempTomlFile(_taskConfigFilePath, TESTING_DIRECTORY, _salt); - (accountAccesses, actions,,, rootSafe) = multisigTask.simulate(configFilePath, _childSafes); + (accountAccesses, actions,, rootSafe) = multisigTask.simulate(configFilePath, _childSafes); MultisigTaskTestHelper.removeFile(configFilePath); addrRegistry = multisigTask.addrRegistry(); @@ -273,7 +273,7 @@ contract NestedMultisigTaskTest is Test { address[] memory childSafes = Solarray.addresses(foundationChildMultisig); - (VmSafe.AccountAccess[] memory accountAccesses, Action[] memory actions,,, address rootSafe) = + (VmSafe.AccountAccess[] memory accountAccesses, Action[] memory actions,, address rootSafe) = multisigTask.simulate(opcmTaskConfigFilePath, childSafes); address[] memory allSafes = MultisigTaskTestHelper.getAllSafes(rootSafe, foundationChildMultisig); @@ -303,7 +303,7 @@ contract NestedMultisigTaskTest is Test { // Snapshot before running the task so we can roll back to this pre-state uint256 newSnapshot = vm.snapshotState(); - (accountAccesses, actions,,,) = multisigTask.simulate(opcmTaskConfigFilePath, childSafes); + (accountAccesses, actions,,) = multisigTask.simulate(opcmTaskConfigFilePath, childSafes); bytes32 taskHash = multisigTask.getHash( testData.rootSafeCalldata, address(testData.rootSafe), 0, testData.originalRootSafeNonce, testData.allSafes ); @@ -320,7 +320,7 @@ contract NestedMultisigTaskTest is Test { string memory toml = "l2chains = [{name = \"OP Mainnet\", chainId = 10}]\n" "\n" "templateName = \"SetEIP1967Implementation\"\n contractIdentifier = \"OptimismPortalProxy\"\n newImplementation = \"0x0000000FFfFFfffFffFfFffFFFfffffFffFFffFf\"\n"; string memory configFilePath = MultisigTaskTestHelper.createTempTomlFile(toml, TESTING_DIRECTORY, "005"); - (,,,, address rootSafe) = + (,,, address rootSafe) = multisigTask.simulate(configFilePath, Solarray.addresses(SECURITY_COUNCIL_CHILD_MULTISIG)); assertEq(multisigTask.isNestedSafe(rootSafe), true, "Expected isNestedSafe to be true"); MultisigTaskTestHelper.removeFile(configFilePath); @@ -594,7 +594,7 @@ contract NestedMultisigTaskTest is Test { string memory config = MultisigTaskTestHelper.createTempTomlFile(_taskConfigToml, TESTING_DIRECTORY, "002"); - (_accountAccesses, _actions,,,) = multisigTask.simulate(config, _testData.childSafes); + (_accountAccesses, _actions,,) = multisigTask.simulate(config, _testData.childSafes); MultisigTaskTestHelper.removeFile(config); diff --git a/test/tasks/Regression.t.sol b/test/tasks/Regression.t.sol index f700e709c3..eef37579f8 100644 --- a/test/tasks/Regression.t.sol +++ b/test/tasks/Regression.t.sol @@ -949,7 +949,7 @@ contract RegressionTest is Test { childSafes = new address[](0); } allOriginalNonces = MultisigTaskTestHelper.getAllOriginalNonces(allSafes); - (, actions,,,) = multisigTask.simulate(taskConfigFilePath, childSafes); + (, actions,,) = multisigTask.simulate(taskConfigFilePath, childSafes); } /// @notice Assert that the call data generated by the multisig task matches the expected call data. diff --git a/test/tasks/SingleMultisigTask.t.sol b/test/tasks/SingleMultisigTask.t.sol index baa32a420b..5666653ad0 100644 --- a/test/tasks/SingleMultisigTask.t.sol +++ b/test/tasks/SingleMultisigTask.t.sol @@ -48,7 +48,7 @@ contract SingleMultisigTaskTest is Test { returns (VmSafe.AccountAccess[] memory accountAccesses, Action[] memory actions, address rootSafe) { multisigTask = new GasConfigTemplate(); - (accountAccesses, actions,,, rootSafe) = multisigTask.simulate(taskConfigFilePath, new address[](0)); + (accountAccesses, actions,, rootSafe) = multisigTask.simulate(taskConfigFilePath, new address[](0)); } function toSuperchainAddrRegistry(AddressRegistry _addrRegistry) @@ -106,7 +106,7 @@ contract SingleMultisigTaskTest is Test { vm.expectRevert("No actions found"); localMultisigTask.processTaskActions(actions); - (accountAccesses, actions,,,) = localMultisigTask.simulate(taskConfigFilePath, new address[](0)); + (accountAccesses, actions,,) = localMultisigTask.simulate(taskConfigFilePath, new address[](0)); addrRegistry = localMultisigTask.addrRegistry(); diff --git a/test/tasks/StateOverrideManager.t.sol b/test/tasks/StateOverrideManager.t.sol index 7f3a264ea4..51f6e41c81 100644 --- a/test/tasks/StateOverrideManager.t.sol +++ b/test/tasks/StateOverrideManager.t.sol @@ -283,7 +283,7 @@ contract StateOverrideManagerUnitTest is Test { string memory fileName = helper.createTempTomlFile(nonNestedSafeToml, TESTING_DIRECTORY, "011"); MockSetEIP1967ImplTask si = new MockSetEIP1967ImplTask(); - (,,,, address rootSafe) = si.simulate(fileName, new address[](0)); + (,,, address rootSafe) = si.simulate(fileName, new address[](0)); // Only parent overrides will be checked because child multisig is not set. Simulation.StateOverride[] memory allOverrides = assertDefaultStateOverrides(1, si, new address[](0), rootSafe); @@ -555,7 +555,7 @@ contract StateOverrideManagerUnitTest is Test { returns (MultisigTask task, address rootSafe) { task = new MockMultisigTask(); - (,,,, rootSafe) = task.simulate(fileName, Solarray.addresses(childMultisig)); + (,,, rootSafe) = task.simulate(fileName, Solarray.addresses(childMultisig)); return (task, rootSafe); } diff --git a/test/tasks/TaskManager.t.sol b/test/tasks/TaskManager.t.sol index 0e9ba7538e..53c5f5a7c0 100644 --- a/test/tasks/TaskManager.t.sol +++ b/test/tasks/TaskManager.t.sol @@ -93,45 +93,6 @@ contract TaskManagerUnitTest is StateOverrideManager, Test { tm.requireSignerOnSafe(signer, safe); } - function testNormalizedHashCheck_Passes() public { - TaskManager tm = new TaskManager(); - TaskConfig memory config = TaskConfig({ - optionalL2Chains: new L2Chain[](0), - basePath: "test/tasks/example/eth/004-fp-set-respected-game-type", - configPath: "", - templateName: "", - rootSafe: address(0), - isNested: true, - task: address(0) - }); - // Doesn't have a VALIDATION markdown file. - assertTrue(tm.checkNormalizedHash(bytes32(hex"1230"), config)); - assertTrue(tm.checkNormalizedHash(bytes32(hex"1234"), config)); - - // Does have a VALIDATION markdown file and hash matches. - config.basePath = "src/tasks/eth/013-gas-params-op"; - assertTrue( - tm.checkNormalizedHash( - bytes32(hex"2576512ad010b917c049a392e916bb02de1c168477fe29c4f8cbc4fcb016a4b0"), config - ) - ); - } - - function testNormalizedHashCheck_Fails() public { - TaskManager tm = new TaskManager(); - TaskConfig memory config = TaskConfig({ - optionalL2Chains: new L2Chain[](0), - basePath: "src/tasks/eth/013-gas-params-op", - configPath: "", - templateName: "", - rootSafe: address(0), - isNested: true, - task: address(0) - }); - // Does have a VALIDATION markdown file and hash does not match. - assertFalse(tm.checkNormalizedHash(bytes32(hex"10"), config)); - } - function testDataToSignCheck_Passes() public { vm.createSelectFork("mainnet"); // Pinning to a block. TaskManager tm = new TaskManager(); @@ -252,7 +213,7 @@ contract TaskManagerUnitTest is StateOverrideManager, Test { TaskManager tm = new TaskManager(); L2Chain[] memory l2Chains = new L2Chain[](1); l2Chains[0] = L2Chain({chainId: 10, name: "OP Mainnet"}); - (,, bytes memory dataToSign) = tm.executeTask( + (, bytes memory dataToSign) = tm.executeTask( TaskConfig({ optionalL2Chains: l2Chains, basePath: "test/tasks/example/eth/006-system-config-gas-params", From d8277e1baeb146030d39fe5afe893a2a52f62ae8 Mon Sep 17 00:00:00 2001 From: JosepBove Date: Fri, 10 Oct 2025 17:55:49 +0200 Subject: [PATCH 10/18] Change the signer to be swapped and ad docs on 030 and 031 eth (#1246) * feat: change addy and docs * fix: links --- src/tasks/eth/030-fus-rotation/README.md | 21 ++++++ src/tasks/eth/030-fus-rotation/VALIDATION.md | 68 +++++++++++++++++++ src/tasks/eth/030-fus-rotation/config.toml | 2 +- src/tasks/eth/031-fos-rotation/README.md | 21 ++++++ src/tasks/eth/031-fos-rotation/VALIDATION.md | 69 ++++++++++++++++++++ src/tasks/eth/031-fos-rotation/config.toml | 6 +- 6 files changed, 183 insertions(+), 4 deletions(-) create mode 100644 src/tasks/eth/030-fus-rotation/README.md create mode 100644 src/tasks/eth/030-fus-rotation/VALIDATION.md create mode 100644 src/tasks/eth/031-fos-rotation/README.md create mode 100644 src/tasks/eth/031-fos-rotation/VALIDATION.md diff --git a/src/tasks/eth/030-fus-rotation/README.md b/src/tasks/eth/030-fus-rotation/README.md new file mode 100644 index 0000000000..77f89a8036 --- /dev/null +++ b/src/tasks/eth/030-fus-rotation/README.md @@ -0,0 +1,21 @@ +# 030-fus-rotation + +Status: [READY TO SIGN]() + +## Objective + +This task removes a FoundationUpgradeSafe owner and replaces it with a new one. + +## Simulation & Signing + +Simulation commands: +```bash +cd src/tasks/eth/030-fus-rotation +SIMULATE_WITHOUT_LEDGER=1 just simulate +``` + +Signing commands: +```bash +cd src/tasks/eth/030-fus-rotation +just sign +``` diff --git a/src/tasks/eth/030-fus-rotation/VALIDATION.md b/src/tasks/eth/030-fus-rotation/VALIDATION.md new file mode 100644 index 0000000000..8ad06c05fb --- /dev/null +++ b/src/tasks/eth/030-fus-rotation/VALIDATION.md @@ -0,0 +1,68 @@ +# Validation + +This document can be used to validate the inputs and result of the execution of the transaction which you are +signing. + +The steps are: + +1. [Validate the Domain and Message Hashes](#expected-domain-and-message-hashes) +2. [Verifying the transaction input](#understanding-task-calldata) + +## Expected Domain and Message Hashes + +First, we need to validate the domain and message hashes. These values should match both the values on your ledger and +the values printed to the terminal when you run the task. + +> [!CAUTION] +> +> Before signing, ensure the below hashes match what is on your ledger. +> +> ### Single Safe Signer Data +> +> - Domain Hash: `0xa4a9c312badf3fcaa05eafe5dc9bee8bd9316c78ee8b0bebe3115bb21b732672` +> - Message Hash: `0x3895b6a145b0f2e6dd7441c80c3f73260a7d8554b92266e44cb94c14ff00c839` + +## Understanding Task Calldata + +This document provides a detailed analysis of the final calldata executed on-chain for the signer rotation. + +By reconstructing the calldata, we can confirm that the execution precisely implements the approved plan with no unexpected modifications or side effects. + +### Inputs to `safe.swapOwner()` + +`safe.swapOwner()` function is called with the address to be removed and the previous module: + +The address of the new signer: 0x69acfE2096Dfb8d5A041eF37693553c48d9BFd02 +The address of the signer to be removed: 0x7cb07fe039a92b3d784f284d919503a381bec54f +The address of the previous signer: 0x4d014f3c5f33aa9cd1dc29ce29618d07ae666d15 (this gets calculated by the template) + +Thus, the command to encode the calldata is: + +```bash +cast calldata 'swapOwner(address, address, address)' "0x4d014f3c5f33aa9cd1dc29ce29618d07ae666d15" "0x7cb07fe039a92b3d784f284d919503a381bec54f" "0x69acfE2096Dfb8d5A041eF37693553c48d9BFd02" +``` + +### Inputs to `Multicall3DelegateCall` + +The output from the previous section becomes the `data` in the argument to the `Multicall3DelegateCall.aggregate3Value()` function. + +This function is called with a tuple of four elements: + +Call3 struct for Multicall3DelegateCall: + +- `target`: 0x847B5c174615B1B7fDF770882256e2D3E95b9D92 - Foundation Upgrade Safe +- `allowFailure`: false +- `value`: 0 +- `callData`: `0xe318b52b0000000000000000000000004d014f3c5f33aa9cd1dc29ce29618d07ae666d150000000000000000000000007cb07fe039a92b3d784f284d919503a381bec54f00000000000000000000000069acfe2096dfb8d5a041ef37693553c48d9bfd02` (output from the previous section) + +Command to encode: + +```bash +cast calldata 'aggregate3Value((address,bool,uint256,bytes)[])' "[(0x847B5c174615B1B7fDF770882256e2D3E95b9D92,false,0,0xe318b52b0000000000000000000000004d014f3c5f33aa9cd1dc29ce29618d07ae666d150000000000000000000000007cb07fe039a92b3d784f284d919503a381bec54f00000000000000000000000069acfe2096dfb8d5a041ef37693553c48d9bfd02)]" +``` + +The resulting calldata sent from the ProxyAdminOwner safe is thus: + +``` +0x174dea71000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000020000000000000000000000000847b5c174615b1b7fdf770882256e2d3e95b9d920000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000064e318b52b0000000000000000000000004d014f3c5f33aa9cd1dc29ce29618d07ae666d150000000000000000000000007cb07fe039a92b3d784f284d919503a381bec54f00000000000000000000000069acfe2096dfb8d5a041ef37693553c48d9bfd0200000000000000000000000000000000000000000000000000000000 +``` \ No newline at end of file diff --git a/src/tasks/eth/030-fus-rotation/config.toml b/src/tasks/eth/030-fus-rotation/config.toml index 17c190fe3f..0270fa6b4c 100644 --- a/src/tasks/eth/030-fus-rotation/config.toml +++ b/src/tasks/eth/030-fus-rotation/config.toml @@ -2,7 +2,7 @@ templateName = "GnosisSafeRotateSigner" safeAddressString = "FoundationUpgradeSafe" -ownerToRemove = "0xBF93D4d727F7Ba1F753E1124C3e532dCb04Ea2c8" +ownerToRemove = "0x7cB07FE039a92B3D784f284D919503A381BEC54f" ownerToAdd = "0x69acfE2096Dfb8d5A041eF37693553c48d9BFd02" [stateOverrides] diff --git a/src/tasks/eth/031-fos-rotation/README.md b/src/tasks/eth/031-fos-rotation/README.md new file mode 100644 index 0000000000..641da74796 --- /dev/null +++ b/src/tasks/eth/031-fos-rotation/README.md @@ -0,0 +1,21 @@ +# 031-fos-rotation + +Status: [READY TO SIGN]() + +## Objective + +This task removes a FoundationOperationsSafe owner and replaces it with a new one. + +## Simulation & Signing + +Simulation commands: +```bash +cd src/tasks/eth/031-fos-rotation +SIMULATE_WITHOUT_LEDGER=1 just simulate +``` + +Signing commands: +```bash +cd src/tasks/eth/031-fos-rotation +just sign +``` diff --git a/src/tasks/eth/031-fos-rotation/VALIDATION.md b/src/tasks/eth/031-fos-rotation/VALIDATION.md new file mode 100644 index 0000000000..9418c2df85 --- /dev/null +++ b/src/tasks/eth/031-fos-rotation/VALIDATION.md @@ -0,0 +1,69 @@ +# Validation + +This document can be used to validate the inputs and result of the execution of the transaction which you are +signing. + +The steps are: + +1. [Validate the Domain and Message Hashes](#expected-domain-and-message-hashes) +2. [Verifying the transaction input](#understanding-task-calldata) + +## Expected Domain and Message Hashes + +First, we need to validate the domain and message hashes. These values should match both the values on your ledger and +the values printed to the terminal when you run the task. + +> [!CAUTION] +> +> Before signing, ensure the below hashes match what is on your ledger. +> +> ### Single Safe Signer Data +> +> - Domain Hash: `0x4e6a6554de0308f5ece8ff736beed8a1b876d16f5c27cac8e466d7de0c703890` +> - Message Hash: `0xec0f740e6db2c7fb9f4910ecb3f4c65fb8157d95f1e755aeff9fcdaa77e6d8a6` + +## Understanding Task Calldata + +This document provides a detailed analysis of the final calldata executed on-chain for the signer rotation. + +By reconstructing the calldata, we can confirm that the execution precisely implements the approved plan with no unexpected modifications or side effects. + + +### Inputs to `safe.swapOwner()` + +`safe.swapOwner()` function is called with the address to be removed and the previous module: + +The address of the new signer: 0x69acfE2096Dfb8d5A041eF37693553c48d9BFd02 +The address of the signer to be removed: 0x7cb07fe039a92b3d784f284d919503a381bec54f +The address of the previous signer: 0x4d014f3c5f33aa9cd1dc29ce29618d07ae666d15 (this gets calculated by the template) + +Thus, the command to encode the calldata is: + +```bash +cast calldata 'swapOwner(address, address, address)' "0x4d014f3c5f33aa9cd1dc29ce29618d07ae666d15" "0x7cb07fe039a92b3d784f284d919503a381bec54f" "0x69acfE2096Dfb8d5A041eF37693553c48d9BFd02" +``` + +### Inputs to `Multicall3DelegateCall` + +The output from the previous section becomes the `data` in the argument to the `Multicall3DelegateCall.aggregate3Value()` function. + +This function is called with a tuple of four elements: + +Call3 struct for Multicall3DelegateCall: + +- `target`: 0x9BA6e03D8B90dE867373Db8cF1A58d2F7F006b3A - Foundation Operations Safe +- `allowFailure`: false +- `value`: 0 +- `callData`: `0xe318b52b0000000000000000000000004d014f3c5f33aa9cd1dc29ce29618d07ae666d150000000000000000000000007cb07fe039a92b3d784f284d919503a381bec54f00000000000000000000000069acfe2096dfb8d5a041ef37693553c48d9bfd02` (output from the previous section) + +Command to encode: + +```bash +cast calldata 'aggregate3Value((address,bool,uint256,bytes)[])' "[(0x9BA6e03D8B90dE867373Db8cF1A58d2F7F006b3A,false,0,0xe318b52b0000000000000000000000004d014f3c5f33aa9cd1dc29ce29618d07ae666d150000000000000000000000007cb07fe039a92b3d784f284d919503a381bec54f00000000000000000000000069acfe2096dfb8d5a041ef37693553c48d9bfd02)]" +``` + +The resulting calldata sent from the ProxyAdminOwner safe is thus: + +``` +0x174dea710000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000200000000000000000000000009ba6e03d8b90de867373db8cf1a58d2f7f006b3a0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000064e318b52b0000000000000000000000004d014f3c5f33aa9cd1dc29ce29618d07ae666d150000000000000000000000007cb07fe039a92b3d784f284d919503a381bec54f00000000000000000000000069acfe2096dfb8d5a041ef37693553c48d9bfd0200000000000000000000000000000000000000000000000000000000 +``` \ No newline at end of file diff --git a/src/tasks/eth/031-fos-rotation/config.toml b/src/tasks/eth/031-fos-rotation/config.toml index c3cfa2d64d..b88cbda45d 100644 --- a/src/tasks/eth/031-fos-rotation/config.toml +++ b/src/tasks/eth/031-fos-rotation/config.toml @@ -1,12 +1,12 @@ templateName = "GnosisSafeRotateSigner" -safeAddressString = "FoundationOperationSafe" +safeAddressString = "FoundationOperationsSafe" -ownerToRemove = "0xBF93D4d727F7Ba1F753E1124C3e532dCb04Ea2c8" +ownerToRemove = "0x7cB07FE039a92B3D784f284D919503A381BEC54f" ownerToAdd = "0x69acfE2096Dfb8d5A041eF37693553c48d9BFd02" [stateOverrides] 0x9BA6e03D8B90dE867373Db8cF1A58d2F7F006b3A = [ - {key = "0x0000000000000000000000000000000000000000000000000000000000000005", value = 108} + {key = "0x0000000000000000000000000000000000000000000000000000000000000005", value = 110} ] \ No newline at end of file From a5d6c1b0fd41a081c7c85dd74a71222e7eecf83c Mon Sep 17 00:00:00 2001 From: Wazabie <48911235+Wazabie@users.noreply.github.com> Date: Fri, 10 Oct 2025 20:41:06 +0200 Subject: [PATCH 11/18] Update README.md (#1247) --- src/tasks/sep/035-uni-sep-fusaka-prestate/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tasks/sep/035-uni-sep-fusaka-prestate/README.md b/src/tasks/sep/035-uni-sep-fusaka-prestate/README.md index 04b86dc2f4..70b9574c03 100644 --- a/src/tasks/sep/035-uni-sep-fusaka-prestate/README.md +++ b/src/tasks/sep/035-uni-sep-fusaka-prestate/README.md @@ -1,6 +1,6 @@ # 035-uni-sep-fusaka-prestate -Status: [READY TO SIGN] +Status: [[EXECUTED](https://sepolia.etherscan.io/tx/0xa260348e1aacf29ac53487ee3ef34b2b638adbb3589a93a1d36aec18fc156395)] ## Objective From 7d3e452d3a701e9d64c2310d8d8849543a7057d2 Mon Sep 17 00:00:00 2001 From: Wazabie <48911235+Wazabie@users.noreply.github.com> Date: Fri, 10 Oct 2025 21:18:20 +0200 Subject: [PATCH 12/18] Update README.md (#1248) --- src/tasks/sep/034-op-ink-sep-fusaka-prestate/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tasks/sep/034-op-ink-sep-fusaka-prestate/README.md b/src/tasks/sep/034-op-ink-sep-fusaka-prestate/README.md index 13bba45d9f..58c45bf0f6 100644 --- a/src/tasks/sep/034-op-ink-sep-fusaka-prestate/README.md +++ b/src/tasks/sep/034-op-ink-sep-fusaka-prestate/README.md @@ -1,6 +1,6 @@ # 034-op-ink-sep-fusaka-prestate -Status: [READY TO SIGN] +Status: [[EXECUTED](https://sepolia.etherscan.io/tx/0x02d13afd79fb045ab34a05e5f9bc7f23738afd739a042f97bc314ad9e90e282e)] ## Objective From 74af8a4e76d070eb1f9ef209fdcdeb5acffbaa2b Mon Sep 17 00:00:00 2001 From: smartcontracts <14298799+smartcontracts@users.noreply.github.com> Date: Tue, 14 Oct 2025 16:43:52 -0400 Subject: [PATCH 13/18] feat: mark 28 and 29 as executed (#1250) --- src/tasks/eth/028-arena-z-main-u13-to-u16a/README.md | 2 +- src/tasks/eth/029-swell-main-u13-to-u16a/README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/tasks/eth/028-arena-z-main-u13-to-u16a/README.md b/src/tasks/eth/028-arena-z-main-u13-to-u16a/README.md index 9835ceb7af..6c519f6c10 100644 --- a/src/tasks/eth/028-arena-z-main-u13-to-u16a/README.md +++ b/src/tasks/eth/028-arena-z-main-u13-to-u16a/README.md @@ -1,6 +1,6 @@ # 028-arena-z-main-u13-to-u16a -Status: [READY TO SIGN]() +Status: [EXECUTED](https://etherscan.io/tx/0x38010d19d63d834b95936728d49e38dbf71ae7b3a44f555b235b890cd2f40eaf) ## Objective diff --git a/src/tasks/eth/029-swell-main-u13-to-u16a/README.md b/src/tasks/eth/029-swell-main-u13-to-u16a/README.md index e48862d0f9..57974e6f3b 100644 --- a/src/tasks/eth/029-swell-main-u13-to-u16a/README.md +++ b/src/tasks/eth/029-swell-main-u13-to-u16a/README.md @@ -1,6 +1,6 @@ # 029-swell-main-u13-to-u16a -Status: [READY TO SIGN]() +Status: [EXECUTED](https://etherscan.io/tx/0x6e9cfd7c22acaf263f9a5afaaa701934c77fa2015b9a65262fe29dee108b092c) ## Objective From a319e4895399a6e61debae34e469a254e18612fa Mon Sep 17 00:00:00 2001 From: smartcontracts <14298799+smartcontracts@users.noreply.github.com> Date: Tue, 14 Oct 2025 16:59:16 -0400 Subject: [PATCH 14/18] feat: add validation instructions for op-txverify (#1251) * feat: add validation instructions for op-txverify * fix: link target --- src/NESTED.md | 16 ++++++++++++++++ src/SINGLE.md | 16 ++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/src/NESTED.md b/src/NESTED.md index 7c461dfe06..4e3d34db09 100644 --- a/src/NESTED.md +++ b/src/NESTED.md @@ -119,6 +119,22 @@ message hash: `0x1901[domain hash][message hash]`. Note down this value. You will need to compare it with the ones displayed on the Ledger screen at signing. +#### 3.4 Validate with op-txverify + +Use [op-txverify](https://github.com/ethereum-optimism/op-txverify) to confirm +the transaction and the message/domain hashes. + +1. Run the `simulate` command for your given Superchain Ops task. +2. Find the **`OP-TXVERIFY LINK`** section of the Superchain Ops task output. +3. Copy the link as shown in the output (links to `op-txverify.optimism.io`) and send it to another device (phone is easiest). +4. Run `op-txverify qr` on your first device. This will open a QR scanner. +5. Open the link on your second device. This will start flashing a series of QR codes. +6. Place your second device in front of the camera of the first device. Once enough QR codes have been scanned, you’ll see a success message. +7. Return to the first device and view the output of `op-txverify`. +8. Review the transaction contents and confirm that it matches the transaction that you expect to send, including all addresses and input parameters. +9. Review the message hash and domain hash and confirm that it matches the hashes presented in Superchain Ops. +10. Return to Superchain Ops to complete signing. + ### 4. Approve the signature on your ledger Once the validations are done, it's time to actually sign the diff --git a/src/SINGLE.md b/src/SINGLE.md index fbabb86075..a697282f9c 100644 --- a/src/SINGLE.md +++ b/src/SINGLE.md @@ -114,6 +114,22 @@ message hash: `0x1901[domain hash][message hash]`. Note down this value. You will need to compare it with the ones displayed on the Ledger screen at signing. +#### 3.4 Validate with op-txverify + +Use [op-txverify](https://github.com/ethereum-optimism/op-txverify) to confirm +the transaction and the message/domain hashes. + +1. Run the `simulate` command for your given Superchain Ops task. +2. Find the **`OP-TXVERIFY LINK`** section of the Superchain Ops task output. +3. Copy the link as shown in the output (links to `op-txverify.optimism.io`) and send it to another device (phone is easiest). +4. Run `op-txverify qr` on your first device. This will open a QR scanner. +5. Open the link on your second device. This will start flashing a series of QR codes. +6. Place your second device in front of the camera of the first device. Once enough QR codes have been scanned, you’ll see a success message. +7. Return to the first device and view the output of `op-txverify`. +8. Review the transaction contents and confirm that it matches the transaction that you expect to send, including all addresses and input parameters. +9. Review the message hash and domain hash and confirm that it matches the hashes presented in Superchain Ops. +10. Return to Superchain Ops to complete signing. + ### 4. Approve the signature on your ledger Once the validations are done, it's time to actually sign the From 8a1bd4d19d8643c4e526a155c551e16a47b94e82 Mon Sep 17 00:00:00 2001 From: Wazabie <48911235+Wazabie@users.noreply.github.com> Date: Fri, 17 Oct 2025 17:07:12 +0200 Subject: [PATCH 15/18] Update SetRespectedGameTypeTemplate.sol (#1252) * Update SetRespectedGameTypeTemplate.sol * Update files with ASR and guardian references * Update Regression.t.sol * Update Regression.t.sol * Update SetRespectedGameTypeTemplate.sol * Update SetRespectedGameTypeTemplate.sol * Update Regression.t.sol * Update Regression.t.sol * Update .env --- src/template/SetRespectedGameTypeTemplate.sol | 42 +++++++++---------- test/tasks/Regression.t.sol | 26 ++++++------ .../eth/004-fp-set-respected-game-type/.env | 3 +- .../config.toml | 4 +- 4 files changed, 37 insertions(+), 38 deletions(-) diff --git a/src/template/SetRespectedGameTypeTemplate.sol b/src/template/SetRespectedGameTypeTemplate.sol index 871060a1b6..ee48f8e717 100644 --- a/src/template/SetRespectedGameTypeTemplate.sol +++ b/src/template/SetRespectedGameTypeTemplate.sol @@ -3,10 +3,6 @@ pragma solidity 0.8.15; import {VmSafe} from "forge-std/Vm.sol"; import {stdToml} from "forge-std/StdToml.sol"; -import { - IDeputyGuardianModule, - IOptimismPortal2 -} from "lib/optimism/packages/contracts-bedrock/interfaces/safe/IDeputyGuardianModule.sol"; import {GameType} from "lib/optimism/packages/contracts-bedrock/src/dispute/lib/Types.sol"; import {L2TaskBase} from "src/tasks/types/L2TaskBase.sol"; @@ -14,8 +10,7 @@ import {SuperchainAddressRegistry} from "src/SuperchainAddressRegistry.sol"; import {Action} from "src/libraries/MultisigTypes.sol"; /// @title SetRespectedGameTypeTemplate -/// @notice This template is used to set the respected game type in the OptimismPortal2 contract -/// for a given chain or set of chains. +/// @notice Sets the respected game type in AnchorStateRegistry for a given chain or set of chains. contract SetRespectedGameTypeTemplate is L2TaskBase { using stdToml for string; @@ -28,17 +23,15 @@ contract SetRespectedGameTypeTemplate is L2TaskBase { /// @notice Mapping of chain ID to configuration for the task. mapping(uint256 => SetRespectedGameTypeTaskConfig) public cfg; - /// @notice Returns the string identifier for the safe executing this transaction. + /// @notice Execute as the Guardian safe (authorized on ASR). function safeAddressString() public pure override returns (string memory) { - return "FoundationOperationsSafe"; + return "GuardianSafe"; } /// @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] = "DeputyGuardianModule"; - storageWrites[1] = "Guardian"; - storageWrites[2] = "OptimismPortalProxy"; + string[] memory storageWrites = new string[](1); + storageWrites[0] = "AnchorStateRegistryProxy"; return storageWrites; } @@ -55,15 +48,14 @@ contract SetRespectedGameTypeTemplate is L2TaskBase { /// @notice Write the calls that you want to execute for the task. function _build(address) internal override { - // Load the DeputyGuardianModule contract. - IDeputyGuardianModule dgm = IDeputyGuardianModule(superchainAddrRegistry.get("DeputyGuardianModule")); - // Iterate over the chains and set the respected game type. SuperchainAddressRegistry.ChainInfo[] memory chains = superchainAddrRegistry.getChains(); for (uint256 i = 0; i < chains.length; i++) { uint256 chainId = chains[i].chainId; - address portalAddress = superchainAddrRegistry.getAddress("OptimismPortalProxy", chainId); - dgm.setRespectedGameType(IOptimismPortal2(payable(portalAddress)), cfg[chainId].gameType); + address asrAddress = superchainAddrRegistry.getAddress("AnchorStateRegistryProxy", chainId); + + // Call ASR to set the current respected game type: + IAnchorStateRegistry(asrAddress).setRespectedGameType(cfg[chainId].gameType); } } @@ -73,12 +65,20 @@ contract SetRespectedGameTypeTemplate is L2TaskBase { SuperchainAddressRegistry.ChainInfo[] memory chains = superchainAddrRegistry.getChains(); for (uint256 i = 0; i < chains.length; i++) { uint256 chainId = chains[i].chainId; - address portalAddress = superchainAddrRegistry.getAddress("OptimismPortalProxy", chainId); - IOptimismPortal2 portal = IOptimismPortal2(payable(portalAddress)); - assertEq(portal.respectedGameType().raw(), cfg[chainId].gameType.raw()); + address asrAddress = superchainAddrRegistry.getAddress("AnchorStateRegistryProxy", chainId); + IAnchorStateRegistry asr = IAnchorStateRegistry(asrAddress); + assertEq(asr.respectedGameType().raw(), cfg[chainId].gameType.raw()); } } /// @notice Override to return a list of addresses that should not be checked for code length. - function _getCodeExceptions() internal pure override returns (address[] memory) {} + function _getCodeExceptions() internal pure override returns (address[] memory) { + return new address[](0); + } +} + +// Minimal local copy; only what this template needs. +interface IAnchorStateRegistry { + function respectedGameType() external view returns (GameType); + function setRespectedGameType(GameType _gameType) external; } diff --git a/test/tasks/Regression.t.sol b/test/tasks/Regression.t.sol index eef37579f8..4a662becfd 100644 --- a/test/tasks/Regression.t.sol +++ b/test/tasks/Regression.t.sol @@ -103,29 +103,27 @@ contract RegressionTest is Test { ); } - /// @notice expected call data and data to sign generated by manually running the SetRespectedGameTypeTemplate at block 21724199 on mainnet using script: - /// forge script src/template/SetRespectedGameTypeTemplate.sol --sig "simulate(string)" test/tasks/mock/configs/SetRespectedGameTypeTemplate.toml --rpc-url mainnet --fork-block-number 21724199 -vv + /// @notice expected call data and data to sign generated by manually running the SetRespectedGameTypeTemplate at block 23591161 on mainnet using script: + /// cd test/tasks/example/eth/004-fp-set-respected-game-type + /// SIMULATE_WITHOUT_LEDGER=1 just --dotenv-path $(pwd)/.env --justfile ../../../../../src/justfile simulate council function testRegressionCallDataMatches_SetRespectedGameTypeTemplate() public { - string memory taskConfigFilePath = "test/tasks/mock/configs/SetRespectedGameTypeTemplate.toml"; + string memory taskConfigFilePath = "test/tasks/example/eth/004-fp-set-respected-game-type/config.toml"; string memory expectedCallData = - "0x174dea71000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000020000000000000000000000000c6901f65369fc59fc1b4d6d6be7a2318ff38db5b0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000044a1155ed9000000000000000000000000beb5fc579115071764c7423a4f12edde41f106ed000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"; + "0x174dea710000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000200000000000000000000000004890928941e62e273da359374b105f803329f47300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000247fc48504000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"; MultisigTask multisigTask = new SetRespectedGameTypeTemplate(); - address rootSafe = address(0x9BA6e03D8B90dE867373Db8cF1A58d2F7F006b3A); // FoundationOperationSafe + address rootSafe = address(0x09f7150D8c019BeF34450d6920f6B3608ceFdAf2); // GuardianSafe address[] memory allSafes = MultisigTaskTestHelper.getAllSafes(rootSafe); (Action[] memory actions, uint256[] memory allOriginalNonces) = - _setupAndSimulate(taskConfigFilePath, 21724199, "mainnet", multisigTask, allSafes); + _setupAndSimulate(taskConfigFilePath, 23591161, "mainnet", multisigTask, allSafes); - bytes memory rootSafeCalldata = - _assertCallDataMatches(multisigTask, actions, allSafes, allOriginalNonces, expectedCallData); - uint256 rootSafeNonce = allOriginalNonces[allOriginalNonces.length - 1]; + _assertCallDataMatches(multisigTask, actions, allSafes, allOriginalNonces, expectedCallData); - string memory expectedDataToSign = - "0x19014e6a6554de0308f5ece8ff736beed8a1b876d16f5c27cac8e466d7de0c7038904a8d9abb28e3fbba3ffc928e3357077c716885b5b1e2c51f2ee976a24f02445d"; + string[] memory expectedDataToSign = new string[](1); + expectedDataToSign[0] = + "0x1901df53d510b56e539b90b369ef08fce3631020fbf921e3136ea5f8747c20bce96784a424304de6939f40116e6a55f5653b172e5aa34a67b95ea28feb3d8c507528"; - _assertDataToSignSingleMultisig( - rootSafe, rootSafeCalldata, expectedDataToSign, rootSafeNonce, MULTICALL3_ADDRESS - ); + _assertDataToSignNestedMultisig(multisigTask, actions, expectedDataToSign, MULTICALL3_ADDRESS, rootSafe); } /// @notice expected call data and data to sign generated by manually running the example task test/tasks/example/sep/020-blacklist-games-v140: diff --git a/test/tasks/example/eth/004-fp-set-respected-game-type/.env b/test/tasks/example/eth/004-fp-set-respected-game-type/.env index dc044ed770..08903d7662 100644 --- a/test/tasks/example/eth/004-fp-set-respected-game-type/.env +++ b/test/tasks/example/eth/004-fp-set-respected-game-type/.env @@ -1 +1,2 @@ -FORK_BLOCK_NUMBER=22990492 \ No newline at end of file +FORK_BLOCK_NUMBER=23591161 +NESTED_SAFE_NAME_DEPTH_1=council diff --git a/test/tasks/example/eth/004-fp-set-respected-game-type/config.toml b/test/tasks/example/eth/004-fp-set-respected-game-type/config.toml index fe8b90b4cf..44e9a189e6 100644 --- a/test/tasks/example/eth/004-fp-set-respected-game-type/config.toml +++ b/test/tasks/example/eth/004-fp-set-respected-game-type/config.toml @@ -1,10 +1,10 @@ l2chains = [ - {name = "OP Mainnet", chainId = 10} + {name = "Soneium", chainId = 1868} ] templateName = "SetRespectedGameTypeTemplate" [gameTypes] configs = [ - {chainId = 10, gameType = 1} + {chainId = 1868, gameType = 0} ] From 37b2e1e398087a76ad9340b83ec3a994d5e102ba Mon Sep 17 00:00:00 2001 From: Wazabie <48911235+Wazabie@users.noreply.github.com> Date: Fri, 17 Oct 2025 17:37:02 +0200 Subject: [PATCH 16/18] add new template for addGameType (#1253) * add new template for addGameType * Update AddGameTypeTemplate.sol * Update AddGameTypeTemplate.sol * Update AddGameTypeTemplate.sol * Update AddGameTypeTemplate.sol * Update Regression.t.sol * Update AddGameTypeTemplate.sol * Update AddGameTypeTemplate.sol * Update AddGameTypeTemplate.sol * Update AddGameTypeTemplate.sol * Delete AddGameTypeTemplate.toml * Add safe name depth to env file * Update with sony sepolia data in example task * Update imports * Update AddGameTypeTemplate.sol --- src/template/AddGameTypeTemplate.sol | 147 ++++++++++++++++++ test/tasks/Regression.t.sol | 29 ++++ test/tasks/example/sep/012-add-game-type/.env | 2 + .../example/sep/012-add-game-type/config.toml | 24 +++ 4 files changed, 202 insertions(+) create mode 100644 src/template/AddGameTypeTemplate.sol create mode 100644 test/tasks/example/sep/012-add-game-type/.env create mode 100644 test/tasks/example/sep/012-add-game-type/config.toml diff --git a/src/template/AddGameTypeTemplate.sol b/src/template/AddGameTypeTemplate.sol new file mode 100644 index 0000000000..ba62f9129c --- /dev/null +++ b/src/template/AddGameTypeTemplate.sol @@ -0,0 +1,147 @@ +// 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/tasks/types/OPCMTaskBase.sol"; +import {SuperchainAddressRegistry} from "src/SuperchainAddressRegistry.sol"; +import {Action} from "src/libraries/MultisigTypes.sol"; + +import {GameType, Claim, Duration} from "@eth-optimism-bedrock/src/dispute/lib/Types.sol"; +import { + IOPContractsManager, + IDisputeGameFactory, + IFaultDisputeGame, + IBigStepper, + IProxyAdmin, + IDelayedWETH, + ISystemConfig +} from "@eth-optimism-bedrock/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 Address of the OPCM contract. + address private OPCM; + + /// @notice Returns string identifiers for addresses that are expected to have their storage written to. + function _taskStorageWrites() internal view virtual override returns (string[] memory) { + string[] memory storageWrites = new string[](1); + storageWrites[0] = "DisputeGameFactoryProxy"; + return storageWrites; + } + + /// @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 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"); + require(OPCM != address(0), "OPCM not set"); + vm.label(OPCM, "OPCM"); + + // Set OPCM as the target for delegatecalls. + OPCM_TARGETS = new address[](1); + OPCM_TARGETS[0] = OPCM; + } + + /// @notice Write the calls that you want to execute for the task. + function _build(address) 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, address) 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 view virtual override returns (address[] memory) {} + + /// @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 4a662becfd..44091785ee 100644 --- a/test/tasks/Regression.t.sol +++ b/test/tasks/Regression.t.sol @@ -39,6 +39,7 @@ 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"; +import {AddGameTypeTemplate} from "src/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 @@ -898,6 +899,34 @@ contract RegressionTest is Test { ); } + /// @notice Expected call data and data to sign generated by manually running the AddGameType template at block 9431469 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"; + string memory expectedCallData = + "0x82ad56cb0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000200000000000000000000000003bb6437aba031afbf9cb3538fa064161e2bf2d780000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000002441661a2e900000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000001a00000000000000000000000004ca9608fef202216bc21d543798ec854539baad3000000000000000000000000ff9d236641962cebf9dbfb54e7b8e91f99f10db0000000000000000000000000b39c1730dff54f25f9e45667c119e0a8fee7315600000000000000000000000000000000000000000000000000000000000000000339db503776757491b9f3038bf6f1d37b7988a2f75e823fe2656c1352ef2f910000000000000000000000000000000000000000000000000000000000000049000000000000000000000000000000000000000000000000000000000000001e0000000000000000000000000000000000000000000000000000000000002a300000000000000000000000000000000000000000000000000000000000049d40000000000000000000000000000000000000000000000000011c37937e08000000000000000000000000000007babe08ee4d07dba236530183b24055535a7011000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000147468697320697320612073616c74206d6978657200000000000000000000000000000000000000000000000000000000000000000000000000000000"; + MultisigTask multisigTask = new AddGameTypeTemplate(); + address rootSafe = address(0x1Eb2fFc903729a0F03966B917003800b145F56E2); + address foundationChildMultisig = address(0xDEe57160aAfCF04c34C887B5962D0a69676d3C8B); + address[] memory allSafes = MultisigTaskTestHelper.getAllSafes(rootSafe, foundationChildMultisig); + + (Action[] memory actions, uint256[] memory allOriginalNonces) = + _setupAndSimulate(taskConfigFilePath, 9431469, "sepolia", multisigTask, allSafes); + + _assertCallDataMatches(multisigTask, actions, allSafes, allOriginalNonces, expectedCallData); + + string[] memory expectedDataToSign = new string[](2); + // Foundation + expectedDataToSign[0] = + "0x190137e1f5dd3b92a004a23589b741196c8a214629d4ea3a690ec8e41ae45c689cbb219585bec52931beea5abdd75155cfff7596dc1bc3697006ee74dcd3683a9062"; + // Security council + expectedDataToSign[1] = + "0x1901be081970e9fc104bd1ea27e375cd21ec7bb1eec56bfe43347c3e36c5d27b853367060414e1617ef70777db275e9b27086a8ad00395f28214b4b9d1099a6d99df"; + + _assertDataToSignNestedMultisig(multisigTask, actions, expectedDataToSign, MULTICALL3_ADDRESS, rootSafe); + } + /// @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) 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..b076a25c96 --- /dev/null +++ b/test/tasks/example/sep/012-add-game-type/.env @@ -0,0 +1,2 @@ +FORK_BLOCK_NUMBER=9431469 +NESTED_SAFE_NAME_DEPTH_1=foundation 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..af1c29b61d --- /dev/null +++ b/test/tasks/example/sep/012-add-game-type/config.toml @@ -0,0 +1,24 @@ +templateName = "AddGameTypeTemplate" + +l2chains = [ + {name = "Soneium Testnet Minato", chainId = 1946}, +] + +[[configs]] +chainId = 1946 +saltMixer = "this is a salt mixer" +systemConfig = "0x4Ca9608Fef202216bc21D543798ec854539bAAd3" +proxyAdmin = "0xff9d236641962Cebf9DBFb54E7b8e91F99f10Db0" +delayedWETH = "0xB39c1730DFF54f25F9e45667c119e0a8FeE73156" +disputeGameType = 0 +disputeAbsolutePrestate = "0x0339db503776757491b9f3038bf6f1d37b7988a2f75e823fe2656c1352ef2f91" +disputeMaxGameDepth = 73 +disputeSplitDepth = 30 +disputeClockExtension = 10800 +disputeMaxClockDuration = 302400 +initialBond = 80000000000000000 +vm = "0x07babe08ee4d07dba236530183b24055535a7011" +permissioned = false + +[addresses] +OPCM = "0x3bb6437aba031afbf9cb3538fa064161e2bf2d78" From 5d968de92e51ea8fab120673d2e55318ffa3c950 Mon Sep 17 00:00:00 2001 From: 0xchin Date: Fri, 17 Oct 2025 14:10:08 -0300 Subject: [PATCH 17/18] fix: broken simulations --- test/tasks/Regression.t.sol | 2 +- test/template/RevenueShareUpgradePath.t.sol | 2 +- test/template/late-opt-in-rev-share/LateOptInRevenueShare.t.sol | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/test/tasks/Regression.t.sol b/test/tasks/Regression.t.sol index f7834883c7..fa8b7db220 100644 --- a/test/tasks/Regression.t.sol +++ b/test/tasks/Regression.t.sol @@ -1167,4 +1167,4 @@ contract RegressionTest is Test { assertEq(keccak256(bytes(dataToSign)), keccak256(bytes(expectedDataToSign[i]))); } } -} \ No newline at end of file +} diff --git a/test/template/RevenueShareUpgradePath.t.sol b/test/template/RevenueShareUpgradePath.t.sol index 1dc7646b3c..e691aa4460 100644 --- a/test/template/RevenueShareUpgradePath.t.sol +++ b/test/template/RevenueShareUpgradePath.t.sol @@ -140,7 +140,7 @@ contract RevenueShareUpgradePathTest is Test { string memory _actionCountMessage ) internal { // Step 1: Run simulate to prepare everything and get the actions - (, Action[] memory _actions,,, address _rootSafe) = template.simulate(_configPath, new address[](0)); + (, Action[] memory _actions,, address _rootSafe) = template.simulate(_configPath, new address[](0)); // Verify we got the expected safe and action count assertEq(_rootSafe, PROXY_ADMIN_OWNER, "Root safe should be ProxyAdminOwner"); diff --git a/test/template/late-opt-in-rev-share/LateOptInRevenueShare.t.sol b/test/template/late-opt-in-rev-share/LateOptInRevenueShare.t.sol index cd220206f3..5ae7211b52 100644 --- a/test/template/late-opt-in-rev-share/LateOptInRevenueShare.t.sol +++ b/test/template/late-opt-in-rev-share/LateOptInRevenueShare.t.sol @@ -98,7 +98,7 @@ contract LateOptInRevenueShareTest is Test { address calculator ) internal { // Step 1: Run simulate to prepare everything and get the actions - (, Action[] memory actions,,, address rootSafe) = template.simulate(configPath, new address[](0)); + (, Action[] memory actions,, address rootSafe) = template.simulate(configPath, new address[](0)); // Verify we got the expected safe and action count assertEq(rootSafe, PROXY_ADMIN_OWNER, "Root safe should be ProxyAdminOwner"); From bd67ee1bf960d8ac568aa8f743afa03ae69eff7d Mon Sep 17 00:00:00 2001 From: 0xchin Date: Fri, 17 Oct 2025 14:12:50 -0300 Subject: [PATCH 18/18] fix: wrong location of synced regression tests --- test/tasks/Regression.t.sol | 52 ++++++++++++++++++------------------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/test/tasks/Regression.t.sol b/test/tasks/Regression.t.sol index fa8b7db220..e3d7cb0b81 100644 --- a/test/tasks/Regression.t.sol +++ b/test/tasks/Regression.t.sol @@ -870,6 +870,32 @@ contract RegressionTest is Test { _assertDataToSignNestedMultisig(multisigTask, actions, expectedDataToSign, MULTICALL3_ADDRESS, rootSafe); } + // @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 Expected call data and data to sign generated by manually running the RevenueShareV100UpgradePath template at block 23434662 on mainnet. /// Simulate from task directory (test/tasks/example/eth/015-revenue-share-upgrade/config.toml) with: /// just --dotenv-path $(pwd)/.env --justfile ../../../../../src/improvements/justfile simulate (foundation|council) @@ -1008,32 +1034,6 @@ contract RegressionTest is Test { _assertDataToSignNestedMultisig(multisigTask, actions, expectedDataToSign, MULTICALL3_ADDRESS, rootSafe); } - /// @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 Expected call data and data to sign generated by manually running the LateOptInRevenueShare template at block 23197819 on mainnet, using a custom calculator. /// Simulate from task directory (test/tasks/example/eth/017-opt-in-revenue-share-late-custom-calc/config.toml) with: /// just --dotenv-path $(pwd)/.env --justfile ../../../../../src/improvements/justfile simulate (foundation|council)