LayerZero Tutorial for Beginners

Tim
5 min readApr 1, 2022

--

Send A Cross-chain Message

Before Start the Tutorial

We will build a simple cross-chain message transfer contract use LayerZero, and use the default UA configuration.

Prerequisites

This tutorial assume that you have some familiarity with Solidity Hardhat.

Overview

At the first, let’s get an overview of LayerZero.LayerZero is an Omnichain Interoperability Protocol designed for lightweight message passing across chains. LayerZero provides authentic and guaranteed message delivery with configurable trustlessness. The protocol is implemented as a set of gas-efficient, non-upgradable smart contracts.

Setup for the Tutorial

  1. Create a hardhat project

create an npm project by going to an empty folder, runningnpm init,and following its instructions. Once your project is ready, you should runnpm install --save-dev hardhat.

To create Hardhat project run npx hardhatin project folder:

We can select Create an advanced sample project to create a hardhat project for the demo.

To send cross chain messages, contracts will use an endpoint to send() from the source chain and lzReceive() to receive the message on the destination chain.In order to use it, we need to import the interface from LayerZero repository

https://github.com/LayerZero-Labs/LayerZero/tree/main/contracts/interfaces.

  1. Create a contract

Create a contract file LayerZeroDemo1.sol

//SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;
pragma abicoder v2;
import "../interfaces/ILayerZeroEndpoint.sol";
import "../interfaces/ILayerZeroReceiver.sol";
import "hardhat/console.sol";
contract LayerZeroDemo1 is ILayerZeroReceiver {
event ReceiveMsg(
uint16 _srcChainId,
address _from,
uint16 _count,
bytes _payload
);
ILayerZeroEndpoint public endpoint;
uint16 public messageCount;
bytes public message;
constructor(address _endpoint) {
endpoint = ILayerZeroEndpoint(_endpoint);
}
function sendMsg(
uint16 _dstChainId,
bytes calldata _destination,
bytes calldata payload
) public payable {
endpoint.send{value: msg.value}(
_dstChainId,
_destination,
payload,
payable(msg.sender),
address(this),
bytes("")
);
}
function lzReceive(
uint16 _srcChainId,
bytes memory _from,
uint64,
bytes memory _payload
) external override {
require(msg.sender == address(endpoint));
address from;
assembly {
from := mload(add(_from, 20))
}
if (
keccak256(abi.encodePacked((_payload))) ==
keccak256(abi.encodePacked((bytes10("ff"))))
) {
endpoint.receivePayload(
1,
bytes(""),
address(0x0),
1,
1,
bytes("")
);
}
message = _payload;
messageCount += 1;
emit ReceiveMsg(_srcChainId, from, messageCount, message);
}
// Endpoint.sol estimateFees() returns the fees for the message
function estimateFees(
uint16 _dstChainId,
address _userApplication,
bytes calldata _payload,
bool _payInZRO,
bytes calldata _adapterParams
) external view returns (uint256 nativeFee, uint256 zroFee) {
return
endpoint.estimateFees(
_dstChainId,
_userApplication,
_payload,
_payInZRO,
_adapterParams
);
}
}

The contract send a msg from source chain to destination chain, we will need to construct it with an Endpoint address.And will need two interfaces.ILayerZeroEndpoint ILayerZeroReceiver.

Custom function sendMsg that wraps endpoint.send(...) which will cause lzReceive() to be called on the destination chain.

Override function lzReceivewill automatically invoked on the receiving chain after the source chain calls endpoint.send(...).

Custom function estimateFeesthat wraps endpoint.estimateFees(...) which will returns the fees for the cross-chain message.

  1. Deploy the contract on different chains

Create a deploy script for Fantom testnet

const hre = require("hardhat");async function main() {
const LayerZeroDemo1 = await hre.ethers.getContractFactory("LayerZeroDemo1");
const layerZeroDemo1 = await LayerZeroDemo1.deploy(
"0x7dcAD72640F835B0FA36EFD3D6d3ec902C7E5acf"
);
await layerZeroDemo1.deployed(); console.log("layerZeroDemo1 deployed to:", layerZeroDemo1.address);
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});

Deploy contract on Fantom testnet:

npx hardhat run scripts/deploy_testnet.js --network testnet

Create a deploy script for Mumbai(Polygon testnet)

const hre = require("hardhat");async function main() {
const LayerZeroDemo1 = await hre.ethers.getContractFactory("LayerZeroDemo1");
const layerZeroDemo1 = await LayerZeroDemo1.deploy(
"0xf69186dfBa60DdB133E91E9A4B5673624293d8F8"
);
await layerZeroDemo1.deployed(); console.log("layerZeroDemo1 deployed to:", layerZeroDemo1.address);
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});

Deploy contract on Mumbai:

npx hardhat run scripts/deploy_mumbai.js --network mumbai

After we deployed two contracts success, we got the contract address.

In this case:

Mubai: 0x37587469690CC37EE19Ff6163ce7275BB1b17d3b

Fantom testnet: 0xD67D01D6893cC4a2E17557765987d41E778fadca

  1. Test

Create a javascript test script for Mumbai:

const hre = require("hardhat");
const { ethers } = require("ethers");
async function main() {
const LayerZeroDemo1 = await hre.ethers.getContractFactory("LayerZeroDemo1");
const layerZeroDemo1 = await LayerZeroDemo1.attach(
"0x37587469690CC37EE19Ff6163ce7275BB1b17d3b"
);
const count = await layerZeroDemo1.messageCount();
const msg = await layerZeroDemo1.message();
console.log(count);
console.log(ethers.utils.toUtf8String(msg));
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});

The script attached the contract instance with the address 0x37587469690CC37EE19Ff6163ce7275BB1b17d3b which we have deployed in step 3.

It get the message count and the last message in the contract, now it will be 0and empty string.

Run the script with hardhat:

npx hardhat run scripts/demo1_mumbai.js --network mumbai

Create a javascript test script for Fantom testnet:

const { formatBytes32String } = require("ethers/lib/utils");
const { ethers } = require("ethers");
const hre = require("hardhat");
async function main() {
const LayerZeroDemo1 = await hre.ethers.getContractFactory("LayerZeroDemo1");
const layerZeroDemo1 = await LayerZeroDemo1.attach(
"0xD67D01D6893cC4a2E17557765987d41E778fadca"
);
const fees = await layerZeroDemo1.estimateFees(
10009,
"0x37587469690CC37EE19Ff6163ce7275BB1b17d3b",
formatBytes32String("Hello LayerZero"),
false,
[]
);
console.log(ethers.utils.formatEther(fees[0].toString()));
await layerZeroDemo1.sendMsg(
10009,
"0x37587469690CC37EE19Ff6163ce7275BB1b17d3b",
formatBytes32String("Hello LayerZero"),
{ value: ethers.utils.parseEther("1") }
);
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});

The Fantom testnet test script attached address 0xD67D01D6893cC4a2E17557765987d41E778fadca. It will send a message "Hello LayerZero" from Fantom testnet to the contract 0x37587469690CC37EE19Ff6163ce7275BB1b17d3b on the Mumbai, and it will get the estimate fees for demonstration purposes.At last it will send message with the fee, for simplicity send with value 1FTM.If the source transaction is cheaper than the amount of value passed,it will refund the additional amount to the address we have passed the _refundAddress.

Run the script with hardhat:

npx hardhat run scripts/demo1_testnet.js --network testnet

After the script finished, we could search the transaction in the FTMScan testnet,the contract hava called LayerZero endpoint 0xd67d01d6893cc4a2e17557765987d41e778fadca.

Run the Mumbai test script again, the console will print:

The task is finish,the contract on Mumbai have received the message send from Fantom testnet, and increase the counter. LayerZero make the whole process very simple.

Source code:https://github.com/The-dLab/LayerZero-Demo

LayerZero Testnet: https://layerzero.gitbook.io/docs/technical-reference/testnet/testnet-addresses

Follow us on twitter

Connect with us on telegram

GitHub: github.com/The-dLab

--

--