Bridge tokens via Arbitrum's standard `ERC-20` gateway
In this how-to you’ll learn how to bridge your own token between Ethereum (parent chain) and Arbitrum (child chain), 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, Remix) 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 the parent chain
We‘ll begin the process by creating and deploying on the parent chain a sample token to bridge. If you already have a token contract on the parent chain, you don’t need to perform this step.
We first create a standard ERC-20
contract using OpenZeppelin’s implementation. We make only one 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 the parent chain.
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 ERC-20
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 two 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
ERC-20
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 the parent chain (Ethereum) to the child chain (Arbitrum).
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 the parent chain (Ethereum) to the child chain (Arbitrum), you’ll need to interact with the
L1GatewayRouter
contract, by calling theoutboundTransferCustomRefund
method. This router contract will relay your request to the appropriate gateway contract, in this case, theL1ERC20Gateway
contract. To get the address of the gateway contract that’s going to be used, you can call thegetGateway
function in theL1GatewayRouter
contract. - When bridging from the child chain (Arbitrum) to the parent chain (Ethereum), you’ll need to interact with the
L2GatewayRouter
contract, by calling theoutBoundTransfer
method. This router contract will relay your request to the appropriate gateway contract, in this case, theL2ERC20Gateway
contract. To get the address of the gateway contract that’s going to be used, you can call thegetGateway
function in theL2GatewayRouter
contract.
You can find the addresses of the contracts involved in the process in this page.
Step 3: Approve token allowance for the gateway contract
The gateway contract will be the one that will transfer the tokens to be bridged over. So the next step is to allow the gateway contract to do so.
We typically do that by using the approve
method of the token, but you can use Arbitrum’s SDK to abstract this process, by calling the method approveToken
of the Erc20Bridger
class, which will call the approve method of the token passed by parameter, and set the allowance to the appropriate gateway contract.
/**
* Use l2Network to create an Arbitrum SDK Erc20Bridger instance
* We'll use Erc20Bridger for its convenience methods around transferring token to L2
*/
const l2Network = await getArbitrumNetwork(l2Provider);
const erc20Bridge = new Erc20Bridger(l2Network);
/**
* The Standard Gateway contract will ultimately be making the token transfer call; thus, that's the contract we need to approve.
* erc20Bridger.approveToken handles this approval
* Arguments required are:
* (1) l1Signer: The L1 address transferring token to L2
* (2) erc20L1Address: L1 address of the ERC20 token to be deposited to L2
*/
console.log('Approving:');
const l1Erc20Address = l1DappToken.address;
const approveTx = await erc20Bridger.approveToken({
parentSigner: l1Wallet,
erc20ParentAddress: l1Erc20Address,
});
const approveRec = await approveTx.wait();
console.log(
`You successfully allowed the Arbitrum Bridge to spend DappToken ${approveRec.transactionHash}`,
);
As mentioned before, you can also call the approve
method of the token and send as a parameter the address of the gateway contract, which you can find by calling the method getGateway
function in the router contract.
Step 4: Start the bridging process through the router contract
After allowing the gateway contract to transfer the tokens, we can now start the bridging process.
You can use Arbitrum’s SDK to abstract this process, by calling the method deposit
of the Erc20Bridger
class, which will estimate the gas parameters (maxGas
, gasPriceBid
and maxSubmissionCost
, explained below) and call the outboundTransferCustomRefund
method of the router contract. You will only need to specify the following parameters:
amount
: Amount of tokens to bridgeerc20L1Address
: Parent chain address of theERC-20
token being bridgedl1Signer
: Signer object of the account transferring the tokens, connected to the parent chain networkl2Provider
: Provider connected to the child chain network
/**
* Deposit DappToken to L2 using erc20Bridger. This will escrow funds in the Gateway contract on L1, and send a message to mint tokens on L2.
* The erc20Bridge.deposit method handles computing the necessary fees for automatic-execution of retryable tickets — maxSubmission cost & l2 gas price * gas — and will automatically forward the fees to L2 as callvalue
* Also note that since this is the first DappToken deposit onto L2, a standard Arb ERC20 contract will automatically be deployed.
* Arguments required are:
* (1) amount: The amount of tokens to be transferred to L2
* (2) erc20L1Address: L1 address of the ERC20 token to be depositted to L2
* (2) l1Signer: The L1 address transferring token to L2
* (3) l2Provider: An l2 provider
*/
const depositTx = await erc20Bridger.deposit({
amount: tokenDepositAmount,
erc20ParentAddress: l1Erc20Address,
parentSigner: l1Wallet,
childProvider: l2Provider,
});
As mentioned before, you can also call the method outboundTransferCustomRefund
manually in the router contract and specify the following parameters:
- address
_token
: Parent chain address of theERC-20
token being bridged - address
_refundTo
: Account to be credited with the excess gas refund on the child chain - address
_to
: Account to be credited with the tokens on the child chain - uint256
_amount
: Amount of tokens to bridge - uint256
_maxGas
: Max gas deducted from user’s child chain balance to cover the execution on the child chain - uint256
_gasPriceBid
: Gas price for the execution on the child chain - bytes
_data
: 2 pieces of data encoded:- uint256
maxSubmissionCost
: Max gas deducted from user's child chain balance to cover base submission fee - bytes
extraData
: “0x”
- uint256
Step 5: Wait for execution on the child chain
After calling the deposit method (or the outboundTransferCustomRefund
if you’re choosing the manual way), you’ll have to wait a bit until the message is executed on the child chain. We will verify the status of the underlying retryable ticket created to bridge the tokens. Check this page to learn more about parent-to-child chain messages, also known as retryables.
You can programmatically wait for the execution of the transaction on the child chain using Arbitrum’s SDK. You should first wait for the execution of the submission transaction (the one sent to the router contract) and then the execution of the child chain transaction.
/**
* Now we wait for L1 and L2 side of transactions to be confirmed
*/
const depositRec = await depositTx.wait();
const l2Result = await depositRec.waitForChildTransactionReceipt(l2Provider);
/**
* The `complete` boolean tells us if the l1 to l2 message was successful
*/
l2Result.complete
? console.log(`L2 message successful: status: ${L1ToL2MessageStatus[l2Result.status]}`)
: console.log(`L2 message failed: status ${L1ToL2MessageStatus[l2Result.status]}`);
If you’re going the manual way, you can verify if the message has been executed on the child chain through the Retryables Dashboard. Just paste the hash of transaction submitted to the router contract and the tool will tell you whether it’s been redeemed or not.
Step 6: Check the new token contract created on the child chain
Finally, let’s find the token contract that has been created on the child chain.
Using Arbitrum’s SDK, you can call method getChildErc20Address
of the Erc20Bridger
class, which will return the address of the token contract on the child chain that corresponds to the parent chain token contract sent as parameter.
/**
* Check if our l2Wallet DappToken balance has been updated correctly
* To do so, we use erc20Bridge to get the l2Token address and contract
*/
const l2TokenAddress = await erc20Bridger.getChildErc20Address(l1Erc20Address, l1Provider);
const l2Token = erc20Bridger.getChildTokenContract(l2Provider, l2TokenAddress);
To do this operation manually, you can call method calculateL2TokenAddress
of the router contract.
If you visit that address in Arbiscan, you’ll notice that it is a copy of the contract StandardArbERC20
. This is the standard contract that is automatically created the first time a token that doesn’t exist in Arbitrum is bridged. The token bridge conceptual page has more information about this contract.
Conclusion
After finishing this process, you’ll now have a counterpart token contract automatically created on the child chain. You can bridge tokens between parent and child chain using the original token contract on the parent chain and the standard created contract on the child chain, along with the router and gateway contracts from each layer.