Builders
Dapp Developers
Tutorials
Bridging Your Standard ERC-20 Token to OP Mainnet

Bridging your Standard ERC-20 token to OP Mainnet using the Standard Bridge

For an L1/L2 token pair to work on the Standard Bridge the L2 token contract must implement IOptimismMintableERC20 (opens in a new tab) interface.

If you do not need any special processing on L2, just the ability to deposit, transfer, and withdraw tokens, you can use OptimismMintableERC20Factory (opens in a new tab).

Before You Begin

This tutorial assumes you already have Node.js (opens in a new tab), pnpm (opens in a new tab), and yarn (opens in a new tab) installed on your system.

Deploying the token

Make a Project Folder and Initialize It

mkdir project-folder-name
cd folder-name
pnpm init

Create Two Files for the Folder

  • Create a .env file
.env file
MNEMONIC=xxxx
L1_ALCHEMY_KEY=xxxx
L2_ALCHEMY_KEY=xxxx
L1_TOKEN_ADDRESS=0x32B3b2281717dA83463414af4E8CfB1970E56287
  • Create a hardhat.config.js file
hardhat.config.js
// Plugins
require('@nomiclabs/hardhat-ethers')
 
// Load environment variables from .env
require('dotenv').config();
 
 
const words = process.env.MNEMONIC.match(/[a-zA-Z]+/g).length
validLength = [12, 15, 18, 24]
if (!validLength.includes(words)) {
   console.log(`The mnemonic (${process.env.MNEMONIC}) is the wrong number of words`)
   process.exit(-1)
}
 
module.exports = {
  networks: {
    'optimism-goerli': {
      chainId: 420,
      url: `https://opt-goerli.g.alchemy.com/v2/${process.env.L2_ALCHEMY_KEY}`,
      accounts: { mnemonic: process.env.MNEMONIC }
    },
    'optimism-mainnet': {
      chainId: 10,
      url: `https://opt-mainnet.g.alchemy.com/v2/${process.env.L2_ALCHEMY_KEY}`,
      accounts: { mnemonic: process.env.MNEMONIC }
    }
  },
  solidity: '0.8.13',
}

Install the dependencies

pnpm add @eth-optimism/sdk dotenv @openzeppelin/contracts

Edit .env file to set the deployment parameters

  • MNEMONIC, the mnemonic for an account that has enough ETH for the deployment.
  • L1_ALCHEMY_KEY, the key for the alchemy application for a Goerli endpoint.
  • L2_ALCHEMY_KEY, the key for the alchemy application for an OP Goerli endpoint.
  • L1_TOKEN_ADDRESS, the address of the L1 ERC20 which you want to bridge. The default value, 0x32B3b2281717dA83463414af4E8CfB1970E56287 (opens in a new tab) is a test ERC-20 contract on Goerli that lets you call faucet to give yourself test tokens.

Open the hardhat console

yarn hardhat console --network optimism-goerli

Connect to OptimismMintableERC20Factory

fname = "node_modules/@eth-optimism/contracts-bedrock/forge-artifacts/OptimismMintableERC20Factory.sol/OptimismMintableERC20Factory.json"
ftext = fs.readFileSync(fname).toString().replace(/\n/g, "")
optimismMintableERC20FactoryData = JSON.parse(ftext)
optimismMintableERC20Factory = new ethers.Contract(
   "0x4200000000000000000000000000000000000012", 
   optimismMintableERC20FactoryData.abi, 
   await ethers.getSigner())

Deploy the contract

deployTx = await optimismMintableERC20Factory.createOptimismMintableERC20(
   process.env.L1_TOKEN_ADDRESS,
   "Token Name on L2",
   "L2-SYMBOL"
)
deployRcpt = await deployTx.wait()

Transferring tokens

Get the token addresses

l1Addr = process.env.L1_TOKEN_ADDRESS
event = deployRcpt.events.filter(x => x.event == "OptimismMintableERC20Created")[0]
l2Addr = event.args.localToken

Get the data for OptimismMintableERC20

fname = "node_modules/@eth-optimism/contracts-bedrock/forge-artifacts/OptimismMintableERC20.sol/OptimismMintableERC20.json"
ftext = fs.readFileSync(fname).toString().replace(/\n/g, "")
optimismMintableERC20Data = JSON.parse(ftext)

Get the L2 contract

l2Contract = new ethers.Contract(l2Addr, optimismMintableERC20Data.abi, await ethers.getSigner())   

Get setup for L1 (provider, wallet, tokens, etc)

Get the L1 wallet

l1Url = `https://eth-goerli.g.alchemy.com/v2/${process.env.L1_ALCHEMY_KEY}`
l1RpcProvider = new ethers.providers.JsonRpcProvider(l1Url)
hdNode = ethers.utils.HDNode.fromMnemonic(process.env.MNEMONIC)
privateKey = hdNode.derivePath(ethers.utils.defaultPath).privateKey
l1Wallet = new ethers.Wallet(privateKey, l1RpcProvider)

Get the L1 contract

l1Factory = await ethers.getContractFactory("OptimismUselessToken")
l1Contract = new ethers.Contract(process.env.L1_TOKEN_ADDRESS, l1Factory.interface, l1Wallet)

Get tokens on L1 (and verify the balance)

faucetTx = await l1Contract.faucet()
faucetRcpt = await faucetTx.wait()
await l1Contract.balanceOf(l1Wallet.address)

Transfer tokens

Create and use CrossDomainMessenger (opens in a new tab) (the Optimism SDK object used to bridge assets). The SDK supports multiple OP Chains: OP, Base, etc. To see whether a specific OP Chain is supported directly, see the documentation (opens in a new tab). Chains that aren't officially supported just take a few extra steps. Get the L1 contract addresses, and provide them to the SDK. Once you do that, you can use the SDK normally.

Import the Optimism SDK

optimismSDK = require("@eth-optimism/sdk")

Create the cross domain messenger

l1ChainId = (await l1RpcProvider.getNetwork()).chainId
l2ChainId = (await ethers.provider.getNetwork()).chainId
l2Wallet = await ethers.provider.getSigner()
crossChainMessenger = new optimismSDK.CrossChainMessenger({
   l1ChainId: l1ChainId,
   l2ChainId: l2ChainId,
   l1SignerOrProvider: l1Wallet,
   l2SignerOrProvider: l2Wallet
})

Deposit (from Ethereum to OP Mainnet, or Goerli to OP Goerli)

Give the L1 bridge an allowance to use the user's token

The L2 address is necessary to know which bridge is responsible and needs the allowance.

depositTx1 = await crossChainMessenger.approveERC20(l1Contract.address, l2Addr, 1e9)
await depositTx1.wait()

Check your balances on L1 and L2

Note that l1Wallet and l2Wallet have the same address, so it doesn't matter which one we use.

await l1Contract.balanceOf(l1Wallet.address) 
await l2Contract.balanceOf(l1Wallet.address)

Complete the actual deposit

depositTx2 = await crossChainMessenger.depositERC20(l1Addr, l2Addr, 1e9)
await depositTx2.wait()

Wait for the deposit to be relayed

await crossChainMessenger.waitForMessageStatus(depositTx2.hash, optimismSDK.MessageStatus.RELAYED)

Check your balances on L1 and L2

await l1Contract.balanceOf(l1Wallet.address) 
await l2Contract.balanceOf(l1Wallet.address)

Withdrawal (from OP Mainnet to Ethereum or OP Goerli to Goerli)

Initiate the withdrawal on L2

withdrawalTx1 = await crossChainMessenger.withdrawERC20(l1Addr, l2Addr, 1e9)
await withdrawalTx1.wait()

Prove the withdrawal after root state is published

  • Wait until the root state is published on L1.

  • This is likely to take less than 240 seconds.

  • Prove the withdrawal.

    await crossChainMessenger.waitForMessageStatus(withdrawalTx1.hash, optimismSDK.MessageStatus.READY_TO_PROVE)
    withdrawalTx2 = await crossChainMessenger.proveMessage(withdrawalTx1.hash)
    await withdrawalTx2.wait()

Finish the withdrawal after the fault challenge period

  • Wait the fault challenge period, which is a short period on Goerl or seven days on the production network

  • Finish the withdrawal.

    await crossChainMessenger.waitForMessageStatus(withdrawalTx1.hash, optimismSDK.MessageStatus.READY_FOR_RELAY)
    withdrawalTx3 = await crossChainMessenger.finalizeMessage(withdrawalTx1.hash)
    await withdrawalTx3.wait()   

Check your balances on L1 and L2

The balance on L2 should be back to zero.

await l1Contract.balanceOf(l1Wallet.address) 
await l2Contract.balanceOf(l1Wallet.address)