LayerZero is a blockchain interoperability protocol (sometimes called an “omnichain” protocol) that allows different blockchains to send messages, data, tokens etc between each other in a secure, flexible, and trust-minimized way.
How It Works
- Endpoints: On each supported blockchain, there is a smart contract (called a “LayerZero Endpoint”) that acts as the entry/exit point for messages.
- Ultra-Light Nodes (ULNs): Rather than full or heavy clients or having to trust a single validator network, LayerZero uses ULNs plus external oracles + relayers to verify whether a message from one chain is valid before executing it on another chain.
- Oracles & Relayers: These are different off-chain components:
• The oracle supplies block headers from source chains.
• The relayer supplies proofs that specific transactions or events happened.
Both are required in the typical message flow so that neither alone can fake a message. This separation is part of the “trust-minimized” architecture. - Modular Security: Applications built on LayerZero (sometimes called OApps) can configure their own security parameters (which oracle/relayer or verifier networks to use, how to set finality, etc.) rather than being forced into a one-size-fits-all bridge.
- Omnichain Token Standards: It defines standards like OFT (Omnichain Fungible Tokens) and ONFT (Omnichain Non-Fungible Tokens) to help tokens move across chains more natively.
Basic Flow:
oapp ➡️ Executor ➡️ send and receive messages
To explore messages, use the LayerZero Explorer: https://layerzeroscan.com/
Introduction to LayerZero Security Checklist
After looking for latest resources and documentation to check the security and weak points of the LayerZero integration against a L1, didn’t find much detailed information regardless these documents:
- Two years old security checklist: https://github.com/ComposableSecurity/SCSVS/blob/master/2.0/0x300-Integrations/0x304-I4-Cross-Chain.md#security-requirements-for-integration-with-layerzero
- Integration checklist: https://docs.layerzero.network/v2/developers/evm/technical-reference/integration-checklist
With that, I decided to make a much more extensive practical guideline to check what is important not to miss and preserve the security of the protocol, users and tokens.
Advanced Security Checklist
1. Check confirmations, requiredDVNCount and requiredDVNs
MUST match on sending and receiving parts!
Example: Transferring USDC from Base to Ethereum: requiredDVNnames
: [Nethermind, Stargate] and confirmations: [10]
2. Number of decimal checking: 6 by default
Mismatch in the number of decimal between two tokens could impact on financial loss.
Seems OFT handles that in the following way:
/** * @title OFTCore * @dev Abstract contract for the OftChain (OFT) token. */ abstract contract OFTCore is IOFT, OApp, OAppPreCrimeSimulator, OAppOptionsType3 { /** * @dev Constructor. * @param _localDecimals The decimals of the token on the local chain (this chain). * @param _endpoint The address of the LayerZero endpoint. * @param _delegate The delegate capable of making OApp configurations inside of the endpoint. */ constructor(uint8 _localDecimals, address _endpoint, address _delegate) OApp(_endpoint, _delegate) { if (_localDecimals < sharedDecimals()) revert InvalidLocalDecimals(); decimalConversionRate = 10 ** (_localDecimals - sharedDecimals()); } [...] /** * @dev Retrieves the shared decimals of the OFT. * @return The shared decimals of the OFT. * * @dev Sets an implicit cap on the amount of tokens, over uint64.max() will need some sort of outbound cap / totalSupply cap * Lowest common decimal denominator between chains. * Defaults to 6 decimal places to provide up to 18,446,744,073,709.551615 units (max uint64). * For tokens exceeding this totalSupply(), they will need to override the sharedDecimals function with something smaller. * ie. 4 sharedDecimals would be 1,844,674,407,370,955.1615 */ function sharedDecimals() public view virtual returns (uint8) { return 6; }
🚧
Seems that
sharedDecimals()
must be overloaded by the final OFT contract to avoid problems.
3. Multiple OFT adapters
⚠️ Can only have 1 per chain ⚠️
DANGER section: https://docs.layerzero.network/v2/developers/evm/technical-reference/integration-checklist#token-bridging-guidelines
⛔ DANGER ⛔
There can only be one OFT Adapter used in an OFT deployment. Multiple OFT Adapters break omnichain unified liquidity by effectively creating token pools.
If you create OFT Adapters on multiple chains, you have no way to guarantee finality for token transfers due to the fact that the source chain has no knowledge of the destination pool’s supply (or lack of supply). This can create race conditions where if a sent amount exceeds the available supply on the destination chain, those sent tokens will be permanently lost.
From OFTAdapter.sol:
/** * @title OFTAdapter Contract * @dev OFTAdapter is a contract that adapts an ERC-20 token to the OFT functionality. * * @dev For existing ERC20 tokens, this can be used to convert the token to crosschain compatibility. * @dev WARNING: ONLY 1 of these should exist for a given global mesh, * unless you make a NON-default implementation of OFT and needs to be done very carefully.
4. Tokens integration
For new tokens, inherit from OFT
or ONFT
.
⚠️ For existing tokens, use OFTAdapter
or ONFTAdapter
. ⚠️
For non-EVM tokens, select the correct VM from the navbar and see the equivalent sections.
5. Risk of centralization
Two things to keep in mind to minimize risk of centralization:
- Owners are EOA instead of multisig contracts: Owner of the contract should be a multisig wallet with at least 3 independent signers
- requiredDVNs is low: There should be at least 2 DVNs, but more is recommended.
6. OFTAdapter lossless transfers by default
From OFTAdapter.sol:
* @dev WARNING: The default OFTAdapter implementation assumes LOSSLESS transfers, ie. 1 token in, 1 token out. * IF the 'innerToken' applies something like a transfer fee, the default will NOT work... * a pre/post balance check will need to be done to calculate the amountSentLD/amountReceivedLD.
Overload _credit()
/ _debit()
methods on those cases.
7. setPeer()
_oapp uses setPeer()
to whitelist blockchains, interactions, etc. It must be setup!!! Use this to check easily:
npx hardhat lz:oapp:peers:get --oapp-config layerzero.config.ts
⚠️ On the other side, if a threat actor calls setPeer()
with a malicious OApp
address, it can mint tokens and own completely your implementation ⚠️
8. setSendLibrary() / setReceiveLibrary()
If an OApp had NOT called setSendLibrary
or setReceiveLibrary
, the LayerZero Endpoint will fallback to the default configuration, which may be different than the MessageLib you have configured.
9. Lack of logic to block or revert TXs
⚠️ The protocol has to implement it in their contracts.
References
- Awesome intro: https://docs.layerzero.network/v2/developers/evm/oft/quickstart
- How to send a message to the endpoint: https://docs.layerzero.network/v1/developers/evm/evm-guides/send-messages
- Deployment templates: https://www.npmjs.com/package/@layerzerolabs/solidity-examples?activeTab=code
- OApp Security Stack and Executor Configuration: https://docs.layerzero.network/v2/developers/evm/protocol-gas-settings/default-config
Annex
- ULN: Ultra Light Node
- DVN: Decentralized Verifier Networks
- OFT: Omnichain Fungible Token
- ONFT: Omnichain Non-Fungible Token
- OAPP: Omnichain App
Other tests to perform for better coverage:
- Send a message through the CLI / low-level
- Check parameters, origin, destination addresses spoofing and NULL addresses
- Try also fuzzing and invariant testing through foundry in a local deployment
No responses yet