Developers building with LayerZero sometimes struggle to understand the protocol, which can lead to critical security issues. We present a quick checklist that helps to avoid the most common mistakes.
LayerZero is a protocol designed for message passing across chains. It enables the realization of various cross-chain applications, be it a cross-chain decentralized exchange or a multi-chain yield aggregator. Currently, LayerZero supports Ethereum and EVM-compatible chains like Binance Smart Chain, Avalanche, Polygon, Moonbeam, Arbitrum, Optimism, Fantom, Swimmer, DFK, and Harmony.
LayerZero enables the realization of various cross-chain applications, be it a cross-chain decentralized exchange or a multi-chain yield aggregator. Currently, LayerZero supports Ethereum and EVM-compatible chains like Binance Smart Chain, Avalanche, Polygon, Moonbeam, Arbitrum, Optimism, Fantom, Swimmer, DFK, and Harmony.
1. Set the correct Endpoint address
Messages in LayerZero are sent and received by LayerZero Endpoint smart contracts; any blockchain that wants to communicate with other blockchains via the protocol must have this particular contract deployed.
Your LayerZero application needs to know the address of the Endpoint contract on its respective blockchain to send and receive messages cross-chain.
Naturally, the address where this contract is deployed is different on each supported chain. So make no mistake here and remember to change the Endpoint address accordingly when deploying contracts to various chains or from testnet to mainnet. Addresses for mainnets can be found here and for testnets here.
2. Verify the message origin carefully
To receive cross-chain messages, your contract must implement the lzReceive function; this function is called by the Endpoint each time it has a new message for your contract.
Now there are two levels of message authentication. Anyone can call this function: you need to check that the sender of this transaction is the Endpoint contract. This is the first and obvious thing to do.
However, on the source chain, anyone can send a cross-chain message to your address on your chain – the Endpoint will deliver this message to your contract. Therefore, the second layer of authentication should be built to verify the message sender on the source chain.
For example, suppose you are building a cross-chain token between Ethereum and BSC. In that case, lzReceive on the Ethereum smart contract is called by the Ethereum Endpoint, and the message should originate only from the BSC token smart contract on the BSC.
require(_msgSender() == address(lzEndpoint)); // on dest chain require(_srcAddress == bscTokenAddress); // on src chain
When building a cross-chain dApp, it makes sense to have a trusted address(es) for each chain and retain the ability of the owner to change them.
3. Message from untrusted address must not be executed
Make sure that under no circumstances a message from an untrusted remote address (on a source chain) can be executed. To give an example:
Let’s say that you’re already building a LayerZero cross-chain dApp. Most likely, you have already built a logic for storing and rerunning failed messages (i.e., messages that are received and verified, but their execution somehow fails; they may be out-of-gas exceptions, etc.). It can happen that anyone can rerun a stored failed message with no checks. The owner of the contract, you, also has the ability to change the trusted remote address.
Now imagine that your contract receives a message from a trusted address, but it fails on execution; it is stored. However, something bad happens with the trusted contract on the source chain, so you change the trusted address. We have now arrived at a situation where anyone can execute the failed message that came from an untrusted remote address.
We recommend doing the appropriate checks every time before the execution of the message.
4. Different contracts on different chains
You may need different contracts on different chains. This is obvious once you think about it a bit.
For example, if you are developing a cross-chain wrapped Ether transferable through the LayerZero protocol, the contract on the Ethereum chain must have functions to deposit and withdraw (between ETH and your wrapped ETH cross-chain token) in addition to send and receive functions.
Meanwhile, contracts on the other chains should be different; they should have the necessary send and receive functions for cross-chain interoperability, but they must not have the deposit and withdraw functions. Otherwise, for example, users would be able to deposit 10 BNB on the BSC, send the wrapped asset to Ethereum, and withdraw 10 ETH there.
5. LayerZero example contracts
LayerZero provides many examples of cross-chain contracts. In particular, there are various cross-chain tokens as well as a basic template for LayerZero applications: LzApp and NonblockingLzApp. The LayerZero documentation even encourages developers to use these templates to implement LayerZero messaging in their contracts.
We are proud to say that we audited these example contracts and that we, therefore, believe they are safe. We encourage you to take the time to review them to see if they can be of any use to you. However, use them wisely and be cautious when building on top of them.
6. Custom Relayer
To function properly and deliver messages cross-chain, the LayerZero protocol relies on two parties: the Oracle and the Relayer. The only requirement is that these entities are independent, so they cannot conspire with each other.
All user applications will use the Relayer provided by LayerZero by default. However, if you want to achieve uncompromising security, you can develop your own Relayer. This way, you can be sure that the two necessary parties will always be independent.
That’s it – 6 checks for developing dApps with LayerZero. We hope you found this post useful and that it helps you in building your own applications.