Prerequisites
Before diving into LayerZero V2 Horizontal Composability, it’s essential to have a foundational understanding of the following concepts:- Solidity Interfaces: Knowledge of defining and implementing interfaces in Solidity.
- Solidity Interface Composability: Grasping how interfaces facilitate composability between contracts.
Workflow
LayerZero V2 supports both Vertical and Horizontal Composability within crosschain calls.What is Vertical Composability?
Vertical Composability is the traditional model of composability in blockchain applications, where multiple function calls from different contracts are stacked within a single transaction.What is Horizontal Composability?
Horizontal Composability is an implementation in LayerZero V2 to address the limitations of vertical composability in crosschain interactions. Unlike vertical composability, which relies on a single, linear stack of function calls, horizontal composability allows for multiple, sequential calls across different chains within a single overarching operation. This facilitates the orchestration of complex, multi-step interactions across multiple chains without being constrained by the depth or complexity of a single call stack.How Horizontal Composability Works
LayerZero’s horizontal composability leverages composed messages that are treated as separate, containerized message packets. These packets are processed independently, allowing for more flexible and controlled interactions across chains. Workflow Overview:-
Sending Application Logic: The sender application uses the
OApp._lzSend()function to dispatch a crosschain message. -
Receiving Application Logic: A destination application receives the message from
EndpointV2.lzReceive(), does some state change, and then callsEndpointV2.sendCompose()to send a new message to the target composer.Crucially, either thesenderorreceivershould construct an additional message directed at acomposer, which will handle subsequent operations in a new method,EndpointV2.lzCompose().This dual-message approach ensures that both the immediate and follow-up actions are clearly defined and routed appropriately. -
Composer Application Logic: A composer application receives the composed message in
lzCompose()and does a state change to follow up on the first state changes created inlzReceive().
Horizontally Composing Supported Contracts
Implementing horizontal composability involves crafting composed messages to expand on existing crosschain contract workflows. By default, both theOFT and ONFT standards support horizontally composed calls out of the box.
This allows OFT or ONFT token holders to send tokens crosschain to a trusted composer contract on the destination, and trigger some action on behalf of the token holders (e.g., token swaps, token staking, etc).
For more advanced implementations, you can design complex OApp contracts that have other crosschain composer implications.
Installation
To create acomposer contract, you can install the OApp package to an existing project:
foundry.toml under [profile.default]:
LayerZero contracts work with both OpenZeppelin V5 and V4 contracts. Specify your desired version in your project’s
package.json:Usage
To implement acomposer contract, simply inherit the IOAppComposer.sol interface from the oapp-evm package:
Composed Message Execution Options
Longercomposer messages, which contain more bytes encoded instructions, increase the cost of calling EndpointV2.lzReceive().
Typically, the reason for the gas increase can be found in the additional length being added to your crosschain message, as well as the cost of invoking EndpointV2.sendCompose() inside your OApp._lzReceive() function.
Ensure that when calling OFT.send() and ONFT.send() or your own custom OApp, that you correctly estimate the cost of calling endpoint.sendCompose() and add the additional LzReceiveOption gas limit to your SendParam.extraOptions or OApp specific options argument:
EndpointV2.lzReceive(), you should also take into account the cost of your actual composer.lzCompose(). Similar to lzReceive(), you can specify the gas limit and msg.value the Executor should use when calling the composer contract:
-
_index: Identifies the specific composed call within a batch of composed messages. This allows for distinct execution settings for each call. -
_gas: Specifies the gas limit allocated for the composed call’s execution on the destination chain. Gas requirements may vary across chains due to different opcode costs and gas mechanisms. -
_value: Determines the amount of native currency (e.g., ETH) to be sent alongside the composed call, facilitating payable functions or covering additional costs.
Composing an OFT / ONFT
Both theOFT and ONFT support sending a composed message along with the crosschain token transfers.
send(), specify the composer as the to address, encode a composeMsg based on the composer’s specification, and add a ComposeExecutionOption gas limit and/or msg.value depending on the composer’s needs.
When creating the composeMsg, the OFT / ONFT will already encode specific parameters along with your message for use in the composer.
Below is how the OFTCore and ONFT721Core contracts encode the composeMsg and send it to the composer:
ONFT721Core contract encodes the composeMsg and sends it to the composer:
msg.sender for specific checks, along with the other composer encodings.
Message Encoding Reference
Both OFT and ONFT use a two-step message flow when composing. This section documents the message structures using OFT as the primary example.Source Chain Message
When sending a crosschain transfer with acomposeMsg, the token contract encodes the message for transit. For OFT, this uses OFTMsgCodec:
Composed Message (What Your Composer Receives)
After processing the token transfer in_lzReceive(), the destination contract re-encodes the data and calls endpoint.sendCompose(). This is the message your composer receives in lzCompose().
OFT Composed Message (OFTComposeMsgCodec):
OFT: amountSD vs amountLD
The OFT converts from shared decimals (amountSD, uint64) to local decimals (amountLD, uint256) before calling your composer. Your composer receives the full precision amount in the destination chain’s native token decimals.ONFTComposeMsgCodec):
The ONFT uses a simpler structure without an amount field (since NFTs are unique):
Codec Functions
Import the appropriate codec in your composer contract:
The
ONFTComposeMsgCodec provides the same functions except amountLD() (since NFTs don’t have amounts).
For example, see the following composer example which mocks an ERC20 token swap after receiving from an OFT:
Composing an OApp
-
Source OApp: Sends a crosschain message via
_lzSend()to a destination chain. -
Destination OApp: Receives the crosschain message via
_lzReceive()and initiates composed calls usingEndpointV2.sendCompose():
- Composer: Contracts that implement business logic to handle incoming composed messages via
EndpointV2.lzCompose().