Introduction

Supersim is a lightweight tool to simulate the Superchain (with a single L1 and multiple OP-Stack L2s).

Run multiple local nodes with one command, and coordinate message passing between these chains.

It does not require a complicated devnet setup and is run using cli commands with configuration options that fall back to sensible defaults if they are not specified. Each chain is an instance of anvil, though future versions may support other local testing tools.

Features

  • spin up multiple anvil nodes
  • predeployed OP Stack contracts and useful mock contracts (ERC20)
  • fork multiple remote chains (fork the entire Superchain)
  • simulate L1 <> L2 message passing (deposits)
  • simulate L2 <> L2 message passing (interoperability) and auto-relayer
  • (Coming soon) Withdrawals
  • (Coming soon) ERC-4337 account abstraction services (bundlers / paymasters / wallet implementation)

Installation

1. Prerequisites: foundry

supersim requires anvil to be installed.

Follow the guide here to install the Foundry toolchain.

2. Install supersim

Precompiled Binaries

Download the executable for your platform from the GitHub releases page.

Homebrew (OS X, Linux)

brew tap ethereum-optimism/tap
brew install supersim

3. Start supersim in vanilla mode

supersim

Vanilla mode will start 3 chains, with the OP Stack contracts already deployed.

  • (1) L1 Chain
    • Chain 900
  • (2) L2 Chains
    • Chain 901
    • Chain 902

First steps

supersim allows testing multichain features locally. Previously, testing multichain features required complex docker setups or using a testnet.

To see it in practice, let's first try sending some ETH from the L1 to the L2.

Deposit ETH from the L1 into the L2 (L1 to L2 message passing)

1. Check initial balance on the L2 (chain 901)

Grab the balance of the sender account on L2:

cast balance 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 --rpc-url http://127.0.0.1:9545

2. Send the Ether
It exists two different methods to do this action

First method - OptimismPortal

Send the Ether to OptimismPortal contract of the respective L2 (on chain 900)**

Initiate a bridge transaction on the L1:
In the case of the chain 901 the contract is 0x37a418800d0c812A9dE83Bc80e993A6b76511B57

cast send 0x37a418800d0c812A9dE83Bc80e993A6b76511B57 --value 0.1ether --rpc-url http://127.0.0.1:8545 --private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80

Second method - L1StandardBridge

Call bridgeETH function on the L1StandardBridgeProxy / L1StandardBridge contract of the respective L2 on L1 (chain 900) In the case of the chain 901 the contract is 0x8d515eb0e5F293B16B6bBCA8275c060bAe0056B0

Initiate a bridge transaction on the L1:

cast send 0x8d515eb0e5F293B16B6bBCA8275c060bAe0056B0 "bridgeETH(uint32 _minGasLimit, bytes calldata _extraData)" 50000 0x --value 0.1ether --rpc-url http://127.0.0.1:8545 --private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80

3. Check the balance on the L2 (chain 901)

Verify that the ETH balance of the sender has increased on the L2:

cast balance 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 --rpc-url http://127.0.0.1:9545

Send an interoperable SuperchainERC20 token from chain 901 to 902 (L2 to L2 message passing)

In a typical L2 to L2 cross-chain transfer, two transactions are required:

  1. Send transaction on the source chain – This initiates the token transfer on Chain 901.
  2. Relay message transaction on the destination chain – This relays the transfer details to Chain 902.

To simplify this process, you can use the --interop.autorelay flag. This flag automatically triggers the relay message transaction once the initial send transaction is completed on the source chain, improving the developer experience by removing the need to manually send the relay message.

1. Start supersim with the autorelayer enabled

supersim --interop.autorelay 

2. Mint tokens to transfer on chain 901

Run the following command to mint 1000 L2NativeSuperchainERC20 tokens to the recipient address:

cast send 0x420beeF000000000000000000000000000000001 "mint(address _to, uint256 _amount)"  0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 1000  --rpc-url http://127.0.0.1:9545 --private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80

3. Initiate the send transaction on chain 901

Send the tokens from Chain 901 to Chain 902 using the following command:

cast send 0x4200000000000000000000000000000000000028 "sendERC20(address _token, address _to, uint256 _amount, uint256 _chainId)" 0x420beeF000000000000000000000000000000001 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 1000 902 --rpc-url http://127.0.0.1:9545 --private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80

4. Wait for the relayed message to appear on chain 902

In a few seconds, you should see the RelayedMessage on chain 902:

# example
INFO [08-30|14:30:14.698] SuperchainTokenBridge#RelayERC20 token=0x420beeF000000000000000000000000000000001 from=0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 to=0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 amount=1000 source=901

5. Check the balance on chain 902

Verify that the balance of the L2NativeSuperchainERC20 on chain 902 has increased:

cast balance --erc20 0x420beeF000000000000000000000000000000001 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 --rpc-url http://127.0.0.1:9546

With the steps above, you've now successfully completed both an L1 to L2 ETH bridge and an L2 to L2 interoperable SuperchainERC20 token transfer, all done locally using supersim. This approach simplifies multichain testing, allowing you to focus on development without the need for complex setups or relying on external testnets.

supersim (vanilla mode)

Overview

Start supersim in vanilla (non-forked) mode

supersim

Vanilla mode will start 3 chains, with the OP Stack contracts & periphery contracts already deployed.

  • (1) L1 Chain
    • Chain 900
  • (2) L2 Chains
    • Chain 901
    • Chain 902

Example startup logs

Available Accounts
-----------------------
(0): 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
(1): 0x70997970C51812dc3A010C7d01b50e0d17dc79C8
(2): 0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC
(3): 0x90F79bf6EB2c4f870365E785982E1f101E93b906
(4): 0x15d34AAf54267DB7D7c367839AAf71A00a2C6A65
(5): 0x9965507D1a55bcC2695C58ba16FB37d819B0A4dc
(6): 0x976EA74026E726554dB657fA54763abd0C3a0aa9
(7): 0x14dC79964da2C08b23698B3D3cc7Ca32193d9955
(8): 0x23618e81E3f5cdF7f54C3d65f7FBc0aBf5B21E8f
(9): 0xa0Ee7A142d267C1f36714E4a8F75612F20a79720

Private Keys
-----------------------
(0): 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
(1): 0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d
(2): 0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a
(3): 0x7c852118294e51e653712a81e05800f419141751be58f605c371e15141b007a6
(4): 0x47e179ec197488593b187f80a00eb0da91f1b9d0b13f8733639f19c30a34926a
(5): 0x8b3a350cf5c34c9194ca85829a2df0ec3153be0318b5e2d3348e872092edffba
(6): 0x92db14e403b83dfe3df233f83dfa3a0d7096f21ca9b0d6d6b8d88b2b4ec1564e
(7): 0x4bbbf85ce3377467afe5d46f804f221813b2bb87f24d81f60f1fcdbf7cbf4356
(8): 0xdbda1821b80551c9d65939329250298aa3472ba22feea921c0cf5d620ea67b97
(9): 0x2a871d0798f97d79848a013d4936a73bf4cc922c825d33c1cf7073dff6d409c6

Orchestrator Config:
L1:
  Name: L1    Chain ID: 900    RPC: http://127.0.0.1:8545    LogPath: /var/folders/0w/ethers-phoenix/T/anvil-chain-900
L2:
  Name: OPChainA    Chain ID: 901    RPC: http://127.0.0.1:9545    LogPath: /var/folders/0w/ethers-phoenix/T/anvil-chain-901
  Name: OPChainB    Chain ID: 902    RPC: http://127.0.0.1:9546    LogPath: /var/folders/0w/ethers-phoenix/T/anvil-chain-902

Configuration

NAME:
   supersim - Superchain Multi-L2 Simulator

USAGE:
   supersim [global options] command [command options]

VERSION:
   untagged

DESCRIPTION:
   Local multichain optimism development environment

COMMANDS:
   fork     Locally fork a network in the superchain registry
   help, h  Shows a list of commands or help for one command

GLOBAL OPTIONS:

    --interop.autorelay                 (default: false)                   ($SUPERSIM_INTEROP_AUTORELAY)
          Automatically relay messages sent to the L2ToL2CrossDomainMessenger using
          account 0xa0Ee7A142d267C1f36714E4a8F75612F20a79720

    --interop.delay value               (default: 0)                       ($SUPERSIM_INTEROP_DELAY)
          Delay before relaying messages sent to the L2ToL2CrossDomainMessenger

    --l1.port value                     (default: 8545)                    ($SUPERSIM_L1_PORT)
          Listening port for the L1 instance. `0` binds to any available port

    --l2.starting.port value            (default: 9545)                    ($SUPERSIM_L2_STARTING_PORT)
          Starting port to increment from for L2 chains. `0` binds each chain to any
          available port

    --log.color                         (default: false)                   ($SUPERSIM_LOG_COLOR)
          Color the log output if in terminal mode

    --log.format value                  (default: text)                    ($SUPERSIM_LOG_FORMAT)
          Format the log output. Supported formats: 'text', 'terminal', 'logfmt', 'json',
          'json-pretty',

    --log.level value                   (default: INFO)                    ($SUPERSIM_LOG_LEVEL)
          The lowest log level that will be output

    --log.pid                           (default: false)                   ($SUPERSIM_LOG_PID)
          Show pid in the log

    --logs.directory value                                                 ($SUPERSIM_LOGS_DIRECTORY)
          Directory to store logs

   MISC


    --help, -h                          (default: false)
          show help

    --version, -v                       (default: false)
          print the version

supersim fork (forked mode)

Overview

supersim fork

The supersim fork command simplifies the process of forking multiple chains in the Superchain ecosystem simultaneously. It determines the appropriate block heights for each chain and launches both the L1 and L2 chains based on these values.

If you're relying on contracts already deployed on testnet / mainnet chains, you can use fork mode to simulate and interact with the state of the chain without needing to re-deploy or modify the contracts.

Locally fork any of the available chains in a superchain network of the superchain registry, default mainnet versions.

Example startup logs

Available Accounts
-----------------------
(0): 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
--- truncated for brevity ---

Private Keys
-----------------------
(0): 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
--- truncated for brevity ---

Orchestrator Config:
L1:
  Name: mainnet    Chain ID: 1    RPC: http://127.0.0.1:8545    LogPath: /var/folders/0w/ethers-phoenix/T/anvil-chain-1-1521250718
L2:
  Name: op    Chain ID: 10    RPC: http://127.0.0.1:9545    LogPath: /var/folders/0w/ethers-phoenix/T/anvil-chain-10
  Name: base    Chain ID: 8453    RPC: http://127.0.0.1:9546    LogPath: /var/folders/0w/ethers-phoenix/T/anvil-chain-8453
  Name: zora    Chain ID: 7777777    RPC: http://127.0.0.1:9547    LogPath: /var/folders/0w/ethers-phoenix/T/anvil-chain-7777777

Configuration

NAME:
   supersim fork - Locally fork a network in the superchain registry

USAGE:
   supersim fork [command options]

OPTIONS:

          --l1.fork.height value              (default: 0)                       ($SUPERSIM_L1_FORK_HEIGHT)
                L1 height to fork the superchain (bounds L2 time). `0` for latest

          --chains value                                                         ($SUPERSIM_CHAINS)
                chains to fork in the superchain, mainnet options: [base, lyra, metal, mode, op,
                orderly, race, tbn, zora]. In order to replace the public rpc endpoint for a
                chain, specify the ($SUPERSIM_RPC_URL_<CHAIN>) env variable. i.e
                SUPERSIM_RPC_URL_OP=http://optimism-mainnet.infura.io/v3/<API-KEY>

          --network value                     (default: "mainnet")               ($SUPERSIM_NETWORK)
                superchain network. options: mainnet, sepolia, sepolia-dev-0. In order to
                replace the public rpc endpoint for the network, specify the
                ($SUPERSIM_RPC_URL_<NETWORK>) env variable. i.e
                SUPERSIM_RPC_URL_MAINNET=http://mainnet.infura.io/v3/<API-KEY>

          --interop.enabled                   (default: true)                    ($SUPERSIM_INTEROP_ENABLED)
                enable interop predeploy and functionality

          --l1.port value                     (default: 8545)                    ($SUPERSIM_L1_PORT)
                Listening port for the L1 instance. `0` binds to any available port

          --l2.starting.port value            (default: 9545)                    ($SUPERSIM_L2_STARTING_PORT)
                Starting port to increment from for L2 chains. `0` binds each chain to any
                available port

          --interop.autorelay                 (default: false)                   ($SUPERSIM_INTEROP_AUTORELAY)
                Automatically relay messages sent to the L2ToL2CrossDomainMessenger using
                account 0xa0Ee7A142d267C1f36714E4a8F75612F20a79720
          
          --interop.delay value               (default: 0)                       ($SUPERSIM_INTEROP_DELAY)
                Delay before relaying messages sent to the L2ToL2CrossDomainMessenger

          --logs.directory value                                                 ($SUPERSIM_LOGS_DIRECTORY)
                Directory to store logs

          --log.level value                   (default: INFO)                    ($SUPERSIM_LOG_LEVEL)
                The lowest log level that will be output

          --log.format value                  (default: text)                    ($SUPERSIM_LOG_FORMAT)
                Format the log output. Supported formats: 'text', 'terminal', 'logfmt', 'json',
                'json-pretty',

          --log.color                         (default: false)                   ($SUPERSIM_LOG_COLOR)
                Color the log output if in terminal mode

          --log.pid                           (default: false)                   ($SUPERSIM_LOG_PID)
                Show pid in the log

          --help, -h                          (default: false)
                show help

Notes

Fork height

The fork height is determined by L1 block height (default latest). This is then used to derive the corresponding L2 block to start from.

Interoperability contracts

By default, interop contracts are not deployed on forked networks. To include them, run supersim with the --interop.enabled flag.

supersim fork --chains=op,base,zora --interop.enabled

Included contracts

The chain environment includes contracts already deployed to help replicate the Superchain environment. You can see an example of contract addresses for a L2 system here.

OP Stack system contracts (L1)

These are the L1 contracts that are required for a rollup as part of the OP Stack protocol. Examples are the OptimismPortal, L1StandardBridge, and L1CrossDomainMessenger.

View examples of these contracts here the official OP docs or the source code here in the Optimism monorepo

OP Stack L2 contracts (L2)

The OP Stack system contracts on the L2 are included at the standard addresses by default.

Periphery contracts (L2)

L2 chains running on supersim also includes some useful contracts for testing purposes that are not part of the OP Stack by default.

L2NativeSuperchainERC20

A simple ERC20 that adheres to the SuperchainERC20 standard. It includes permissionless minting for easy testing.

Source: L2NativeSuperchainERC20.sol

Deployed address: 0x420beeF000000000000000000000000000000001

Minting new tokens

cast send 0x420beeF000000000000000000000000000000001 "mint(address _to, uint256 _amount)" $RECIPIENT_ADDRESS 1ether  --rpc-url $L2_RPC_URL

Network details

By default, two OP Stack systems will be spun up in vanilla mode

  • OPChainA (chainID 901)
  • OPChainB (chainID 902)

Both "roll up" into a single L1 chain (chainID 900).

OPChainA

Network details

ParameterValue
NameOPChainA
Chain ID901
RPC URLhttp://127.0.0.1:9545

Contract addresses

L1 contracts

{
  "AddressManager": "0x78d21C9820A9135215202A9a8D6521483D4b75cD",
  "AnchorStateRegistry": "0x21799f09394c50220CCD95E7dAc1cdD774FC871a",
  "AnchorStateRegistryProxy": "0xa6F40d5770b3509aB40B2effa5cb544D29743ec7",
  "DelayedWETH": "0x49BBFf1629824A1e7993Ab5c17AFa45D24AB28c9",
  "DelayedWETHProxy": "0x309DA6B9a8fE16afD7D067528d358E55314bEa6b",
  "DisputeGameFactory": "0x20B168142354Cee65a32f6D8cf3033E592299765",
  "DisputeGameFactoryProxy": "0x444689B81D485bc58AB81aC02A95a937fAa152D7",
  "L1CrossDomainMessenger": "0x094e6508ba9d9bf1ce421fff3dE06aE56e67901b",
  "L1CrossDomainMessengerProxy": "0xe5bda89cd85cE0DfB80E053281cA070D65B738e6",
  "L1ERC721Bridge": "0x5C4F5e749A61a9503c4AAE8a9393e89609a0e804",
  "L1ERC721BridgeProxy": "0x018dC24a6617c47cAa00C3fA25097214B2D4F447",
  "L1StandardBridge": "0xb7900B27Be8f0E0fF65d1C3A4671e1220437dd2b",
  "L1StandardBridgeProxy": "0xa01ae68902e205B420FD164435F299E07b0C778b",
  "L2OutputOracle": "0x19652082F846171168Daf378C4fD3ee85a0D4A60",
  "L2OutputOracleProxy": "0x6cE0530E823e23be85D8e151FB023605eB4F6d43",
  "Mips": "0xB3A0348310a0ff78E5FbDB7f14BB7d3e02d40773",
  "OptimismMintableERC20Factory": "0x39Aea2Dd53f2d01c15877aCc2791af6BDD7aD567",
  "OptimismMintableERC20FactoryProxy": "0x15c855966C196Be3a8ca747E8A8Bf40928d4741f",
  "OptimismPortal": "0xb7461Fb347f68f9717e6fD12C8407dEcee063bdc",
  "OptimismPortal2": "0xfcbb237388CaF5b08175C9927a37aB6450acd535",
  "OptimismPortalProxy": "0xF5fe61a258CeBb54CCe428F76cdeD04Cbc12F53d",
  "PreimageOracle": "0x3bd7E801E51d48c5d94Ea68e8B801DFFC275De75",
  "ProtocolVersions": "0xfbfD64a6C0257F613feFCe050Aa30ecC3E3d7C3F",
  "ProtocolVersionsProxy": "0x6dA4f6489039d9f4F3144954DDF5bb2F4986e90b",
  "ProxyAdmin": "0xe32a4D31ffD5596542DAc8239a1DE3Fff9d63475",
  "SafeProxyFactory": "0x4a05c09875DE2DD5B81Bc01dd46eD4699b181bfA",
  "SafeSingleton": "0x99A395CE6d6b37CaaCBad64fB42d556b6CA73a48",
  "SuperchainConfig": "0x068E44eB31e111028c41598E4535be7468674D0A",
  "SuperchainConfigProxy": "0x7E6c6ebCF109fa23277b86bdA39738035C21BB86",
  "SystemConfig": "0x6167B477F8d9138aa509f54b2800443857e28c0f",
  "SystemConfigProxy": "0xf32919Ed2490b56EaD65E72749894aE4C9523320",
  "SystemOwnerSafe": "0xc052b7316C87390E555aF97D42bCd5FB6d5eEFDa"
}

L2 contracts

{
  // OP Stack predeploys
  "L2ToL1MessagePasser": "0x4200000000000000000000000000000000000016",
  "L2CrossDomainMessenger": "0x4200000000000000000000000000000000000007",
  "L2StandardBridge": "0x4200000000000000000000000000000000000010",
  "L2ERC721Bridge": "0x4200000000000000000000000000000000000014",
  "SequencerFeeVault": "0x4200000000000000000000000000000000000011",
  "OptimismMintableERC20Factory": "0x4200000000000000000000000000000000000012",
  "OptimismMintableERC721Factory": "0x4200000000000000000000000000000000000017",
  "L1BlockInterop": "0x4200000000000000000000000000000000000015",
  "GasPriceOracle": "0x420000000000000000000000000000000000000F",
  "ProxyAdmin": "0x4200000000000000000000000000000000000018",
  "BaseFeeVault": "0x4200000000000000000000000000000000000019",
  "L1FeeVault": "0x420000000000000000000000000000000000001A",
  "GovernanceToken": "0x4200000000000000000000000000000000000042",
  "SchemaRegistry": "0x4200000000000000000000000000000000000020",
  "EAS": "0x4200000000000000000000000000000000000021",
  "CrossL2Inbox": "0x4200000000000000000000000000000000000022",
  "L2ToL2CrossDomainMessenger": "0x4200000000000000000000000000000000000023",
  "SuperchainWETH": "0x4200000000000000000000000000000000000024",
  "SuperchainTokenBridge": "0x4200000000000000000000000000000000000028",

  // Periphery
  "L2NativeSuperchainERC20": "0x420beeF000000000000000000000000000000001"
}

OPChainB

Network details

ParameterValue
NameOPChainB
Chain ID902
RPC URLhttp://127.0.0.1:9546

Contract addresses

L1 contracts

{
  "AddressManager": "0xafB51A0f73C8409AeA1207DF7f39885c927BeA46",
  "AnchorStateRegistry": "0x05493149c84A71063f7948127bb931f8377F779C",
  "AnchorStateRegistryProxy": "0xfd0269a716A59fF125Bd7eb65Cd3427C8555bab7",
  "DelayedWETH": "0x49BBFf1629824A1e7993Ab5c17AFa45D24AB28c9",
  "DelayedWETHProxy": "0xA63353128502269b4A4A4c2677fE316cd9ad4397",
  "DisputeGameFactory": "0x20B168142354Cee65a32f6D8cf3033E592299765",
  "DisputeGameFactoryProxy": "0x5F416fEb15c8B382d338FDBDb7D44967ca2b59BC",
  "L1CrossDomainMessenger": "0x094e6508ba9d9bf1ce421fff3dE06aE56e67901b",
  "L1CrossDomainMessengerProxy": "0xCB9768921831677Ae15cE4B64A10B94F49cD88E2",
  "L1ERC721Bridge": "0x5C4F5e749A61a9503c4AAE8a9393e89609a0e804",
  "L1ERC721BridgeProxy": "0xDCE41E6C0901586EE27Eac329EBD4b5fe5A7170d",
  "L1StandardBridge": "0xb7900B27Be8f0E0fF65d1C3A4671e1220437dd2b",
  "L1StandardBridgeProxy": "0x2D8543c236a4d626f54B51Fa8bc229a257C5143E",
  "L2OutputOracle": "0x19652082F846171168Daf378C4fD3ee85a0D4A60",
  "L2OutputOracleProxy": "0x006Af3fB62c4BE4fB0393995d364BbFe6b0F3CB2",
  "Mips": "0xB3A0348310a0ff78E5FbDB7f14BB7d3e02d40773",
  "OptimismMintableERC20Factory": "0x39Aea2Dd53f2d01c15877aCc2791af6BDD7aD567",
  "OptimismMintableERC20FactoryProxy": "0x1A2A942d891e525D1Ab192578a378980729fD585",
  "OptimismPortal": "0xb7461Fb347f68f9717e6fD12C8407dEcee063bdc",
  "OptimismPortal2": "0xfcbb237388CaF5b08175C9927a37aB6450acd535",
  "OptimismPortalProxy": "0xdfC9DEAbEEbDaa7620C71e2E76AEda32919DE5f2",
  "PreimageOracle": "0x3bd7E801E51d48c5d94Ea68e8B801DFFC275De75",
  "ProtocolVersions": "0xfbfD64a6C0257F613feFCe050Aa30ecC3E3d7C3F",
  "ProtocolVersionsProxy": "0xE139cB0CDa5EF722870068ea331d0989776A7aDf",
  "ProxyAdmin": "0xff5E6C2Af859f70B875BA59B958BEde60E36bf69",
  "SafeProxyFactory": "0xb68f3B057fE3c6CdDF9DB35837Ea769FCc81978a",
  "SafeSingleton": "0xeeB44D84d505AbD958d032e90704c56443eB3ED0",
  "SuperchainConfig": "0x068E44eB31e111028c41598E4535be7468674D0A",
  "SuperchainConfigProxy": "0x2ED4AA34573c36bF3856e597501aEf9d9Dc1687C",
  "SystemConfig": "0x6167B477F8d9138aa509f54b2800443857e28c0f",
  "SystemConfigProxy": "0x2Db03FE998D7c20E4B65afD1f50f04Ec4BfAb694",
  "SystemOwnerSafe": "0xBF3830711B7c559042453B7546dB4736eFB4245e"
}

L2 contracts

{
  // OP Stack predeploys
  "L2ToL1MessagePasser": "0x4200000000000000000000000000000000000016",
  "L2CrossDomainMessenger": "0x4200000000000000000000000000000000000007",
  "L2StandardBridge": "0x4200000000000000000000000000000000000010",
  "L2ERC721Bridge": "0x4200000000000000000000000000000000000014",
  "SequencerFeeVault": "0x4200000000000000000000000000000000000011",
  "OptimismMintableERC20Factory": "0x4200000000000000000000000000000000000012",
  "OptimismMintableERC721Factory": "0x4200000000000000000000000000000000000017",
  "L1BlockInterop": "0x4200000000000000000000000000000000000015",
  "GasPriceOracle": "0x420000000000000000000000000000000000000F",
  "ProxyAdmin": "0x4200000000000000000000000000000000000018",
  "BaseFeeVault": "0x4200000000000000000000000000000000000019",
  "L1FeeVault": "0x420000000000000000000000000000000000001A",
  "GovernanceToken": "0x4200000000000000000000000000000000000042",
  "SchemaRegistry": "0x4200000000000000000000000000000000000020",
  "EAS": "0x4200000000000000000000000000000000000021",
  "CrossL2Inbox": "0x4200000000000000000000000000000000000022",
  "L2ToL2CrossDomainMessenger": "0x4200000000000000000000000000000000000023",
  "SuperchainWETH": "0x4200000000000000000000000000000000000024",
  "SuperchainTokenBridge": "0x4200000000000000000000000000000000000028",

  // Periphery
  "L2NativeSuperchainERC20": "0x420beeF000000000000000000000000000000001"
}

Deposit transactions

Interoperability

Using viem to relay interop messages (TypeScript)

This guide describes how to use viem to send and relay interop messages using the L2ToL2CrossDomainMessenger

We'll perform the SuperchainERC20 interop transfer in First steps and Manually relaying interop messages with cast again, this time using viem to relay the message without the autorelayer.

Steps

The full code snippet can be found here

1. Start supersim

supersim

2. Install TypeScript packages

npm i viem @eth-optimism/viem

3. Imports & Setup

import {
	http,
	encodeFunctionData,
	createWalletClient,
	parseAbi,
	defineChain,
	publicActions,
} from "viem";
import { privateKeyToAccount } from "viem/accounts";
import {
    contracts,
	publicActionsL2,
	walletActionsL2,
    supersimL2A,
    supersimL2B,
    createInteropSentL2ToL2Messages,
    decodeRelayedL2ToL2Messages,
} from "@eth-optimism/viem";

// SuperERC20 is in development so we manually define the address here
const L2_NATIVE_SUPERCHAINERC20_ADDRESS = "0x420beeF000000000000000000000000000000001";
const SUPERCHAIN_TOKEN_BRIDGE_ADDRESS = "0x4200000000000000000000000000000000000028";

// Account for 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
const account = privateKeyToAccount("0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80");

// Define chains
// ... left out as we'll use the supersim chain definitions

// Configure clients with optimism extension
const opChainAClient = createWalletClient({
	transport: http(),
	chain: supersimL2A,
	account,
}).extend(walletActionsL2())
	.extend(publicActionsL2())
	.extend(publicActions);

const opChainBClient = createWalletClient({
	transport: http(),
	chain: supersimL2B,
	account,
}).extend(walletActionsL2())
	.extend(publicActionsL2())
	.extend(publicActions);

4. Mint and Bridge L2NativeSuperchainERC20 from source chain

// #######
// OP Chain A
// #######

// 1. Mint 1000 `L2NativeSuperchainERC20` token on chain A

const mintTxHash = await opChainAClient.writeContract({
	address: L2_NATIVE_SUPERCHAINERC20_ADDRESS,
	abi: parseAbi(["function mint(address to, uint256 amount)"]),
	functionName: "mint",
	args: [account.address, 1000n],
});

await opChainAClient.waitForTransactionReceipt({ hash: mintTxHash });

// 2. Initiate sendERC20 tx to bridge funds to chain B

console.log("Initiating sendERC20 on OPChainA to OPChainB...");
const sendERC20TxHash = await opChainAClient.writeContract({
	address: SUPERCHAIN_TOKEN_BRIDGE_ADDRESS,
	abi: parseAbi([
		"function sendERC20(address _token, address _to, uint256 _amount, uint256 _chainId)",
	]),
	functionName: "sendERC20",
	args: [L2_NATIVE_SUPERCHAINERC20_ADDRESS, account.address, 1000n, BigInt(supersimL2B.id)],
});

const sendERC20Receipt = await opChainAClient.waitForTransactionReceipt({ hash: sendERC20TxHash });

// 3. Construct the interoperable log data from the sent message

const { sentMessages } = await createInteropSentL2ToL2Messages(opChainAClient, { receipt: sendERC20Receipt })
const sentMessage = sentMessages[0] // We only sent 1 message

5. Relay the sent message on the destination chain


// ##########
// OP Chain B
// ##########

// 4. Relay the sent message

console.log("Relaying message on OPChainB...");
const relayTxHash = await opChainBClient.relayL2ToL2Message({
    sentMessageId: sentMessage.id,
    sentMessagePayload: sentMessage.payload,
});

const relayReceipt = await opChainBClient.waitForTransactionReceipt({ hash: relayTxHash });

// 5. Ensure the message was relayed successfully

const { successfulMessages, failedMessages } = decodeRelayedL2ToL2Messages({ receipt: relayReceipt });
if (successfulMessages.length != 1) {
    throw new Error("failed to relay message!")
}

// 6. Check balance on OPChainB
const balance = await opChainBClient.readContract({
	address: L2_NATIVE_SUPERCHAINERC20_ADDRESS,
	abi: parseAbi(["function balanceOf(address) view returns (uint256)"]),
	functionName: "balanceOf",
	args: [account.address],
});

console.log(`Balance on OPChainB: ${balance}`);

Full code snippet

Click to view
// Using viem to transfer L2NativeSuperchainERC20

import {
	http,
	encodeFunctionData,
	createWalletClient,
	parseAbi,
	defineChain,
	publicActions,
} from "viem";
import { privateKeyToAccount } from "viem/accounts";
import {
    contracts,
	publicActionsL2,
	walletActionsL2,
    supersimL2A,
    supersimL2B,
    createInteropSentL2ToL2Messages,
    decodeRelayedL2ToL2Messages,
} from "@eth-optimism/viem";

// SuperERC20 is in development so we manually define the address here
const L2_NATIVE_SUPERCHAINERC20_ADDRESS =
	"0x420beeF000000000000000000000000000000001";

// account for 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
const account = privateKeyToAccount("0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80");

// Define chains
// ... left out as we'll use the supersim chain definitions

// Configure op clients
const opChainAClient = createWalletClient({
	transport: http(),
	chain: supersimL2A,
	account,
}).extend(walletActionsL2())
	.extend(publicActionsL2())
	.extend(publicActions);

const opChainBClient = createWalletClient({
	transport: http(),
	chain: supersimL2B,
	account,
}).extend(walletActionsL2())
	.extend(publicActionsL2())
	.extend(publicActions);

// #######
// OP Chain A
// #######

// 1. Mint 1000 `L2NativeSuperchainERC20` token

const mintTxHash = await opChainAClient.writeContract({
	address: L2_NATIVE_SUPERCHAINERC20_ADDRESS,
	abi: parseAbi(["function mint(address to, uint256 amount)"]),
	functionName: "mint",
	args: [account.address, 1000n],
});

await opChainAClient.waitForTransactionReceipt({ hash: mintTxHash });

// 2. Initiate sendERC20 tx to bridge funds to chain B

console.log("Initiating sendERC20 on OPChainA...");
const sendERC20TxHash = await opChainAClient.writeContract({
	address: SUPERCHAIN_TOKEN_BRIDGE_ADDRESS,
	abi: parseAbi([
		"function sendERC20(address _token, address _to, uint256 _amount, uint256 _chainId)",
	]),
	functionName: "sendERC20",
	args: [L2_NATIVE_SUPERCHAINERC20_ADDRESS, account.address, 1000n, BigInt(supersimL2B.id)],
});

const sendERC20Receipt = await opChainAClient.waitForTransactionReceipt({ hash: sendERC20TxHash });

// 3. Construct the interoperable log data from the sent message

const { sentMessages } = await createInteropSentL2ToL2Messages(opChainAClient, { receipt: sendERC20Receipt })
const sentMessage = sentMessages[0] // We only sent 1 message

// ##########
// OP Chain B
// ##########

// 4. Relay the sent message

console.log("Relaying message on OPChainB...");
const relayTxHash = await opChainBClient.relayL2ToL2Message({
    sentMessageId: sentMessage.id,
    sentMessagePayload: sentMessage.payload,
});

const relayReceipt = await opChainBClient.waitForTransactionReceipt({ hash: relayTxHash });

// 5. Ensure the message was relayed successfully

const { successfulMessages, failedMessages } = decodeRelayedL2ToL2Messages({ receipt: relayReceipt });
if (successfulMessages.length != 1) {
    throw new Error("failed to relay message!")
}

// 6. Check balance on OPChainB
const balance = await opChainBClient.readContract({
	address: L2_NATIVE_SUPERCHAINERC20_ADDRESS,
	abi: parseAbi(["function balanceOf(address) view returns (uint256)"]),
	functionName: "balanceOf",
	args: [account.address],
});

console.log(`Balance on OPChainB: ${balance}`);

Manually relaying interop messages with cast and L2ToL2CrossDomainMessenger

This guide describes how to form a message identifier to relay a L2ToL2CrossDomainMessenger cross chain call.

We'll perform the SuperchainERC20 interop transfer in First steps again, this time manually relaying the message without the autorelayer.

Overview

Contracts used

High level steps

Sending an interop message using the L2ToL2CrossDomainMessenger:

On source chain (OPChainA 901)

  1. Invoke L2NativeSuperchainERC20.sentERC20 to bridge funds
    • this leverages L2ToL2CrossDomainMessenger.sendMessage to make the cross chain call
  2. Retrieve the log identifier and the message payload for the SentMessage event.

On destination chain (OPChainB 902)

  1. Relay the message with L2ToL2CrossDomainMessenger.relayMessage
    • which then calls L2NativeSuperchainERC20.relayERC20

Message identifier

A message identifier uniquely identifies a log emitted on a chain. The sequencer and smart contracts (CrossL2Inbox) use the identifier to perform invariant checks to confirm that the message is valid.

struct Identifier {
    address origin;      // Account (contract) that emits the log
    uint256 blocknumber; // Block number in which the log was emitted
    uint256 logIndex;    // Index of the log in the array of all logs emitted in the block
    uint256 timestamp;   // Timestamp that the log was emitted
    uint256 chainid;     // Chain ID of the chain that emitted the log
}

Steps

1. Start supersim

supersim

2. Mint tokens to transfer on chain 901

Run the following command to mint 1000 L2NativeSuperchainERC20 tokens to the recipient address:

cast send 0x420beeF000000000000000000000000000000001 "mint(address _to, uint256 _amount)"  0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 1000  --rpc-url http://127.0.0.1:9545 --private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80

3. Initiate the send transaction on chain 901

Send the tokens from Chain 901 to Chain 902 using the following command:

cast send 0x4200000000000000000000000000000000000028 "sendERC20(address _token, address _to, uint256 _amount, uint256 _chainId)" 0x420beeF000000000000000000000000000000001 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 1000 902 --rpc-url http://127.0.0.1:9545 --private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80

4. Get the log emitted by the L2ToL2CrossDomainMessenger

The token contract calls the L2ToL2CrossDomainMessenger, which emits a message (log) that can be relayed on the destination chain.

$ cast logs --address 0x4200000000000000000000000000000000000023 --rpc-url http://127.0.0.1:9545

address: 0x4200000000000000000000000000000000000023
blockHash: 0x3905831f1b109ce787d180c1ed977ebf0ff1a6334424a0ae8f3731b035e3f708
blockNumber: 4
data: 0x000000000000000000000000420beef00000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000064d9f50046000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb92266000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb9226600000000000000000000000000000000000000000000000000000000000003e800000000000000000000000000000000000000000000000000000000
logIndex: 1
topics: [
      0x382409ac69001e11931a28435afef442cbfd20d9891907e8fa373ba7d351f320
      0x0000000000000000000000000000000000000000000000000000000000000386
      0x000000000000000000000000420beef000000000000000000000000000000001
      0x0000000000000000000000000000000000000000000000000000000000000000
]
...

5. Retrieve the block timestamp the log was emitted in

Since the message identifier requires the block timestamp, fetch the block info to get the timestamp.

$ cast block 0xREPLACE_WITH_CORRECT_BLOCKHASH --rpc-url http://127.0.0.1:9545
...
timestamp            1728507703
...

6. Prepare the message identifier & payload

Now we have all the information needed for the message (log) identifier.

ParameterValueNote
origin0x4200000000000000000000000000000000000023L2ToL2CrossDomainMessenger
blocknumber4from step 4
logIndex1from step 4
timestamp1728507703from step 5
chainid901OPChainA chainID

The message payload is the concatenation of the [...topics, data] in order.

0x + 382409ac69001e11931a28435afef442cbfd20d9891907e8fa373ba7d351f320
   + 0000000000000000000000000000000000000000000000000000000000000386
   + 000000000000000000000000420beef000000000000000000000000000000001
   + 0000000000000000000000000000000000000000000000000000000000000000
   + 000000000000000000000000420beef00000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000064d9f50046000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb92266000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb9226600000000000000000000000000000000000000000000000000000000000003e800000000000000000000000000000000000000000000000000000000

Payload: 0x382409ac69001e11931a28435afef442cbfd20d9891907e8fa373ba7d351f3200000000000000000000000000000000000000000000000000000000000000386000000000000000000000000420beef0000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000420beef00000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000064d9f50046000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb92266000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb9226600000000000000000000000000000000000000000000000000000000000003e800000000000000000000000000000000000000000000000000000000

7. Send the relayMessage transaction

Call relayMessage on the L2ToL2CrossDomainMessenger

// L2ToL2CrossDomainMessenger.sol (truncated for brevity)

contract L2ToL2CrossDomainMessenger {

  // ...

  function relayMessage(
      ICrossL2Inbox.Identifier calldata _id,
      bytes calldata _sentMessage
  ) payable

  // ...
}

relayMessage parameters

  • ICrossL2Inbox.Identifier calldata _id: identifier pointing to the SentMessage log on the source chain
  • bytes memory _sentMessage: encoding of the log topics & data

Below is an example call, but make sure to replace them with the correct values you received in previous steps.

$ cast send 0x4200000000000000000000000000000000000023 --gas-limit 200000 \
    "relayMessage((address, uint256, uint256, uint256, uint256), bytes)" \
    "(0x4200000000000000000000000000000000000023, 4, 1, 1728507703, 901)" \
    0x382409ac69001e11931a28435afef442cbfd20d9891907e8fa373ba7d351f3200000000000000000000000000000000000000000000000000000000000000386000000000000000000000000420beef0000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000420beef00000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000064d9f50046000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb92266000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb9226600000000000000000000000000000000000000000000000000000000000003e800000000000000000000000000000000000000000000000000000000 \
    --rpc-url http://127.0.0.1:9546 \
    --private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80

9. Check the balance on chain 902

Verify that the balance of the L2NativeSuperchainERC20 on chain 902 has increased:

cast balance --erc20 0x420beeF000000000000000000000000000000001 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 --rpc-url http://127.0.0.1:9546

Alternatives

This is obviously very tedious to do by hand 😅. Here are some alternatives

  • use supersim --interop.autorelay - this only works on supersim, but relayers for the testnet/prod environment will be available soon!
  • use viem bindings/actions - if you're using typescript, we have bindings available to make fetching identifiers and relaying messages easy

Bridging SuperchainWETH

Crosschain ETH transfers in the Superchain are facilitated through the SuperchainWETH contract. For more information on this checkout the spec for SuperchainWETH: https://specs.optimism.io/interop/superchain-weth.html.

Send native ETH from chain 901 to 902 via SuperchainWETH

This outlines how to send native ETH from chain 901 to 902. To simplify these steps supersim will be run with the --interop.autorelay flag. The --interop.autorelay flag automatically triggers the relay message transaction once the initial send transaction is completed on the source chain, improving the developer experience by removing the need to manually send the relay message.

1. Start supersim with the autorelayer enabled

supersim --interop.autorelay 

2. Wrap the native ETH to SuperchainWETH on chain 901

Wrap 10 ETH to SuperchainWETH. The SuperchainWETH contract is a predeploy at address 0x4200000000000000000000000000000000000024

cast send 0x4200000000000000000000000000000000000024 "deposit()" --value 10ether --rpc-url http://127.0.0.1:9545 --private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 

3. Check balance of SuperchainWETH on chain 901

Verify that the balance of the SuperchainWETH on chain 901 has increased by 10000000000000000000:

cast balance --erc20 0x4200000000000000000000000000000000000024 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 --rpc-url http://127.0.0.1:9545

4. Initiate the send transaction on chain 901 through the SuperchainTokenBridge

cast send 0x4200000000000000000000000000000000000028 "sendERC20(address _token, address _to, uint256 _amount, uint256 _chainId)" 0x4200000000000000000000000000000000000024  0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 10000000000000000000 902 --rpc-url http://127.0.0.1:9545 --private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80

5. Wait for the relayed message to appear on chain 902

In a few seconds, you should see the RelayedMessage on chain 902:

# example
INFO [08-30|14:30:14.698] SuperchainWETH#CrosschainMint to=0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 amount=10,000,000,000,000,000,000

6. Check the balance of SuperchainWETH on chain 902

Verify that the balance of SuperchainWETH on chain 902 has increased by 10000000000000000000:

cast balance --erc20 0x4200000000000000000000000000000000000024 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 --rpc-url http://127.0.0.1:9546

7. Unwrap the SuperchainWETH to native ETH on chain 902

cast send 0x4200000000000000000000000000000000000024 "withdraw(uint256)" 10000000000000000000 --rpc-url http://127.0.0.1:9546 --private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 

8. Check the balance of ETH on chain 902 Verify that the balance of ETH on chain 902 has increased:

cast balance 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 --rpc-url http://127.0.0.1:9546

(Experimental) auto-wrapping and unwrapping of native ETH and sending from chain 901 to 902 using L2 to L2 message passing

Note: this example uses a contract SuperchainETHWrapper written for prototyping and testing purposes. This contract has not been audited and it may contain bugs or security vulnerabilities. We are not liable for any issues arising from its use. It is strongly advised that this contract not be used with actual funds and should only be used for testing on testnets or in a controlled development environment. This contract is deployed at a special address 0x420beeF000000000000000000000000000000002 only on the vanilla version of supersim and will not be found at this address in fork mode or in any other environments outside of supersim.

In a typical L2 to L2 ETH cross-chain transfer, four transactions are required:

  1. Wrap Native ETH to SuperchainWETH on source chain
  2. Send SuperchainWETH to recipient using SuperchainTokenBridge#SendERC20
  3. Relay message transaction on the destination chain to receive SuperchainWETH on destination
  4. Unwrap SuperchainWETH to ETH on destination chain

To simplify this process, you can use the SuperchainETHWrapper and the --interop.autorelay flag to get this down to just one step. The SuperchainETHWrapper#SendETH function handles wrapping the native ETH to SuperchainWETH and initiating the message to relay and unwrap the SuperchainWETH on the destination. The --interop.autorelay flag automatically triggers the relay message transaction once the initial send transaction is completed on the source chain, improving the developer experience by removing the need to manually send the relay message.

1. Start supersim with the autorelayer enabled

supersim --interop.autorelay 

2. Initiate the send transaction on chain 901

Send ETH from Chain 901 to account 0xCE35738E4bC96bB0a194F71B3d184809F3727f56 on Chain 902 using the following command:

cast send 0x420beeF000000000000000000000000000000002 "sendETH(address,uint256,bytes)" 0xCE35738E4bC96bB0a194F71B3d184809F3727f56 902 0x --value 10ether --rpc-url http://127.0.0.1:9545 --private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80

3. Wait for the relayed message to appear on chain 902

In a few seconds, you should see the RelayedMessage on chain 902:

# example
INFO [08-30|14:30:14.698] SuperchainWETH#CrosschainMint to=0x420bEEF000000000000000000000000000000002 amount=10,000,000,000,000,000,000

4. Check the balance on chain 902

Verify that the balance of ETH for account 0xCE35738E4bC96bB0a194F71B3d184809F3727f56 on chain 902 has increased:

cast balance 0xCE35738E4bC96bB0a194F71B3d184809F3727f56 --rpc-url http://127.0.0.1:9546

Cross Chain Contract via the L2ToL2CrossDomainMessenger

This guide walks through the CrossChainPingPong.sol contract, focusing on high level design and steps on integrating the L2ToL2CrossChainMessenger contract. The source code can be found here.

High level overview

CrossChainPingPong.sol implements a cross-chain ping-pong game using the L2ToL2CrossDomainMessenger.

  • Players hit a virtual ** ball ** back and forth between allowed L2 chains. The game starts with a serve
  • from a designated start chain, and each hit increases the rally count. The contract tracks the last hitter's address, chain ID, and the current rally count.

Diagram

sequenceDiagram
    participant Chain1 as Chain 1
    participant Chain2 as Chain 2
    
    Note over Chain1: 🚀 Game Starts (starting chain)
    Note over Chain1: 🏓 Hit Ball
    Chain1->>Chain2: 📤 Send PingPongBall {rallyCount: 1, lastHitter: Chain1}
    Chain1-->>Chain1: Emit BallSent event
    activate Chain2
    Note over Chain2: 📥 Receive Ball
        Chain2-->>Chain2: Emit BallReceived event

    Note over Chain2: 🏓 Hit Ball
    Chain2->>Chain1: 📤 Send PingPongBall {rallyCount: 2, lastHitter: Chain2}
    Chain2-->>Chain2: Emit BallSent event
    deactivate Chain2
    activate Chain1
    Note over Chain1: 📥 Receive Ball
        Chain1-->>Chain1: Emit BallReceived event

    Note over Chain1,Chain2: Game continues...

Flow

1. Contract Deployment

  • Deployed on all participating chains
  • Utilizes CREATE2 with the same parameter, _serverChainId, resulting in the same address and initial state.

2. Hit the Ball (Starting Move)

  • Call hitBallTo on the chain with the ball, specifying a destination chain.
  • Contract uses L2ToL2CrossDomainMessenger to send the ball data to the specified chain.
  • The reference to the ball is deleted from the serving chain.

3. Receive on Destination Chain

  • L2ToL2CrossDomainMessenger on destination chain calls receiveBall.
  • Contract verifies the message sender and origin.
  • Ball data is stored, indicating its presence on this chain.

4. Continue Game (Hit)

  • Any user on the chain currently holding the ball calls hitBallTo to send it to another chain.
  • Contract updates the PingPongBall data (increment rally count, update last hitter).
  • Process repeats from step 2.

Walkthrough

Here's an explanation of the functions in the contract, with a focus on how it interacts with L2ToL2CrossChainMessenger.

Initializing contract state

Constructor Setup

constructor(uint256 _serverChainId) {
    if (block.chainid == _serverChainId) {
        ball = PingPongBall(1, block.chainid, msg.sender);
    }
}

If the starting chain, initialize the ball allowing it to be hittable.

Reliance on CREATE2 for cross chain consistency

While not explicitly mentioned in the code, this contract's design implicitly assumes the use of CREATE2 for deployment. Here's why CREATE2 is crucial for this setup:

  1. Predictable Addresses: CREATE2 enables deployment at the same address on all chains, crucial for cross-chain message verification:

    if (messenger.crossDomainMessageSender() != address(this)) revert InvalidCrossDomainSender();
    
  2. Self-referential Messaging: The contract sends messages to itself on other chains:

    messenger.sendMessage(_toChainId, address(this), _message);
    

    This requires address(this) to be consistent across chains.

  3. Initialization State Considerations:

    The starting chain id is apart of the initcode, meaning a deployment with a differing value would result in a different address via CREATE2. This is a nice feature as there's an implicit agreement on the starting chain from the address.

    Without CREATE2, you would need to:

    • Manually track contract addresses for each chain.
    • Implement a more complex initialization process to register contract addresses across chains.
    • Potentially redesign the security model that relies on address matching.

Hitting the ball

hitBallTo: This function is used to hit the ball, when present, to another chain

1. Hitting Constraints

function hitBallTo(uint256 _toChainId) public {
    if (ball.lastHitterAddress == address(0)) revert BallNotPresent();
    if (_toChainId == block.chainid) revert InvalidDestination();
    ...
}
  • The ball contract variable is populated on the chain, indicating its presence
  • The destination must be a different chain

2. Define The Receiving Handler

modifier onlyCrossDomainCallback() {
    if (msg.sender != address(messenger)) revert CallerNotL2ToL2CrossDomainMessenger();
    if (messenger.crossDomainMessageSender() != address(this)) revert InvalidCrossDomainSender();

    _;
}

function receiveBall(PingPongBall memory _ball) onlyCrossDomainCallback() external {
    // Hold reference to the ball
    ball = _ball;

    emit BallReceived(messenger.crossDomainMessageSource(), block.chainid, _ball);
}
  • The handler simply stores reference to the received ball
  • The handler can only be invokable by the cross chain messenger
  • Since the contract is self-referential, the cross chain sender must be the same contract address

3. Hit The Ball Cross Chain

function hitBallTo(uint256 _toChainId) public {
    ...

    // Construct a new ball
    PingPongBall memory newBall = PingPongBall(ball.rallyCount + 1, block.chainid, msg.sender);

    // Delete current reference
    delete ball;

    // Send to the destination
    messenger.sendMessage(_toChainId, address(this), abi.encodeCall(this.receiveBall, (newBall)));

    emit BallSent(block.chainid, _toChainId, newBall);
}
  • Populate a new ball with updated properties
  • Delete reference to the current ball so it's no longer hittable
  • Invoke the contract on the destination chain matching the receiveBall handler defined in (2).

Takeaways

This is just one of many patterns to use the L2ToL2CrossDomainMessenger in your contract to power cross chain calls. Key points to remember:

  1. Simple Message Passing: This design sends simple messages between identical contracts on different chains. Each message contains only the essential game state (rally count, last hitter). More complex systems might involve multiple contracts, intermediary relayers.

  2. Cross Chain Sender Verification: Always verify the sender of cross-chain messages. This includes checking both the immediate caller (the messenger) and the original sender on the source chain.

  3. Cross Chain Contract Coordination: This design uses CREATE2 for consistent contract addresses across chains, simplifying cross-chain verification. Alternative approaches include:

    • Beacon proxy patterns for upgradeable contracts
    • Post-deployment setup where contract addresses are specified after deployment

Cross-chain tic-tac-toe