Developers
Ecosystem
About
July 15, 2022

Axelar Code: Build a DApp that Calls a Contract and Transfers Tokens on Chain B from Chain A

author
Axelar Team

In this tutorial, we will show you how to use Axelar services and functions to build a dApp that calls a contract on chain B from chain A and transfer tokens, for example to purchase an NFT, to send an airdrop, or to distribute tokens from a DAO.

technology

In this tutorial, we will show you how to use Axelar services and functions to build a dApp that calls a contract on chain B from chain A and transfer tokens, for example to purchase an NFT, to send an airdrop, or to distribute tokens from a DAO. The contract on chain B can do anything, as long as it implements the IAxelarExecutable interface.

1. Install nodejs.

The first thing you’ll want to do is to install Node if you don’t have it already.

Run node -v  to check your installation. It should show something like this:

$ node -v
v17.8.0

2. Clone the axelar-local-gmp-examples repo.

We have created a library of example apps — go ahead and clone it onto your computer.

git clone <https://github.com/axelarnetwork/axelar-local-gmp-examples.git>

3. Build contracts and tests.

Install all the node modules and build the contracts.

npm update && npm install
npm run build

4. Understand the call-contract-with-tokens code.

💡 The following code snippets are for explanatory purposes only. They are incomplete and should not be directly copied. Instead, follow the instructions to clone the GitHub and run the code from there.

For this particular tutorial, we will be using the example application in the examples/call-contract-with-tokens folder.

cd examples/call-contract-with-tokens

This folder should contain two files, an index.js and an DistributionExecutable.sol.

Let’s take a look at our index.js. This is the “frontend” to our dApp and contains two functions, a deploy and a test. Calling deploy will deploy our backend solidity contract, DistributionExecutable.sol on each chain, with the class initialized with values from a config file. We will do this later.

'use strict'; const { getDefaultProvider, Contract, constants: { AddressZero }, } = require('ethers'); const { utils: { deployContract }, } = require('@axelar-network/axelar-local-dev'); const DistributionExecutable = require('../../artifacts/examples/call-contract-with-token/DistributionExecutable.sol/DistributionExecutable.json'); const Gateway = require('../../artifacts/@axelar-network/axelar-cgp-solidity/contracts/interfaces/IAxelarGateway.sol/IAxelarGateway.json'); const IERC20 = require('../../artifacts/@axelar-network/axelar-cgp-solidity/contracts/interfaces/IERC20.sol/IERC20.json'); async function deploy(chain, wallet) { console.log(`Deploying DistributionExecutable for ${chain.name}.`); const contract = await deployContract(wallet, DistributionExecutable, [chain.gateway, chain.gasReceiver]); chain.distributionExecutable = contract.address; console.log(`Deployed DistributionExecutable for ${chain.name} at ${chain.distributionExecutable}.`); }

Now, let’s take a look at the test function, which calls functions in the smart contract code. In your own app, you will probably have a real frontend, where interactions with the smart contract will be triggered by UI elements.

First, the test function initializes the distributionExecutable contract on each chain. (Each chain will have a distributionExecutable running on it).

async function test(chains, wallet, options) { ... for (const chain of [source, destination]) { const provider = getDefaultProvider(chain.rpc); chain.wallet = wallet.connect(provider); chain.contract = new Contract(chain.distributionExecutable, DistributionExecutable.abi, chain.wallet); chain.gateway = new Contract(chain.gateway, Gateway.abi, chain.wallet); const usdcAddress = chain.gateway.tokenAddresses('aUSDC'); chain.usdc = new Contract(usdcAddress, IERC20.abi, chain.wallet); } ... }

The test function takes in command-line parameters. The first parameter is chain A that you are calling the contract with tokens from. The second parameter is chain B that you are sending the message with tokens to.

async function test(chains, wallet, options) { const args = options.args || []; ... const source = chains.find((chain) => chain.name == (args[0] || 'Avalanche')); const destination = chains.find((chain) => chain.name == (args[1] || 'Fantom')); const amount = Math.floor(parseFloat(args[2])) * 1e6 || 10e6; const accounts = args.slice(3); ... }

An application that wants Axelar to automatically execute contract calls on chain B needs to pre-pay the gas cost. To calculate the estimated gas cost:

  • Estimate the 

    gasLimit

     that the contract call will require on chain B.

  • Query

     

    our API

     

    (

    getGasPrice

    ) for the relative gas cost.

  • Calculate the amount of token to be paid as 

    gasLimit * gasPrice

async function test(chains, wallet, options) { ... const getGasPrice = options.getGasPrice; ... //Set the gasLimit to 3e6 (a safe overestimate) and get the gas price. const gasLimit = 3e6; const gasPrice = await getGasPrice(source, destination, AddressZero); ... }

Finally, we call the sendToMany() method on the DistributionExecutable contract that has been deployed on chain A. This method allows us to execute a contract on and send tokens to multiple accounts simultaneously.

The sendToMany() method takes in the name of chain B, the address of the contract on chain B, the accounts to send tokens to, the token symbol and token amount to be credited, and the gas amount calculated above:

async function test(chains, wallet, options) { ... await ( await source.contract.**sendToMany**(destination.name, destination.distributionExecutable, accounts, 'aUSDC', amount, { value: BigInt(Math.floor(gasLimit * gasPrice)), }) ).wait(); while (BigInt(await destination.usdc.balanceOf(accounts[0])) == balance) { await sleep(2000); } ... }

Now let’s hop over to DistributionExecutable.sol to see what the smart contract is actually doing.

Axelar provides a relayer service IAxelarGasService that provides execution of approved messages. You can import the service from the core Axelar Solidity libraries.

//SPDX-License-Identifier: MIT pragma solidity 0.8.9; import { IAxelarExecutable } from '@axelar-network/axelar-cgp-solidity/contracts/interfaces/IAxelarExecutable.sol'; import { IERC20 } from '@axelar-network/axelar-cgp-solidity/contracts/interfaces/IERC20.sol'; import { IAxelarGasService } from '@axelar-network/axelar-cgp-solidity/contracts/interfaces/IAxelarGasService.sol'; contract DistributionExecutable is IAxelarExecutable { IAxelarGasService gasReceiver; constructor(address _gateway, address _gasReceiver) IAxelarExecutable(_gateway) { gasReceiver = IAxelarGasService(_gasReceiver); } ... }

The IAxelarGasService can receive gas via a few different methods, but in this example application, we will be paying for the gas with chain A’s own token by calling the payNativeGasForContractCall method on the service. Once the gas has been paid, we can make a matching call to the Axelar Gateway service deployed on chain A.

function sendToMany( ... ) external payable { address tokenAddress = gateway.tokenAddresses(symbol); IERC20(tokenAddress).transferFrom(msg.sender, address(this), amount); IERC20(tokenAddress).approve(address(gateway), amount); bytes memory payload = abi.encode(destinationAddresses); if (msg.value > 0) { gasReceiver.**payNativeGasForContractCallWithToken**{ value: msg.value }( ... ); } gateway.callContractWithToken(destinationChain, destinationAddress, payload, symbol, amount); }

To call chain B from chain A and send some tokens along the way, the user needs to call callContractWithToken on the gateway of chain A, specifying:

  • The destination chain: must be an EVM chain from 

    Chain names

    .

  • The destination contract address: must implement the 

    IAxelarExecutable

     interface defined in 

    IAxelarExecutable.sol

    .

  • The payload 

    bytes

     to pass to the destination contract.

  • The symbol of the token to transfer: must be a supported asset [

    Mainnet

     | 

    Testnet

     | 

    Testnet-2

    ].

  • The amount of the token to transfer.

 

💡 To call a contract and send tokens, the user needs to specify the destination chain, the destination contract address, the payload bytes to be sent, the symbol of the token**,** and the amount.

 

Finally, IAxelarExecutable has an _executeWithToken function that will be triggered by the Axelar network after the callContractWithToken function has been executed. In other words, when the contract on chain B is called via callContractWithToken from chain A, the _executeWithToken method on the contract on chain B runs.

executeWithToken() has the following signature:

function _executeWithToken( string memory sourceChain, string memory sourceAddress, bytes calldata payload, string memory tokenSymbol, uint256 amount ) internal virtual {}

You can write any custom logic inside the _executeWithToken. In our _executeWithToken function, we decode the intended recipients of the tokens, then transfer tokens to each of the recipients.

function _executeWithToken( string memory, string memory, bytes calldata payload, string memory tokenSymbol, uint256 amount ) internal override { address[] memory recipients = abi.decode(payload, (address[])); address tokenAddress = gateway.tokenAddresses(tokenSymbol); uint256 sentAmount = amount / recipients.length; for (uint256 i = 0; i < recipients.length; i++) { IERC20(tokenAddress).transfer(recipients[i], sentAmount); } }

We can also imagine, however, other scenarios where you might want to call a contract on another chain and transfer some tokens. For example, suppose you’re building a cross-chain NFT marketplace. You want to make it so that a buyer can buy an NFT for sale from any chain. Your _executeWithToken function might in that case not only transfer the token to the destination address, but also call another contract method on the destination chain, say purchaseNFT(uint256 nftId) .

To do something like this you would first want to encode the purchaseNFT function signature string using abi.encode along with any function parameter values. You also want to encode the destination address.

function purchaseNFT( ... ) external payable { address tokenAddress = gateway.tokenAddresses(symbol); IERC20(tokenAddress).transferFrom(msg.sender, address(this), amount); IERC20(tokenAddress).approve(address(gateway), amount); bytes memory payload = abi.encode("purchaseNFT(uint256)", 56, destinationAddress) if (msg.value > 0) { gasReceiver.**payNativeGasForContractCallWithToken**{ value: msg.value }( ... ); } gateway.callContractWithToken(destinationChain, destinationAddress, payload, symbol, amount); }

In _executeWithToken you could then directly do address(this).call(payload) to execute purchaseNFT(56) before forwarding the tokens received to the NFT seller’s address.

function _executeWithToken( string memory, string memory, bytes calldata payload, string memory tokenSymbol, uint256 amount ) internal override { // call purchaseNFT(56) address(this).call(payload) (/*ignore method signature and argument*/, address seller) = abi.decode(payload, (string, uint256, address)); // get ERC-20 address from gateway address tokenAddress = gateway.tokenAddresses(tokenSymbol); // transfer received tokens to the recipient IERC20(tokenAddress).transfer(recipient, amount); }

5. Run the local Axelar node

Now that we understand the call-contract-with-token code, let’s see that it works. First, spin up a local Axelar node.

node scripts/createLocal

Leave this node running on a separate terminal before deploying and testing the dApps.

6. Deploy the contract

Deploy the contract with the command below.

node scripts/deploy examples/call-contract-with-token local

You should see some printout like this that shows the DistributionExecutable was deployed on each chain.

node scripts/deploy examples/call-contract-with-token local Deploying DistributionExecutable for Moonbeam. Deploying DistributionExecutable for Avalanche. Deploying DistributionExecutable for Fantom. Deploying DistributionExecutable for Ethereum. Deploying DistributionExecutable for Polygon. Deployed DistributionExecutable for Fantom at 0x775C53cd1F4c36ac74Cb4Aa1a3CA1508e9C4Bd24. Deployed DistributionExecutable for Ethereum at 0x775C53cd1F4c36ac74Cb4Aa1a3CA1508e9C4Bd24. Deployed DistributionExecutable for Avalanche at 0x775C53cd1F4c36ac74Cb4Aa1a3CA1508e9C4Bd24. Deployed DistributionExecutable for Moonbeam at 0x775C53cd1F4c36ac74Cb4Aa1a3CA1508e9C4Bd24. Deployed DistributionExecutable for Polygon at 0x775C5

7. Run the test

Finally, run test (which calls ExecutableSample). The command below sends 100 tokens from Moonbeam to the Ethereum address 0xBa86A5719722B02a5D5e388999C25f3333c7A9fb.

node scripts/test examples/call-contract-with-token local "Moonbeam" "Ethereum" 100 0xBa86A5719722B02a5D5e388999C25f3333c7A9fb

You should see some printout like this that shows the message passing occurred successfully. (Axelar takes a transaction fee which accounts for the discrepancy between 99 and 100).

--- Initially --- 0xBa86A5719722B02a5D5e388999C25f3333c7A9fb has 100 aUSDC --- After --- 0xBa86A5719722B02a5D5e388999C25f3333c7A9fb has 199 aUSDC

To run the test on Axelar testnet rather than against a local node, you can use the following commands instead:

node scripts/deploy examples/call-contract-with-token testnet node scripts/test examples/call-contract-with-token testnet "Moonbeam" "Ethereum" 100 0xBa86A5719722B02a5D5e388999C25f3333c7A9fb

blog cover
November 30, 2022

How Much Gas to Get Over the Bridge?

Satellite gas fees, explained: In this post, we explain cross-chain gas costs, where they are charged, how they are estimated — and how Axelar keeps cross-chain gas payment complexities in the background for users.

blog cover
November 18, 2022

Biconomy & Axelar to Power a Chainless User Experience with the Biconomy SDK

Developers can tap into powerful, composable SDK modules to onboard users, control gas fees, and bundle transactions easily, among other UX benefits.

news
blog cover
October 26, 2022

Axelar & Polygon to Deliver Secure Cross-Chain Communication to Polygon Supernets

Axelar is one of the early adopters of Polygon Supernets which will expand the interoperability of Polygon Supernets — high-performance app-specific chains that can be optimized for a dApp or a category of dApps. 

news
Axelar powers the cross-chain future.