Introduction
Interoperability has become a widely known limitation of blockchain technology, where multiple blockchains can’t communicate or exchange data seamlessly between their disparate environments. This limitation has brought about the need to build cross-chain solutions to foster interaction and communication between different blockchain ecosystems. As a result, this allows assets, data, and smart contracts to flow freely across chains, fostering collaboration and creating a more connected blockchain ecosystem.
In order to overcome the interoperability limitations, LayerZero comes into play. LayerZero is a trust-less omni-chain interoperability protocol that provides a powerful, low-level communication primitive upon which a diverse set of cross-chain applications can be built. LayerZero allows for advanced interoperability and functionality between what would otherwise be isolated ecosystems.
Using LayerZero’s omni-chain solution, developers can now build dApps that can tap into functionality from Ethereum, Klaytn, Polygon Optimism, and beyond, which was previously difficult or impossible. In this article, you will learn about LayerZero and how you can use it for sending cross-chain messages from Klaytn Baobab Testnet to another chain, Mumbai.
Overview of LayerZero protocol
As rightly said, LayerZero enables cross-chain messaging across different chains and acts as an innovative solution for blockchain interoperability. Its inter-communication protocol is based on the idea of valid delivery, which states that a message is delivered to a destination chain if and only if a transaction on the source chain (tA) is committed and valid. As a result, if two independent entities confirm the validity of a transaction (in this case, tA) then the destination chain can be sure that tA is valid. It is on this foundation that the LayerZero protocol thrives.
At its core, it is built on the following components:
- LayerZero Endpoint: A lightweight on-chain client (smart contracts). This client exists on each (supported) chain, and any chain with a LayerZero Endpoint can conduct cross-chain transactions involving any other chain with a LayerZero Endpoint. The purpose of the endpoints is to allow the user to send a message using the LayerZero protocol backend, guaranteeing valid delivery.
- Oracle: In the bid for independent entities to confirm the validity of the transaction, LayerZero makes use of an Oracle to provide the block header for the block containing tA on the source chain to the destination chain.
- Relayer: As the other independent entity for validating transactions, relayers provide the proof associated with the aforementioned transaction.
Note: The oracle can be any third-party decentralized oracle provider, and the user can even implement their own relayer service. In practice, LayerZero provides the Relayer service, while the Oracle is handled by Chainlink’s decentralized Oracle network.
Getting Started
Having learned about the LayerZero protocol, let’s go ahead with sending cross-chain messages on Klaytn Testnet to Polygon Mumbai. In this tutorial, we will be deploying a smart contract that will interact with the LayerZero endpoint to receive and send messages.
Prerequisites
Before you begin this tutorial you’ll need the following:
- Remix IDE
- MetaMask wallet installed
- Configure the Klaytn Baobab Network and Mumbai Network if you have not
- EOA funded with Test KLAY and MATIC.
Creating CrossChainHelloWorld Contract
In this section, we will be creating our CrossChainHelloWorld messaging contract on Remix IDE. On Remix IDE, navigate to File Explorer and create a new file named `crossChainHelloWorld.sol` in the contracts folder. In the newly created file, paste the code below:
solidity// SPDX-License-Identifier: Unlicensed
pragma solidity ^0.8.17;
// This line imports the NonblockingLzApp contract from LayerZero's solidity-examples Github repo.
import "https://github.com/LayerZero-Labs/solidity-examples/blob/main/contracts/lzApp/NonblockingLzApp.sol";
// This contract is inheriting from the NonblockingLzApp contract.
contract CrossChainHelloWorld is NonblockingLzApp
// A public string variable named "data" is declared. This will be the message sent to the destination.
string public data = "Nothing received yet";
// A uint16 variable named "destChainId" is declared to hold the LayerZero Chain Id of the destination blockchain.
uint16 destChainId;
//This constructor initializes the contract with our source chain's _lzEndpoint.
constructor(address _lzEndpoint, address initialOwner) NonblockingLzApp(_lzEndpoint) Ownable(initialOwner)
// Below is an "if statement" to simplify wiring our contract's together.
// In this case, we're auto-filling the dest chain Id based on the source endpoint.
// For example: if our source endpoint is Klaytn Baobab, then the destination is Polygon Mumbai.
// NOTE: This is to simplify our tutorial, and is not standard wiring practice in LayerZero contracts.
// Wiring 1: If Source == Klaytn Baobab, then Destination Chain = Polygon Mumbai
if (_lzEndpoint == 0x6aB5Ae6822647046626e83ee6dB8187151E1d5ab) destChainId = 10109;
// Wiring 2: If Source == Polygon Mumbai, then Destination Chain = Klaytn Baobab
if (_lzEndpoint == 0xf69186dfBa60DdB133E91E9A4B5673624293d8F8) destChainId = 10150;
// This function is called when data is received. It overrides the equivalent function in the parent contract.
function _nonblockingLzReceive(uint16, bytes memory, uint64, bytes memory _payload) internal override
// The LayerZero _payload (message) is decoded as a string and stored in the "data" variable.
data = abi.decode(_payload, (string));
// This function is called to send the data string to the destination.
// It's payable, so that we can use our native gas token to pay for gas fees.
function send(string memory _message) public payable
// The message is encoded as bytes and stored in the "payload" variable.
bytes memory payload = abi.encode(_message);
// The data is sent using the parent contract's _lzSend function.
_lzSend(destChainId, payload, payable(msg.sender), address(0x0), bytes(""), msg.value);
// This function allows the contract owner to designate another contract address to trust.
// It can only be called by the owner due to the "onlyOwner" modifier.
// NOTE: In standard LayerZero contract's, this is done through SetTrustedRemote.
function trustAddress(address _otherContract) public onlyOwner
trustedRemoteLookup[destChainId] = abi.encodePacked(_otherContract, address(this));
// This function estimates the fees for a LayerZero operation.
// It calculates the fees required on the source chain, destination chain, and by the LayerZero protocol itself.
// @param dstChainId The LayerZero endpoint ID of the destination chain where the transaction is headed.
// @param adapterParams The LayerZero relayer parameters used in the transaction.
// Default Relayer Adapter Parameters = 0x00010000000000000000000000000000000000000000000000000000000000030d40
// @param _message The message you plan to send across chains.
// @return nativeFee The estimated fee required denominated in the native chain's gas token.
function estimateFees(uint16 dstChainId, bytes calldata adapterParams, string memory _message) public view returns (uint nativeFee, uint zroFee)
//Input the message you plan to send.
bytes memory payload = abi.encode(_message);
// Call the estimateFees function on the lzEndpoint contract.
// This function estimates the fees required on the source chain, the destination chain, and by the LayerZero protocol.
return lzEndpoint.estimateFees(dstChainId, address(this), payload, false, adapterParams);
The code above inherits NonblockingLzApp.sol, a layer that automatically handles all errors and exceptions so that the message queue at the destination LayerZero Endpoint will never be blocked. Kindly read through the comment provided in the code above to understand its workings.
However, we will be doing a code walkthrough of function in the order of execution for this tutorial:
- trustAddress: By default, we are meant to execute the `SetTrustedRemoteAddress ` function, but for the sake of this guide, we created a `trustAddress` function to store a contract address with which your LayerZero User Application contract will accept messages from. For more information about setting up trusted remotes, see Set Trusted Remotes.
- estimateFees: This function helps get the quantity of native gas tokens to pay to send a message. To achieve this, LayerZero makes use of an Oracle and Relayer service given the destination chainId, adapter parameters, and message to be sent. For this tutorial, we will be using 10109 as the destination chainId, HelloWorld from Klaytn as the message, and `0x00010000000000000000000000000000000000000000000000000000000000030d40` for the adapter parameters.You can check out Relayer Adapter Parameters on how to encode adapter parameters. To understand in detail how fees are estimated, see Estimating Message Fees.
- send: This function sends the message you wish to send to the destination chain. Note that this is a payable function; you have to send the pre-computed fee with the transaction. To learn more about the send functionality, see Send Messages.
Deploying contract with Remix on Klaytn Baobab TestNet
Having created our crossChainHelloWorld contract, in this section we will be deploying the same contract on both our source (Klaytn Baobab) and destination chains (Mumbai).
Deploying to Source chain (Klaytn Baobab)
We will be deploying to the source chain in the following steps:
On Remix IDE
- Compile the crossChainHelloWorld contract in the Solidity Compiler Tab.
- Connect MetaMask using the Injected Web3 Environment in the Deploy & run transaction Tab. Ensure your MetaMask is connected to Klaytn Baobab.
- Paste in the Klaytn Baobab LayerZero endpoint contract address `0x6aB5Ae6822647046626e83ee6dB8187151E1d5ab` in the constructor argument field before deploying.
- Click on the deploy button, and after a successful deployment, copy down its address.
Deploying to Destination chain (Mumbai)
We will be deploying to the destination chain in the following steps:
On Remix IDE
- Compile the crossChainHelloWorld contract in the Solidity Compiler Tab
- Connect MetaMask using the Injected Web3 Environment in the Deploy & run transaction Tab. Ensure your MetaMask is connected to Mumbai
- Paste in the Polygon Mumbai LayerZero endpoint contract address `0xf69186dfBa60DdB133E91E9A4B5673624293d8F8` in the constructor argument field before deploying
- Click on the deploy button, and after a successful deployment, copy down its address.
Note: If you desire to use another EVM chain other than Mumbai as the destination chain, you can find the other supported chains (Testnet) endpoints here.
Adding Trusted Sources
In this section, we will be setting up trusted sources. This is key because, from a security standpoint, contracts should only receive messages from known contracts, thus making them securely connected with each other.
To achieve this, in the LZApp.sol, LayerZero stored a single trusted source from each chain in the trustedRemoteLookup map. Therefore, as previously explained, to set the trusted remote, one needs to call the `SetTrustedRemoteAddress` function. For simplicity’s sake, we abstracted the function into the trustAddress function, which is what we will be calling to connect our contracts together.
Now let’s connect our contracts together!
Connecting on Source Chain
On Remix IDE
- Make sure that you are in the Injected Provider environment and that the contract is still “CrossChainHelloWorld.sol”
- If the deployed contract on Klaytn Baobab is still available on the Deployed Contract Tab, find the `trustAddress` function and paste the address of the contract you deployed on the Mumbai network. Else, take the address of the source contract (Klaytn Baobab) and paste it into the At Address input to load the contract instance. Once this is done, find the trustAddress function and paste the address of the contract you deployed on the Mumbai network.
- Click Transact and confirm your transaction in MetaMask.
Connecting on Destination Chain
On Remix IDE
- Make sure that you are in the Injected Provider environment and that the contract is still “CrossChainHelloWorld.sol”
- If the deployed contract on the Mumbai network is still available on the Deployed Contract Tab, find the `trustAddress` function and paste the address of the contract you deployed on the Klaytn Baobab network. Else, take the address of the destination contract (Mumbai) and paste it into the At Address input to load the contract instance. Once this is done, find the trustAddress function and paste the address of the contract you deployed on the Klaytn Baobab network.
- Click Transact and confirm your transaction in MetaMask.
Estimating gas fees for cross-chain transaction
Having connected our contracts together, we should be able to send transactions between the connected contracts. But before then, let’s estimate the gas fee for the cross-chain transaction.
As previously explained, we will do this by calling the estimateFees function. This function takes three things as arguments:
destination chainId: 10109
adapter Parameter: 0x00010000000000000000000000000000000000000000000000000000000000030d40
message: Hello World from Klaytn
After executing the function, you should get the gas fee estimated in Wei. This was the value obtained after executing the function: `78536182898815077`
Sending a cross-chain message from Klaytn Baobab with LayerZero
Now it’s time to perform a cross-chain transaction. To do this, kindly call the Send function by inputting the same message above and the Wei value calculated above in the value field to successfully execute this function.
If you want to see the message sent to the destination chain, make sure to connect to the destination network through MetaMask. Make sure that you are in the Injected Provider environment and that the contract selected is still “CrossChainHelloWorld.sol”. Then take the address of the destination contract and paste it into the At Address input. Press it, and you should be able to use the outcome contract to view the `data` variable on the destination contract.
Verifying cross-chain message transaction on Explorers
After sending your transaction, you should be able to go into the Klaytn Baobab Explorer to take a look at the transaction using its transaction hash. If successful, it should be confirmed, and you’ll be able to see traces of the input of your transaction.
Also, you can check the status of the cross-chain message by pasting the transaction hash from the source chain into the LayerZero scan.
Conclusion
Congratulations on making it to the end of this tutorial! In this guide, you learned about LayerZero and how to send cross-chain messages on Klaytn. From here, you can unlock the power and opportunities of connecting different blockchain systems using LayerZero. The opportunities are endless, as you can build various classes of large-scale applications that were previously impossible, such as cross-chain decentralized exchanges, cross-chain lending, etc.
This tutorial only scratched the surface of LayerZero’s capabilities; there’s still more to this cross-chain interoperability protocol. Head out and read the LayerZero Docs to learn more. If you have any questions, visit the Klaytn Forum. However, below is a list of useful resources you might need:
Author: Andrew Estrada
Last Updated: 1698601322
Views: 1429
Rating: 3.5 / 5 (50 voted)
Reviews: 98% of readers found this page helpful
Name: Andrew Estrada
Birthday: 1923-05-30
Address: 10584 Padilla Station, Alexismouth, MS 23554
Phone: +3623205645759914
Job: Graphic Designer
Hobby: Chocolate Making, Metalworking, Scuba Diving, Pottery, Sewing, Beekeeping, Backpacking
Introduction: My name is Andrew Estrada, I am a rare, talented, esteemed, dear, steadfast, intrepid, capable person who loves writing and wants to share my knowledge and understanding with you.