Skip to main content
Crosschain composability has long been a goal for developers building advanced, interconnected decentralized applications. LayerZero V2 introduces horizontal composability — a concept that empowers developers to spread out crosschain calls into multiple, discrete steps.

Prerequisites

Before diving into LayerZero V2 Horizontal Composability, it’s essential to have a foundational understanding of the following concepts: Having familiarity with these topics will enable a smoother comprehension of the concepts discussed.

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.
All function calls in the stack execute atomically. This means that either all operations succeed, or the entire transaction reverts if any single operation fails.
Vertical composability can present potential Atomicity Issues in crosschain interactions:
  • If an operation on one contract fails, it can produce unintended reversions or inconsistencies across the entire stack. This limits the ability to have instant finality guarantees when receiving crosschain messages.
In crosschain contracts, you should minimize the impact of potential message failure by performing only one action per message.

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:
  1. Sending Application Logic: The sender application uses the OApp._lzSend() function to dispatch a crosschain message.
  2. Receiving Application Logic: A destination application receives the message from EndpointV2.lzReceive(), does some state change, and then calls EndpointV2.sendCompose() to send a new message to the target composer.
    Crucially, either the sender or receiver should construct an additional message directed at a composer, 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.
  3. 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 in lzReceive().
This workflow creates a way for delivering some critical state change information in separate steps, reducing the complexity of the call stack and enabling non-critical reverts on the destination chain.

Horizontally Composing Supported Contracts

Implementing horizontal composability involves crafting composed messages to expand on existing crosschain contract workflows. By default, both the OFT 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 a composer contract, you can install the OApp package to an existing project:
Then add to your 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 a composer contract, simply inherit the IOAppComposer.sol interface from the oapp-evm package:

Composed Message Execution Options

Longer composer 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:
Besides the increase cost of 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.
Review the existing documentation on Message Execution Options to learn more.
If not enough gas limit or msg.value is provided, the EndpointV2.lzReceive() will not execute, and will need to be manually retried either via the LayerZero Scan explorer, or manual contract call.

Composing an OFT / ONFT

Both the OFT and ONFT support sending a composed message along with the crosschain token transfers.
When calling 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:
Below is how the ONFT721Core contract encodes the composeMsg and sends it to the composer:
This means that in your composer application, you can decode the 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 a composeMsg, 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.
ONFT Composed Message (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:
OFTComposeMsgCodec Functions: The ONFTComposeMsgCodec provides the same functions except amountLD() (since NFTs don’t have amounts).
The composeFrom field is useful for authorization checks or refunds back to the source chain.
For example, see the following composer example which mocks an ERC20 token swap after receiving from an OFT:

Composing an OApp

  1. Source OApp: Sends a crosschain message via _lzSend() to a destination chain.
  2. Destination OApp: Receives the crosschain message via _lzReceive() and initiates composed calls using EndpointV2.sendCompose():
  1. Composer: Contracts that implement business logic to handle incoming composed messages via EndpointV2.lzCompose().