Skip to content

feat: add L1PortalExecuteL2Call template for L2 actions through OptimismPortal#1173

Closed
0xiamflux wants to merge 23 commits into
ethereum-optimism:mainfrom
defi-wonderland:sc-feat/l1portalexecutel2call-template
Closed

feat: add L1PortalExecuteL2Call template for L2 actions through OptimismPortal#1173
0xiamflux wants to merge 23 commits into
ethereum-optimism:mainfrom
defi-wonderland:sc-feat/l1portalexecutel2call-template

Conversation

@0xiamflux
Copy link
Copy Markdown
Contributor

Description

  • Add L1→L2 portal execution template
    • src/improvements/template/L1PortalExecuteL2Call.sol
    • Uses full IOptimismPortal2 (depositTransaction) to dispatch an L2 call from an L1 Safe
    • Config-driven params via config.toml: portal, l2Target, l2Data, gasLimit (uint64), value (default 0), isCreation (default false)
    • Extends SimpleTaskBase for L1 multisig flows; safeAddressString() defaults to ProxyAdminOwner
    • Validation: asserts exactly one portal call with expected calldata/value; allowlists storage/balance changes for OptimismPortal
  • Add full portal interface
    • src/improvements/interfaces/IOptimismPortal.sol (complete IOptimismPortal2)
  • Scaffold nested rehearsal task
    • src/improvements/tasks/eth/rehearsals/2025-08-11-l1-to-l2-noop/
    • config.toml placeholders and signer README.md with simulate/validate/sign steps

Tests

  • Template-level validation enforces encoded calldata and a single portal action
  • Manual simulate run expected:
    • Tenderly diff: no unintended changes; shows portal call and event emission


# Portal + L2 call params
portal = "0xbEb5Fc579115071764c7423A4f12eDde41f106Ed" # L1 OptimismPortal
l2Target = "0x1234567890123456789012345678901234567890" # replace with real OptimismGovernor Proxy
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 placeholder address 0x1234567890123456789012345678901234567890 should be replaced with the actual OptimismGovernor Proxy address on L2 before using this configuration for the rehearsal. Using a real address ensures that the simulation and validation steps will accurately reflect the intended behavior when executed on-chain.

Spotted by Diamond

Fix in Graphite


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

# Portal + L2 call params
portal = "0xbEb5Fc579115071764c7423A4f12eDde41f106Ed" # L1 OptimismPortal
l2Target = "0x1234567890123456789012345678901234567890" # replace with real OptimismGovernor Proxy
l2Data = "0x3659cfe60000000000000000000000000000000000000000000000000000000000001234" # upgradeTo(0x00..1234)
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 placeholder address 0x00..1234 in the L2Data is not a valid implementation address for a production deployment. For a proper rehearsal, consider replacing this with either:

  1. The current implementation address (for a true no-op upgrade)
  2. A valid implementation contract address that exists on L2

This ensures the rehearsal accurately simulates a real governance action. The test example in 014-noop-call-optimismportal demonstrates this approach by using the current implementation address.

Spotted by Diamond

Fix in Graphite


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

@@ -0,0 +1,265 @@
# Rehearsal 4 - No-op Governor Upgrade
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.

Why is this being put into the rehearsals directory?

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.

GM, our goal here was to add a rehearsal task for a no-op upgrade on the Optimism Governor Proxy on L2 using a newly added template. What would be the best place to do 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.

hm, this rehearsals directory contains templates and ceremonies related to onboarding Security Council members. I don't think this is what you want to do right?

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.

The task was to create a rehearsal upgrade ceremony for the Security Council to complete an end-to-end process of a no-op upgrade in a way that is repeatable.

Which would be a better suited place to put it?

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.

Is this simply to test this templates functionality? Or have you been in touch with the security council and agreed that this should be a new rehearsal for them to perform when onboarding a security council memeber?

If it's the former, you don't need to add this as a rehearsal. When you create a new template, you just need to follow this guide: https://github.com/ethereum-optimism/superchain-ops/blob/main/src/improvements/doc/NEW_TEMPLATE_GUIDE.md (1. make template 2. make Regression test 3. make example task

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.

We just removed the rehearsal docs in the rehearsal folder on this commit.

Comment thread foundry.toml Outdated
Comment thread src/improvements/template/L1PortalExecuteL2Call.sol Outdated
import {Action} from "../../libraries/MultisigTypes.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.

Suggested change

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 here.

Comment thread src/improvements/template/L1PortalExecuteL2Call.sol Outdated
}

/// @notice Validate that exactly one action to the portal with the expected calldata and value was captured.
function _validate(VmSafe.AccountAccess[] memory, Action[] memory _actions, address) internal override {
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.

Suggested change
function _validate(VmSafe.AccountAccess[] memory, Action[] memory _actions, address) internal override {
function _validate(VmSafe.AccountAccess[] memory, Action[] memory _actions, address) internal view override {

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 here.

/// @notice Build the portal deposit action. WARNING: State changes here are reverted after capture.
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?

Comment thread src/improvements/template/L1PortalExecuteL2Call.sol
Comment thread src/improvements/template/L1PortalExecuteL2Call.sol
IOptimismPortal2(portal).depositTransaction{value: valueWei}(l2Target, valueWei, gasLimit, isCreation, l2Data);
}

/// @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.

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.

Comment thread security-council-rehearsals/README.md Outdated
## 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

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

// -------- Config inputs --------
address payable public portal; // L1 OptimismPortal address
address public l2Target; // L2 target address
bytes public l2Data; // Inner L2 calldata
Copy link
Copy Markdown
Contributor

@mds1 mds1 Sep 19, 2025

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`


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.

Comment on lines +57 to +62
// 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"));
}
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!

Comment on lines +68 to +71
// 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");
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

Comment on lines +97 to +99
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

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

contract L1PortalExecuteL2Call is SimpleTaskBase {
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.

@0xOneTony 0xOneTony force-pushed the sc-feat/l1portalexecutel2call-template branch from d26ced1 to 3f3b8d9 Compare September 23, 2025 18:15
@0xiamflux
Copy link
Copy Markdown
Contributor Author

Continue review in #1231, leaving this open for now for future reference.

@0xiamflux 0xiamflux closed this Oct 2, 2025
@0xiamflux 0xiamflux deleted the sc-feat/l1portalexecutel2call-template branch October 9, 2025 15:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants