Using the Standard Bridge

Using the Standard Bridge

Certain interactions, like transferring BNB and BEP20 tokens between the two networks, are common enough that we've built the "Standard Bridge" to make moving these assets between L1 and L2 as easy as possible.

The standard bridge functionality provides a method for an BEP20 token to be deposited and locked on L1 in exchange of the same amount of an equivalent token on L2. This process is known as "bridging a token", e.g. depositing 100 USDC on L1 in exchange for 100 USDC on L2 and also the reverse - withdrawing 100 USDC on L2 in exchange for the same amount on L1. In addition to bridging tokens the standard bridge is also used for BNB.

The Standard Bridge is composed of two main contracts the L1StandardBridgeopen in new window(for Layer 1) and the L2StandardBridgeopen in new window(for Layer 2).

Here we'll go over the basics of using this bridge to move BEP20 assets between Layer 1 and Layer 2.

Adding an BEP20 token to the Standard Bridge

Bridging your Standard ERC20 token to Combo network using the Standard Bridge

For an L1/L2 token pair to work on the Standard Bridge the L2 token contract must implement ILegacyMintableERC20open in new window interface.

If you do not need any special processing on L2, just the ability to deposit, transfer, and withdraw tokens, you can use OptimismMintableERC20Factoryopen in new window.

Warning: The standard bridge does not support certain BEP-20 configurations:

  • Fee on transfer tokens
  • Tokens that modify balances without emitting a Transfer event

Deploying the token

  1. Download the necessary packagesopen in new window.
  1. Copy .env.example to .env.
cp .env.example .env
  1. Edit .env to set the deployment parameters:
  • MNEMONIC, the mnemonic for an account that has enough BNB for the deployment.
  • L1_ALCHEMY_KEY, the key for the alchemy application for a BNB Chain endpoint.
  • L2_ALCHEMY_KEY, the key for the alchemy application for an Combo network endpoint.
  • L1_TOKEN_ADDRESS, the address of the L1 BEP20 which you want to bridge.
  1. Open the hardhat console.
yarn hardhat console --network combo-testnet
  1. Connect to OptimismMintableERC20Factory.
fname = "node_modules/@eth-optimism/contracts-bedrock/artifacts/contracts/universal/OptimismMintableERC20Factory.sol/OptimismMintableERC20Factory.json"
ftext = fs.readFileSync(fname).toString().replace(/\n/g, "")
optimismMintableERC20FactoryData = JSON.parse(ftext)
optimismMintableERC20Factory = new ethers.Contract(
   await ethers.getSigner())
  1. Deploy the contract.
deployTx = await optimismMintableERC20Factory.createOptimismMintableERC20(
   "Token Name on L2",
deployRcpt = await deployTx.wait()

Transferring tokens

  1. Get the token addresses.
l1Addr = process.env.L1_TOKEN_ADDRESS
event = => x.event == "OptimismMintableERC20Created")[0]
l2Addr = event.args.localToken
  1. Get the data for OptimismMintableERC20:
fname = "node_modules/@eth-optimism/contracts-bedrock/artifacts/contracts/universal/OptimismMintableERC20.sol/OptimismMintableERC20.json"
ftext = fs.readFileSync(fname).toString().replace(/\n/g, "")
optimismMintableERC20Data = JSON.parse(ftext)
  1. Get the L2 contract.
l2Contract = new ethers.Contract(l2Addr, optimismMintableERC20Data.abi, await ethers.getSigner())   
Get setup for L1 (provider, wallet, tokens, etc)

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

  1. Get the L1 wallet.
l1Url = ``
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)
  1. Get the L1 contract.
l1Factory = await ethers.getContractFactory("OptimismUselessToken")
l1Contract = new ethers.Contract(process.env.L1_TOKEN_ADDRESS, l1Factory.interface, l1Wallet)
  1. 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 CrossDomainMessengeropen in new window (the Optimism SDK object used to bridge assets).

  1. Import the Optimism SDK.
optimismSDK = require("@eth-optimism/sdk")
  1. 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,
   bedrock: true

Deposit (from BNB Chain to Combo network)

  1. 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()
  1. 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)
  1. Do the actual deposit
depositTx2 = await crossChainMessenger.depositERC20(l1Addr, l2Addr, 1e9)
await depositTx2.wait()
  1. Wait for the deposit to be relayed.
await crossChainMessenger.waitForMessageStatus(depositTx2.hash, optimismSDK.MessageStatus.RELAYED)
  1. Check your balances on L1 and L2.
await l1Contract.balanceOf(l1Wallet.address) 
await l2Contract.balanceOf(l1Wallet.address)

Withdrawal (from Combo network to BNB Chain)

  1. Initiate the withdrawal on L2
withdrawalTx1 = await crossChainMessenger.withdrawERC20(l1Addr, l2Addr, 1e9)
await withdrawalTx1.wait()
  1. Wait until the root state is published on L1, and then prove the withdrawal. This is likely to take less than 240 seconds.
await crossChainMessenger.waitForMessageStatus(withdrawalTx1.hash, optimismSDK.MessageStatus.READY_TO_PROVE)
withdrawalTx2 = await crossChainMessenger.proveMessage(withdrawalTx1.hash)
await withdrawalTx2.wait()
  1. Wait the fault challenge period (a short period on Goerli, seven days on the production network) and then finish the withdrawal.
await crossChainMessenger.waitForMessageStatus(withdrawalTx1.hash, optimismSDK.MessageStatus.READY_FOR_RELAY)
withdrawalTx3 = await crossChainMessenger.finalizeMessage(withdrawalTx1.hash)
await withdrawalTx3.wait()   
  1. 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)