header banner
Default

How to Use LayerZero on Klaytn to Send Cross-Chain Messages


Klaytn

Klaytn

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

Image from LayerZero

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:

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

  1. Compile the crossChainHelloWorld contract in the Solidity Compiler Tab.
  2. Connect MetaMask using the Injected Web3 Environment in the Deploy & run transaction Tab. Ensure your MetaMask is connected to Klaytn Baobab.
  3. Paste in the Klaytn Baobab LayerZero endpoint contract address `0x6aB5Ae6822647046626e83ee6dB8187151E1d5ab` in the constructor argument field before deploying.
  4. 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

  1. Compile the crossChainHelloWorld contract in the Solidity Compiler Tab
  2. Connect MetaMask using the Injected Web3 Environment in the Deploy & run transaction Tab. Ensure your MetaMask is connected to Mumbai
  3. Paste in the Polygon Mumbai LayerZero endpoint contract address `0xf69186dfBa60DdB133E91E9A4B5673624293d8F8` in the constructor argument field before deploying
  4. 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

  1. Make sure that you are in the Injected Provider environment and that the contract is still “CrossChainHelloWorld.sol
  2. 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.
  3. Click Transact and confirm your transaction in MetaMask.

Connecting on Destination Chain

On Remix IDE

  1. Make sure that you are in the Injected Provider environment and that the contract is still “CrossChainHelloWorld.sol
  2. 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.
  3. 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:

Sources


Article information

Author: Andrew Estrada

Last Updated: 1698601322

Views: 1064

Rating: 3.5 / 5 (50 voted)

Reviews: 98% of readers found this page helpful

Author information

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.