Messaging API -V3

Messaging API -V3

Contract Level Implementation

As we have seen a small sneak peek into Hyperlane in the Introduction blog, this blog series involves the implementation of Messaging API of Hyperlane on the contracts to let your contracts work on different chains and communicate with each other.

Hyperlane makes interchain communication straightforward by providing a simple on-chain API for sending and receiving messages.

Messaging API is very useful for sending data from one chain to data onto another chain. It can be used in cases of

  • Interchain DAOs

  • Inetrchain DeFi applications

  • Interchain Data storage solutions etc

This is a highly effective way to seamlessly transfer data on a permissionless deployment at a very low cost. As this technology is state-of-the-art and enables you to build an interchain ecosystem of contracts for your dapp, leveraging a wide range of elements such as user bases on other chains or the uniqueness of other chains, building a contract using the Messaging API becomes the stepping stone to facilitate the communication link between your contracts.

So let's get started by writing our own contract and implementing Hyperlane's Messaging API into it!

Contracts

In this tutorial, we will be building a simple Voting contract that lets you vote for proposals.

The section is divided into 2 parts. The main contract and the child contract.

As we have seen that Hyperlane connects 2 different chains, we will be building 2 different contracts where the main contract resides on chain X where we want the dApp to be and the child contract will be positioned on other chains which will let users on those chains interact with the Main contract positioned on chain X.

The implementations differ only in a few terms for both contracts, hence it is beginner friendly.

This is usually the architecture that is followed for Hyperlane integration where we have a set of contracts on other chains and they interact with the contract on the main chain and vice versa based on architecture, through Hyperlane.

For writing and interacting with the contract we will be using Remix IDE, an online IDE to write smart contracts. You can access it from here. Initially let's create files under the contracts section.

Let's create 2 new files named VoteMain.sol and VoteRouter.sol . The .sol extension is for solidity smart contracts. Here the voteMain.sol is the main contract. And voteRouter.sol is the child contracts.

Here is a GitHub link to the contracts which you can use as a reference to build the contract as we will be explaining every snippet of code to be integrated.

GitHub : github.com/HyperlaneIndia/Messaging-API

Head over to the src folder and you will find the contracts

We have deployed two smart contracts for this example. One is VoteMain.sol which is our main voting contract. The voting logic is implemented in this contract. This contract will be deployed to a single chain and the other contract will be deployed onto multiple chains to bridge the votes. We have assumed two types of votes for this example namely FOR and AGAINST.

enum Vote{ FOR, AGAINST } // Creating enums to denote two types of vote

After this, we have declared a struct to store the details of the proposals. Anybody can create a proposal and all the details of the proposal will be stored using this struct. As of this example, we have store title, description, amount of forVotes, amount of againstVotes, the timestamp when the proposal was created, and the voting period for that particular proposal.

// Structure of the proposal
struct Proposal{
    string title;
    string description;
    uint256 forVotes;   
    uint256 againstVotes;
    uint256 createdTimestamp;
    uint256 votingPeriod;
}

Next, we will declare some important variables required for the working of the smart contract. We have declared the “proposals” variable as a mapping to store the proposal by indexing their proposalId(Later in this article we will know about how the proposalId is generated). Similarly, we need the “votes” variable to track, if a particular user has voted on a proposal or not(Think of it as preventing double spending). And lastly, we need to store the Hyperlane’s mailbox contract address to filter out function calls of the handle function.

mapping (uint256 => Proposal) proposals; // Mapping to store the proposals
mapping (address => mapping(uint256 => bool)) votes; // Mapping to store the votes in order to prevent double voting

address mailbox; // address of mailbox contract

After this, we will declare the constructor, modifier, and events for the contract. The constructor just takes in the mailbox contract address and initializes it by storing it in the variable. The onlyMailbox modifier is required to prevent spam handle function calls to the smart contract. And lastly, we need two events one to track out proposals created and the other to track out the votes.

constructor(address _mailbox){
    mailbox = _mailbox;
}

// Modifier so that only mailbox can call particular functions
modifier onlyMailbox(){
    require(msg.sender == mailbox, "Only mailbox can call this function !!!");
    _;
}

// Events to track out proposals and votes
event ProposalCreated(uint256 indexed _proposalId, string _title, string _description, uint256 _createdTimestamp, uint256 _votingPeriod);
event VoteCasted(uint256 indexed _proposalId, address indexed voter, Vote _voteType);

The createProposal function is declared in order to create new proposals. It takes in the title, description, and voting period of the proposal as arguments and returns the proposalId as a result. To generate the proposalId, it hashes the title, description, and voting period using the keccak256 hash algorithm and generates a uint256 proposalId. Then, we check if this proposal already exists, and if not we create a new entry to the “proposals” variable for the generated proposalId and emit the “ProposalCreated” event.

// Function to create a new proposal
function createProposal(string memory _title, string memory _description, uint256 _votingPeriod) external returns(uint256 proposalId){
    proposalId = uint256(keccak256(abi.encode(_title, _description, _votingPeriod)));
    require(proposals[proposalId].createdTimestamp == 0, "Proposal already created !!!");
    proposals[proposalId] = Proposal(_title, _description, 0, 0, block.timestamp, _votingPeriod);
    emit ProposalCreated(proposalId, _title, _description, block.timestamp, _votingPeriod);
}

Now, we need to create _vote function which will be having the core voting logic of the contract. It takes the proposalId, voter’s address, and vote type as arguments. First, it checks for multiple conditions to be true such as if the proposal exists, if the proposal timeline has ended, and if the voter has already voted. After all the checks are complete it adds up the vote count for the particular vote type of the given proposalId and emits the VoteCasted event.

// Internal voting function which holds the voting logic
function _vote(uint256 _proposalId, address _voter, Vote _voteType) internal{
    require(proposals[_proposalId].createdTimestamp != 0, "Proposal doesn't exist !!!");
    require(proposals[_proposalId].createdTimestamp + proposals[_proposalId].votingPeriod >= block.timestamp, "Voting period already ended !!!");
    require(!votes[_voter][_proposalId], "Voter already voted !!!");
    if(_voteType == Vote.FOR){
        proposals[_proposalId].forVotes += 1;
    }else if(_voteType == Vote.AGAINST){
        proposals[_proposalId].againstVotes += 1;
    }
    votes[_voter][_proposalId] = true;
    emit VoteCasted(_proposalId, _voter, _voteType);
}

The function, that we have created above is an internal function as we will be implementing two voting functions. One is if a user directly votes on the main contract or the user can bridge the vote from a remote chain using the router contract. The voteProposal function can be used if you directly want to vote, or we have the handle function which can be called only by the mailbox contract to register a vote from a remote chain. Over in the handle function, we decode the required data from the message body we received from the remote chain and then call the _vote function.

// You can cast a vote directly by calling this function
function voteProposal(uint256 _proposalId, Vote _voteType) external {
    _vote(_proposalId, msg.sender, _voteType);
}
// handle function which is called by the mailbox to bridge votes from other chains
function handle(uint32 _origin, bytes32 _sender, bytes memory _body) external onlyMailbox{
    (uint256 _proposalId, address _voter, Vote _voteType) = abi.decode(_body, (uint256, address, Vote));
    _vote(_proposalId, _voter, _voteType);
}

Lastly in this contract, we have the getVotes function which helps us to get the votes registered for a given proposalId. It just reads the data from the proposals mapping and returns it to the user.

// function to get votes for a particular proposal
function getVotes(uint256 _proposalId) external view returns(uint256 _for, uint256 _against){
    _for = proposals[_proposalId].forVotes;
    _against = proposals[_proposalId].againstVotes;
}

Now let’s move on to the next VoteRouter.sol contract using which you can bridge your votes from any remote chain to your parent chain. Similar to the VoteMain contract we implement two types of the vote over here. Next, we declare some variables and initialize them with the constructor. We need the mailbox contract address to send interchain messages using Hyperlane. Along with this we also need the VoteMain contract address on the parent chain along with the domainId of the parent chain. (Check this for contract addresses and domainId).

// variables to store important contract addresses and domain identifiers
address mailbox;
uint32 domainId;
address voteContract;

constructor(address _mailbox, address _interchainGasPaymaster, uint32 _domainId, address _voteContract){
    mailbox = _mailbox;
    domainId = _domainId;
    voteContract = _voteContract;
}

After this, we declare the sendVote function using which you can send your votes to the parent chain. It takes the proposalId and vote type as function arguments. Firstly, we call the quoteDispatch function of the mailbox contract by passing in the destination domainId, the recipient’s address, and the data to be sent as the function argument. This function will return us the expected interchain gas we need to pay. After we get this quote, we will call the dispatch function of the mailbox with the same arguments we passed in the quoteDispatch function along with a value payment of the quote we got previously.

// By calling this function you can cast your vote on other chain
function sendVote(uint256 _proposalId, Vote _voteType) payable external {
    uint256 quote = IMailbox(mailbox).quoteDispatch(domainId, addressToBytes32(voteContract), abi.encode(_proposalId, msg.sender, _voteType));
    IMailbox(mailbox).dispatch{value: quote}(domainId, addressToBytes32(voteContract), abi.encode(_proposalId, msg.sender, _voteType));
}

We need to use the addressToBytes32 function, to convert the recipient address into bytes32 format because the mailbox contract accepts only bytes32. We just wrap up the address first into uint and then into bytes32.

// converts address to bytes32
function addressToBytes32(address _addr) internal pure returns (bytes32) {
    return bytes32(uint256(uint160(_addr)));
}

In this example, we have used the IMailbox to interact with Hyperlane smart contracts. IMailbox helps us to interact with Hyperlane’s mailbox core contract using which you can get the quote of the interchain gas you need to pay as well as dispatch and pay the gas fees required for your message to be delivered on the remote chain. The mailbox smart contract is an on-chain API for sending and receiving interchain messages. Validators and relayers index the mailbox contract for the interchain messages to validate and deliver to remote chains respectively.

Let’s have a look at the usage of the IMailbox interface. Over in the sendVote function, we have used this interface. We are calling the quoteDispatch function of the mailbox contract using this interface. We are passing the domainId of the remote chain, the contract address of the recipient, and the data. As a result, we get a quote for the interchain gas fee needed from the mailbox contract which will be used later to pay the gas fees for your particular message.

uint256 quote = IMailbox(mailbox).quoteDispatch(domainId, addressToBytes32(voteContract), abi.encode(_proposalId, msg.sender, _voteType));

After getting the quote for the interchain gas fees needed to be paid we will move ahead to initiate the interchain message. To do so, we will call the dispatch function of the mailbox along with the same function arguments you passed for the quoteDispatch function previously. The most important thing to notice is that we will be passing the required interchain gas fees as the value in the function call as per the quote we got previously.

IMailbox(mailbox).dispatch{value: quote}(domainId, addressToBytes32(voteContract), abi.encode(_proposalId, msg.sender, _voteType));

Now that we have written our contract let's deploy it and test out the first-ever interchain transaction you will be doing!

Quickly head to the Solidity compiler section in remix:(Make sure to set the compiler version to 0.8.19 because some chains may report a PUSH0 issue)

And Press compile. Your contract file would be there usually. This is where a contract has complied.

After compilation, head to the Deploy and Run Transactions section:

Under the contract slide in the above image, your contract would be there.

It would look like this when the voteMain.sol has to be deployed:

And when voteRouter.sol is being deployed it would look like this:

We fill in the constructor details with the addresses found at Hyperlane docs here, based on the chain we are deploying into, i.e. the InterchainGasPayMaster address and MailBox address.

Here we have deployed the main contract onto Sepolia and the child contract to Mumbai.

For them, the constructor inputs would be:

Once we have pressed Deploy after inputting the necessary inputs in the voteMain contract(You will need to fill in the mailbox contract address in Sepolia which is 0xfFAEF09B3cd11D9b20d1a19bECca54EEC2884766) you will see the contract under the deployed contract section. (Find all the contract addresses over here, you can find tables for each contract with reference to different chains)

After that copy the contract address of the VoteMain contract and fill up the VoteRouter constructor(Mailbox address:- 0x2d1889fe5B092CD988972261434F7E5f26041115, domainid:- 11155111)(don’t forget to change your network to another chain, in this example Mumbai) and click on deploy. We would have both contracts deployed and it would look something like this:

Now let’s create a proposal on the voteMain contract. To this, we will open the voteMain contract deployment as shown below and input the following details. After that, we will press the transact button.

Don’t forget to check the logs of the created proposal in the Remix console to get the proposalId, which will be used while voting. Now let's vote on the proposal created on the Sepolia chain on the voteMain contract through the voteRouter contract on Mumbai by letting Hyperlane do the magic. (while voting don’t forget to set the value field of the Remix deploy section to 10 Finney because we need to pay for the interchain gas fees)

To see the transaction's success we will be using the Hyperlane Explorer and querying using the contract address.

You can check yours by going to this link and input the contract address.

Woohoo! You have your first interchain communication through the Hyperlane interchain Highway! This is how simple it is to integrate Hyperlane into your contracts.

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.