Liquidity Layer API

Liquidity Layer API

In this article, we will explore and learn about the Liquidity Layer API of Hyperlane. Hyperlane's Liquidity layer wraps token bridges to allow developers to send tokens alongside their message. If you have checked our previous articles on Messaging API and Warp Route, analogically LL API is used when you want to do both send tokens and messages in a single cross-chain transaction.

At this moment, LL API supports Circle and Portal bridges to transfer your assets from one chain to another along with your interchain message. Circle’s bridge is used to natively bridge USDC tokens across multiple chains. It was developed by the core team who built the USDC token. If you want to bridge any other token, then you can prefer the Portal bridge. In this tutorial, we will be using Circle’s native CCTP bridge to transfer USDC tokens. Also, we will be using Goerli and Fuji as our two chains where we will deploy our code and call the Liquidity Router.

Before moving ahead in this tutorial, I will suggest you get some testnet USDC from the faucet. Here is the link to the faucet. Fill up your wallet address and click on the tweet option to create a tweet about USDC. Once you tweeted about it, copy the link to the tweet, fill up the tweet URL field, and click on the submit button to get 10 testnet USDCs.

Below is the Github repo link, where you can find all the important contracts needed for this tutorial. We will be explaining every snippet of code of all these contracts in this tutorial. You can find all the important contracts in the “src” folder in this repo.

GitHub - HyperlaneIndia/LL-API

Contracts

In this tutorial, we will build a simple smart contract using which you can send tokens with messages, and once it receives the token along with the message we will emit an event that we can later use to track interchain transactions. As mentioned above, we will be using Circle’s bridge to bridge USDC to multiple chains.

As you can see in the workflow given below, we will be calling the dispatchWithTokens() function of the Liquidity Router, which will then pass on the interchain message using Hyperlane and the tokens will be bridged using Circle’s CCTP bridge. On the receiver’s side, we need to have a handleWithTokens() function which the Liquidity Router will call to confirm a transaction.

Now let’s dive into the implementation of Liquidity Layer with Hyperlane. We will be developing a single smart contract “LiquidityRouter.sol” which you can deploy onto multiple chains and bridge tokens with interchain messages. First, we will import and create all the important interfaces that we will be using in this contract. We need three interfaces, the IInterchainGasPaymaster will be used to pay for the interchain gas fees(more on this later), the IERC20 interface as we need to call a few approve and transfer functions for the USDC contract, and most importantly ILiquidityLayerRouter interface so that we call call the dispatchWithTokens() function which is present in the Liquidity Layer contract.

import "@hyperlane-xyz/core/contracts/interfaces/IInterchainGasPaymaster.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";

interface ILiquidityLayerRouter {
    function dispatchWithTokens(
        uint32 _destinationDomain,
        bytes32 _recipientAddress,
        address _token,
        uint256 _amount,
        string calldata _bridge,
        bytes calldata _messageBody
    ) external returns (bytes32);
}

After importing all these interfaces, we will declare our contract and declare a few variables. We are naming our contract “LiquidityRouter”. For the variables, we need three variables to store the InterchainGasPaymaster contract address, the LiquidityLayerRouter contract address, and the USDC contract address. Also, we will declare two events to keep track of the interchain transactions. The TokenSentWithMessage event will be emitted when a new interchain transaction originates from this contract and the TokenReceivedWithMessage event will be emitted when a new interchain transaction is received through this contract.

address liquidityRouter;
address interchainGasPaymaster;
address USDCAddress;
event TokenSentWithMessage(bytes32 indexed messageId, uint32 indexed destDomain, address indexed recipientAddress, uint256 amount, string message);
event TokenRecievedWithMessage(uint32 indexed origin, address indexed sender, string message, uint256 amount);

After this, we will create the constructor which will be used to initialize the three variables we already created. We will accept three variables as arguments in the constructor and then initialize the variables.

constructor(address _lrouter, address _igp, address _usdc){
    liquidityRouter = _lrouter;
    interchainGasPaymaster = _igp;
    USDCAddress = _usdc;
}

Now it’s time to write the core part of this contract which is the “send” function. We will be implementing five things in this function as described below:-

  • Transfer USDC tokens from the user’s account to our contract.

  • Approve USDC tokens from our contract to the LiquidityLayer contract.

  • Call the dispatchWithTokens() function with all the necessary arguments.

  • Get a quote for the interchain gas fees we need to pay.

  • Pay the interchain gas fees and then emit the TokenSentWithMessage event.

In this function, we will be taking the destination domain ID, recipient’s address, token amount, and interchain message as arguments. In the first step, we will be transferring the USDC tokens to our contract by calling the “transferFrom()” function and passing in the arguments(The first one is the sender i.e. msg.sender, the second one is the receiver i.e. our contract address, and the third one is the amount of tokens to be transferred). After this, we will call the “approve()” function by which we will be given approval to transfer a certain amount of tokens from our contract to the LiquidityLayer contract.

Then we will call the dispatchWithTokens() function in which we pass in the destination domain ID, recipient’s address as bytes32(we need to convert it by calling the addressToBytes32 functions), the USDC token address, amount of tokens to transfer, “Circle” as a string as we will be using Circle’s CCTP bridge and the interchain message by encoding it as bytes. We will be returned a messageId which we will use later to pay for the interchain gas fees.

After generating the interchain message, we will call the quoteGasPayment function to get a quote of how much interchain gas fees we need to pay. We will pass the destination domain Id and expected gas to this function. It returns a quote in Wei. Then we will call the payForGas function by passing the quote received as value for the function call and passing in the message Id received from the “dispatchWithTokens()” function call, destination domain ID, expected gas, and the user’s address(This is required because the user can get a refund of the extra gas fees paid during the transaction).

function send(uint32 _dest, address _recipient, uint256 _amount, string memory _message) payable external{
    IERC20(USDCAddress).transferFrom(msg.sender, address(this), _amount);
    IERC20(USDCAddress).approve(liquidityRouter, _amount);
    bytes32 messageId = ILiquidityLayerRouter(liquidityRouter).dispatchWithTokens(
        _dest,
        addressToBytes32(_recipient),
        USDCAddress,
        _amount,
        "Circle",
        abi.encode(_message)
    );
    uint256 quote = IInterchainGasPaymaster(interchainGasPaymaster).quoteGasPayment(_dest, 300000);
    IInterchainGasPaymaster(interchainGasPaymaster).payForGas{value: quote}(
        messageId,
        _dest,
        300000,
        msg.sender
    );
    emit TokenSentWithMessage(messageId, _dest, _recipient, _amount, _message);
}

Above we wrote the logic to send an interchain transaction, now we will write the logic to receive an interchain transaction. We just need to write a simple handleWithTokens() with pre-defined arguments and inside it, we will just emit the TokenReceivedWithMessage event with the data we receive from the arguments.

function handleWithTokens(
        uint32 _origin,
        bytes32 _sender,
        bytes calldata _message,
        address _token,
        uint256 _amount
    ) external{
        emit TokenReceivedWithMessage(_origin, bytes32ToAddress(_sender), abi.decode(_message, (string)), _amount);
    }

Now let’s move to the remix implementation of this project and test our Liquidity Layer API. Copy the code from the GitHub repo and let’s move on.

Let’s first deploy the LiquidityRouter contract. After copying the code, click on the compile tab and compile the contract.

After compiling, move ahead to the deploy tab. On the top, choose the injected provider as the environment. Now either choose, Goerli or Fuji as your network. The next step is to fill up the constructor arguments. Click over here to go the Hyperlane's Contract Addresses page to find out the addresses. Moreover, you will also need testnet’s USDC address which you can find over here. To make it more easy for you we have mentioned the contract addresses used as the constructor over here:-

  • Goerli

LiquidityLayerRouter:- 0x2abe0860D81FB4242C748132bD69D125D88eaE26

Default InterchainGasPaymaster :- 0xF90cB82a76492614D07B82a7658917f3aC811Ac1

USDC address:- 0x07865c6E87B9F70255377e024ace6630C1Eaa37F

  • Fuji

LiquidityLayerRouter:- 0x2abe0860D81FB4242C748132bD69D125D88eaE26

Default InterchainGasPaymaster:- 0xF90cB82a76492614D07B82a7658917f3aC811Ac1

USDC address:- 0x5425890298aed601595a70AB815c96711a31Bc65

Deploy the contract on both networks, with the given set of contract addresses as arguments, and then we can move ahead to the next step which is approving the transfer of the tokens from the user’s account to our contract. Now move ahead to the USDC token page. Click below as per the network you have chosen(where you have your testnet USDCs).

Click on the connect to Web3 button and use your Metamask wallet to connect it with the scanner website. Then click, on the approve function and fill in the arguments. In the spender field, enter the contract address you deployed(Remember to give the address of the contract deployed on the same chain) and in the value field enter the amount of tokens you want to send. And lastly, click on the write button and approve the transaction popup.

You can view the transaction details by clicking the “View your transaction” button. Now you are ready to make your first transaction with the Liquidity Layer. Move back to the Remix terminal and expand the deployed contract you can interact with.

Let’s expand the send function and fill in the function arguments. In this case, I am sending test USDC tokens from Goerli to Fuji. So, fill up the domain ID with the domain ID of Fuji chain which is 43113. (You can check more on domain id over here). The recipient address is the contract address you deployed on the Fuji chain. Fill in the amount of tokens you want to transfer in the amount field and the interchain message you want to send in the message field.

Now before sending the transaction, make sure that you have filled up the value field in the deploy tab because we will be paying the interchain gas fees. For this example, we will be paying 10 Finney as gas fees. (Don’t worry extra gas fees will be refunded to your account). Then click on the transact button to submit the transaction. Now copy the transaction hash from the Remix log and search it in the block explorer. Click here you check one of the transactions. Move into the logs section and scroll down to get the Hyperlane dispatch Id which looks something like this:-

You can copy this dispatch ID and search it in Hyperlane Explorer. And you will find out something like this:-

Click on the transaction that happened in Fuji to learn more about the transaction. Over here you can see that 0.0001 USDC was bridged into Fuji from Goerli. Also, you can go to the logs section and check the 4th argument of the 1st event emitted and you will see the result is “Hello from Goerli” which we sent from the Remix terminal.

You have successfully created a project with Hyperlane’s LL API !!!!! Try a lot of different use cases that you can build with LL API and ping us on our social media handles.

In conclusion, Hyperlane's interchain communication technology provides an easy way to bridge communication across multiple chains. With the ability to deploy smart contracts on multiple chains and bridge them together, developers can create new and innovative decentralized applications that were previously impossible. By using Hyperlane's tools, developers can leverage the power of multiple chains to create a more robust and efficient blockchain ecosystem. The code snippets provided in this article serve as an introduction to Hyperlane and its capabilities. With further exploration and experimentation, developers can create even more complex and powerful decentralized applications.