Bridging Custom ERC20 Tokens Using Standard Bridge

Mantle v2 Tectonic has been released, please move to the new documentation!

For an L1/L2 token pair to work on the Standard Bridge, there has to be a layer of original mint (where the minting and burning of tokens is controlled by the business logic), and a bridged layer where the Standard Bridge controls minting and burning. The most common configuration is to have L1 as the layer of original mint, and L2 as the bridged layer, this allows for ERC-20 contracts that were written with no knowledge of Mantle Mainnet to be bridged. The contract on the bridged layer has to implement the legacy IL2StandardERC20 interface (only if the bridged layer is L2).

For this to be done securely, the only entity that is allowed to mint and burn tokens on the bridged layer has to be the Standard Bridge, to ensure that the tokens on the bridged layer are backed up by real tokens on the layer of original mint. It is also necessary that the ERC-20 token contract on the layer of original mint not implement either of the interfaces, to make sure the bridge contracts don't get confused and think it is the bridged layer.

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

For the purpose we import the L2StandardERC20 from the @mantleio/contracts package. This standard token implementation is based on the OpenZeppelin ERC20 contract and implements the required IL2StandardERC20 interface.

You can import @mantleio/contracts to use the Mantle contracts within your own codebase. Install via npm or yarn:

npm install @mantleio/contracts

Within your contracts:

import { L2StandardERC20 } from "@mantleio/contracts/standards/L2StandardERC20.sol";

Deploying the custom token

  1. Download the necessary packages.

  1. Copy .env.example to .env.

cp .env.example .env
  1. Edit .env to set the deployment parameters:

  • PRIVATE_KEY, the hex private key for an account that has enough ETH for the deployment.

  • L1_RPC, Ethereum endpoint RPC URL.

  • L2_RPC, Mantle endpoint RPC URL.

  • L1_BRIDGE, L1 standard bridge contract address.

  • L2_BRIDGE, L2 standard bridge contract address.

  • L1_TOKEN_ADDRESS, the address of the L1 ERC20 which you want to bridge. The default value, 0xeE7Bf96bFd25931976F45a16C4483d336169Bc0F is a test ERC-20 contract on Goerli that lets you call faucet to give yourself test tokens.

  1. Open the hardhat console.

yarn hardhat console --network mantle-network
  1. Deploy the contract.

l2CustomERC20Factory = await ethers.getContractFactory("L2CustomERC20")   
l2CustomERC20 = await l2CustomERC20Factory.deploy(

Transferring tokens

  1. Get the token addresses.

l1Addr = process.env.L1_TOKEN_ADDRESS
l2Addr = l2CustomERC20.address

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

  1. Get the L1 wallet.

l1RpcProvider = new ethers.providers.JsonRpcProvider(process.env.L1_RPC)
const privateKey = process.env.PRIVATE_KEY
l1Wallet = new ethers.Wallet(privateKey, l1RpcProvider)
  1. Get the L1 contract.

l1Factory = await ethers.getContractFactory("MantleUselessToken")
l1Contract = new ethers.Contract(process.env.L1_TOKEN_ADDRESS, l1Factory.interface, l1Wallet)
  1. Get tokens on L1 (and verify the balance)

tx = await l1Contract.faucet()
rcpt = await tx.wait()
await l1Contract.balanceOf(l1Wallet.address)

Transfer tokens

Create and use CrossDomainMessenger (the Mantle SDK object used to bridge assets).

  1. Import the Mantle SDK.

const mantleSDK = require("@mantleio/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 mantleSDK.CrossChainMessenger({
   l1ChainId: l1ChainId,
   l2ChainId: l2ChainId,
   l1SignerOrProvider: l1Wallet,
   l2SignerOrProvider: l2Wallet,

Deposit (from Goerli to Mantle Testnet, or Ethereum to Mantle Mainnet)

  1. Give the L2 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.

await l1Contract.balanceOf(l1Wallet.address) 
await l2CustomERC20.balanceOf(l1Wallet.address)
  1. Do the actual deposit

depositTx2 = await crossChainMessenger.depositERC20(l1Contract.address, l2Addr, 1e9)
await depositTx2.wait()
  1. Wait for the deposit to be relayed.

await crossChainMessenger.waitForMessageStatus(depositTx2.hash, mantleSDK.MessageStatus.RELAYED)
  1. Check your balances on L1 and L2.

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

Withdrawal (from Mantle Mainnet to Ethereum, or Mantle Testnet to Goerli)

  1. Initiate the withdrawal on L2

withdrawalTx1 = await crossChainMessenger.withdrawERC20(l1Contract.address, 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 within 30 minutes.

await crossChainMessenger.waitForMessageStatus(withdrawalTx1.hash, mantleSDK.MessageStatus.READY_TO_PROVE)
withdrawalTx2 = await crossChainMessenger.proveMessage(withdrawalTx1.hash)
await withdrawalTx2.wait()
  1. Wait the fraud challenge period (a short period on Goerli, currently 7 days(but may be adjusted in the future which can be checked here) on the production network) and then finish the withdrawal.

await crossChainMessenger.waitForMessageStatus(withdrawalTx1.hash, mantleSDK.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 l2CustomERC20.balanceOf(l1Wallet.address)

Use Factory Contract to Deploy L2StandardERC20

The token contracts deployed using Factory contract are standard IL2StandardERC20 contracts and can only take the name "L2StandardERC20". If you want to use a different name and methods, you can inherit the L2StandardERC20 contract and deploy is as a custom contract.

  1. Call the createStandardL2Token method of the L2StandardTokenFactory contract by passing the l1Token, name, symbol, and decimal parameters. The mainnet factory contract is deployed at: 0x4200000000000000000000000000000000000012

  1. The corresponding contract address will be generated upon a successful call. The creation transaction can be found under the internal transaction tab on the Explorer. Examples:

Internal transactions on mainnet can take upto ~12 minutes before they show up on the Explorer.

  1. The contract verification status, contract code, and the relevant parameters can also be found on the Explorer. Example:

Last updated