With a small sneak peek into Hyperlane done and dusted in the Quickread, this blog will deal with the implementation of the Messaging API of Hyperlane on the contracts. This will 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!
We would walk you through the following:
Introduction:
Since we've observed that Hyperlane connects two different chains, we'll construct 2 distinct contracts. The main contract will reside on chain X, where we want the dApp to be, and the child contract will be located on the other chain. This setup allows the user on the chains to interact with the Main contract positioned on chain X.
The implementations differ only slightly for both contracts, making it beginner-friendly.
This is typical architecture 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.
Setting up Remix:
For writing and interacting with the contract we will be using Remix IDE, an online IDE to write smart contracts. You can access it here. To start with, 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: https://github.com/HyperlaneIndia/Messaging-API
Head over to the src folder and you will find the contracts
Vote Main Contract:
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 stored 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 a _vote function which will have 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;
}
Vote Router Contract:
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 and interchainGasPaymater 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;
address interchainGasPaymaster;
uint32 domainId;
address voteContract;
constructor(address _mailbox, address _interchainGasPaymaster, uint32 _domainId, address _voteContract){
mailbox = _mailbox;
interchainGasPaymaster = _interchainGasPaymaster;
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 dispatch function of the mailbox contract to get the messageId for our message. We pass in domainId, contract address(as bytes32), and data(by encoding the proposalId, voter address, and vote type) to the mailbox contract. After getting the messageId, we need to pay the gas fees for the message to be delivered in the parent chain. To do so first, we get a quote on how much gas fees we need to pay using “quoteGasPayment” and then we pay it using the “payForGas” function.
// By calling this function you can cast your vote on other chain
function sendVote(uint256 _proposalId, Vote _voteType) payable external {
bytes32 messageId = IMailbox(mailbox).dispatch(domainId, addressToBytes32(voteContract), abi.encode(_proposalId, msg.sender, _voteType));
uint256 quote = IInterchainGasPaymaster(interchainGasPaymaster).quoteGasPayment(domainId, 10000);
IInterchainGasPaymaster(interchainGasPaymaster).payForGas{value: quote}(
messageId,
domainId,
10000,
msg.sender
);
}
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 two interfaces namely IMailbox and IInterchainGasPaymaster to interact with Hyperlane smart contracts. IMailbox helps us to interact with Hyperlane’s mailbox core contract and the IInterchainGasPaymaster helps us to interact with the interchain gasPaymaster contract using which you can 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. The InterchainGasPaymaster contract helps the users to pay the gas fees needed to deliver the message in the remote chain in terms of native tokens of the origin chain. It works with gas oracles, to track conversion rates and decides the amount to be paid.
First, we will have a look at the usage of the IMailbox interface. Over in the sendVote function, we have used this interface. We are calling the dispatch 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 messageId from the mailbox contract which will be used later to pay the gas fees for your particular message.
bytes32 messageId = IMailbox(mailbox).dispatch(domainId, addressToBytes32(voteContract), abi.encode(_proposalId, msg.sender, _voteType));
After getting the messageId, now we will use the IInterchainGasPaymaster interface to pay the gas fees for your message to be delivered in the remote chain. To execute this functionality, we need to call two functions “quoteGasPayment” and “payForGas”. The quoteGasPayment function returns us the amount of gas fees we need to pay for the delivery of your message on the remote chain based on the approximate gas it will take to execute the handle functions on the recipient side. We pass the domainId and the approximate gas values to this function and it returns how much you need to pay in terms of Wei. After getting this value, now it’s time to pay for your messageId and for this, we call the “payForGas” function. We pass in the received quote as the value of the function call along with the messageId we received from the dispatch function, domainId of the remote chain, approximate gas required in the handle function of the recipient contract, and an address to return the extra gas fees if we paid more than required as function arguments.
uint256 quote = IInterchainGasPaymaster(interchainGasPaymaster).quoteGasPayment(domainId, 10000);
IInterchainGasPaymaster(interchainGasPaymaster).payForGas{value: quote}(
messageId,
domainId,
10000,
msg.sender
);
Deploy:
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:
And Press compile. Your contract file would be there usually. This is where a contract has complied.
After compilation, next 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 the said above, the constructor inputs would be:
Once we have pressed Deploy after inputting the necessary inputs in the voteMain contract you will see the contract under the deployed contract section.
After that copy the contract address of the VoteMain contract, fill up the VoteRouter constructor(don’t forget to change your network to another chain), and click on deploy. We would have both contracts deployed and it would look something like this:
Test:
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.
Conclusion:
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 messaging API and its capabilities. With further exploration and experimentation, developers can create even more complex and powerful decentralized applications.
In the next blog, we will be explaining a simple front-end integration to this example contract.
Welcome to the future of interoperability!