Skip to content
Closed
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
289bdd4
chore: add remapping for optimism lib
oxlumi Aug 19, 2025
fb1de79
feat: L1PortalExecuteL2Call task implementation
oxlumi Aug 19, 2025
27de7e3
feat: rehearsals specs for L1PortalExecuteL2Call task
oxlumi Aug 19, 2025
64e7689
test: add regression test for new template L1PortalExecuteL2Call
0xiamflux Aug 23, 2025
a4bc70d
feat: create new rehearsal ceremony to make an upgrade to L2 Governor
0xiamflux Aug 25, 2025
0f97595
chore: update comment on Optimism Portal address
0xiamflux Aug 27, 2025
dfadb1f
Merge pull request #2 from defi-wonderland/test/l1portalexecutel2call
0xiamflux Aug 27, 2025
9b22137
chore: sync with main branch
0xiamflux Aug 27, 2025
212a84e
refactor: remove nested try-catch
0xiamflux Aug 28, 2025
fe44a7e
chore: update fmt
0xiamflux Aug 28, 2025
b823c6a
chore: complies with ci
0xiamflux Aug 28, 2025
a9b18e6
chore: remove unecessary remapping
0xiamflux Sep 15, 2025
51d63c5
chore: remove rehearsal docs for no-op upgrade
0xiamflux Sep 18, 2025
543b409
chore: fix compiler warnings
0xiamflux Sep 18, 2025
cc569d3
chore: fix just simulate sep command failure
0xiamflux Sep 18, 2025
fcc89ba
chore: remove newline in sc rehearsals README
0xiamflux Sep 22, 2025
fadfa08
refactor: add OptimismPortal interface in template to avoid coupling
0xiamflux Sep 22, 2025
a3ca79e
chore: update comment natspec on template
0xiamflux Sep 22, 2025
0caf083
feat: revert before reaching portal with a descriptive message when i…
0xiamflux Sep 22, 2025
c0cd688
refactor: prefe readBytes over manually parsing
0xiamflux Sep 22, 2025
0af20c9
fix: use encodeCall over encodeWithSelector
0xiamflux Sep 22, 2025
cde2b35
chore: undo diff for canceled task
0xiamflux Sep 22, 2025
3f3b8d9
chore: fix README newlines
0xiamflux Sep 22, 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
1 change: 1 addition & 0 deletions security-council-rehearsals/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ This directory contains templates and ceremonies related to onboarding Security
## Creating and signing a new rehearsal ceremony

To create a new rehearsal ceremony, follow the instructions in the _Facilitator_ section of the README files for each of the following rehearsals:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit, can undo this diff since we don't touch the rehearsals dir anywhere else here

1. [rehearsal-1-welcome](./rehearsal-1-welcome/README.md)
2. [rehearsal-2-remove-signer](./rehearsal-2-remove-signer/README.md)
3. [rehearsal-3-nested-upgrade](./rehearsal-3-jointly-upgrade/README.md)
Expand Down

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This task was recently changed to CANCELLED so we should be able to undo the diff in this task's dir

Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ First, we need to validate the domain and message hashes. These values should ma
>
> ### Worldchain Proxy Admin Owner (`0x945185C01fb641bA3E63a9bdF66575e35a407837`)
>
> - Domain Hash: `0x6faec9c52949ba8274340008df12c69faedd5c44e77f77c956d2ca8e4bcd877e`
> - Message Hash: `0x20556e9d7d2847116caef0b79cdcd207b399adcdb01776e368fcbc9011559a4e`
> - Domain Hash: `0x6faec9c52949ba8274340008df12c69faedd5c44e77f77c956d2ca8e4bcd877e`
> - Message Hash: `0x9d228a76bf7ab77438ffffc6b869643ff697f20fbf681147caa88c93752aa06f`

## Normalized State Diff Hash Attestation

Expand Down Expand Up @@ -74,8 +74,8 @@ Note: The changes listed below do not include threshold, nonce and owner mapping

- **Key:** `0x0000000000000000000000000000000000000000000000000000000000000005`
- **Decoded Kind:** `uint256`
- **Before:** `44` - (`cast --to-dec 0x2c` is `44`)
- **After:** `45` - (`cast --to-dec 0x2d` is `45`)
- **Before:** `51` - (`cast --to-dec 0x33` is `51`)
- **After:** `52` - (`cast --to-dec 0x34` is `52`)
- **Summary:** nonce
- **Detail:** Nonce update for the parent multisig.

Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
l2chains = [{name = "Worldchain Sepolia", chainId = 4801}]
l2chains = [{name = "Worldchain Sepolia", chainId = 4801}]
templateName = "TransferOwners"
safeAddressString = "ProxyAdminOwner"

# Standard Sepolia L1PAO - https://github.com/ethereum-optimism/superchain-registry/blob/93c5073d233cb9011a95aebf275270fd00346400/validation/standard/standard-config-roles-sepolia.toml#L3
newOwner = "0x1Eb2fFc903729a0F03966B917003800b145F56E2"

[stateOverrides]
0x945185c01fb641ba3e63a9bdf66575e35a407837 = [ # Worldchain L1PAO
{key = "0x0000000000000000000000000000000000000000000000000000000000000005", value = 45}
{key = "0x0000000000000000000000000000000000000000000000000000000000000005", value = 51}
]
117 changes: 117 additions & 0 deletions src/improvements/template/L1PortalExecuteL2Call.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.15;

import {VmSafe} from "forge-std/Vm.sol";
import {stdToml} from "forge-std/StdToml.sol";

import {IOptimismPortal2} from "lib/optimism/packages/contracts-bedrock/interfaces/L1/IOptimismPortal2.sol";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of importing the interface, let's define the relevant interface in this file with interface IOptimismPortal2 { ... }. The reason for this is so that, in the future, the template can keep working for chains running older versions of the OP stack, even if the monorepo submodule version is bumped. In other words, we don't want templates to suddenly break when we bump the submodule version (i.e. if we change the portal interface in the future) because not all chains are necessarily on the latest version at all times, so we prefer to make sure templates continue to work

import {MultisigTaskPrinter} from "../../libraries/MultisigTaskPrinter.sol";
import {Action} from "../../libraries/MultisigTypes.sol";
import {SimpleTaskBase} from "../tasks/types/SimpleTaskBase.sol";

/// @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.
contract L1PortalExecuteL2Call is SimpleTaskBase {
Comment thread
blmalone marked this conversation as resolved.
using stdToml for string;

// -------- Config inputs --------

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment // -------- Config inputs -------- should use triple-slash Solidity NatSpec format: /// -------- Config inputs -------- per the Solidity style guide requirements.

Suggested change
// -------- Config inputs --------
/// -------- Config inputs --------

Spotted by Diamond (based on custom rule: Custom rules)

Fix in Graphite


Is this helpful? React 👍 or 👎 to let us know.

address payable public portal; // L1 OptimismPortal address
address public l2Target; // L2 target address
bytes public l2Data; // Inner L2 calldata

@mds1 mds1 Sep 19, 2025

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit, just to be more precise, assuming this is correct

Suggested change
bytes public l2Data; // Inner L2 calldata
bytes public l2Data; // Inner L2 calldata, i.e. the calldata to execute on `l2Target`

uint256 public valueWei; // ETH value to forward to L2 (defaults to 0)
uint64 public gasLimit; // L2 gas limit (required)
bool public isCreation; // Whether to create a contract on L2 (defaults to false)

/// @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] = "OptimismPortal";
return _storageWrites;
}

/// @notice The contracts expected to have balance changes during execution.
/// Allowlist the OptimismPortal to receive ETH (value) in the deposit call.
function _taskBalanceChanges() internal pure override returns (string[] memory) {
string[] memory _balanceChanges = new string[](1);
_balanceChanges[0] = "OptimismPortal";
return _balanceChanges;
}

/// @notice Parse config and initialize template variables.
/// Expected TOML keys:
/// - portal: address (L1 OptimismPortal) OR addresses.OptimismPortal in [addresses]
/// - l2Target: address (L2 target address)
/// - l2Data: hex string (e.g. 0x1234...)
/// - gasLimit: uint (will be cast to uint64)
/// - value: uint (optional, default 0)
/// - isCreation: bool (optional, default false)
function _templateSetup(string memory _taskConfigFilePath, address) internal override {
string memory _toml = vm.readFile(_taskConfigFilePath);

// Resolve portal from registry first if available, else read explicit field.
try simpleAddrRegistry.get("OptimismPortal") returns (address p) {
portal = payable(p);
} catch {
portal = payable(_toml.readAddress(".portal"));
}
Comment on lines +75 to +80

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the revshare use case, it might be better UX (fewer playbooks, fewer signatures) to have this template support doing a deposit transaction for many L2s in the same transaction. In that case, we would want to inherit from L2TaskBas instead and have the config inputs be arrays corresponding to the deposit. What do you think here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you think it makes sense to have 2 separate templates one for simpler use cases and the other for multi chain calls?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As we continued working on the Rev Share use case, we have came to the conclusion it should have it’s own, separate template and we can keep this as a general purpose template for more simple cases. Wdyt?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Having a separate template is good with me!

require(portal != address(0), "portal must be set (addresses.OptimismPortal or .portal)");

l2Target = _toml.readAddress(".l2Target");
require(l2Target != address(0), "l2Target must be set");

// Read hex string and parse to bytes.
string memory _dataHex = _toml.readString(".l2Data");
l2Data = vm.parseBytes(_dataHex);
require(l2Data.length > 0, "l2Data must be set");
Comment on lines +86 to +88

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could just do _toml.readBytes here


uint256 _gasLimitTmp = _toml.readUint(".gasLimit");
require(_gasLimitTmp > 0 && _gasLimitTmp <= type(uint64).max, "invalid gasLimit");
gasLimit = uint64(_gasLimitTmp);

// Optional fields
valueWei = 0;
try vm.parseTomlUint(_toml, ".value") returns (uint256 _v) {
valueWei = _v;
} catch {}

isCreation = false;
try vm.parseTomlBool(_toml, ".isCreation") returns (bool _b) {
isCreation = _b;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The portal has this check so it's arguably unnecessary, but might be better UX to check and revert here if isCreation && l2Target != address(0). Up to you

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do like this, makes sense to early revert before reaching the portal.

} catch {}
}

/// @notice Build the portal deposit action. WARNING: State changes here are reverted after capture.
Comment thread
blmalone marked this conversation as resolved.
function _build(address) internal override {
// Record the L1 portal call with value for action extraction.
IOptimismPortal2(portal).depositTransaction{value: valueWei}(l2Target, valueWei, gasLimit, isCreation, l2Data);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does this need to take a value? I'm curious as I don't think we've fully tested if the value actually gets transferred.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the particular use case we had in mind when adding the template it does not need the value, but I think for a general purpose solution is good to have it there. What would be the proper way to fully test and document this?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you hardcode 0 for this first version?

When performing an upgrade action i.e. portal.depositTransaction, this function is executed inside a Multicall3 contract. The safe delegatecall's to the Multicall3 contract. Right now, this will fail because of the require check inside the Multicall3 contract: https://github.com/mds1/multicall3/blob/main/src/Multicall3.sol#L160 (see: stackexchange)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in here.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

got it, do you want to close this PR in favor of #1231?

}

/// @notice Validate that exactly one action to the portal with the expected calldata and value was captured.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The _validate function is missing natspec documentation. According to the Solidity Guide, functions should use triple-slash natspec comment style with @notice tags instead of single-line comments. Replace the single-line comment with proper natspec documentation using /// @notice.

Spotted by Diamond (based on custom rule: Custom rules)

Fix in Graphite


Is this helpful? React 👍 or 👎 to let us know.

function _validate(VmSafe.AccountAccess[] memory, Action[] memory _actions, address) internal view override {
bytes memory _expected = abi.encodeWithSelector(
IOptimismPortal2.depositTransaction.selector, l2Target, valueWei, gasLimit, isCreation, l2Data
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit, prefer abi.encodeCall


bool _found;
uint256 _matches;
for (uint256 _i = 0; _i < _actions.length; _i++) {
if (_actions[_i].target == portal && _actions[_i].value == valueWei) {
if (keccak256(_actions[_i].arguments) == keccak256(_expected)) {
_found = true;
_matches++;
}
}
}
require(_found && _matches == 1, "expected one portal deposit action");
MultisigTaskPrinter.printTitle("Validated portal deposit action");
}

/// @notice No code exceptions required for this template.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The _getCodeExceptions function is missing natspec documentation. According to the Solidity Guide, functions should use triple-slash natspec comment style with @notice tags instead of single-line comments. Replace the single-line comment with proper natspec documentation using /// @notice.

Spotted by Diamond (based on custom rule: Custom rules)

Fix in Graphite


Is this helpful? React 👍 or 👎 to let us know.

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 @@ -29,6 +29,7 @@ import {WelcomeToSuperchainOps} from "src/improvements/template/WelcomeToSuperch
import {GnosisSafeRemoveOwner} from "src/improvements/template/GnosisSafeRemoveOwner.sol";
import {SetEIP1967Implementation} from "src/improvements/template/SetEIP1967Implementation.sol";
import {UnpauseSuperchainConfigV400} from "src/improvements/template/UnpauseSuperchainConfigV400.sol";
import {L1PortalExecuteL2Call} from "src/improvements/template/L1PortalExecuteL2Call.sol";
import {UniFix} from "src/improvements/template/UniFix.sol";
import {DeputyPauseKeyRotationTemplate} from "src/improvements/template/DeputyPauseKeyRotationTemplate.sol";
import {BlacklistGamesV140} from "src/improvements/template/BlacklistGamesV140.sol";
Expand Down Expand Up @@ -678,6 +679,34 @@ 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 UniFix template at block 8029861 on sepolia.
/// Simulate from task directory (test/tasks/example/sep/004-replace-superchain-config/config.toml) with:
/// SIMULATE_WITHOUT_LEDGER=1 just --dotenv-path "$(pwd)"/.env --justfile ../../../../../src/improvements/justfile simulate
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
13 changes: 13 additions & 0 deletions test/tasks/example/eth/014-noop-call-optimismportal/config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
templateName = "L1PortalExecuteL2Call"

# Portal + L2 call params
portal = "0xbEb5Fc579115071764c7423A4f12eDde41f106Ed" # L1 OptimismPortal
l2Target = "0xcDF27F107725988f2261Ce2256bDfCdE8B382B10" # OptimismGovernor Proxy
l2Data = "0x3659cfe6000000000000000000000000ecbf4ed9f47302f00f0f039a691e7db83bdd2624" # upgradeTo(currentImpl) -> 0xecbf4ed9f47302f00f0f039a691e7db83bdd2624
gasLimit = 500000
value = 0
isCreation = false

[addresses]
ProxyAdminOwner = "0x5a0Aae59D09fccBdDb6C6CcEB07B7279367C3d2A" # 2-of-2 between council and foundation
OptimismPortal = "0xbEb5Fc579115071764c7423A4f12eDde41f106Ed"