In this article we are going to look at how to send messages or data from one chain to another. In this LayerZero example we will be sending a string from Ethereum’s Goerli Testnet to Optimisms Layer 2 Goerli Testnet.
- Cross-Chain Tutorial Video
- How LayerZero Works
- What Chains Is Layer Zero On
- LayerZero Messaging Example
- Cross-Chain Tokens Example
- Conclusion
Cross-Chain Tutorial Video
How LayerZero Works
LayerZero comprises of a network of nodes which pass communications between different EVM blockchains.
A user will call a transaction and pay a fee in the native asset on one chain. This will fire an event which will be tracked by a p2p network of external nodes. These nodes form the LayerZero network and relay the data on to a receiver contract on the destination blockchain.
What Chains Is Layer Zero On
As of time of publishing in December 2022 LayerZero is currently deployed to the following chains:
Mainnets | Testnets |
---|---|
Ethereum | Goerli Testnet |
BNB Chain | BNB Testnet |
Avalanche | Fuji Testnet |
Polygon | Mumbai Testnet |
Optimism L2 | Goerli Optimism |
Arbitrum L2 | Goerli Arbitrum |
Aptos | Aptos Testnet |
Fantom | Fantom Tesntet |
Swimmer | |
DFK | |
Harmony | Harmony Testnet |
Moonbeam | Moonbeam Testnet |
Celo | Celo Testnet |
Dexalot | Dexalot Testnet |
Fuse | |
Gnosis | Gnosis Tesnet |
Klaytn | Klaytn Testnet |
Metis | Metis Testnet |
Intain | |
zkSync Testnet | |
CoreDao Testnet | |
Portal Fantasy |
LayerZero Messaging Example
This LayerZero example Solidity code and full instructions are available on Github:
https://github.com/jamesbachini/LayerZero-Example
Thank you to @Gangstarr60 for updating it for the latest OpenZeppelin libraries.
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.17;
import "https://github.com/LayerZero-Labs/solidity-examples/blob/main/contracts/lzApp/NonblockingLzApp.sol";
contract LayerZeroTest is NonblockingLzApp {
string public data = "Nothing received yet";
uint16 destChainId;
constructor(address _lzEndpoint) NonblockingLzApp(_lzEndpoint) Ownable(msg.sender) {
if (_lzEndpoint == 0xae92d5aD7583AD66E49A0c67BAd18F6ba52dDDc1) destChainId = 10121;
if (_lzEndpoint == 0xbfD2135BFfbb0B5378b56643c2Df8a87552Bfa23) destChainId = 10132;
}
function _nonblockingLzReceive(uint16, bytes memory, uint64, bytes memory _payload) internal override {
data = abi.decode(_payload, (string));
}
function send(string memory _message) public payable {
bytes memory payload = abi.encode(_message);
_lzSend(destChainId, payload, payable(msg.sender), address(0x0), bytes(""), msg.value);
}
function trustAddress(address _otherContract) public onlyOwner {
trustedRemoteLookup[destChainId] = abi.encodePacked(_otherContract, address(this));
}
}
I ran this on Remix you’ll need to add Optimism Goerli testnet to Metamask using Chainlist.
Note that the chainId’s that LayerZero uses are different from the real chain ID’s that you would use to connect to RPC nodes etc.
Instructions
Copy the code into remix
Deploy on both networks using the lzEndpoints from here in the constructor argument.
Then use the trustAddress(address _otherContract) function to approve the other contract address on both deployed contracts.
Once that’s done you can send messages both ways using send(“Hello World”). It can take a few minutes for the data to be relayed from one chain to the other and the state to be updated on the destination chain.
Note that you will need to call the send function with some funds by putting a value amount in. I sent 12345678 gwei which seemed to work on testnet and any surplus is refunded back to your wallet. There’s an option for this near the gas limit in the top left panel on Remix.
Cross-Chain Tokens Example
This LayerZero Example contract is also available in my LayerZero Example Repository, note that there is a more sophisticated OFT example here.
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.17;
import "https://github.com/LayerZero-Labs/solidity-examples/blob/main/contracts/lzApp/NonblockingLzApp.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract CrossChainToken is NonblockingLzApp, ERC20 {
uint16 destChainId;
constructor(address _lzEndpoint) NonblockingLzApp(_lzEndpoint) ERC20("Cross Chain Token", "CCT") Ownable(msg.sender) {
if (_lzEndpoint == 0xae92d5aD7583AD66E49A0c67BAd18F6ba52dDDc1) destChainId = 10121;
if (_lzEndpoint == 0xbfD2135BFfbb0B5378b56643c2Df8a87552Bfa23) destChainId = 10132;
_mint(msg.sender, 1000000 * 10 ** decimals());
}
function _nonblockingLzReceive(uint16, bytes memory, uint64, bytes memory _payload) internal override {
(address toAddress, uint amount) = abi.decode(_payload, (address,uint));
_mint(toAddress, amount);
}
function bridge(uint _amount) public payable {
_burn(msg.sender, _amount);
bytes memory payload = abi.encode(msg.sender, _amount);
_lzSend(destChainId, payload, payable(msg.sender), address(0x0), bytes(""), msg.value);
}
function trustAddress(address _otherContract) public onlyOwner {
trustedRemoteLookup[destChainId] = abi.encodePacked(_otherContract, address(this));
}
}
Remember to add some ETH to the value field when calling the bridge() function. You can check it’s gone through by checking the balanceOf() for the owner address.
Conclusion
LayerZero makes cross-chain communications relatively simple from within Solidity. There are security concerns given the number of exploits we’ve seen with bridges over the last 12 months but the future is on L2s and this could become the go to solution for interchain communications.
I hope that eventually Ethereum will add a data layer for coms between mainnet and L2s which will add a lot of value to the “chain of chain” narrative. Until then we are reliant on 3rd party networks to relay data between chains and LayerZero does that well.