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.
Transferring SuperchainERC20 tokens
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 (opens in a new tab).
Overview
Always verify your addresses and amounts before sending transactions. Cross-chain transfers cannot be reversed.
What you'll build
- A TypeScript application to transfer
SuperchainERC20
tokens between chains
What you'll learn
- How to send
SuperchainERC20
tokens 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 issuing transactions
- TypeScript: For implementation
- Node: For running TypeScript code from the command line
- Viem: For blockchain interaction
Directions
Preparation
You need onchain SuperchainERC20
tokens.
You can deploy your own token, but in this tutorial we will use CustomSuperchainToken
(opens in a new tab), existing SuperchainERC20
token on the Interop devnet.
-
Create environment variables for the RPC endpoints for the blockchains and the token address.
RPC_DEV0=https://interop-alpha-0.optimism.io RPC_DEV1=https://interop-alpha-1.optimism.io TOKEN_ADDRESS=0xF3Ce0794cB4Ef75A902e07e5D2b75E4D71495ee8
-
Set
PRIVATE_KEY
to the private key of an address that has Sepolia ETH (opens in a new tab).export PRIVATE_KEY=0x<private key here> MY_ADDRESS=`cast wallet address $PRIVATE_KEY`
-
Send ETH to the two L2 blockchains.
cast send --rpc-url https://endpoints.omniatech.io/v1/eth/sepolia/public --private-key $PRIVATE_KEY --value 0.02ether 0x7385d89d38ab79984e7c84fab9ce5e6f4815468a cast send --rpc-url https://endpoints.omniatech.io/v1/eth/sepolia/public --private-key $PRIVATE_KEY --value 0.02ether 0x55f5c4653dbcde7d1254f9c690a5d761b315500c
-
Wait a few minutes until you can see the ETH on the block explorer (opens in a new tab) for your address.
Sanity check
Check the ETH balance of your address on both blockchains.
cast balance --ether $MY_ADDRESS --rpc-url $RPC_DEV0 cast balance --ether $MY_ADDRESS --rpc-url $RPC_DEV1
-
Obtain tokens on Interop devnet 0. When using
CustomSuperchainToken
, there are two ways to do this:-
Use the block explorer (opens in a new tab) and a browser wallet to run the faucet (opens in a new tab) function.
-
Use
cast
to call thefaucet
function.cast send --rpc-url $RPC_DEV0 --private-key $PRIVATE_KEY $TOKEN_ADDRESS "faucet()"
Sanity check
Run this command to check your token balance.
cast call --rpc-url $RPC_DEV0 $TOKEN_ADDRESS "balanceOf(address)" $MY_ADDRESS | cast --from-wei
-
Transfer tokens using TypeScript
We are going to use a Node (opens in a new tab) project, to be able to use @eth-optimism/viem
(opens in a new tab) to send the executing message.
We use TypeScript (opens in a new tab) to have type safety (opens in a new tab) combined with JavaScript functionality.
-
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
-
Edit
package.json
to add thestart
script.{ "name": "xfer-erc20", "version": "1.0.0", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "start": "tsx src/xfer-erc20.mts" }, "keywords": [], "author": "", "license": "ISC", "type": "module", "description": "", "devDependencies": { "@eth-optimism/viem": "^0.3.2", "@types/node": "^22.13.4", "tsx": "^4.19.3", "viem": "^2.23.3" } }
-
Create
src/xfer-erc20.mts
:import { createWalletClient, http, publicActions, getContract, Address, } from 'viem' import { privateKeyToAccount } from 'viem/accounts' import { interopAlpha0, interopAlpha1 } from '@eth-optimism/viem/chains' import { walletActionsL2, publicActionsL2, createInteropSentL2ToL2Messages, } from '@eth-optimism/viem' const tokenAddress = "0xF3Ce0794cB4Ef75A902e07e5D2b75E4D71495ee8" 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: interopAlpha0, transport: http(), account }).extend(publicActions) .extend(publicActionsL2()) .extend(walletActionsL2()) const wallet1 = createWalletClient({ chain: 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)} `) } await reportBalances() const sendTxnHash = await wallet0.interop.sendSuperchainERC20({ tokenAddress, to: account.address, amount: 1000000000, chainId: wallet1.chain.id }) console.log(`Send transaction: https://sid.testnet.routescan.io/tx/${sendTxnHash}`) const sendTxnReceipt = await wallet0.waitForTransactionReceipt({ hash: sendTxnHash }) const sentMessage = (await createInteropSentL2ToL2Messages(wallet0, { receipt: sendTxnReceipt })) .sentMessages[0] const relayTxnHash = await wallet1.interop.relayMessage({ sentMessageId: sentMessage.id, sentMessagePayload: sentMessage.payload, }) const relayTxnReceipt = await wallet1.waitForTransactionReceipt({ hash: relayTxnHash }) console.log(`Relay transaction: https://sid.testnet.routescan.io/tx/${relayTxnHash}`) await reportBalances()
Explanation of
xfer-erc20.mts
const sendTxnHash = await wallet0.interop.sendSuperchainERC20({ tokenAddress, to: account.address, amount: 1000000000, chainId: wallet1.chain.id })
Use
@eth-optimism/viem
'swalletActionsL2().sendSuperchainERC20
to send theSuperchainERC20
tokens. Internally, this function callsSuperchainTokenBridge.sendERC20
(opens in a new tab) to send the tokens.💡Normally we expect Superchain blockchains to run an autorelayer and relay your messages automatically. However, for performance reasons or reliability, you might decide to submit the executing message manually. See below to learn how to do that.const sendTxnReceipt = await wallet0.waitForTransactionReceipt({ hash: sendTxnHash })
To relay a message, we need the information in the receipt. Also, we need to wait until the transaction with the relayed message is actually part of a block.
const sentMessage = (await createInteropSentL2ToL2Messages(wallet0, { receipt: sendTxnReceipt })) .sentMessages[0]
A single transaction can send multiple messages. But here we know we sent just one, so we look for the first one in the list.
const relayTxnHash = await wallet1.interop.relayMessage({ sentMessageId: sentMessage.id, sentMessagePayload: sentMessage.payload, })
This is how you use
@eth-optimism/viem
to create an executing message. -
Run the TypeScript program, and see the change in your
CustomSuperchainToken
balances.npm start
Next steps
- Read the Superchain Interop Explainer or check out this Superchain interop design video walk-thru (opens in a new tab).
- Learn how this works.
- Use Supersim, a local dev environment that simulates Superchain interop for testing applications against a local version of the Superchain.