Skip to main content
The OApp Standard provides developers with a generic message passing interface to send and receive arbitrary pieces of data between contracts existing on different blockchain networks. How exactly the data is interpreted and what other actions they trigger, depend on the specific OApp implementation.

Quickstart

Example

For the step-by-step instructions on how to build, deploy and wire a Solana OApp, view the Solana OApp example.

Scaffold

Spin up a new Solana OApp project (based on the example) in seconds:
Specify the directory, select OApp (Solana) and proceed with the installation. The example contains a string-passing OApp that works across Solana and EVM. Follow the provided README instructions to deploy the example and make your first crosschain message between Solana and an EVM chain. The following sections will highlight the several code excerpts of the Solana OApp that are essential to it functioning.

Developing Solana OApps vs EVM OApps

Due to VM and programming paradigm differences, developing OApps on Solana works differently from doing the same on EVM chains. On EVM chains, OApps can simply inherit the OApp contract standard to unlock crosschain functionality. On Solana, there is no similar inheritance. The table below outlines the main differences when developing:

High-level message flow

Due to the need for the inclusion of accounts, the flow of an inbound message on Solana is different from that on an EVM chain:

Accounts and Instructions Discovery

The discovery of accounts and instructions is handled by lz_receive_types_v2.

What lz_receive_types_v2 introduces

lz_receive_types_v2 has the following key features:
  • Support for multiple ALTs: Expands the capacity for account inclusion by allowing multiple Address Lookup Tables, increasing the number of accounts accessible within a transaction.
  • Compact and flexible account reference model: Implements the AddressLocator, enabling OApps to reference accounts with a leaner, more efficient structure while maintaining adaptability for future upgrades.
  • Context account from the Executor: Provides runtime metadata at execution, ensuring that OApps have contextual awareness without requiring redundant account fetching or manual setup. In the code, this is referred to as the ExecutionContext.
  • Explicit support for multiple EOA signers: Enables dynamic data account initialization by allowing multiple externally owned accounts (EOAs) to participate in signing and setup. This improves flexibility for multi-party or multi-step workflows.
  • Multi-instruction execution model: Empowers OApps to compose complex workflows within a single atomic transaction, combining several instructions while preserving consistency and rollback guarantees.

How lz_receive_types_v2 works

Overall, lz_receive_types_v2 has the following execution flow:
  1. lz_receive_types_info
    • requires two accounts in this exact order (must not be changed):
      • oapp_account - the OApp identity/account. In the code examples here, the oapp_account is the store account.
      • lz_receive_types_accounts - PDA derived with seeds = [LZ_RECEIVE_TYPES_SEED, &oapp_account.key().to_bytes()].
    • returns (version, versioned_data)
      • version: u8 — A protocol-defined version identifier for the LzReceiveType logic and return type, starting from 2.
      • versioned_data: Any — A value of type Any, representing a version-specific structure. The Executor decodes this payload based on the version and uses it to construct the full set of accounts needed to invoke lz_receive_types_v2 (LzReceiveTypesV2Accounts).
  2. lz_receive_types_v2 - this instruction is called with LzReceiveTypesV2Accounts as the supplied accounts and returns:
    • the context_version
    • ALTs used (if any)
    • The full list of instructions for lz_receive
  3. build and submit transaction - now the Executor can prepare the full transaction and submit it based on what was returned by lz_receive_types_v2. Before submitting, the Executor will also prepend (pre_execute) and append (post_execute) instructions to prepare the execution context and ensure safety:
    • pre_execute - Initializes a fee-limited execution context at the start of the transaction (allowing up to two ComputeBudget instructions), records the payer’s starting balance, and ensures a matching PostExecute will end the transaction.
    • post_execute - validates pairing with PreExecute, enforces the fee limit against the payer’s balance change, checks signer invariants, and resets the context.
All the instructions above are called by the Executor. As the OApp developer, you only need to ensure that you implement the required instructions and PDAs in your OApp program.

Required PDAs

The following are the PDAs that are required for a Solana OApp. Note that except for the Store PDA, the PDA seeds are not customizable.
Peer Config PDAs are initialized by the wiring step.

Required Instructions

Initialize the OApp PDA

This init instruction initializes 2 required PDAs: OApp Store and lz_receive_types_accounts.
Key points
  • You must call oapp::endpoint_cpi::register_oapp to register your OApp
  • Registration is one-time; afterwards the Executor knows this Store PDA = OApp.
  • You do not pass a “trusted remote” mapping here—that’s what the Peer PDAs enforce.
  • You can extend your OApp’s store PDA with other fields required by your use case.

Implement lz_receive — business logic + Endpoint::clear

The lz_receive instruction is where your OApp’s business logic is defined and also where the call to Endpoint::clear is made. Additionally, it is also where compose messages are handled, if implemented.
Rules of thumb
  • Call clear() before touching any user state—this burns the nonce and prevents re-entry.
  • Use ctx.remaining_accounts instead of hard-wiring anything—keeps lz_receive_types_v2 and lz_receive perfectly in sync.
  • Don’t forget is_signer: true zero-pubkey placeholders for ATA init or rent payer.
Security Reminders
  • Validate the Peer account first (constraint = params.sender == peer.address).
  • Store the Endpoint ID inside state (store.endpoint_program) and assert it every CPI.

Implement lz_receive_types_v2

Define LzReceiveTypesAccounts anywhere in your state module tree:
store here is the OApp account referred to as oapp_account in the flow description.
Create an lz_receive_types_info instruction:
Implement lz_receive_types_v2:
Ensure that you have registered the new instruction handlers in the program module in your lib.rs:

OApp-Specific Message Codec

Since at its core, the OApp Standard simply gives you the interface for generic message passing (raw bytes), you need to implement for yourself how the raw bytes are interpreted. In the Solana OApp example and also in the OFT implementation, this logic is encapusalated in a msg_codec.rs file.
Key points
  • Every OApp would have its own Message Codec implementation
  • The above Message Codec example involves an OApp that expects the message to contain only a length and the actual string
  • If sending across VMs, ensure the codec on the other VM matches.

Gotchas & common errors