Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
175 changes: 175 additions & 0 deletions script/universal/MultisigScriptDeposit.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.15;

import {CBMulticall} from "src/utils/CBMulticall.sol";

import {MultisigScript} from "./MultisigScript.sol";
import {Enum} from "./IGnosisSafe.sol";

/// @notice Interface for OptimismPortal2's depositTransaction function
interface IOptimismPortal2 {
/// @notice Creates a deposit transaction on L2
/// @param _to Target address on L2
/// @param _value ETH value to send with the transaction
/// @param _gasLimit Minimum gas limit for L2 execution
/// @param _isCreation Whether the transaction creates a contract
/// @param _data Calldata for the L2 transaction
function depositTransaction(address _to, uint256 _value, uint64 _gasLimit, bool _isCreation, bytes memory _data)
external
payable;
}

/// @title MultisigScriptDeposit
/// @notice Extension of MultisigScript for L1 → L2 deposit transactions.
///
/// @dev This contract simplifies the creation of L1 multisig transactions that trigger actions on L2
/// via the OptimismPortal's depositTransaction mechanism. Task writers only need to define the
/// L2 calls they want to execute; this contract handles wrapping them in the appropriate
/// depositTransaction call automatically.
///
/// Example usage:
/// ```solidity
/// contract MyL2Task is MultisigScriptDeposit {
/// function _ownerSafe() internal view override returns (address) {
/// return vm.envAddress("OWNER_SAFE");
/// }
///
/// function _buildL2Calls() internal view override returns (CBMulticall.Call3Value[] memory) {
/// CBMulticall.Call3Value[] memory calls = new CBMulticall.Call3Value[](1);
/// calls[0] = CBMulticall.Call3Value({
/// target: L2_CONTRACT,
/// allowFailure: false,
/// callData: abi.encodeCall(IL2Contract.someFunction, (arg1, arg2)),
/// value: 0
/// });
/// return calls;
/// }
/// }
/// ```
///
/// The example above uses the default implementation for `_optimismPortal()` (chain-based).
/// Task writers must set the `L2_GAS_LIMIT` environment variable or override `_l2GasLimit()`.
///
/// @dev Future Enhancements:
/// 1. L2 Post-Check Hook: Currently, `_postCheck` runs on L1 and cannot verify L2 state changes.
/// A future enhancement could add an `_postCheckL2` hook that forks L2 state and simulates
/// the deposit transaction's effect. This is non-trivial because deposit transactions are
/// not immediately reflected on L2.
abstract contract MultisigScriptDeposit is MultisigScript {
//////////////////////////////////////////////////////////////////////////////////////
/// Constants ///
//////////////////////////////////////////////////////////////////////////////////////

/// @notice OptimismPortalProxy address on L1 Mainnet (for Base Mainnet)
address internal constant OPTIMISM_PORTAL_MAINNET = 0x49048044D57e1C92A77f79988d21Fa8fAF74E97e;

/// @notice OptimismPortalProxy address on L1 Sepolia (for Base Sepolia)
address internal constant OPTIMISM_PORTAL_SEPOLIA = 0x49f53e41452C74589E85cA1677426Ba426459e85;

//////////////////////////////////////////////////////////////////////////////////////
/// Virtual Functions ///
//////////////////////////////////////////////////////////////////////////////////////

/// @notice Returns the OptimismPortal address on L1
/// @dev Default implementation returns the correct address based on chain ID.
/// Supports L1 Mainnet (chain 1) and L1 Sepolia (chain 11155111).
/// Override this function for other chains or custom portal addresses.
function _optimismPortal() internal view virtual returns (address) {
if (block.chainid == 1) {
return OPTIMISM_PORTAL_MAINNET;
} else if (block.chainid == 11155111) {
return OPTIMISM_PORTAL_SEPOLIA;
}
revert("MultisigScriptDeposit: unsupported chain, override _optimismPortal()");
}

/// @notice Returns the minimum gas limit for L2 execution
/// @dev Default implementation reads from the `L2_GAS_LIMIT` environment variable.
/// All signers must use the same gas limit to produce matching signatures.
///
/// To specify a fixed gas limit, override this function in your task contract:
/// ```solidity
/// function _l2GasLimit() internal pure override returns (uint64) {
/// return 200_000; // Your estimated gas limit
/// }
/// ```
///
/// Common gas limit starting points:
/// - Single simple call: 100,000 - 200,000
/// - Multiple calls or complex operations: 500,000+
///
/// If the gas limit is too low, the L2 transaction will fail but the deposit
/// will still be recorded (ETH may be stuck until manually recovered).
function _l2GasLimit() internal view virtual returns (uint64) {
return uint64(vm.envUint("L2_GAS_LIMIT"));
}

/// @notice Build the calls that will be executed on L2
/// @dev Task writers implement this to define what actions should occur on L2.
/// These calls will be batched into a single CBMulticall.aggregate3Value call
/// and wrapped in a depositTransaction to the OptimismPortal.
///
/// The `value` field in each Call3Value struct specifies ETH to send with that
/// specific L2 call. The total ETH will be bridged via the deposit transaction.
/// @return calls Array of calls to execute on L2 via CBMulticall
function _buildL2Calls() internal view virtual returns (CBMulticall.Call3Value[] memory);

//////////////////////////////////////////////////////////////////////////////////////
/// Overridden Functions ///
//////////////////////////////////////////////////////////////////////////////////////

/// @notice Wraps L2 calls in a depositTransaction to the OptimismPortal
/// @dev Task writers should NOT override this function. Instead, implement `_buildL2Calls`
/// to define the L2 operations. This function handles the L1 deposit wrapping automatically.
///
/// The L2 calls are encoded as a CBMulticall.aggregate3Value call, which is then
/// passed as the data payload to OptimismPortal.depositTransaction. This allows
/// multiple L2 operations to be batched into a single deposit transaction.
///
/// ETH bridging: If any L2 calls include a non-zero `value`, the total ETH is
/// summed and sent with the deposit transaction. The CBMulticall.aggregate3Value
/// function on L2 automatically distributes the ETH to each call according to its
/// specified `value` field - no additional developer action is required.
function _buildCalls() internal view virtual override returns (Call[] memory) {
CBMulticall.Call3Value[] memory l2Calls = _buildL2Calls();
uint256 totalValue = _sumL2CallValues(l2Calls);

// Encode L2 calls as a multicall
// Note: We use aggregate3Value to support per-call ETH distribution on L2
bytes memory l2Data = abi.encodeCall(CBMulticall.aggregate3Value, (l2Calls));

// Wrap in depositTransaction call to OptimismPortal
Call[] memory l1Calls = new Call[](1);
l1Calls[0] = Call({
operation: Enum.Operation.Call,
target: _optimismPortal(),
data: abi.encodeCall(
IOptimismPortal2.depositTransaction,
(
CB_MULTICALL, // L2 target: CBMulticall at same address on L2
totalValue, // ETH to bridge
_l2GasLimit(), // Gas limit for L2 execution
false, // Not a contract creation
l2Data // Encoded multicall
)
),
value: totalValue
});

return l1Calls;
}

//////////////////////////////////////////////////////////////////////////////////////
/// Internal Functions ///
//////////////////////////////////////////////////////////////////////////////////////

/// @notice Sums the ETH values from an array of L2 calls
/// @param l2Calls The array of L2 calls to sum values from
/// @return total The total ETH value across all calls
function _sumL2CallValues(CBMulticall.Call3Value[] memory l2Calls) internal pure returns (uint256 total) {
for (uint256 i; i < l2Calls.length; i++) {
total += l2Calls[i].value;
}
return total;
}
}
8 changes: 8 additions & 0 deletions script/universal/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,14 @@ This is the core script for building Forge scripts that interact with Gnosis Saf
- **Workflows**: Provides standard functions for `sign` (generating signatures), `approve` (onchain approval for nested Safes), `simulate` (dry-run with state overrides), and `run` (execution).
- **Simulation**: Integrates with `Simulation.sol` to provide detailed simulation links and state diffs.

### `MultisigScriptDeposit.sol`

An extension of `MultisigScript` for L1 → L2 deposit transactions. Task writers define L2 calls via `_buildL2Calls()`, and the framework automatically wraps them in an `OptimismPortal.depositTransaction` call. Features:

- **ETH Bridging**: Supports bridging ETH along with the message.
- **Chain-Aware Defaults**: Provides default `OptimismPortal` addresses for Mainnet and Sepolia.
- **Gas Limit**: Set via `L2_GAS_LIMIT` env var (all signers must use the same value).

### `Simulation.sol`

A library for simulating multisig transactions with state overrides. It is particularly useful for:
Expand Down
Loading