Bridge tokens via Arbitrum's standard ERC20 gateway
In this how-to you’ll learn how to bridge your own token between Ethereum (Layer 1 or L1) and Arbitrum (Layer 2 or L2), using Arbitrum’s standard ERC20 gateway. For alternative ways of bridging tokens, don’t forget to check out this overview.
Familiarity with Arbitrum’s token bridge system, smart contracts, and blockchain development is expected. If you’re new to blockchain development, consider reviewing our Quickstart: Build a dApp with Arbitrum (Solidity, Hardhat) before proceeding. We will use Arbitrum’s SDK throughout this how-to, although no prior knowledge is required.
We will go through all steps involved in the process. However, if you want to jump straight to the code, we have created this script in our tutorials repository that encapsulates the entire process.
Step 1: Create a token and deploy it on L1
We‘ll begin the process by creating and deploying on L1 a sample token to bridge. If you already have a token contract on L1, you don’t need to perform this step.
We first create a standard ERC20 contract using OpenZeppelin’s implementation. We make only 1 adjustment to that implementation, for simplicity, although it is not required: we specify an initialSupply to be pre-minted and sent to the deployer address upon creation.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract DappToken is ERC20 {
/**
* @dev See {ERC20-constructor}.
*
* An initial supply amount is passed, which is preminted to the deployer.
*/
constructor(uint256 _initialSupply) ERC20("Dapp Token", "DAPP") {
_mint(msg.sender, _initialSupply * 10 ** decimals());
}
}
We now deploy that token to L1.
const { ethers } = require('hardhat');
const { providers, Wallet } = require('ethers');
require('dotenv').config();
const walletPrivateKey = process.env.DEVNET_PRIVKEY;
const l1Provider = new providers.JsonRpcProvider(process.env.L1RPC);
const l1Wallet = new Wallet(walletPrivateKey, l1Provider);
/**
* For the purpose of our tests, here we deploy an standard ERC20 token (DappToken) to L1
* It sends its deployer (us) the initial supply of 1000
*/
const main = async () => {
console.log('Deploying the test DappToken to L1:');
const L1DappToken = await (await ethers.getContractFactory('DappToken')).connect(l1Wallet);
const l1DappToken = await L1DappToken.deploy(1000);
await l1DappToken.deployed();
console.log(`DappToken is deployed to L1 at ${l1DappToken.address}`);
/**
* Get the deployer token balance
*/
const tokenBalance = await l1DappToken.balanceOf(l1Wallet.address);
console.log(`Initial token balance of deployer: ${tokenBalance}`);
};
main()
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});
Step 2: Identify the bridge contracts to call (concepts summary)
As stated in the token bridge conceptual page, when using Arbitrum’s standard ERC20 gateway, you don’t need to do any pre-configuration process. Your token will be “bridgeable” out of the box.
As explained in the conceptual page, there are 2 contracts that we need to be aware of when bridging tokens:
- Router contract: this is the contract that we’ll interact with. It keeps a mapping of the gateway contracts assigned to each token, fallbacking to a default gateway for standard ERC20 tokens.
- Gateway contract: this is the contract that escrows or burns the tokens in the layer of origin, and sends the message over to the counterpart layer to mint or release the tokens there.
For simplicity, in this how-to we’ll focus on the first case: bridging from Ethereum (L1) to Arbitrum (L2).
We’ll explain below what specific contracts and methods need to be called to bridge your token, but you can abstract this whole process of finding the right addresses by using Arbitrum’s SDK. You can use the deposit function of the Erc20Bridger class to bridge your tokens, which will use the appropriate router contract based on the network you’re connected to, and will relay the request to the appropriate gateway contract. You can also use the function getParentGatewayAddress to get the address of the gateway contract that’s going to be used. But don’t worry about any of this yet, we’ll use those functions in the next steps.
Now, here’s an explanation of the contracts and methods that need to be called to manually bridge your token:
- When bridging from Ethereum (L1) to Arbitrum (L2), you’ll need to interact with the
L1GatewayRoutercontract, by calling theoutboundTransferCustomRefundmethod. This router contract will relay your request to the appropriate gateway contract, in this case, theL1ERC20Gatewaycontract. To get the address of the gateway contract that’s going to be used, you can call thegetGatewayfunction in theL1GatewayRoutercontract. - When bridging from Arbitrum (L2) to Ethereum (L1), you’ll need to interact with the
L2GatewayRoutercontract, by calling theoutBoundTransfermethod. This router contract will relay your request to the appropriate gateway contract, in this case, theL2ERC20Gatewaycontract. To get the address of the gateway contract that’s going to be used, you can call thegetGatewayfunction in theL2GatewayRoutercontract.
You can find the addresses of the contracts involved in the process in this page.