Comment on page

Bridging $MNT using Mantle SDK

Deposit and withdraw $MNT using the SDK
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.