Skip to content

Superchain interop example: Crosschain Multisend

License

Notifications You must be signed in to change notification settings

ethereum-optimism/superchain-starter-multisend

Β 
Β 

Repository files navigation

Superchain Starter Kit: CrossChainMultisend

Generated from superchain-starter. See the original repository for a more detailed development guide.

Example Superchain app (contract + frontend) that uses interop to send ETH to multiple recipients on a different chain.

Screenshot 2025-02-18 at 8 17 02β€―AM

πŸ”— Contracts

  • Enables sending ETH to multiple recipients on a different chain
  • Uses L2ToL2CrossDomainMessenger for cross-chain message passing
  • Leverages SuperchainWETH for cross-chain ETH transfers
  • Implements a two-step process:
    1. Bridges ETH to the destination chain using SuperchainWETH
    2. Distributes ETH to multiple recipients on the destination chain
  • Includes safety checks for message verification and ETH transfer success

πŸ“ Overview

This contract sends two cross-chain messages through the L2ToL2CrossDomainMessenger:

  1. (Message 1) to send ETH using SuperchainWETH from source to destination chain - triggered by SuperchainWETH#sendETH
  2. (Message 2) to disperse the received ETH to the recipients on the destination chain - triggered by CrossChainMultisend#send

Message 2 depends on the success of Message 1, which is enforced by the CrossDomainMessageLib.requireMessageSuccess(_sendWethMsgHash) check in the relay function. This ensures that ETH bridging is completed before distribution occurs.

🎯 Patterns

1. Contract deployed on same address on multiple chains

The CrossChainMultisend contract is designed to be deployed at the same address on all chains. This allows the contract to:

  • "Trust" that the send message was emitted as a side effect of a specific sequence of events
  • Process cross-chain messages from itself on other chains
  • Maintain consistent behavior across the Superchain
      CrossDomainMessageLib.requireCrossDomainCallback();

      ...

      function requireCrossDomainCallback() internal view {
        requireCallerIsCrossDomainMessenger();

        if (
            IL2ToL2CrossDomainMessenger(PredeployAddresses.L2_TO_L2_CROSS_DOMAIN_MESSENGER).crossDomainMessageSender()
                != address(this)
        ) revert InvalidCrossDomainSender();
    }

The above CrossDomainMessageLib.requireCrossDomainCallback() performs two checks

  1. That the msg.sender is L2ToL2CrossDomainMessenger
  2. That the message being sent was originally emitted on the source chain by the same contract address

Without the second check, it will be possible for ANY address on the source chain to send the message. This is undesirable because now there is no guarantee that the message was generated as a result of someone calling CrossChainMultisend.send

2. Returning msgHash from functions that emit cross domain messages

The contract captures the msgHash from SuperchainWETH's sendETH call and passes it to the destination chain. This enables:

  • Verification that the ETH bridge operation completed successfully
  • Reliable cross-chain message dependency tracking

This is a pattern for composing cross domain messages. Functions that emit a cross domain message (such as SuperchainWeth.sendEth) should return the message hash so that other contracts can consume / depend on it.

This "returning msgHash pattern" is also used in the CrossChainMultisend.sol, making it possible for a different contract to compose on this.

function send(uint256 _destinationChainId, Send[] calldata _sends) public payable returns (bytes32)

3. Dependent cross-chain messages

The contract implements a pattern for handling dependent cross-chain messages:

  1. First message (ETH bridging) must complete successfully
  2. Second message (ETH distribution) verifies the first message's success, otherwise reverts
  3. Auto-relayer handles the dependency by waiting for the first message before processing the second
CrossDomainMessageLib.requireMessageSuccess(_sendWethMsgHash);

The above check calls the L2ToL2CrossDomainMessenger.successfulMessages mapping to check that the message corresponding to msgHash was correctly relayed already.

While you can revert using any custom error, it is recommended that such cases emit

error RequiredMessageNotSuccessful(bytes32 msgHash)

(which CrossDomainMessageLib.requireMessageSuccess does under the hood)

This allows indexers / relayers to realize dependencies between messages, and recognize that a failed relayed message should be retried once the dependent message succeeds at some point in the future.

4. Using SuperchainWETH for cross-chain ETH transfers

The contract leverages SuperchainWETH to handle cross-chain ETH transfers:

  • Automatically wraps and unwraps ETH as needed
  • Provides reliable message hashes for tracking transfers
  • Maintains ETH value consistency across chains

The high level flow is:

Source chain

function sendETH(address _to, uint256 _chainId) external payable returns (bytes32 msgHash_);

  1. converts ETH to SuperchainWETH
  2. sends SuperchainWETH (which follows SuperchainERC20 standard) using a crossdomain message

Destination chain

function relayETH(address _from, address _to, uint256 _amount) external;

  1. relays the SuperchainWETH to the destination chain
  2. converts the SuperchainWETH to ETH

πŸš€ Getting started

Prerequisites: Foundry & Node

Follow this guide to install Foundry

1. Create a new repository using this template:

Click the "Use this template" button above on GitHub, or generate directly

2. Clone your new repository

git clone <your-new-repository-url>
cd superchain-starter-multisend

3. Install dependencies

pnpm i

4. Get started

pnpm dev

This command will:

  • Start a local Superchain network (1 L1 chain and 2 L2 chains) using supersim
  • Launch the frontend development server at (http://localhost:5173)
  • Deploy the smart contracts to your local network

Start building on the Superchain!

Security notice

This contract is not production ready. For one, the contract does not consider the case when the dispersal fails on the destination chain

  1. if one of the recipient is a contract that has a reverting receive() handler
  2. relay(...) call will revert, meaning none of the recipients will be able to receive the dispersal

One unimplemented mitigation is to add a withdrawal flow for any failed recipients such that one recipient's failure to receive doesn't prevent the others

πŸ“š More resources

😎 Moooaaar examples

Want to see more? Here are more example crosschain apps for inspiration / patterns!

  • ⚑ Crosschain Flash Loan
    • Dependent cross-chain messages (compose multiple cross-domain messages)
    • Using SuperchainTokenBridge for cross-chain ERC20 transfers
    • Multichain lending vaults using L2ToL2CrossDomainMessenger
  • πŸ’Έ Multisend
    • How to set up cross-chain callbacks (contract calling itself on another chain)
    • Using SuperchainWETH for cross-chain ETH transfers
    • Dependent cross-chain messages (compose multiple cross-domain messages)
  • πŸͺ™ SuperchainERC20
    • Using ERC-7802 interface for SuperchainERC20 tokens
    • How to upgrade existing ERC20s into SuperchainERC20
    • Minting supply on only one chain
    • Deterministic address deployment on all chains
  • πŸ“ CrossChainPingPong
    • Simple example of passing state between multiple chains using cross domain messenger
    • How to set up cross-chain callbacks (contract calling itself on another chain)
  • πŸ•ΉοΈ CrossChainTicTacToe
    • Allows players to play each other from any chain without cross-chain calls, instead relying on cross-chain event reading
    • Creating horizontally scalable apps with interop

βš–οΈ License

Files are licensed under the MIT license.

License information

About

Superchain interop example: Crosschain Multisend

Resources

License

Code of conduct

Security policy

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages

  • TypeScript 70.8%
  • Solidity 20.9%
  • CSS 6.1%
  • JavaScript 1.5%
  • HTML 0.7%