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 1/3] 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 0f2032038..89f4ead34 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 000000000..cfedd993b --- /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 70455e273..b16ffbd3c 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 000000000..d4a7b7605 --- /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 000000000..318a8809d --- /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 2/3] 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 20695cf62..cb900f8f4 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 c260dab88..bcc31c366 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 adf17d369..205f6ffe6 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 d32a8453f..a4792f156 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 16c732040..ec1f573a1 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 dd2913447..12100f664 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 a13b10499..d1ec5bb71 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 ff1ee8afb..0e9ba7538 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 b4b63fd55..3504c5992 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 3/3] 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 a4792f156..cfb02dd2f 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