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
- 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 hardhat
in 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.
- 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 lzReceive
will automatically invoked on the receiving chain after the source chain calls endpoint.send(...)
.
Custom function estimateFees
that wraps endpoint.estimateFees(...)
which will returns the fees for the cross-chain message.
- 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
- 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 0
and 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
GitHub: github.com/The-dLab