Skip to main content

Bridging ETH with the Mantle SDK

info

If you want to test the functionality of the Mantle SDK, we recommend using the testnet environment instead of the mainnet environment. The challenge period will be much shorter in the testnet (~40 minutes) than in the mainnet (7 days).

This tutorial guides you on using the Mantle SDK to transfer ETH between layer 1 (L1) and layer 2 (L2). Check the complete code here.

Setup

  1. Ensure that your computer has the following installed:

  2. Start L1 and L2 environments. Currently, we support the local environment or the testnet environment. If you want to deploy your own L1 and L2, please follow the instructions below.

    git clone https://github.com/mantlenetworkio/mantle-v2.git
    cd mantle/ops
    make up
    # check status
    make ps

    We highly recommend using the testnet environment, which means you can skip this step, you can apply your own L1 RPC here and replace the L1 RPC URL in the .env file.

  3. Clone this repository and navigate to it.

    git clone https://github.com/mantlenetworkio/mantle-tutorial.git
    cd mantle-tutorial/cross-dom-bridge-eth
  4. Install the necessary packages.

    yarn

Run the Sample Code

The sample code is in index.js, the whole execution flow will automatically start after running it.

Node Environment

If you want to test by using your own nodes, you should configure the missing or changing environment variables in file .env.local.tmp then use yarn local to execute index.js.

If you want to have a test in our testnet network you should do the same for .env.testnet.tmp and then use yarn testnet to execute index.js.

How Does It Work?

Import the Necessary Libraries

const ethers = require('ethers');
const mantleSDK = require('@mantleio/sdk');

In this tutorial, we initialize the required libraries:

  • ethers: A JavaScript library for interacting with the Ethereum blockchain. It provides an easy-to-use interface for tasks like creating wallets, sending transactions, and interacting with smart contracts.

  • mantleSDK: The Mantleio SDK, which facilitates cross-chain transactions between Layer 1 (L1) and Layer 2 (L2) blockchains. It abstracts away complexities, making it easier to perform operations like depositing and withdrawing assets.

Network Configuration and Wallet Setup

Next, the code defines some configuration parameters. We fetch the specified network and wallet configurations from the .env file.

const key = process.env.PRIV_KEY;
const l2ETH = process.env.L2_ETH;
  • key: The private key retrieved from the environment variables.

  • l2ETH: The address of the L2 ETH token.

// Global variable because we need them almost everywhere
let crossChainMessenger;
let addr; // Our address
  • crossChainMessenger: A global variable initialized later in the setup function, representing the Mantle SDK's CrossChainMessenger object.

  • addr: A variable that will store the user's address.

Then create wallet objects by passing the private key and RPC addresses as parameters for L1 and L2.

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);

Setup CrossChainMessenger Object

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.

const setup = async () => {
addr = l1Wallet.address
crossChainMessenger = new mantleSDK.CrossChainMessenger({
l1ChainId: process.env.L1_CHAINID,
l2ChainId: process.env.L2_CHAINID,
l1SignerOrProvider: l1Wallet,
l2SignerOrProvider: l2Wallet,
bedrock: true,
})
}
......

Report the Balances

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.

const reportBalances = async () => {
const l1Balance = await crossChainMessenger.l1Signer.getBalance();
const ETH = new ethers.Contract(l2ETH, erc20ABI, l2Wallet);
const l2Balance = await ETH.balanceOf(
crossChainMessenger.l2Signer.getAddress(),
);

console.log(`On L1:${l1Balance} On L2:${l2Balance} `);
};

Deposit

The depositETH function deposits 0.01 ETH token to L2 via the Mantle bridge. The asynchronous function prints out the transaction hash and waits for the message to get relayed. Finally, we display the updated ETH balance on L1 and L2.

To show that the deposit actually happened we show before and after balances.

console.log('Deposit ETH');
await reportBalances();

crossChainMessenger.depositETH() creates and sends the deposit transaction on L1.

const start = new Date();

const response = await crossChainMessenger.depositETH(eth);

Of course, it takes time for the transaction to actually be processed on L1.

console.log(`Transaction hash (on L1): ${response.hash}`);
await response.wait();

After the transaction is processed on L1 it needs to be picked up by an off-chain service and relayed to L2. To show that the deposit actually happened we need to wait until the message is relayed.

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,
);

Once the message is relayed the balance change on L2 is practically instantaneous. We can just report the balances and see that the L2 balance rose by 0.01 ETH.

await reportBalances();
console.log(`depositETH took ${(new Date() - start) / 1000} seconds\n\n`);

Withdraw

This function shows how to withdraw ETH from L2 to L1.

To show that the withdrawal actually happened we show before and after balances.

console.log('#################### Withdraw ETH ####################');
await reportBalances();

We need to make sure the allowance is approved. We can do that by sending an approval transaction on L1.

const approve = await crossChainMessenger.approveERC20(
ethers.constants.AddressZero,
l2ETH,
doubleeth,
{signer: l2Wallet, gasLimit: 300000},
);
console.log(`Approve transaction hash (on L2): ${approve.hash}`);

Then we withdraw the token from L2.

const response = await crossChainMessenger.withdrawERC20(
ethers.constants.AddressZero,
l2ETH,
eth,
{gasLimit: 300000},
);
console.log(`Transaction hash (on L2): ${response.hash}`);
await response.wait();

We need to wait until the message is ready to prove.

console.log('Waiting for status to be READY_TO_PROVE');
console.log(`Time so far ${(new Date() - start) / 1000} seconds`);
await crossChainMessenger.waitForMessageStatus(
response.hash,
mantleSDK.MessageStatus.READY_TO_PROVE,
);

Wait until the state that includes the transaction gets past the challenge period, at which time we can finalize (also known as claim) the transaction. (Make sure your service and network are running well)

console.log(`Time so far ${(new Date() - start) / 1000} seconds`);
await crossChainMessenger.proveMessage(response.hash);

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,
mantleSDK.MessageStatus.IN_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,
);

Finalizing the message also takes a bit of time.

console.log('Ready for relay, finalizing message now');
console.log(`Time so far ${(new Date() - start) / 1000} seconds`);
await crossChainMessenger.finalizeMessage(response.hash);

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,
);

main

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

const main = async () => {
await setup();
await depositETH();
await withdrawETH();
};

main()
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});

Conclusion

You should now be able to write applications using our SDK and bridge to transfer ETH between L1 and L2.