Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
ca5166b
chore: add remapping for optimism lib
oxlumi Aug 19, 2025
d525891
feat: L1PortalExecuteL2Call task implementation
oxlumi Aug 19, 2025
f1ed0dd
feat: rehearsals specs for L1PortalExecuteL2Call task
oxlumi Aug 19, 2025
9339f01
test: add regression test for new template L1PortalExecuteL2Call
0xiamflux Aug 23, 2025
abb4eff
feat: create new rehearsal ceremony to make an upgrade to L2 Governor
0xiamflux Aug 25, 2025
a05ce62
chore: update comment on Optimism Portal address
0xiamflux Aug 27, 2025
f5567eb
refactor: remove nested try-catch
0xiamflux Aug 28, 2025
2906167
chore: update fmt
0xiamflux Aug 28, 2025
9f790bc
chore: complies with ci
0xiamflux Aug 28, 2025
0487cc5
chore: remove unecessary remapping
0xiamflux Sep 15, 2025
d1f8316
chore: remove rehearsal docs for no-op upgrade
0xiamflux Sep 18, 2025
f6c81c7
chore: fix compiler warnings
0xiamflux Sep 18, 2025
cecebc3
chore: fix just simulate sep command failure
0xiamflux Sep 18, 2025
8c0a793
chore: remove newline in sc rehearsals README
0xiamflux Sep 22, 2025
e43921e
refactor: add OptimismPortal interface in template to avoid coupling
0xiamflux Sep 22, 2025
2bbef1b
chore: update comment natspec on template
0xiamflux Sep 22, 2025
cdc54f6
feat: revert before reaching portal with a descriptive message when i…
0xiamflux Sep 22, 2025
9dd87c9
refactor: prefe readBytes over manually parsing
0xiamflux Sep 22, 2025
059e459
fix: use encodeCall over encodeWithSelector
0xiamflux Sep 22, 2025
f972e55
chore: undo diff for canceled task
0xiamflux Sep 22, 2025
d43bb9f
chore: fix README newlines
0xiamflux Sep 22, 2025
101b92a
chore: add fmt fix to template
0xiamflux Sep 25, 2025
c5fae27
fix: lib import path in template
0xiamflux Sep 25, 2025
2b47db1
refactor: make value passed to portal 0
0xiamflux Sep 25, 2025
876a669
fix: library path
0xiamflux Sep 25, 2025
f85ac3b
refactor: use L2TaskBase instead of SimpleTaskBase
0xiamflux Sep 29, 2025
ffa8ace
chore: add version tag of OptimismPortal
0xiamflux Sep 29, 2025
8a225bb
refactor: renaming loop variables
0xiamflux Sep 29, 2025
400311c
fix: portal from value change allowlist, remove value natspec ref
0xiamflux Sep 30, 2025
bb98b11
Merge pull request #20 from defi-wonderland/fix/value-removal
0xiamflux Sep 30, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
118 changes: 118 additions & 0 deletions src/template/L1PortalExecuteL2Call.sol
Original file line number Diff line number Diff line change
@@ -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;
}
Comment thread
blmalone marked this conversation as resolved.

/// @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.
Comment thread
blmalone marked this conversation as resolved.
/// 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) {}
Comment thread
blmalone marked this conversation as resolved.

/// @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");
Comment thread
blmalone marked this conversation as resolved.

uint256 _gasLimitTmp = _toml.readUint(".gasLimit");
require(_gasLimitTmp > 0 && _gasLimitTmp <= type(uint64).max, "invalid gasLimit");
gasLimit = uint64(_gasLimitTmp);
Comment thread
blmalone marked this conversation as resolved.

// 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");
Comment thread
blmalone marked this conversation as resolved.
}

/// @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);
Comment thread
blmalone marked this conversation as resolved.
}
}

/// @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 =
Comment thread
blmalone marked this conversation as resolved.
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) {}
}
29 changes: 29 additions & 0 deletions test/tasks/Regression.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down
3 changes: 3 additions & 0 deletions test/tasks/example/eth/014-noop-call-optimismportal/.env
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
TENDERLY_GAS=10000000
NESTED_SAFE_NAME_DEPTH_1=council
FORK_BLOCK_NUMBER=23197819
Original file line number Diff line number Diff line change
@@ -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