Skip to main content
Version: Endpoint V2

OApp Composing

A composed message refers to an OApp that invokes the LayerZero Endpoint method sendCompose to deliver a composed call to another contract on the destination chain via lzCompose.

Composed Light Composed Dark

This pattern demonstrates horizontal composability, which differs from vertical composability in that the external call is now containerized as a new message packet; enabling the application to ensure that certain receipt logic remains separate from the external call itself.

info

Since each composable call is created as a separate message packet via lzCompose, this pattern can be extended for as many steps as your application needs (B1 -> B2 -> B3, etc).


Composing an OApp

There are 3 relevant contract interactions when composing an OApp:

  1. Source OApp: the OApp sending a cross-chain message via _lzSend to a destination.

  2. Destination OApp(s): the OApp receiving a cross-chain message via _lzReceive and calling sendCompose.

  3. Composed Receiver(s): the contract interface implementing business logic to handle receiving a composed message via lzCompose.

Sending Message

The sending OApp is required to pass specific Composed Message Execution Options (more on this below) for the sendCompose call, but is not required to pass any input parameters for the call itself (however this pattern may be useful depending on what arbitrary action you wish to trigger when composing).

For example, this send function packs the destination _composedAddress for the destination OApp to decode and use for the actual composed call.

/// @notice Sends a message from the source to destination chain.
/// @param _dstEid Destination chain's endpoint ID.
/// @param _message The message to send.
/// @param _composedAddress The contract you wish to deliver a composed call to.
/// @param _options Message execution options (e.g., for sending gas to destination).
function send(
uint32 _dstEid,
string memory _message,
address _composedAddress, // the destination contract implementing ILayerZeroComposer
bytes calldata _options
) external payable returns(MessagingReceipt memory receipt) {
// Encodes the message before invoking _lzSend.
bytes memory _payload = abi.encode(_message, _composedAddress);
receipt = _lzSend(
_dstEid,
_payload,
_options,
// Fee in native gas and ZRO token.
MessagingFee(msg.value, 0),
// Refund address in case of failed source message.
payable(msg.sender)
);
}

Sending Compose

The receiving OApp invokes the LayerZero Endpoint's sendCompose method as part of your OApp's _lzReceive business logic.

The sendCompose method takes the following inputs:

  1. _to: the contract implementing the ILayerZeroComposer receive interface.

  2. _guid: the global unique identifier of the source message (provided standard by lzReceive).

  3. _index: the index of the composed message (used for pricing different gas execution amounts along different composed legs of the transaction).

/// @dev the Oapp sends the lzCompose message to the endpoint
/// @dev the composer MUST assert the sender because anyone can send compose msg with this function
/// @dev with the same GUID, the Oapp can send compose to multiple _composer at the same time
/// @dev authenticated by the msg.sender
/// @param _to the address which will receive the composed message
/// @param _guid the message guid
/// @param _message the message
function sendCompose(address _to, bytes32 _guid, uint16 _index, bytes calldata _message) external {
// must have not been sent before
if (composeQueue[msg.sender][_to][_guid][_index] != NO_MESSAGE_HASH) revert Errors.ComposeExists();
composeQueue[msg.sender][_to][_guid][_index] = keccak256(_message);
emit ComposeSent(msg.sender, _to, _guid, _index, _message);
}

This means that when a packet is received (_lzReceive) by the Destination OApp, it will send (sendCompose) a new composed packet via the destination LayerZero Endpoint.

/// @dev Called when data is received from the protocol. It overrides the equivalent function in the parent contract.
/// Protocol messages are defined as packets, comprised of the following parameters.
/// @param _origin A struct containing information about where the packet came from.
/// @param _guid A global unique identifier for tracking the packet.
/// @param payload Encoded message.
function _lzReceive(
Origin calldata _origin,
bytes32 _guid,
bytes calldata payload,
address, // Executor address as specified by the OApp.
bytes calldata // Any extra data or options to trigger on receipt.
) internal override {
// Decode the payload to get the message
(string memory _message, address _composedAddress) = abi.decode(payload, (string, address));
// Storing data in the destination OApp
data = _message;
// Send a composed message[0] to a composed receiver
endpoint.sendCompose(_composedAddress, _guid, 0, payload);
}
info

The above sendCompose call hardcodes _index to 0 and simply forwards the same payload as _lzReceive to lzCompose, however these inputs can also be dynamically adjusted depending on the number and type of composed calls you wish to make.

Composed Message Execution Options

You can decide both the _gas and msg.value that should be used for the composed call(s), depending on the type and quantity of messages you intend to send.

Your configured Executor will use the _options provided in the original _lzSend call to determine the gas limit and amount of msg.value to include per message _index:

// addExecutorLzComposeOption(uint16 _index, uint128 _gas, uint128 _value)
Options.newOptions()
.addExecutorLzReceiveOption(50000, 0)
.addExecutorLzComposeOption(0, 30000, 0)
.addExecutorLzComposeOption(1, 30000, 0);

It's important to remember that gas costs may vary depending on the destination chain. For example, all new Ethereum transactions cost 21000 wei, but other chains may have lower or higher opcode costs, or entirely different gas mechanisms.

You can read more about generating _options and the role of _index in Message Execution Options.

Receiving Compose

The destination must implement the ILayerZeroComposer interface to handle receiving the composed message.

From there, you can decide any additional composed business logic to execute within lzCompose, as shown below:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.22;

import { ILayerZeroComposer } from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/ILayerZeroComposer.sol";

/// @title ComposedReceiver
/// @dev A contract demonstrating the minimum ILayerZeroComposer interface necessary to receive composed messages via LayerZero.
contract ComposedReceiver is ILayerZeroComposer {

/// @notice Stores the last received message.
string public data = "Nothing received yet";

/// @notice Store LayerZero addresses.
address public immutable endpoint;
address public immutable oApp;

/// @notice Constructs the contract.
/// @dev Initializes the contract.
/// @param _endpoint LayerZero Endpoint address
/// @param _oApp The address of the OApp that is sending the composed message.
constructor(address _endpoint, address _oApp) {
endpoint = _endpoint;
oApp = _oApp;
}

/// @notice Handles incoming composed messages from LayerZero.
/// @dev Decodes the message payload and updates the state.
/// @param _oApp The address of the originating OApp.
/// @param /*_guid*/ The globally unique identifier of the message.
/// @param _message The encoded message content.
function lzCompose(
address _oApp,
bytes32 /*_guid*/,
bytes calldata _message,
address,
bytes calldata
) external payable override {
// Perform checks to make sure composed message comes from correct OApp.
require(_oApp == oApp, "!oApp");
require(msg.sender == endpoint, "!endpoint");

// Decode the payload to get the message
(string memory message, ) = abi.decode(_message, (string, address));
data = message;
}
}

Further Reading

For more advanced implementations of sendCompose and lzCompose:

  • Review the OmniCounter.sol for sending composed messages to the same OApp implementation.
  • Read the OFT Composing section to see how to implement composed business logic into your OFTs.