How to Create an EVM-Compatible Bridge on Mantle
05/14/2316 min read
by Mantle
Developers
Training
Tutorials

What is a Bridge?
An EVM-compatible bridge is a blockchain interoperability solution that allows two or more blockchain networks to communicate, and transfer assets and data between each other. The term “EVM-compatible” refers to the fact that the bridge is designed to work with blockchain networks that are compatible with the Ethereum Virtual Machine (EVM), which is the runtime environment that executes smart contracts on the Ethereum network.
By being EVM-compatible, the bridge can easily interact with other EVM-compatible networks without the need for significant changes to the underlying code.
An EVM-compatible bridge typically consists of two components: a smart contract on the source network and a corresponding smart contract on the destination network. The source network smart contract holds the assets being transferred, while the destination network smart contract creates a corresponding representation of the assets on the destination network.
When a user wants to transfer assets from the source network to the destination network, they initiate the transfer by sending a transaction to the source network smart contract. The smart contract verifies that the user has sufficient balance to make the transfer, and emits an event indicating that the transfer has been initiated.
The event is then monitored by a network of validators, who are responsible for verifying the transaction and ensuring that the assets are transferred correctly. Once the validators have verified the transaction, the destination network smart contract creates a corresponding representation of the assets on the destination network, and the assets become available to the user on the destination network.
In this tutorial we will walk you through writing and deploying smart contracts required to bridge tokens between Ethereum Testnet and Mantle Testnet using Hardhat — a development environment to compile, deploy, test, and debug smart contracts. It helps developers create dApps locally before deploying them to a live chain.
Prerequisites
Before you begin developing the smart contracts, please ensure that you have completed this step::
Install both Node.js (>14) and npm on your local machine. To check your Node version, run the following command in your terminal:
node -v
Step 1: Create a Node Project
To create a new node project, navigate to your command line and type the following:
mkdir my-bridge
cd my-bridge
npm init -y
Step 2: Create a Hardhat Project
In your terminal, run the following commands:
npm install --save-dev hardhat
npx hardhat
You should then see a welcome message and options on what you can do. Use your “arrow keys” to navigate the small menu and select “Create a JavaScript Project” by clicking “Enter”.
Step 3: Install OpenZeppelin Contracts Package
We now have our hardhat development environment successfully configured. Let us now install the OpenZeppelin contracts package. We will import this package later in our smart contracts.
npm install @openzeppelin/contracts
Step 4: Create Smart contracts
To better understand how our bridge will work, here is a two step breakdown.
- When the bridge wallet receives tokens from the Ethereum side of the bridge, it should mint new tokens on the Mantle side and transfer to the same account.
- When the bridge wallet receives tokens from the Mantle side of the bridge, it should burn those tokens and transfer back the same amount of tokens from the bridge wallet to the same account.
We can create the contract files inside the contracts folder. I have created two .solidity files: ERC20Token.sol and ERC20BurnableToken.sol.
First contract is a standard ERC20 token for the Ethereum side of the bridge. It mints all tokens once it’s deployed. I named this contract TokenEthTestnet with the symbol TET.
The second one is an ERC20 and ERC20Burnable token for the Mantle side of the bridge. I named this token ‘TokenMantleTestnet’ with the symbol ‘TMT’. Instead of using the default mint() and burnFrom() methods, I have overridden them to add the onlyBridge modifier. Only the bridge will be able to call these methods this way.
ERC20 Token on Ethereum Testnet
pragma solidity ^0.8.0;
import "hardhat/console.sol";
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract TokenEthTestnet is ERC20 {
// create the token passing the name and symbol
constructor(
uint256 _initialSupply
) ERC20("TokenEthTestnet", "TET") {
// mint all tokens and send them to the deployer's wallet
_mint(msg.sender, _initialSupply * (10**uint256(10)));
console.log("Tokens are successfully minted %s", _initialSupply);
console.log("Contract deployed! Tokens sent to %s", msg.sender);
}
}
ERC20 Token on Mantle Testnet
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "hardhat/console.sol";
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import {ERC20Burnable} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Burnable.sol";
contract TokenMantleTestnet is ERC20, ERC20Burnable {
address bridge;
constructor(address _bridge) ERC20("TokenMantleTestnet", "TMT") {
bridge = _bridge;
}
modifier onlyBridge() {
require(
bridge == msg.sender,
"TokenMantleTestnet: only bridge can call this method"
);
_;
}
function mint(address _recipient, uint256 _amount)
public
virtual
onlyBridge
{
_mint(_recipient, _amount);
console.log("Tokens minted for %s", _recipient);
}
function burnFrom(address _account, uint256 _amount)
public
virtual
override(ERC20Burnable)
onlyBridge
{
super.burnFrom(_account, _amount);
console.log("Tokens burned successfully from %s", _account);
}
}
Step 5: Connect MetaMask & Mantle to your Project
Now that we’ve created a smart contract, it’s time to connect with Mantle Testnet.
Every transaction sent from your virtual wallet requires a signature using your unique private key. To provide our program with this permission, we can safely store our private key in an environmental file.This way whenever we push our code to GitHub or any other open source platforms, there wouldn’t be a risk on your accounts by vulnerable private keys.
In order to use the env file, Install the dotenv package in your project directory by running.
npm install dotenv --save
Then, create a .env file in the root directory (Main Folder) of your project, and add your MetaMask private key.
Note: The file name should be “.env” only, not “xyz.env” or “abc.env”
Your .env should look like this:
DEPLOY_ACC_KEY = 0x”your exported private key”
Note: Make sure to replace your exported private key with “your exported private key”.
Step 6: Update hardhat.config.js
We’ve added several dependencies and plugins so far. Now we need to update hardhat.config.js, so that your project knows about all of the configurations to deploy the contract.
Replace the contents of hardhat.config.js with the following:
require("@nomiclabs/hardhat-toolbox")
//load env file
require("dotenv").config()
module.exports = {
solidity: '0.8.0',
networks: {
origin: {
url: "https://goerli.infura.io/v3/"[Your Project Key]"",
accounts: [process.env.DEPLOY_ACC_KEY],
},
"mantle-testnet": {
url: "https://rpc.testnet.mantle.xyz/",
accounts: [process.env.DEPLOY_ACC_KEY],
},
}
To deploy on Ethereum testnet, you will need to create an account on Infura, and create an endpoint on the testnet of your choice. Once you create a new project, go to the dashboard and remember to replace the “[Your Project Key]” with your personal project key.
Step 7: Write the Deployment Script
Now that your contract and configuration file is done, it’s time to write the contract deploy script.
Navigate to the scripts/ folder and create two deployment scripts. To deploy the contracts, I created two scripts: deploy_origin.js and deploy_destination.js. Both of these files are almost identical, the only difference being that we need to pass ‘Token name’ and ‘symbol’ in our second deployment script, deploy_destination.js to the constructor.
deploy_origin.js
const main = async () => {
const [deployer] = await hre.ethers.getSigners()
const accountBalance = await deployer.getBalance()
console.log('Deploying contracts with account: ', deployer.address)
console.log('Account balance: ', accountBalance.toString())
let contractFactory = await hre.ethers.getContractFactory('ERC20Token')
let contract = await contractFactory.deploy(
'ERC20Token',
'ERCT',
1000000
)
await contract.deployed()
console.log(
'contract TokenMantleTestnet deployed to address: ',
contract.address
)
}
const runMain = async () => {
try {
await main()
process.exit(0)
} catch (error) {
console.error(error)
process.exit(1)
}
}
runMain()
Deploy_destination.js
require('dotenv').config()
const main = async () => {
const [deployer] = (await hre.ethers.getSigners())
const accountBalance = (await deployer.getBalance())
console.log('Deploying contracts with the account: ', deployer.address)
console.log('Account balance: ', accountBalance.toString())
let contractFactory = await hre.ethers.getContractFactory(
'TokenMantleTestnet'
)
let contract = await contractFactory.deploy(process.env.BRIDGE_WALLET)
await contract.deployed()
console.log(
'contract TokenMantleTestnet deployed to address: ',
contract.address
)
}
const runMain = async () => {
try {
await main()
process.exit(0)
} catch (error) {
console.error(error)
process.exit(1)
}
}
runMain()
Step 8: Deploy the Contract
We’re finally ready to deploy our smart contract! Navigate back to the root of your project directory, and in the command line run:
npx hardhat run ./scripts/deployOrigin.js --network origin
npx hardhat run ./scripts/deployDestination.js --network mantle-testnet
Once the contracts are successfully compiled and deployed, you should be able to see a similar message in the terminal. It should provide the details like the account that’s used to deploy the contract, and the on-chain smart contract address.
Step 9: Confirm the contract deployment
Now that our contract is deployed, we can go to Mantle Explorer and check if our contract was deployed successfully or not. Paste your smart contract address in the search box and you will get the details about the same.
As you can see our contract was successfully deployed as shown below.
Conclusion:
In this tutorial we have created and deployed smart contracts required to bridge assets between Ethereum Testnet and Mantle Testnet. Once you have deployed these contracts, you can use this as a foundation to build a dApp with frontend and backend to execute the full functionality of a bridge.