Skip to main content
Superchain interop is in active development. Some features may be experimental.

Overview

This tutorial demonstrates how to implement cross-chain communication within the Superchain ecosystem. You’ll build a complete message passing system that enables different chains to interact with each other using the L2ToL2CrossDomainMessenger contract.

What You’ll Build

  • A Greeter contract that stores and updates a greeting
  • A GreetingSender contract that sends cross-chain messages to update the greeting
  • A TypeScript application to relay messages between chains
This tutorial provides step-by-step instructions for implementing cross-chain messaging. For a conceptual overview, see the Message Passing Explainer.
In this tutorial, you will learn how to use the L2ToL2CrossDomainMessenger contract to pass messages between interoperable blockchains.

Setting up your development environment

1

Follow the [Installation Guide](/app-developers/tutorials/supersim/getting-started/installation) to install:

  • Foundry for smart contract development (required in all cases)
  • Supersim for local blockchain simulation (optional)
2

Verify your installation:

forge --version
./supersim --version

Implementing onchain message passing (in Solidity)

The implementation consists of three main components:
  1. Greeter Contract: Deployed on Chain B, receives and stores messages.
  2. GreetingSender Contract: Deployed on Chain A, initiates cross-chain messages.
1

Setting up test networks

  1. If you are using Supersim, go to the directory where Supersim is installed and start it with autorelay.
    ./supersim --interop.autorelay
    
    If you are using the devnets, just skip this step.
    • In a separate shell, store the configuration in environment variables.
      2

      Create the contracts

      1. Create a new Foundry project.
        mkdir onchain-code
        cd onchain-code
        forge init
        
      2. In src/Greeter.sol put this file. This is a variation on Hardhat’s Greeter contract.
            //SPDX-License-Identifier: MIT
            pragma solidity ^0.8.0;
        
            contract Greeter {
                string greeting;
        
                event SetGreeting(
                    address indexed sender,     // msg.sender
                    string greeting
                );
        
                function greet() public view returns (string memory) {
                    return greeting;
                }
        
                function setGreeting(string memory _greeting) public {
                    greeting = _greeting;
                    emit SetGreeting(msg.sender, _greeting);
                }
            }
        
      3. Deploy the Greeter contract to Chain B and store the resulting contract address in the GREETER_B_ADDRESS environment variable.
        GREETER_B_ADDRESS=`forge create --rpc-url $URL_CHAIN_B --private-key $PRIVATE_KEY Greeter --broadcast | awk '/Deployed to:/ {print $3}'`
        
      1. Install the Optimism Solidity libraries into the project.
        cd lib
        npm install @eth-optimism/contracts-bedrock
        cd ..
        echo @eth-optimism/=lib/node_modules/@eth-optimism/ >> remappings.txt
        
      2. Create src/GreetingSender.sol.
            //SPDX-License-Identifier: MIT
            pragma solidity ^0.8.0;
        
            import { Predeploys } from "@eth-optimism/contracts-bedrock/src/libraries/Predeploys.sol";
            import { IL2ToL2CrossDomainMessenger } from "@eth-optimism/contracts-bedrock/src/L2/IL2ToL2CrossDomainMessenger.sol";
        
            import { Greeter } from "src/Greeter.sol";
        
            contract GreetingSender {
                IL2ToL2CrossDomainMessenger public immutable messenger =
                    IL2ToL2CrossDomainMessenger(Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER);
        
                address immutable greeterAddress;
                uint256 immutable greeterChainId;
        
                constructor(address _greeterAddress, uint256 _greeterChainId) {
                    greeterAddress = _greeterAddress;
                    greeterChainId = _greeterChainId;
                }
        
                function setGreeting(string calldata greeting) public {
                    bytes memory message = abi.encodeCall(
                        Greeter.setGreeting,
                        (greeting)
                    );
                    messenger.sendMessage(greeterChainId, greeterAddress, message);
                }
            }
        
      3. Deploy GreetingSender to chain A.
        CHAIN_ID_B=`cast chain-id --rpc-url $URL_CHAIN_B`
        GREETER_A_ADDRESS=`forge create --rpc-url $URL_CHAIN_A --private-key $PRIVATE_KEY --broadcast GreetingSender --constructor-args $GREETER_B_ADDRESS $CHAIN_ID_B | awk '/Deployed to:/ {print $3}'`
        
      3

      Send a message

      Send a greeting from chain A to chain B.
      cast call --rpc-url $URL_CHAIN_B $GREETER_B_ADDRESS "greet()" | cast --to-ascii
      cast send --private-key $PRIVATE_KEY --rpc-url $URL_CHAIN_A $GREETER_A_ADDRESS "setGreeting(string)" "Hello from chain A"
      sleep 4
      cast call --rpc-url $URL_CHAIN_B $GREETER_B_ADDRESS "greet()" | cast --to-ascii
      
      The sleep call is because it can take up to two seconds until the transaction is included in chain A, and then up to two seconds until the relay transaction is included in chain B.

      Sender information

      Run this command to view the events to see who called setGreeting.
      cast logs --rpc-url $URL_CHAIN_B 'SetGreeting(address,string)'
      
      The sender information is stored in the second event topic. However, for cross-chain messages, this value corresponds to the local L2ToL2CrossDomainMessenger contract address (4200000000000000000000000000000000000023), making it ineffective for identifying the original sender. In this section we change Greeter.sol to emit a separate event in it receives a cross domain message, with the sender’s identity (address and chain ID).
      1

      Modify the Greeter contract

      1. Modify src/Greeter.sol to this code.
            //SPDX-License-Identifier: MIT
            pragma solidity ^0.8.0;
        
            import { Predeploys } from "@eth-optimism/contracts-bedrock/src/libraries/Predeploys.sol";
        
            interface IL2ToL2CrossDomainMessenger {
                function crossDomainMessageContext() external view returns (address sender_, uint256 source_);
            }
        
            contract Greeter {
        
                IL2ToL2CrossDomainMessenger public immutable messenger =
                    IL2ToL2CrossDomainMessenger(Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER);
        
                string greeting;
        
                event SetGreeting(
                    address indexed sender,     // msg.sender
                    string greeting
                );
        
                event CrossDomainSetGreeting(
                    address indexed sender,   // Sender on the other side
                    uint256 indexed chainId,  // ChainID of the other side
                    string greeting
                );
        
                function greet() public view returns (string memory) {
                    return greeting;
                }
        
                function setGreeting(string memory _greeting) public {
                    greeting = _greeting;
                    emit SetGreeting(msg.sender, _greeting);
        
                    if (msg.sender == Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER) {
                        (address sender, uint256 chainId) =
                            messenger.crossDomainMessageContext();
                        emit CrossDomainSetGreeting(sender, chainId, _greeting);
                    }
                }
            }
        
      2. Redeploy the contracts. Because the address of Greeter is immutable in GreetingSender, we need to redeploy both contracts.
        GREETER_B_ADDRESS=`forge create --rpc-url $URL_CHAIN_B --private-key $PRIVATE_KEY Greeter --broadcast | awk '/Deployed to:/ {print $3}'`
        GREETER_A_ADDRESS=`forge create --rpc-url $URL_CHAIN_A --private-key $PRIVATE_KEY --broadcast GreetingSender --constructor-args $GREETER_B_ADDRESS $CHAIN_ID_B | awk '/Deployed to:/ {print $3}'`
        
      2

      Verify you can see cross chain sender information

      1. Set the greeting through GreetingSender.
        cast call --rpc-url $URL_CHAIN_B $GREETER_B_ADDRESS "greet()" | cast --to-ascii
        cast send --private-key $PRIVATE_KEY --rpc-url $URL_CHAIN_A $GREETER_A_ADDRESS "setGreeting(string)" "Hello from chain A, with a CrossDomainSetGreeting event"
        sleep 4
        cast call --rpc-url $URL_CHAIN_B $GREETER_B_ADDRESS "greet()" | cast --to-ascii
        
      2. Read the log entries.
        cast logs --rpc-url $URL_CHAIN_B 'CrossDomainSetGreeting(address,uint256,string)'
        echo $GREETER_A_ADDRESS
        echo 0x385=`echo 0x385 | cast --to-dec`
        echo 0x190a85c0=`echo 0x190a85c0 | cast --to-dec`
        
        See that the second topic (the first indexed log parameter) is the same as $GREETER_A_ADDRESS. The third topic can be either 0x385=901, which is the chain ID for supersim chain A, or 0x190a85c0=420120000, which is the chain ID for devnet alpha 0.

      Next steps

      I