Bridging $MNT using Mantle SDK

Deposit and withdraw $MNT using the SDK

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

This tutorial demonstrates how to use the Mantle SDK to deposit and withdraw $MNT tokens between Mantle and Ethereum.

Set up local environment

Make sure you have the following tools installed in your local environment.

Let's start by fetching the example JS scripts that we’ll work with and use to make SDK invocations from the Mantle Github. Clone the repository containing the sample scripts by executing the following command in your project directory.

git clone

Next, we can use yarn to download the SDK along with all the necessary dependencies, as shown below. All the dependencies are defined in the yarn.lock file, so we can just run yarn in the ./cross-dom-bridge-mnt directory.

We'll need a .env file from where we can add and modify wallet and network settings. The main directory contains two .env files, where .env.local specifies the configuration for a local environment, while .env.testnet specifies the configuration to connect to testnet.

All the necessary contracts addresses are already included in the respective .env files, so you can specify your preferred L1 RPC endpoint and your wallet private key to start sending transactions.

# testnet ENV
# rpc url
L1_RPC= # L1 RPC Endpoint

# chain id

# bridge address

# crossDomainMessenger address 

# token address

# local test key 
PRIV_KEY= # Wallet private key

Let's take a look at the main script.

Analyzing and modifying the sample script

The index.js script containing the code we need is located in the ./mantle-tutorial/cross-dom-bridge-eth directory. By default, it is configured to run on a local test environment. You can run L1 and L2 instances on your system and start deploying contracts to test your applications. You can make a copy of the index.js file before we start modifying it if you want to try that out.

Check out the tutorial here that demonstrates the same bridging functionality on a private network.

Importing necessary libraries

const ethers = require("ethers")
const mantleSDK = require("@mantleio/sdk")
const fs = require("fs")

This code does not need to be changed. We import three libraries, and the .env configuration file we created earlier.

  • dotenv : The .env file containing wallet and network configuration

  • ethers : The Ethers.js library comes handy with wallet and contract operations

  • @mantlenetwork/sdk : Mantle SDK instance

  • fs: File system module to read the contract ABI from a JSON file

Generating contract bytecode from ABI

const L1TestERC20 = JSON.parse(fs.readFileSync("TestERC20.json"))

We don't need to modify this either. The contents of the JSON file containing the contract ABI are stored in TestERC20.json which we will be using later.

Network configuration and wallet setup

const l1MntAddr = process.env.L1_MNT
const l2MntAddr = process.env.L2_MNT
const key = process.env.PRIV_KEY

const l1RpcProvider = new ethers.providers.JsonRpcProvider(process.env.L1_RPC)
const l2RpcProvider = new ethers.providers.JsonRpcProvider(process.env.L2_RPC)
const l1Wallet = new ethers.Wallet(key, l1RpcProvider)
const l2Wallet = new ethers.Wallet(key, l2RpcProvider)

We fetch the specified network and wallet configurations from the .env file, and create wallet objects by passing the private key and RPC addresses as parameters for L1 and L2.

CrossChainMessenger object

//Global variables
let crossChainMessenger 
let l1Mnt, l2Mnt
let ourAddr

const setup = async () => {
  ourAddr = l1Wallet.address // Assigning wallet address
  crossChainMessenger = new mantleSDK.CrossChainMessenger({ // CrossChainMessenger object instantiation
    l1ChainId: process.env.L1_CHAINID, // Assigning chain IDs from .env file
    l2ChainId: process.env.L2_CHAINID,
    l1SignerOrProvider: l1Wallet, // Wallets that will sign transactions
    l2SignerOrProvider: l2Wallet
  l1Mnt = new ethers.Contract(l1MntAddr, L1TestERC20.abi, l1Wallet) // Contract objects
  l2Mnt = new ethers.Contract(l2MntAddr, L1TestERC20.abi, l2Wallet)

The CrossChainMessenger object calls the cross chain messenger contracts on L1 and L2 to transfer assets. Here we instantiate the object with chain IDs, wallet objects, and contract objects.

Reporting balances

const reportBalances = async () => {
  const l1Balance = (await l1Mnt.balanceOf(ourAddr)).toString().slice(0, -18)
  const l2Balance = (await l2Mnt.balanceOf(ourAddr)).toString().slice(0, -18)
  console.log(`Token on L1:${l1Balance}     Token on L2:${l2Balance}`)

The reportBalances function fetches L1 and L2 wallet balances and prints them out. We'll use this method to keep track balance change after deposit and withdraw operations.

Deposit function

const depositMNT = async () => {
  console.log("#################### Deposit MNT ####################")
  await reportBalances() // 1. Print balance before deposit
  const start = new Date()
  const allowanceResponse = await crossChainMessenger.approveERC20( // 2. Approve deposit amount
    l1MntAddr, l2MntAddr, depositToken)
  await allowanceResponse.wait()
  console.log(`Time so far ${(new Date() - start) / 1000} seconds`)

  const response = await crossChainMessenger.depositERC20( // 3. Send deposit transaction
    l1MntAddr, l2MntAddr, depositToken)
  console.log(`Deposit transaction hash (on L1): ${response.hash}`) // 4. Print L1 deposit transaction hash
  await response.wait()
  console.log("Waiting for status to change to RELAYED")
  console.log(`Time so far ${(new Date() - start) / 1000} seconds`)
  await crossChainMessenger.waitForMessageStatus(response.hash, mantleSDK.MessageStatus.RELAYED)

  await reportBalances() // 5. Print updated balance after deposit
  console.log(`depositERC20 took ${(new Date() - start) / 1000} seconds\n`)

The depositMNT function deposits 1 $MNT token to L2 via the Mantle bridge. The deposit transaction is sent using the depositERC20 method, which is picked up by an off-chain service and relayed to L2. The asynchronous function prints out the transaction hash and waits for the message to get relayed. Finally, we display the updated $MNT balance on L1 and L2.

Withdraw function

const withdrawMNT = async () => {
  console.log("#################### Withdraw MNT ####################")
  const start = new Date()
  await reportBalances() // 1. Print balance before withdraw

  const response = await crossChainMessenger.withdrawERC20( // 2. Send withdraw transaction
    l1MntAddr, l2MntAddr, withdrawToken)
  console.log(`Transaction hash (on L2): ${response.hash}`) // 3. Print L2 withdraw transaction hash
  await response.wait()

  console.log("Waiting for status to change to IN_CHALLENGE_PERIOD")
  console.log(`Time so far ${(new Date() - start) / 1000} seconds`)
  await crossChainMessenger.waitForMessageStatus(response.hash, // 4. Function waits for transaction to enter challenge period
  console.log("In the challenge period, waiting for status READY_FOR_RELAY")
  console.log(`Time so far ${(new Date() - start) / 1000} seconds`)
  await crossChainMessenger.waitForMessageStatus(response.hash,
    mantleSDK.MessageStatus.READY_FOR_RELAY)  // 5. Check whether transaction is ready for relay 
  console.log("Ready for relay, finalizing message now")
  console.log(`Time so far ${(new Date() - start) / 1000} seconds`)
  await crossChainMessenger.finalizeMessage(response)
  console.log("Waiting for status to change to RELAYED")
  console.log(`Time so far ${(new Date() - start) / 1000} seconds`)
  await crossChainMessenger.waitForMessageStatus(response,
    mantleSDK.MessageStatus.RELAYED)  // 6. Wait for transaction to get relayed
  await reportBalances() // 7. Print updated balance after withdraw
  console.log(`withdrawERC20 took ${(new Date() - start) / 1000} seconds\n\n\n`)

Similarly, the withdrawMNT function withdraws 1 $MNT token from L2 via the Mantle bridge. The function prints out the transaction hash. The transaction then goes into a challenge period. Once it is ready for relay, it is picked up by an off-chain service to be relayed to L1. Finally, we display the updated $MNT balance on L1 and L2.

Invoking deposit and withdraw functions

const main = async () => {
  await setup()
  await depositMNT()
  await withdrawMNT()

main().then(() => process.exit(0))
  .catch((error) => {

We write a main() where we call the functions to perform configuration, deposit, and withdraw operations.

Running the script

Once the configuration is ready, you can run the script using the yarn testnet command. The script will automatically select the testnet configuration to perform both deposit and withdraw operations in the index.js script. If you want to run the script locally, you can run yarn local.


You can use this code to test out the token bridging mechanism via SDK on Mantle testnet and start integrating it to your applications.

Last updated