Skip to main content
The SuperchainERC20 standard is ready for production deployments. Please note that the OP Stack interoperability upgrade, required for crosschain messaging, is currently still in active development.
This guide shows how to transfer SuperchainERC20 tokens between chains programmatically. Note that this tutorial provides step-by-step instructions for transferring SuperchainERC20 tokens using code.
  • For a detailed behind-the-scenes explanation, see the explainer.
  • For a sample UI that bridges a SuperchainERC20 token, see here.

Overview

Always verify your addresses and amounts before sending transactions. Cross-chain transfers cannot be reversed.

What you’ll build

  • Commands to transfer SuperchainERC20 tokens between chains
  • A TypeScript application to transfer SuperchainERC20 tokens between chains

Directions

1

Clone the starter repository

npx degit ethereum-optimism/starter-kit-superchain-erc20 transfer-superchain-erc20
cd transfer-superchain-erc20
2

Install dependencies

pnpm install
3

Preparation

  1. If you are using Supersim, setup the SuperchainERC20 starter kit. The pnpm dev step also starts Supersim.
  2. Store the configuration in environment variables.
    • Obtain tokens on chain A.
      4

      Transfer tokens using the command line

      1. Specify configuration variables.
        TENTH=$(echo 0.1 | cast to-wei)
        INTEROP_BRIDGE=0x4200000000000000000000000000000000000028      
        
      2. See your balance on both blockchains.
        cast call $TOKEN_ADDRESS "balanceOf(address)" $USER_ADDRESS --rpc-url $URL_CHAIN_A | cast from-wei
        cast call $TOKEN_ADDRESS "balanceOf(address)" $USER_ADDRESS --rpc-url $URL_CHAIN_B | cast from-wei      
        
      3. Call SuperchainTokenBridge to transfer tokens.
        cast send $INTEROP_BRIDGE "sendERC20(address,address,uint256,uint256)" $TOKEN_ADDRESS $USER_ADDRESS $TENTH $CHAIN_B_ID --private-key $PRIVATE_KEY --rpc-url $URL_CHAIN_A
        
      4. See your balance on both blockchains.
        cast call $TOKEN_ADDRESS "balanceOf(address)" $USER_ADDRESS --rpc-url $URL_CHAIN_A | cast from-wei
        cast call $TOKEN_ADDRESS "balanceOf(address)" $USER_ADDRESS --rpc-url $URL_CHAIN_B | cast from-wei      
        
      5

      Transfer tokens using TypeScript

      We are going to use a Node project, to be able to use @eth-optimism/viem to send the executing message. We use TypeScript to have type safety combined with JavaScript functionality.
      1. Export environment variables
        export PRIVATE_KEY TOKEN_ADDRESS CHAIN_B_ID
        
      2. Initialize a new Node project.
        mkdir xfer-erc20
        cd xfer-erc20
        npm init -y
        npm install --save-dev -y viem tsx @types/node @eth-optimism/viem
        mkdir src
        
      3. Create src/xfer-erc20.mts:
            import {
                createWalletClient,
                http,
                publicActions,
                getContract,
            } from 'viem'
            import { privateKeyToAccount } from 'viem/accounts'
            import { interopAlpha0, interopAlpha1, supersimL2A, supersimL2B } from '@eth-optimism/viem/chains'
            import { walletActionsL2, publicActionsL2 } from '@eth-optimism/viem'
        
            const tokenAddress = process.env.TOKEN_ADDRESS
            const useSupersim = process.env.CHAIN_B_ID == "902"
        
            const balanceOf = {
                "constant": true,
                "inputs": [{
                    "name": "_owner",
                    "type": "address"
                }],
                "name": "balanceOf",
                "outputs": [{
                    "name": "balance",
                    "type": "uint256"
                }],
                "payable": false,
                "stateMutability": "view",
                "type": "function"
            }
        
            const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`)
        
            const wallet0 = createWalletClient({
                chain: useSupersim ? supersimL2A : interopAlpha0,
                transport: http(),
                account
            }).extend(publicActions)
                .extend(publicActionsL2())
                .extend(walletActionsL2())
            
            const wallet1 = createWalletClient({
                chain: useSupersim ? supersimL2B : interopAlpha1,
                transport: http(),
                account
            }).extend(publicActions)
                .extend(publicActionsL2())
                .extend(walletActionsL2())
        
            const token0 = getContract({
                address: tokenAddress,
                abi: [balanceOf],
                client: wallet0
            })
        
            const token1 = getContract({
                address: tokenAddress,
                abi: [balanceOf],
                client: wallet1
            })
        
        
            const reportBalances = async () => {
                const balance0 = await token0.read.balanceOf([account.address])
                const balance1 = await token1.read.balanceOf([account.address])
        
                console.log(`
                  Address: ${account.address}
                      chain0: ${balance0.toString().padStart(20)}
                      chain1: ${balance1.toString().padStart(20)}
        
                  `)
            }
        
            console.log("Initial balances")
            await reportBalances()
        
            const sendTxnHash = await wallet0.interop.sendSuperchainERC20({
                tokenAddress,
                to: account.address,
                amount: BigInt(1000000000),
                chainId: wallet1.chain.id
            })
        
            console.log(`Send transaction: ${sendTxnHash}`)
            await wallet0.waitForTransactionReceipt({
                hash: sendTxnHash
            })
        
            console.log("Immediately after the transaction is processed")
            await reportBalances()
        
            await new Promise(resolve => setTimeout(resolve, 5000));
        
            console.log("After waiting (hopefully, until the message is relayed)")
            await reportBalances()
        
      4. Run the TypeScript program, and see the change in your token balances.
        pnpm tsx src/xfer-erc20.mts
        

      Next steps

      I