Skip to main content
Version: Endpoint V2

OFT 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.

Because OFT inherits the base OApp implementation, you can also send composed messages within your OFT receive logic.

Composed Light Composed Dark

If you are not familiar with how OApp Composing works, review that section first before continuing here.

Composing an OFT

The OFT Standard comes pre-packaged with methods for delivering composed calls to the destination OFT contract for handling.

  1. Source OFT: The Source OFT specifies in the send call a composed message in bytes for delivering to. You can think of this the same as how _lzSend sends arbitrary bytes to a destination, which the destination contract uses in the _lzReceive business logic.

  2. Destination OFT(s): When the send call is received by the destination OFT, the internal _lzReceive function in OFTCore.sol handles the delivery of tokens along with the composed call.

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

Sending Token

When sending a token from source to destination, the caller has the option to specify an additional composeMsg in bytes.

/**
* @dev Struct representing token parameters for the OFT send() operation.
*/
struct SendParam {
uint32 dstEid; // Destination endpoint ID.
bytes32 to; // Recipient address.
uint256 amountLD; // Amount to send in local decimals.
uint256 minAmountLD; // Minimum amount to send in local decimals.
bytes extraOptions; // Additional options supplied by the caller to be used in the LayerZero message.
bytes composeMsg; // The composed message for the send() operation.
bytes oftCmd; // The OFT command to be executed, unused in default OFT implementations.
}

function send(
SendParam calldata _sendParam,
MessagingFee calldata _fee,
address _refundAddress
) external payable virtual returns (MessagingReceipt memory msgReceipt, OFTReceipt memory oftReceipt) {}

Depending on your implementation, this composed message field can be used to pass any arbitrary information as bytes along with your token to the destination address.

Composed Message Execution Options

You will need to pass both an lzReceiveOption and lzComposeOption as either your enforced or extra options for this call to succeed.

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 values 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.

Sending Compose

By default, the destination OFT's _lzReceive method will check if the the message is composed, and then deliver those arbitrary bytes to the specified toAddress:

// @dev Internal function to handle the receive on the LayerZero endpoint.
if (_message.isComposed()) {
// @dev Proprietary composeMsg format for the OFT.
bytes memory composeMsg = OFTComposeMsgCodec.encode(
_origin.nonce,
_origin.srcEid,
amountReceivedLD,
_message.composeMsg()
);

// @dev Stores the lzCompose payload that will be executed in a separate tx.
// Standardizes functionality for executing arbitrary contract invocation on some non-evm chains.
// @dev The off-chain executor will listen and process the msg based on the src-chain-callers compose options passed.
// @dev The index is used when a OApp needs to compose multiple msgs on lzReceive.
// For default OFT implementation there is only 1 compose msg per lzReceive, thus its always 0.
endpoint.sendCompose(toAddress, _guid, 0 /* the index of the composed message*/, composeMsg);
}

As shown in the sendCompose comments, the base OFT implementation only allows for 1 composed message per lzReceive call.

To add additional composed calls, you will need to override the _lzReceive method and add custom composed logic.

Receiving Compose

The receiving address of the cross-chain token transfer will need to implement custom business logic to handle the composed message, for example, consider this mock contract that swaps an inbound OFT for an ERC20:

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

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import { IOAppCore } from "@layerzerolabs/lz-evm-oapp-v2/contracts/oapp/interfaces/IOAppCore.sol";
import { IOAppComposer } from "@layerzerolabs/lz-evm-oapp-v2/contracts/oapp/interfaces/IOAppComposer.sol";
import { OFTComposeMsgCodec } from "@layerzerolabs/lz-evm-oapp-v2/contracts/oft/libs/OFTComposeMsgCodec.sol";

/// @title SwapMock Contract
/// @dev This contract mocks an ERC20 token swap in response to an OFT being received (lzReceive) on the destination chain.
/// @notice The contract is designed to interact with LayerZero's Omnichain Fungible Token (OFT) Standard,
/// allowing it to respond to cross-chain OFT mint events with a token swap action.
contract SwapMock is IOAppComposer {
using SafeERC20 for IERC20;

IERC20 public erc20;
address public immutable endpoint;
address public immutable oApp;

/// @notice Emitted when a token swap is executed.
/// @param user The address of the user who receives the swapped tokens.
/// @param tokenOut The address of the ERC20 token being swapped.
/// @param amount The amount of tokens swapped.
event Swapped(address indexed user, address tokenOut, uint256 amount);

/// @notice Constructs the SwapMock contract.
/// @dev Initializes the contract.
/// @param _erc20 The address of the ERC20 token that will be used in swaps.
/// @param _endpoint LayerZero Endpoint address
/// @param _oApp The address of the OApp that is sending the composed message.
constructor(address _erc20, address _endpoint, address _oApp) {
erc20 = IERC20(_erc20);
endpoint = _endpoint;
oApp = _oApp;
}

/// @notice Handles incoming composed messages from LayerZero.
/// @dev Decodes the message payload to perform a token swap.
/// This method expects the encoded compose message to contain the swap amount and recipient address.
/// @param _oApp The address of the originating OApp.
/// @param /*_guid*/ The globally unique identifier of the message (unused in this mock).
/// @param _message The encoded message content in the format of the OFTComposeMsgCodec.
/// @param /*Executor*/ Executor address (unused in this mock).
/// @param /*Executor Data*/ Additional data for checking for a specific executor (unused in this mock).
function lzCompose(
address _oApp,
bytes32 /*_guid*/,
bytes calldata _message,
address /*Executor*/,
bytes calldata /*Executor Data*/
) external payable override {
// Perform checks to make sure composed message comes from correct OApp.
// In the case of OFT composing the "oApp" should be OFT address.
require(_oApp == oApp, "!oApp");
require(msg.sender == endpoint, "!endpoint");

// Extract the composed message from the delivered message using the MsgCodec
bytes memory _composeMsgContent = OFTComposeMsgCodec.composeMsg(_message);
// Decode the composed message, in this case, the uint256 amount and address receiver for the token swap
(uint256 _amountToSwap, address _receiver) = abi.decode(_composeMsgContent, (uint256, address));

// Execute the token swap by transferring the specified amount to the receiver
erc20.safeTransfer(_receiver, _amountToSwap);

// Emit an event to log the token swap details
emit Swapped(_receiver, address(erc20), _amountToSwap);
}
}

You will need to use the OFTComposeMsgCodec to extract the composeMsg from the overall message, before decoding it.

Further Reading