Programmatically Create a Canonical Interchain Token Using the Interchain Token Service
If you have an ERC-20 token on one or more blockchains and you want to make the token interoperable across chains, the Interchain Token Service provides a solution. You can transform an ERC-20 token into an Interchain Token by deploying a token manager.
If you would like to create a wrapped, bridgeable version of your ERC-20 token on other chains, you can register it as a Canonical Interchain Token using the InterchainTokenFactory
contract.
Each token can only be registered once as a Canonical Interchain Token. This ensures unique and streamlined token management across different blockchains. Though you can register your Canonical Interchain Token directly through the Interchain Token Portal, there are times where you may want to do so programmatically, such as when you have already deployed a token on one chain and wish to deploy a wrapped version of that token on another chain.
In this tutorial, you will learn how to:
- Programmatically create a Canonical Interchain Token from scratch using Axelar’s Interchain Token Service
- Register a Canonical Interchain Token on the BSC chain
- Deploy remote Canonical Interchain Token on the Avalanche Fuji chain
- Transfer your token between BSC and Avalanche Fuji
Prerequisites
You will need:
- A basic understanding of Solidity and JavaScript
-
- A MetaMask wallet with tBNB and Avax funds for testing. If you don’t have these funds, you can get tBNB from the BSC faucet and Avax from the Avalanche Fuji faucets (1, 2).
Deploy an ERC-20 token on the BSC testnet
Create a Simple ERC-20 token and give it a name and symbol. You can skip this step if you already have an ERC-20 token deployed on the BSC testnet.
Set up your development environment
Create and initialize a project
Open up your terminal and navigate to any directory of your choice. Run the following commands to create and initiate a project:
mkdir canonical-interchain-token-project && cd canonical-interchain-token-projectnpm init -y
Install Hardhat and the AxelarJS SDK
Install Hardhat and the AxelarJS SDK with the following commands:
npm install --save-dev hardhat@2.18.1 dotenv@16.3.1npm install @axelar-network/axelarjs-sdk@0.13.9 crypto@1.0.1 @nomicfoundation/hardhat-toolbox@2.0.2
Set up project ABIs
Next, set up the ABIs for the Interchain Token Service, Interchain Token Factory, and the contract from the token you deployed.
Create a folder named utils
. Inside the folder, create the following new files and add the respective ABIs:
- Add the Interchain Token Service ABI to
interchainTokenServiceABI.json
. - Add the Interchain Token Factory ABI to
interchainTokenFactoryABI.json
. - Add your custom token ABI to
customTokenABI.json
. You can get this from BSC Scan with the address of your deployed token.
Set up an RPC for the local chain
Back in the root directory, set up an RPC for the BSC testnet. You will use this as your local (source) chain.
Create an .env
file
To make sure you’re not accidentally publishing your private key, create an .env
file to store it in:
touch .env
Add your private key to .env
and hardhat.config.js
Export your private key and add it to the .env
file you just created:
PRIVATE_KEY= // Add your account private key here
💡
If you will push this project on GitHub, create a .gitignore
file and include .env
.
Then, create a file with the name hardhat.config.js
and add the following code snippet:
require("@nomicfoundation/hardhat-toolbox");require("dotenv").config({ path: ".env" });
const PRIVATE_KEY = process.env.PRIVATE_KEY;
/** @type import('hardhat/config').HardhatUserConfig */module.exports = { solidity: "0.8.18", networks: { bsc: { url: "https://bsc-testnet.drpc.org", chainId: 97, accounts: [PRIVATE_KEY], }, },};
Register a Canonical Interchain Token on a local chain
Now that you have set up an RPC for the BSC testnet, you can register a Canonical Interchain Token.
Create a canonicalInterchainToken.js
script
Create a new file named canonicalInterchainToken.js
and import the required dependencies:
Ethers.js
- The AxelarJS SDK
- The custom token contract ABI
- The address of the
InterchainTokenService
contract - The address of the
InterchainTokenFactory
contract
const hre = require("hardhat");const crypto = require("crypto");const { AxelarQueryAPI, Environment, EvmChain, GasToken,} = require("@axelar-network/axelarjs-sdk");
const interchainTokenServiceContractABI = require("./utils/interchainTokenServiceABI");const interchainTokenFactoryContractABI = require("./utils/interchainTokenFactoryABI");const customTokenContractABI = require("./utils/customTokenABI");
const MINT_BURN = 0;const LOCK_UNLOCK = 2;
// Addresses on mainnet/testnetconst interchainTokenServiceContractAddress = "0xB5FB4BE02232B1bBA4dC8f81dc24C26980dE9e3C";const interchainTokenFactoryContractAddress = "0x83a93500d23Fbc3e82B410aD07A6a9F7A0670D66";const customTokenAddress = "0x0EF6280417A1BF22c8fF05b54D7A7928a173E605"; // your token address
Get the signer
Next, create a getSigner()
function in canonicalInterchainToken.js
. This will obtain a signer for a secure transaction:
//...
async function getSigner() { const [signer] = await ethers.getSigners(); return signer;}
Get the contract instance
Then, create a getContractInstance()
function in canonicalInterchainToken.js
. This will get the contract instance based on the contract’s address, ABI, and signer:
//...
async function getContractInstance(contractAddress, contractABI, signer) { return new ethers.Contract(contractAddress, contractABI, signer);}
Register Canonical Interchain Token
Now you’re ready to register your token as a Canonical Interchain Token! Create a registerCanonicalInterchainToken()
function for the BSC testnet. This will register a Canonical Interchain Token with your custom token address:
// Register Canonical Interchain Token to the BSC chain.async function registerCanonicalInterchainToken() { // Get a signer to sign the transaction const signer = await getSigner();
// Create contract instances const interchainTokenFactoryContract = await getContractInstance( interchainTokenFactoryContractAddress, interchainTokenFactoryContractABI, signer, ); const interchainTokenServiceContract = await getContractInstance( interchainTokenServiceContractAddress, interchainTokenServiceContractABI, signer, );
// Register a new Canonical Interchain Token const deployTxData = await interchainTokenFactoryContract.registerCanonicalInterchainToken( customTokenAddress, // Your token address );
// Retrieve the token ID of the newly registered token const tokenId = await interchainTokenFactoryContract.canonicalInterchainTokenId( customTokenAddress, );
const expectedTokenManagerAddress = await interchainTokenServiceContract.tokenManagerAddress(tokenId);
console.log( ` Transaction Hash: ${deployTxData.hash}, Token ID: ${tokenId}, Expected Token Manager Address: ${expectedTokenManagerAddress}, `, );}
Add a main()
function
Add a main()
function to execute the canonicalInterchainToken.js
script. It will handle any errors that may arise:
//...
async function main() { const functionName = process.env.FUNCTION_NAME; switch (functionName) { case "registerCanonicalInterchainToken": await registerCanonicalInterchainToken(); break; default: console.error(`Unknown function: ${functionName}`); process.exitCode = 1; return; }}
main().catch((error) => { console.error(error); process.exitCode = 1;});
Run the canonicalInterchainToken.js
script to deploy to BSC
Run the script in your terminal to register and deploy the token, specifying the BSC
testnet:
FUNCTION_NAME=registerCanonicalInterchainToken npx hardhat run canonicalInterchainToken.js --network bsc
If you see something similar to the following on your console, you have successfully registered your token as a Canonical Interchain Token.
Transaction Hash: 0x4ba22486dda650578ed782b8fd4e9a394664e9b4dcb8c2cbd99403543a749325,Token ID: 0x90d04b71cd22ebcf0821ff59dd15d10865bf927ed36f0ad6b8c4a8686f0ffe58,Expected Token Manager Address: 0xEb910958C32fEB6D85cd1f7c04216BD354834D86,
Store the token ID
Copy the token ID and store it somewhere safe. You will need it to initiate a remote token transfer in a later step.
Check the transaction on the BSC testnet scanner
Check the BSC testnet scanner to see if you have successfully registered your token as a Canonical Interchain Token.
Deploy Remote Canonical Interchain Token
You’ve just successfully a Canonical Interchain Token to BSC, which you are using as your local chain. Now, deploy the token remotely to Avalanche Fuji, which will be the remote chain in this tutorial. Remember that you can specify any two chains to be your local and remote chains.
Estimate gas fees
In canonicalInterchainToken.js
, call estimateGasFee()
from the AxelarJS SDK to estimate the actual cost of deploying your remote Canonical Interchain Token on a remote chain:
//...
const api = new AxelarQueryAPI({ environment: Environment.TESTNET });
// Estimate gas costs.async function gasEstimator() { const gas = await api.estimateGasFee( EvmChain.BINANCE, EvmChain.AVALANCHE, GasToken.ETH, 700000, 1.1, );
return gas;}
//...
Perform remote token deployment
Create a deployRemoteCanonicalInterchainToken()
function that will perform token remote canonical deployment on the Avalanche Fuji testnet.
//...
// deployRemoteCanonicalInterchainToken: On Avalanche Fujiasync function deployRemoteCanonicalInterchainToken() { // Get a signer for authorizing transactions const signer = await getSigner();
// Get contract for remote deployment const interchainTokenFactoryContract = await getContractInstance( interchainTokenFactoryContractAddress, interchainTokenFactoryContractABI, signer, );
// Estimate gas fees const gasAmount = await gasEstimator();
// Initiate transaction const txn = await interchainTokenFactoryContract[ "deployRemoteCanonicalInterchainToken(address,string,uint256)" ](customTokenAddress, "Avalanche", gasAmount, { value: gasAmount });
console.log(`Transaction Hash: ${txn.hash}`);}
//...
Update main()
to deploy to remote chains
Update main()
to execute deployRemoteCanonicalInterchainToken()
:
//...
async function main() { const functionName = process.env.FUNCTION_NAME; switch (functionName) { //... case "deployRemoteCanonicalInterchainToken": await deployRemoteCanonicalInterchainToken(); break; default: //... }}
//...
Run the canonicalInterchainToken.js
script to deploy to Avalanche Fuji testnet
Run the script in your terminal to to deploy remote Canonical Interchain Token, once again specifying the BSC
testnet (the source chain where all transactions are taking place):
FUNCTION_NAME=deployRemoteCanonicalInterchainToken npx hardhat run canonicalInterchainToken.js --network bsc
You should see something similar to the following on your console:
Transaction Hash: 0x3c36b04418aca90393435dbe9c2b65d26acc6e9f86aba1f34db27b41d82fda13
Check the transaction on the Axelar testnet scanner
Check the Axelarscan testnet scanner to see if you have successfully deployed the remote Canonical Interchain Token “HM” on the Avalanche Fuji testnet. It should look something like this. Make sure that Axelar shows a successful transaction before continuing on to the next step.
Transfer your token between chains
Now that you have registered and deployed a Canonical Interchain Token both locally to BSC and remotely to Avalanche Fuji, you can transfer between those two chains via the interchainTransfer()
method.
Initiate a remote token transfer
In canonicalInterchainToken.js
, create a transferTokens()
function that will facilitate remote token transfers between chains. Change the token ID to the tokenId
that you saved from an earlier step, and change the address in transfer
to your own wallet address:
//...
// Transfer token between chains.async function transferTokens() { const signer = await getSigner();
const interchainTokenServiceContract = await getContractInstance( interchainTokenServiceContractAddress, interchainTokenServiceContractABI, signer, );
const customTokenContract = await getContractInstance( customTokenAddress, customTokenContractABI, signer, ); // Approve ITS to spend tokens
await customTokenContract.approve( interchainTokenServiceContractAddress, ethers.utils.parseEther("50"), );
const gasAmount = await gasEstimator();
// Send via token const transfer = await interchainTokenServiceContract.interchainTransfer( "0x90d04b71cd22ebcf0821ff59dd15d10865bf927ed36f0ad6b8c4a8686f0ffe58", // token ID from registerCanonicalInterchainToken "Avalanche", // Destination chain "0x510e5EA32386B7C48C4DEEAC80e86859b5e2416C", // Destination address. Update with your own wallet address ethers.utils.parseEther("50"), // Amount to transfer "0x00", gasAmount, // gasValue { value: gasAmount }, );
console.log("Transfer Transaction Hash:", transfer.hash);}
Update main()
to execute token transfer
Update the main()
to execute transferTokens()
:
//...
async function main() { const functionName = process.env.FUNCTION_NAME; switch (functionName) { //... case "transferTokens": await transferTokens(); break; default: //... }}
//...
Run the canonicalInterchainToken.js
script to transfer tokens
Run the script in your terminal, specifying the BSC
testnet:
FUNCTION_NAME=transferTokens npx hardhat run canonicalInterchainToken.js --network bsc
You should see something similar to the following on your console:
Transfer Transaction Hash: 0xd46a1941e04d06084eb7df3b1933617eccfb21a77dbcfb3b0399e1ac1c0ee6c9
If you see this, it means that your interchain transfer has been successful! 🎉
💡
Note: If you get the following nonce too low
error, wait a few minutes
and run canonicalInterchainToken.js
again. Some chains have a longer
transaction time than others.
reason: 'nonce has already been used',code: 'NONCE_EXPIRED',error: ProviderError: nonce too low
Check the transaction on the Axelar testnet scanner
Check the Axelarscan testnet scanner to see if you have successfully transferred HM from the BSC testnet to the Avalanche Fuji testnet. It should look something like this.
Import the new token into your wallet
You can also import the new token into your own wallet with its contract address that you saved from BSC Scan.
Congratulations!
You have now programmatically created a Canonical Interchain Token using Axelar’s Interchain Token Service and transferred it between two chains. You should now be able to confidently create and manage your own Interchain Tokens, opening up a wide range of possibilities for token transfers and asset bridges.
Great job making it this far! To show your support to the author of this tutorial, please post about your experience and tag @axelarnetwork on Twitter (X).
What’s next
For further examples utilizing the Interchain Token Service, check out the following in the axelar-examples
repo on GitHub:
its-custom-token
— Demonstrates how to use the ITS with a custom token implementation.its-interchain-token
— Demonstrates how to deploy Interchain Tokens that are connected across EVM chains and how to send some tokens across.its-canonical-token
- Demonstrates how to deploy canonical Interchain Token and send cross-chain transfer for these tokens.its-lock-unlock-fee
Demonstrates how to deploy deploy interchain token with lock/unlock_fee token manager type.its-executable
Demonstrates how to deploy interchain token and send a cross-chain transfer along with a message.its-mint-burn-from
Demonstrates how to deploy interchain token with usesburnFrom()
on token being bridged rather thanburn()
.