SpeedRun ETH Dex Bonus Challenge With Axelar

SpeedRun ETH Dex Bonus Challenge With Axelar

Intro

When building a web3 application such as a decentralized exchange, cross-chain functionality can offer tremendous growth opportunities for the application. Cross-chain functionality for applications allows for more users to interact with the application and more liquidity to enter the application’s ecosystem. It introduces a unique possibility of transferring value from one network to another, which might not have been feasible before. This involves encapsulating and presenting the yield from one network within a new network, where it can serve as collateral or contribute to other productive purposes.

Axelar is a protocol which allows for easy cross-chain compatibility to be built into an application. Axelar is powered by its own blockchain, which it uses to facilitate cross-chain messages and asset transfers. Applications that integrate with Axelar can integrate with any other chain connected to Axelar.

To enhance the DEX challenge you will be adding cross-chain functionality by integrating with Axelar. At this point, you should have completed the basic Speedrun Eth DEX Challenge. If you want to only do the bonus challenge you can clone this repository for the completed DEX challenge

The objective of the bonus challenge will be to conduct a swap on the DEX from a different blockchain. Specifically, you will be swapping aUSDC (instead of BALLOONS tokens as done in the initial challenge) as the token for Eth via the tokenToEth() function, which is already defined in the DEX.

Axelar Dex Challenge Overview

Contract Setup

First, you must install the necessary dependencies from the cloned repo. To do this run npm install. Next, you need to pass your private key into the .env file so that you can use it to deploy your contract from your own wallet on testnet.

Now in your DexBonus.sol file, you can start with some imports.

import {IAxelarGateway} from '@axelar-network/axelar-gmp-sdk-solidity/contracts/interfaces/IAxelarGateway.sol';
import {IAxelarGasService} from '@axelar-network/axelar-gmp-sdk-solidity/contracts/interfaces/IAxelarGasService.sol';
import {AxelarExecutable} from '@axelar-network/axelar-gmp-sdk-solidity/contracts/executable/AxelarExecutable.sol';
  • The first import is the interface for the Axelar Gateway.

    • The Gateway contract executes and receives messages to/from the Axelar network.
  • Next, is the interface for the Gas Service contract.

    • The Gas Service was created to assist with the payment for an interchain transaction. It will be used to pay for the entirety of the transaction in the token of the source chain so users don’t need to hold the source chain token, Axelar token, and destination chain token for each transaction.
  • Lastly, the Executable contains the functionality which will be automatically executed by an Axelar relayer on the destination chain.

Axelar General Message Passing (GMP) Overview

Building

Source Chain Logic

Now that the required functionality is imported into your contract you can begin to write the functionality that will be executed on the source chain of your interchain transaction.

In your DEX contract, create a new function called interchainSwap() it will take the following parameters.

function interchainSwap(
  string memory _destChain,
  string memory _destContractAddr,
  address _receiver,
  string memory _symbol, 
  uint256 _amount 
) external payable {}

The destChain is the name of the chain your interchain transaction is sending a message to. The destContractAddr is the address on the destination chain that the contract is transacting to. The receiver is the end receiver of the transaction on the destination chain. Lastly, the symbol and amount are the symbol of the token you are sending and the amount of that token you are sending.

Let’s start writing out the interchainSwap() functionality.

First, you will need to transfer the ERC20 token you’re swapping to, to this contract.

//get token address from symbol
address tokenAddress = gateway.tokenAddresses(_symbol);

//send funds to this contract
IERC20(tokenAddress).transferFrom(msg.sender, address(this), _amount);

//approve gateway to spend funds
IERC20(tokenAddress).approve(address(gateway), _amount);

Pass in the symbol of the token you’re transferring to Axelar’s Gateway contract, the Gateway returns the token address. Once you have the address you can call the transferFrom() function to send the tokens from your wallet to this contract. NOTE: you must approve the contract prior to calling this function to transfer these tokens on your behalf or this function will revert. Lastly, you approve the gateway contract to handle the tokens on this contract’s behalf as the tokens will soon be sent out of the custody of this contract.

At this point when the interchainSwap() function is called it will have the tokens in its balance and will have approved the Gateway contract to handle the tokens on its behalf. Now you can encode the GMP message that you will be sending along with the token to the destination chain. The message you are sending is simply the address of the receiving address on the destination chain. You also need to pass in a boolean value of true, which will be used later in the contract. Axelar requires the GMP message to be sent as a bytes type, which is why the message must be encoded.

bytes memory encodedMsg = abi.encode(_receiver, true);

Now you can begin to interact with the related Axelar functionality, namely the Gas Service and the Gateway.

To pay for the interchain transaction you will interact with the Gas Service’s payNativeGasForContractCallWithToken() function

//pay gas from source chain
gasService.payNativeGasForContractCallWithToken{value: msg.value}(
  address(this),
  _destChain,
  _destContractAddr,
  encodedMsg,
  _symbol,
  _amount,
  msg.sender
);

This function takes in the msg.value and specifies the parameters related to the interchain transaction, including; the sender of the transaction, the name of the destination chain, the destination address, the data payload, the symbol of the token sent with the call, the amount of tokens sent with the call. Worth noting is the final parameter where you’re passing in the msg.sender. If any surplus gas is sent in the msg.value then the Gas Service will refund that extra gas amount the msg.sender is the address we are choosing to send this refund to.

Now that the gas has been paid you can trigger the interchain transaction by calling the callContractWithToken() function defined on the Gateway.

//send interchain tx
gateway.callContractWithToken(_destChain, destContractAddr, encodedMsg, symbol, _amount);

This function similarly requires the name of the destination chain, the target address on the destination chain, the data payload, the symbol of the token, and the amount of the token to be sent.

Destination Chain Logic

Great! At this point, once the interchainSwap() function is called you will be sending a token as well as a GMP message containing an encoded _receiver address.

Now once the transaction has been sent through the Axelar blockchain and confirmed by Axelar’s validators the transaction will be received at the address you specified on the destination chain. Recall when writing the interchainSwap() function the second parameter is the address in which this transaction is going to be received on the destination chain.

Once on the destination chain, your contract will handle this transaction by implementing the _executeWithToken() function

function _executeWithToken(
  string calldata,
  string calldata,
  bytes calldata payload,
  string calldata,
  uint256 amount
) internal override {}

The executeWithToken() is defined in the AxelarExecutable contract you inherited earlier. You simply need to override the original definition of the function to implement your own custom logic. The executeWithToken() function is a special function that the Axelar Relayer knows to execute as soon as it receives the confirmation from the Gateway that an approved interchain transaction was received from the Axelar network.

The function takes several parameters that you can see in the source code. For this challenge, the only two parameters you need to use are the payload and the amount. The payload contains the GMP message that was sent in the interchainSwap() function, the amount is the amount that was also sent in the previous function. Note that the payload is of type bytes not address, that is because in the interchainSwap() function the GMP message was encoded to type bytes before passing it into the callContractWithToken() function. So let’s decode the payload to access the encoded address and boolean that was passed in earlier.

(address receiver, bool isInterchainTx) = abi.decode(payload, (address, bool));

Now that the data has been received on the destination chain you can call the already defined tokenToEth() function, using the data you received from your GMP message.

The completed function should now look like this

function _executeWithToken(
  string calldata,
  string calldata,
  bytes calldata payload,
  string calldata,
  uint256 amount
) internal override {
  (address receiver, bool isInterchainTx) = abi.decode(payload, (address, bool));
  tokenToEth(receiver, isInterchainTx, amount);
}

At this point when your interchain transaction is triggered via the interchainSwap() function on the src chain your transaction should successfully transfer the tokens and GMP message via Axelar, then on the destination chain _executeWithToken() will be triggered, which will in turn trigger the tokenToEth() function.

Interchain vs. Non-Interchain Transaction

Great! At this point the Axelar related functionality is complete. The final thing that needs to be completed is a slight tweak to the existing tokenToEth() function to handle the GMP messages you have passed in.

First, make sure to add in two additional parameters; the first being isInterchainTx and the second being tokenInput.

function tokenToEth(
  address recipient,
  bool isInterchainTx,
  uint256 tokenInput
) public returns (uint256 ethOutput) {}

Now let’s create a conditional to see if the call to tokenToEth() is coming from an interchain call via the _executeWithToken() function or if it is being triggered on its own. If the transaction is an interchain transaction, then we can simply send the ETH from this contract’s own balance to the end recipient. Both the end recipient and the conditional are passed in as parameters, which were received via the GMP message sent from the source chain. In the “else” case you can leave the original logic sending the Eth via the transferFrom() function. Your function should now look like this.

function tokenToEth(
  address recipient,
  bool isInterchainTx,
  uint256 tokenInput
) public returns (uint256 ethOutput) {

 require(tokenInput > 0, 'cannot swap 0 tokens');

 uint256 token_reserve = token.balanceOf(address(this));

 ethOutput = price(tokenInput, token_reserve, address(this).balance);

  if (isInterchainTx) {
    (bool sent, ) = payable(recipient).call{value: ethOutput}('');
    require(sent, 'tokenToEth: revert in transferring eth to you!');
  } else {
    require(token.transferFrom(msg.sender, address(this), tokenInput), 'tokenToEth(): reverted swap.');
    (bool sent, ) = msg.sender.call{value: ethOutput}('');
    require(sent, 'tokenToEth: revert in transferring eth to you!');
  }

  emit TokenToEthSwap(msg.sender, 'ERC20 to ETH', ethOutput, tokenInput);

  return ethOutput;
}

Contract Deployment & Testing

Now that the contract is complete you can deploy and test it. To do so make use of the helper scripts available in the scripts folder.

To deploy on Polygon run:

hh run scripts/deployPolygon.ts --network polygon

To deploy on Fantom

hh run scripts/deployFantom.ts --network fantom

The output from each of these should be your address on each blockchain.

To interact with the function open your hardhat console by running

hh console –network fantom

Now in the console get access to a live instance of your contract.

Const Contract = await ethers.getContractFactory(“DexBonus”)
Const contract = await Contract.attach(<YOUR_ADDRESS>)

With your contract instance, you can now call the interchainSwap function.

await contract.interchainSwap(“Polygon”, <CONTRACT_ADDRESS_ON_POLYGON>, “aUSDC”, 2000000, {value: “2000000000000000000”})

The interchainSwap will send 2 aUSDC tokens to the Polygon chain with 2 FTM to cover the gas costs.

Once you execute this, you should see your transaction hash in the console which you can use in the Axelarscan explorer to see your live transaction!

The full execution should look like this in you terminal

Axelar Dex Challenge Contract Interaction Hardhat CLI

On Axelarscan the interchain transaction should look like this.

Axelarscan Live Transaction

Conclusion

At this point, you should have a working contract where you can conduct a cross-chain swap of ERC20 token to a native token on any EVM chain connected to Axelar! For more examples of building interchain applications with Axelar please check out the Axelar Examples Repo and the Axelar technical tutorial Youtube playlist.