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 0x420beeF000000000000000000000000000000001 "sendERC20(address _to, uint256 _amount, uint256 _chainId)" 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. Retrive 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 \
    "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