Skip to main content
Crosschain composability enables multi-step workflows that span multiple chains. LayerZero V2 supports composing follow-up actions as separate messages, improving reliability and flexibility for complex flows. This page explains how to implement composability for Solana programs using the lz_compose_types_v2 typed discovery flow.

Prerequisites

Why composability matters

For a clear, high-level explanation of both the how and the why behind composed messaging, see the conceptual guide: Omnichain Composers.

How composability works

A message may or may not contain a compose message. When it does, we refer to it as a composed message. The following is the workflow for a composed message when the destination chain is Solana. A composed flow is split into distinct steps across messages:
  1. Sending Application: The sender OApp sends a message with a composeMsg attached. For composed messages to Solana, the recipient address should be set to the Composer PDA.
  2. Receiving Application: The destination OApp’s lz_receive is executed, and since there is a composeMsg, a send_compose CPI is made to the Endpoint program to send the compose message to the Composer PDA.
  3. Composer Application: The Executor calls lz_compose on the Composer Program (the program that owns the Composer PDA).
A Composer is the smart contract that is responsible for executing a compose message.
This separation reduces call-stack complexity and allows non-critical reverts in the composed step without rolling back the initial receive.

Installation

You can either extend your existing OApp or OFT program to turn it into a Composer, or create a standalone program to serve as the Composer. In this example, we will scaffold a basic Solana OApp, and extend it to also be a Composer. Use the CLI to scaffold a Solana OApp project you can extend with compose:

Usage

The accounts and instructions needed are similar to those outlined in lz_receive_types_v2:
  • composer - a PDA that can be used to store static addresses needed for lz_compose execution, and more importantly, whose address is used as the ‘Composer address’
  • LzComposeTypesAccounts - a PDA that contains the accounts needed to call lz_compose_types
  • lz_compose_types_info- an instruction that provides versioning info that helps the Executor understand how to proceed with lz_compose_types/lz_compose_types_v2
  • lz_compose_types_v2 - an instruction that returns the list of accounts and execution plan needed to execute lz_compose
  • lz_compose - the instruction that contains the actual business logic that must be executed for a composeMsg

lz_compose_types_v2

lz_compose_types_v2 achieves similar goals to lz_receive_types_v2 (supports more accounts, multiple instructions, multiple signers) but for compose messages. The flow is similar: discover versioned accounts via lz_compose_types_info, return a compact, ALT-aware execution plan via lz_compose_types_v2, then the Executor builds and submits the transaction that includes the lz_compose instruction. Note that for this example, we will assume that the Composer is integrated into the OApp program itself. Whether you adopt this design as well depends on your use case. You may also choose to have a standalone Composer program.

Implementing lz_compose_types_v2

Composer PDA

Define the composer PDA seed in your program’s lib.rs:
Define the struct of the PDA that will hold the static addresses or fields that will be used in lz_compose_types later:

LzComposeTypesAccounts

Define the struct of PDA that holds versioned compose-type discovery data, e.g. LzComposeTypesAccounts:
Initialize the LzComposeTypesAccounts PDA in your init instruction:
  • If you need the Composer PDA to be namespaced by a certain identifier, you can add it into its seeds. The convention is to also store that value in the Composer PDA itself, which requires amending its struct that was defined earlier.
  • The above assumes that the existing init instruction that was already responsible for initializing lz_receive_types_accounts is extended to also initialize lz_compose_types_accounts
  • The init function can be arbitrarily named, as long as it is called

lz_compose_types_info

Create an lz_compose_types_info instruction that returns the version and the accounts needed to construct lz_compose_types_v2:

lz_compose_types_v2

Implement lz_compose_types_v2 and return a compact execution plan including exactly one Instruction::LzCompose:

Composed Message Execution Options

Longer composed flows increase the cost of executing lz_receive on the destination and the follow-up lz_compose call. You must set sufficient gas and value in your Message Options for both steps.
  • Add extra gas for the lzReceive step:
  • Also set gas/value for the composer execution:
Parameters:
  • _index: the index of the composeMsg.
  • _gas: gas/compute budget for the destination execution.
  • _value: native value forwarded with the call if needed.
If insufficient limits are provided, execution will not proceed and a manual retry with higher limits will be required. For an overview of how options work, see Message Options.

Composing an OFT

Sending an OFT from Solana with a compose message

The OFT program (Solana) supports attaching an optional compose_msg to a crosschain send.

Handling a compose message for an OFT on Solana

The reference Solana OFT program’s lz_receive already includes a call to Endpoint::send_composefor when a message contains a compose_msg :
The OFT Program comes with a default compose_msg_codec that’s used for sending and receiving composed messages:
The reference Solana OFT program, however, does not include a dedicated lz_compose handler as the decision of whether to extend the OFT program or have a separate Composer program is for the developer to decide. If your product needs compose flows, use lz_compose_types_v2 to describe execution and choose one of these patterns:
  • Create a separate composer program (recommended for separation of concerns).
  • Modify the OFT program to also act as the composer (tighter coupling).

Composing an OApp

As shown above in “Composing an OFT”, the send_compose call is issued from within lz_receive after Endpoint::clear. Custom OApps follow the same pattern: detect a compose payload, then forward it via send_compose to your Composer PDA (or destination PDA). Ensure your discovery accounts stay in sync with the execution plan described by lz_compose_types_v2. For general receive flow details, see OApp lz_receive — business logic + Endpoint::clear.

Execution notes and troubleshooting

  • Ensure lz_receive_types and lz_compose_types PDAs are initialized and kept in sync with your handlers.
  • Always call Endpoint::clear before touching user state in lz_receive.
  • The Executor enforces a fee-limited execution context; keep instruction counts and compute within limits.
  • If account ordering mismatches, you may see AccountNotWritable/InvalidProgramId—double-check discovery (return of lz_compose_types) vs execution handler (lz_compose) expectations.

Next steps