Skip to main content
Superchain interop is in active development. Some features may be experimental.
This tutorial provides step-by-step instructions for how to send ETH from one chain in the Superchain interop cluster to another. For a conceptual overview, see the interoperable ETH explainer.

Overview

Crosschain ETH transfers in the Superchain are facilitated through the SuperchainETHBridge contract. This tutorial walks through how to send ETH from one chain to another. You can do this on Supersim, the Interop devnet, or production once it is released.

What you’ll build

  • A TypeScript application to transfer ETH chains

What you’ll learn

  • How to send ETH on the blockchain and between blockchains
  • How to relay messages between chains

Prerequisites

Before starting this tutorial, ensure your development environment meets the following requirements:

Technical knowledge

  • Intermediate TypeScript knowledge
  • Understanding of smart contract development
  • Familiarity with blockchain concepts

Development environment

  • Unix-like operating system (Linux, macOS, or WSL for Windows)
  • Node.js version 16 or higher
  • Git for version control

Required tools

The tutorial uses these primary tools:
  • Foundry: For smart contract development
  • Supersim: For local blockchain simulation
  • TypeScript: For implementation
  • Viem: For blockchain interaction
1

Install prerequisite software

  1. Install Foundry.
  2. Install Node.
  3. Install git. The exact mechanism to do this depends on your operating system; most come with it preinstalled.
2

Configure the network

You can run this tutorial either with Supersim running locally, or using the Interop devnet. Select the correct tab and follow the directions.
    3

    Transfer ETH using Foundry

    Run these commands:
    DST_CHAINID=`cast chain-id --rpc-url $DST_URL`
    MY_ADDRESS=`cast wallet address $PRIVATE_KEY`
    SUPERCHAIN_ETH_BRIDGE=0x4200000000000000000000000000000000000024
    BEFORE=`cast balance $MY_ADDRESS --rpc-url $DST_URL | cast from-wei`          
    cast send --rpc-url $SRC_URL --private-key $PRIVATE_KEY $SUPERCHAIN_ETH_BRIDGE "sendETH(address,uint256)" $MY_ADDRESS $DST_CHAINID --value 0.001ether
    sleep 10
    AFTER=`cast balance $MY_ADDRESS --rpc-url $DST_URL | cast from-wei`
    echo -e Balance before transfer\\t$BEFORE
    echo -e Balance after transfer\\t$AFTER
    
    4

    Create the TypeScript project

    Messages are relayed automatically in the interop devnet.
    1

    Create a new TypeScript project

    mkdir transfer-eth
    cd transfer-eth
    npm init -y
    npm install --save-dev -y viem tsx @types/node @eth-optimism/viem typescript
    mkdir src
    
    2

    Download the SuperchainETHBridge ABI

    curl https://raw.githubusercontent.com/ethereum-optimism/optimism/refs/heads/develop/packages/contracts-bedrock/snapshots/abi/SuperchainETHBridge.json > src/SuperchainETHBridge.abi.json
    
    3

    Create src/transfer-eth.mts

        import {
            createWalletClient,
            http,
            publicActions,
            getContract,
            Address,
            formatEther,
            parseEther,
        } from 'viem'
    
        import { privateKeyToAccount } from 'viem/accounts'
    
        import {
            supersimL2A,
            supersimL2B,
            interopAlpha0,
            interopAlpha1
        } from '@eth-optimism/viem/chains'
    
        import {
            walletActionsL2,
            publicActionsL2,
            contracts as optimismContracts
        } from '@eth-optimism/viem'
    
        import superchainEthBridgeAbi from './SuperchainETHBridge.abi.json'
    
        const supersimAddress = '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266'
        const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`)
        const sourceChain = account.address === supersimAddress ? supersimL2A : interopAlpha0
        const destinationChain = account.address === supersimAddress ? supersimL2B : interopAlpha1
    
        const sourceWallet = createWalletClient({
            chain: sourceChain,
            transport: http(),
            account,
        }).extend(publicActions)
            .extend(publicActionsL2())
            .extend(walletActionsL2())
    
        const destinationWallet = createWalletClient({
            chain: destinationChain,
            transport: http(),
            account,
        }).extend(publicActions)
            .extend(publicActionsL2())
            .extend(walletActionsL2())
    
        const ethBridgeOnSource = await getContract({
            address: optimismContracts.superchainETHBridge.address,
            abi: superchainEthBridgeAbi,
            client: sourceWallet,
        })
    
        const reportBalance = async (address: string): Promise<void> => {
            const sourceBalance = await sourceWallet.getBalance({ address })
            const destinationBalance = await destinationWallet.getBalance({ address })
    
            console.log(`
                Address: ${address}
                Balance on source chain: ${formatEther(sourceBalance)}
                Balance on destination chain: ${formatEther(destinationBalance)}
            `)
        }
    
        console.log('Before transfer')
        await reportBalance(account.address)
    
        const sourceHash = await ethBridgeOnSource.write.sendETH({
            value: parseEther('0.001'),
            args: [account.address, destinationChain.id],
        })
        const sourceReceipt = await sourceWallet.waitForTransactionReceipt({
            hash: sourceHash,
        })
    
        console.log('After transfer on source chain')
        await reportBalance(account.address)
    
        const sentMessages = await sourceWallet.interop.getCrossDomainMessages({
            logs: sourceReceipt.logs,
        })
        const sentMessage = sentMessages[0]
        const relayMessageParams = await sourceWallet.interop.buildExecutingMessage({
            log: sentMessage.log,
        })
    
        const relayMsgTxnHash = await destinationWallet.interop.relayCrossDomainMessage(relayMessageParams)
        await destinationWallet.waitForTransactionReceipt({ hash: relayMsgTxnHash })
    
        console.log('After relaying message to destination chain')
        await reportBalance(account.address)
    
    5

    Run the example

    1. Run the example.
      npx tsx src/transfer-eth.mts
      
    2. Read the results.
      Before transfer
      
              Address: 0x7ED53BfaA58B79Dd655B2f229258C093b6C09A8C
              Balance on source chain: 0.020999799151902245
              Balance on destination chain: 0.026999459226731331
      
      The initial state. Note that the address depends on your private key; it should be different from mine.
      After transfer on source chain
      
              Address: 0x7ED53BfaA58B79Dd655B2f229258C093b6C09A8C
              Balance on source chain: 0.019999732176717961
              Balance on destination chain: 0.026999459226731331
      
      After the initiating message the balance on the source chain is immediately reduced. Notice that even though we are sending 0.001 ETH, the balance on the source chain is reduced by a bit more (here, approximately 67 gwei). This is the cost of the initiating transaction on the source chain. Of course, as there has been no transaction on the destination chain, that balance is unchanged.
      After relaying message to destination chain
      
              Address: 0x7ED53BfaA58B79Dd655B2f229258C093b6C09A8C
              Balance on source chain: 0.019999732176717961
              Balance on destination chain: 0.027999278943880868    
      
      Now the balance on the destination chain increases, by slightly less than 0.001 ETH. The executing message also has a transaction cost (in this case, about 180gwei).

    Next steps