Skip to main content

Moonbeam has extended the built-in interoperability of parachains on Polkadot to remote blockchains with connected contracts, which have been further boosted with a new protocol: LayerZero.

Connected contracts allow smart contracts to communicate with each other across chains, opening the possibility for multichain-aware dApps. Moonbeam’s first integration for connected contracts was Axelar, and in this blog post you will be introduced to another popular cross-chain protocol.

LayerZero is designed to be a lightweight interoperability protocol for cross-chain message passing. Benefits of LayerZero’s connected contract design include nonce-ordered messages and configurable trustlessness. Using LayerZero’s connected contracts solution, developers can build dApps that can tap into functionality from Polkadot, Ethereum, Avalanche, Swimmer, and beyond.

Multichain dApps are currently unified interfaces for protocols duplicated across multiple EVMs. Protocols will be able to connect their contracts cross-chain using LayerZero’s message passing to become multichain-aware, allowing for advanced interoperability and functionality between what would otherwise be isolated ecosystems.

LayerZero opperation

To demonstrate the power of connected contracts, this demo will send and store a string from one TestNet EVM to another. The demo requires a MetaMask account with the Moonbase Alpha network added, which can be accomplished by visiting the Moonbeam docs site.

Intro to LayerZero

LayerZero is a trust-configurable protocol that provides secure cross-chain communication. Ultra-light nodes (ULN) in the form of smart contracts provide chains with the block headers of other bridged chains, but only on-demand for efficiency purposes. The LayerZero endpoint, which is a smart contract that houses the ULN, communicates with an oracle and a relayer.

The Oracle component provides and receives block headers. Applications can select from a marketplace of oracles, including Chainlink, and design the oracle component to require agreement from multiple entities. Block headers are published to destination chains, which allow for the on-chain verification of messages. The Relayer receives and provides proofs of cross-chain messages, which can be verified by block headers, allowing for messages to be completed on destination chains. Relayers can be maintained by anyone. In practice, however, LayerZero manages a relayer service that applications can leverage today. Over the next few months, an open source implementation of the relayer will create a marketplace of relayers for applications to select from and design further decentralized relayer systems similar to that of the Oracle component. The safety of the system relies on the separation between the oracle and relayer systems, and thus the trustlessness of the system is inherited from how much trust is required from the relayer and oracle actors.

LayerZero XCM Functionality

Image from LayerZero

This simple smart contract example will only be concerned with one smart contract, NonblockingLzApp. This smart contract will interact with the LayerZero endpoint to receive and send messages, and also stops failed transactions from blocking future messages from being received.

Connected SimpleGeneralMessage Contract

Now try it out in the most basic way possible. To understand what you’re doing, take a look at the contract that you’ll deploy, which has been made available in a GitHub gist.

The contract’s parent is NonblockingLzApp, which was imported from a LayerZero GitHub repository. This parent contract abstracts away the technicalities of working with LayerZero’s endpoint smart contract so that receiving cross-chain messages is very easy.

Notice that NonblockingLzApp inherits from LzApp. To understand the difference between these two contracts, first know that if any connected contract receives a message from LayerZero, and when receiving that message, an uncaught exception occurs, no other messages can be received from LayerZero. In such an instance, the message that causes the exception is blocking other messages from being received, and will not stop blocking until the message stops causing an exception. Failed messages are blocked when inheriting from LzApp, but by using NonblockingLzApp, all exception causing messages are elegantly caught automatically.

With a brief look at how the NonblockingLzApp contract receives messages below, it’s clear that the _blockingLzReceive function (called by blockingLzReceive) from the LzApp parent contract is overridden. This is the function that gets called when the connected contract receives a cross-chain message. This implementation has a line of code that calls the function nonblockingLzReceive, and checks for a success.

LayerZero 3

Review the implementation of the nonblockingLzReceive function. All it does is ensure that the contract is calling itself (hence the previous function), and then calls a wrapper function that the developer can override. The only thing this chain of functions (blockingLzReceive, _blockingLzReceive, nonblockingLzReceive) does is catch every exception and forward the call to _nonblockingLzReceive so that the developer can write custom logic when they receive a message.

LayerZero 4

Next, look at how the SimpleGeneralMessage contract implements the _nonblockingLzReceive function. All it does is take the payload and write it into a mapping, so that it can be read later!

LayerZero 5

What about sending the message? Before going into how to send a message, think again about the process of sending a message. The user will send a transaction to SimpleGeneralMessage on the origin chain, which will then be picked up by LayerZero’s oracle and relayer, and finally a second transaction on the destination chain will be sent by a LayerZero relayer.

Now back to the code. This also only needs two lines. In the first, the contract encodes the message into a bytes format so that it can be sent across chains. In the second, the message is sent across chains to the contract it’s connected to. The parameters are mostly self explanatory, and the ones that aren’t are irrelevant for this contract’s purposes. The main point is that the destination chain ID is set to a function input, the payload is set as the message, the refund address is set as the message sender, and the destination chain’s gas payment (_nativeFee) is the value of the message.

The reason there needs to be a gas payment paid through the _lzSend function is that there are two transactions: one on the origin chain and one on the destination chain. The transaction on the origin chain is paid like you normally would pay for a transaction: through the transaction fee inherit to a blockchain transaction. The second transaction is paid by sending additional native currency (value) through the LayerZero protocol, which will make up for the gas paid by a LayerZero relayer.

LayerZero 6

Doing is the best way to learn, so try to follow along with the deployment and message passing yourself on Moonbase Alpha.

Deploying with Remix on Moonbase Alpha

The easiest way to deploy the single demo contract is through Remix. DEV is required to deploy on Moonbase Alpha, which is available from the faucet if you don’t have any already.

To deploy the contract, first copy and paste the contract into Remix, or access it through this Remix link. Then compile in the Solidity Compiler tab. Ensure that your MetaMask is connected to the Moonbase Alpha network. Then, in the Deploy & Run Transactions tab of Remix, set the environment to Injected Web3. This will use MetaMask as the Web3 provider.

For the contract to interact with LayerZero’s core functionality, it’s going to need to use the LayerZero endpoint contract. Moonbase Alpha’s endpoint is 0xb23b28012ee92E8dE39DEb57Af31722223034747, and you can find the other networks’ endpoints on LayerZero’s documentation site. Use this contract address in the constructor when deploying the contract to Moonbase Alpha.

Once the contract has been deployed on Moonbase Alpha make sure to copy down its address and repeat the process with any of the other EVM TestNets that are connected to LayerZero so it can send a message across chains.

Network & Faucet LayerZero Endpoint LayerZero ChainID
Polygon Mumbai 0xf69186dfBa60DdB133E91E9A4B5673624293d8F8 10109
Avalanche Fuji 0x93f54D755A063cE7bB9e6Ac47Eccc8e33411d706 10106
Fantom TestNet 0x7dcAD72640F835B0FA36EFD3D6d3ec902C7E5acf 10112
Harmony TestNet 0xae92d5aD7583AD66E49A0c67BAd18F6ba52dDDc1 10133
Moonbase Alpha 0xb23b28012ee92E8dE39DEb57Af31722223034747 10126

Adding Trusted Sources

Since the smart contract at hand inherits from the parent LzApp smart contract, there is a trusted contract security system that comes with it. In order for the smart contract to call _nonblockingReceive when receiving a message, the message must come from a trusted source.

To add a contract as a trusted remote, the setTrustedRemote function must be called. Only the contract owner, by default the deployer, can set trusted remote addresses. The _srcChainId is the LayerZero chain ID of the source chain, which can be found on their documentation site and in the table above. The _path is the address to trust in bytes format.

LzApp stores a single trusted source from each chain in the trustedRemoteLookup map. The way it is stored is more complex than simply an address, however. The format requires two addresses packed together into bytes format: the first being the message source and the last being the message destination. This might seem like a strange way of doing it, but it makes the implementation in LzApp easier when checking for unauthorized messages.

For example, if you wanted to send a message from a contract 0xAAAAAAAAAAAAAAAAAAAA on a chain with lzID 100 to a contract 0xBBBBBBBBBBBBBBBBBBBB on a chain with lzID 101, you would format the trusted remote call on the latter chain:

setTrustedRemote(101, 0xBBBBBBBBBBBBBBBBBBBBAAAAAAAAAAAAAAAAAAAA)

LayerZero 7

There is an easier way to do this, however. LayerZero has provided a setTrustedRemoteAddress function in their implementation that helps with the formatting of the message. All that needs to be inputted is the LayerZero chain ID (_remoteChainId) and the address you want the contract to trust on the destination/remote chain (_remoteAddress).

LayerZero 8

Now use Remix to ensure that both of your contracts trust each other. You will have to do this on both contracts that you have deployed. To switch between contracts on different chains, connect to your desired network through MetaMask. Make sure that you are in the Injected Provider environment and that the contract is still “SimpleGeneralMessage”. Then take the address of the destination contract, and paste it into the At Address input.

To add trusted remote addresses, find the setTrustedRemoteAddress function within the SimpleGeneralMessage contract and open it.

When you are on Moonbase Alpha, set the _remoteChainId as the LayerZero chain ID of the other EVM TestNet that you chose (the LayerZero chain ID can be found in their documentation or in the table above). Set the _remoteAddress as the address of the contract you deployed on the other EVM TestNet. Afterwards, transact and confirm in MetaMask.

When you are on the alternate EVM TestNet, set the _remoteChainId as Moonbase Alpha’s LayerZero chain ID (10126). Set the _remoteAddress as the address of the contract you deployed on Moonbase Alpha. Finally, transact and confirm in MetaMask.

Deploy and Run Transaction

In this section you should have sent two transactions on two chains to set trusted remote addresses in both contracts. Afterwards, you should be good to send transactions between the connected contracts.

Sending a Cross Chain Message from Moonbase with LayerZero

To send a cross-chain message with an automatic destination chain transaction, an additional gas payment must be sent with the LayerZero endpoint message to offset the destination chain’s gas cost. This additional gas payment comes in the form of the native currency (GLMR, ETH, etc) that you include in the transaction, set as the value of the message.

For this post, the way to estimate the cross-chain transaction gas fee is out of scope. For simplicity on a TestNet, it’s best to send a large amount, such as 50000000000000000 Gwei of the native currency, with the cross chain message. If there is an amount left over, it may be refunded if deemed inexpensive enough to do so.

Now you can use the Remix interface. This example is going to send a cross-chain message to the Fantom TestNet, but you can substitute the gas value and chain name for whichever EVM you desire. Check the following things:

  • The environment is Injected Provider – Web3 on network 1287 (Moonbase Alpha)You have substantial funds in your wallet from the faucet to cover both the transaction cost and the DEV included for destination chain gas
  • You have the gas fee as 50000000000000000 Gwei in the previous step placed in the value input
  • Put a short message of your choice in the message input of the sendMessage call (in this case “gm”)
  • Put the destination chain’s layer zero ID in the destChainId input of the sendMessage call. Since you’re sending the transaction on TestNets, the ID should be above 10000

Once this is all done, transact the execution and confirm it in MetaMask.

sendMessage

Tracking Cross Chain Messages

After sending your transaction, you should be able to go into the Moonbase Alpha block explorer to take a look at the transaction using its transaction hash. If everything went well, it should be confirmed, and you’ll be able to see traces of the input of your transaction at the very bottom when viewing it as UTF-8.

Moonbase Alpha Block Explorer

In a typical transaction, the status and data of the transaction would be visible on a single page on a single explorer. But, since this is cross-chain messaging, there are really two EVM transactions happening on two chains.

If everything goes smoothly, the transaction will be approved and you will be able to see the lastMessage updated in the origin chain from the successful cross-chain transaction! If it doesn’t automatically update, don’t worry. On average, it takes about 30 seconds to a minute for a transaction to go through.

If you want to see the message stored in the contract, you can do so through Remix. First, connect to the destination network through MetaMask. Make sure that you are in the Injected Provider environment and that the contract selected is still “SimpleGeneralMessage”. 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 last message.

Remix At Address

If you don’t see the result of the message, LayerZero has an explorer to track transactions, which you can use to track the status of the cross-chain message with the transaction hash of the first origin chain message.

LayerZero Explorer

Learn More About Connected Contracts

Moonbeam’s vision for an interoperable hub of networks doesn’t stop here. Learn more on the LayerZero website, including how to send cross-chain messages in the LayerZero docs. Read how connected contracts have positioned Moonbeam as the leader in blockchain interoperability.

If you are interested in Moonbeam and want to learn more, subscribe to the newsletter and follow us on socials (links in the page header).

 

Moonbeam Team

Author Moonbeam Team

More posts by Moonbeam Team