> ## Documentation Index
> Fetch the complete documentation index at: https://docs.layerzero.network/llms.txt
> Use this file to discover all available pages before exploring further.

# LayerZero V2 OApp Quickstart

> Overview of OApp Quickstart on LayerZero V2. Learn the architecture, features, and how to get started building. LayerZero enables secure crosschain messaging.

The **OApp standard** lets your contract send and receive arbitrary *messages* across chains. With OApp, you can update onchain state on one network and trigger custom business logic on another.

<img src="https://mintcdn.com/layerzero/MzSiOdXt8xlDlEr4/images/learn/ABLight.svg?fit=max&auto=format&n=MzSiOdXt8xlDlEr4&q=85&s=26159ac201830230d42dbab6e67168a9" alt="Diagram showing crosschain messaging between Network A and Network B, with an arrow indicating the message flow via LayerZero Send and Receive" className="block dark:hidden" width="1920" height="517" data-path="images/learn/ABLight.svg" />

<img src="https://mintcdn.com/layerzero/MzSiOdXt8xlDlEr4/images/learn/ABDark.svg?fit=max&auto=format&n=MzSiOdXt8xlDlEr4&q=85&s=57ee89cb8ac22fcdd3d6ba916d27f750" alt="Diagram showing crosschain messaging between Network A and Network B, with an arrow indicating the message flow via LayerZero Send and Receive" className="hidden dark:block" width="1920" height="517" data-path="images/learn/ABDark.svg" />

`OApp.sol` implements the core interface for calling LayerZero's Endpoint V2 on EVM chains. It also provides hookable `_lzSend` and `_lzReceive` methods so you can inject your own business logic:

<img src="https://mintcdn.com/layerzero/VohHXGobQ14zkw24/images/oapp-inheritance-light.svg?fit=max&auto=format&n=VohHXGobQ14zkw24&q=85&s=fe9b90f7c3790e2e50692cfeefba6d29" alt="Class inheritance diagram showing OApp.sol implementing the core interface for LayerZero Endpoint V2, with hookable _lzSend and _lzReceive methods for custom business logic" className="block dark:hidden" width="2051" height="1040" data-path="images/oapp-inheritance-light.svg" />

<img src="https://mintcdn.com/layerzero/VohHXGobQ14zkw24/images/oapp-inheritance-dark.svg?fit=max&auto=format&n=VohHXGobQ14zkw24&q=85&s=6abae8ffae545885a42ae94d847c8e34" alt="Class inheritance diagram showing OApp.sol implementing the core interface for LayerZero Endpoint V2, with hookable _lzSend and _lzReceive methods for custom business logic" className="hidden dark:block" width="2051" height="1040" data-path="images/oapp-inheritance-dark.svg" />

<Tip>
  If your use case only involves crosschain token transfers, consider inheriting the [**OFT Standard**](../oft/quickstart) instead of OApp.
</Tip>

## Installation

To start using LayerZero contracts in a new project, use the LayerZero CLI tool, [**create-lz-oapp**](../../../get-started/create-lz-oapp/start). The CLI tool is an npx package that allows developers to create any omnichain application in \<4 minutes! Get started by running the following from your command line:

```bash wrap theme={null}
npx create-lz-oapp@latest --example oapp
```

This will create an example repository containing both the Hardhat and Foundry frameworks, LayerZero development utilities, as well as the **OApp contract package** pre-installed.

To use LayerZero contracts in an existing project, you can install the **OApp package** directly:

<Tabs>
  <Tab title="npm">
    ```bash wrap theme={null}
    npm install @layerzerolabs/oapp-evm
    ```
  </Tab>

  <Tab title="yarn">
    ```bash wrap theme={null}
    yarn add @layerzerolabs/oapp-evm
    ```
  </Tab>

  <Tab title="pnpm">
    ```bash wrap theme={null}
    pnpm add @layerzerolabs/oapp-evm
    ```
  </Tab>

  <Tab title="forge">
    ```bash wrap theme={null}
    forge init
    ```

    ```bash wrap theme={null}
    forge install layerzero-labs/devtools
    forge install layerzero-labs/LayerZero-v2
    forge install OpenZeppelin/openzeppelin-contracts
    git submodule add https://github.com/GNSPS/solidity-bytes-utils.git lib/solidity-bytes-utils
    ```

    Then add to your `foundry.toml` under `[profile.default]`:

    ```toml wrap theme={null}
    [profile.default]
    src = "src"
    out = "out"
    libs = ["lib"]

    remappings = [
        '@layerzerolabs/oapp-evm/=lib/devtools/packages/oapp-evm/',
        '@layerzerolabs/lz-evm-protocol-v2/=lib/layerzero-v2/packages/layerzero-v2/evm/protocol',
        '@layerzerolabs/lz-evm-messagelib-v2/=lib/layerzero-v2/packages/layerzero-v2/evm/messagelib',
        '@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/',
        'solidity-bytes-utils/=lib/solidity-bytes-utils/',
    ]
    ```
  </Tab>
</Tabs>

<Info>
  LayerZero contracts work with both [**OpenZeppelin V5**](https://docs.openzeppelin.com/contracts/5.x/access-control#ownership-and-ownable) and V4 contracts. Specify your desired version in your project's package.json:

  ```typescript wrap theme={null}
  "resolutions": {
      "@openzeppelin/contracts": "^5.0.1",
  }
  ```
</Info>

## Custom OApp Contract

To build your own crosschain application, inherit from `OApp.sol` and implement two key pieces:

1. **Send business logic**: how you encode and dispatch a custom `_message` on the source
2. **Receive business logic**: how you decode and apply an incoming `_message` on the destination

Below is a complete example skeleton structure showing:

* A constructor wiring in the local Endpoint and owner
* A `sendString(...)` function that updates state, encodes a string, and calls `_lzSend(...)`
* An override of `_lzReceive(...)` that decodes the string and applies business logic
* (Optional) a `quoteSendString(...)` function to query the fee details needed to call `sendString(...)`

```solidity wrap theme={null}
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.22;

import { OApp, Origin, MessagingFee } from "@layerzerolabs/oapp-evm/contracts/oapp/OApp.sol";
import { OAppOptionsType3 } from "@layerzerolabs/oapp-evm/contracts/oapp/libs/OAppOptionsType3.sol";
import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol";

contract MyOApp is OApp, OAppOptionsType3 {
    /// @notice Last string received from any remote chain
    string public lastMessage;

    /// @notice Msg type for sending a string, for use in OAppOptionsType3 as an enforced option
    uint16 public constant SEND = 1;

    /// @notice Initialize with Endpoint V2 and owner address
    /// @param _endpoint The local chain's LayerZero Endpoint V2 address
    /// @param _owner    The address permitted to configure this OApp
    constructor(address _endpoint, address _owner) OApp(_endpoint, _owner) Ownable(_owner) {}

    // ──────────────────────────────────────────────────────────────────────────────
    // 0. (Optional) Quote business logic
    //
    // Example: Get a quote from the Endpoint for a cost estimate of sending a message.
    // Replace this to mirror your own send business logic.
    // ──────────────────────────────────────────────────────────────────────────────

    /**
     * @notice Quotes the gas needed to pay for the full omnichain transaction in native gas or ZRO token.
     * @param _dstEid Destination chain's endpoint ID.
     * @param _string The string to send.
     * @param _options Message execution options (e.g., for sending gas to destination).
     * @param _payInLzToken Whether to return fee in ZRO token.
     * @return fee A `MessagingFee` struct containing the calculated gas fee in either the native token or ZRO token.
     */
    function quoteSendString(
        uint32 _dstEid,
        string calldata _string,
        bytes calldata _options,
        bool _payInLzToken
    ) public view returns (MessagingFee memory fee) {
        bytes memory _message = abi.encode(_string);
        // combineOptions (from OAppOptionsType3) merges enforced options set by the contract owner
        // with any additional execution options provided by the caller
        fee = _quote(_dstEid, _message, combineOptions(_dstEid, SEND, _options), _payInLzToken);
    }

    // ──────────────────────────────────────────────────────────────────────────────
    // 1. Send business logic
    //
    // Example: send a simple string to a remote chain. Replace this with your
    // own state-update logic, then encode whatever data your application needs.
    // ──────────────────────────────────────────────────────────────────────────────

    /// @notice Send a string to a remote OApp on another chain
    /// @param _dstEid   Destination Endpoint ID (uint32)
    /// @param _string  The string to send
    /// @param _options  Execution options for gas on the destination (bytes)
    function sendString(uint32 _dstEid, string calldata _string, bytes calldata _options) external payable {
        // 1. (Optional) Update any local state here.
        //    e.g., record that a message was "sent":
        //    sentCount += 1;

        // 2. Encode any data structures you wish to send into bytes
        //    You can use abi.encode, abi.encodePacked, or directly splice bytes
        //    if you know the format of your data structures
        bytes memory _message = abi.encode(_string);

        // 3. Call OAppSender._lzSend to package and dispatch the crosschain message
        //    - _dstEid:   remote chain's Endpoint ID
        //    - _message:  ABI-encoded string
        //    - _options:  combined execution options (enforced + caller-provided)
        //    - MessagingFee(msg.value, 0): pay all gas as native token; no ZRO
        //    - payable(msg.sender): refund excess gas to caller
        //
        //    combineOptions (from OAppOptionsType3) merges enforced options set by the contract owner
        //    with any additional execution options provided by the caller
        _lzSend(
            _dstEid,
            _message,
            combineOptions(_dstEid, SEND, _options),
            MessagingFee(msg.value, 0),
            payable(msg.sender)
        );
    }

    // ──────────────────────────────────────────────────────────────────────────────
    // 2. Receive business logic
    //
    // Override _lzReceive to decode the incoming bytes and apply your logic.
    // The base OAppReceiver.lzReceive ensures:
    //   • Only the LayerZero Endpoint can call this method
    //   • The sender is a registered peer (peers[srcEid] == origin.sender)
    // ──────────────────────────────────────────────────────────────────────────────

    /// @notice Invoked by OAppReceiver when EndpointV2.lzReceive is called
    /// @dev   _origin    Metadata (source chain, sender address, nonce)
    /// @dev   _guid      Global unique ID for tracking this message
    /// @param _message   ABI-encoded bytes (the string we sent earlier)
    /// @dev   _executor  Executor address that delivered the message
    /// @dev   _extraData Additional data from the Executor (unused here)
    function _lzReceive(
        Origin calldata /*_origin*/,
        bytes32 /*_guid*/,
        bytes calldata _message,
        address /*_executor*/,
        bytes calldata /*_extraData*/
    ) internal override {
        // 1. Decode the incoming bytes into a string
        //    You can use abi.decode, abi.decodePacked, or directly splice bytes
        //    if you know the format of your data structures
        string memory _string = abi.decode(_message, (string));

        // 2. Apply your custom logic. In this example, store it in `lastMessage`.
        lastMessage = _string;

        // 3. (Optional) Trigger further onchain actions.
        //    e.g., emit an event, mint tokens, call another contract, etc.
        //    emit MessageReceived(_origin.srcEid, _string);
    }
}
```

### Constructor

* Pass the Endpoint V2 address and owner address into the base contracts.
  * `OApp(_endpoint, _owner)` binds your contract to the local LayerZero Endpoint V2 and registers the owner as the delegate, making it the only address that can change configurations (such as libraries, DVNs, and Executors.
  * `Ownable(_owner)` makes `_owner` the only address that can change configurations (such as peers, enforced options, and delegate).
* After deployment, the owner can call:
  * `setConfig(...)` to adjust library or DVN parameters
  * `setSendLibrary(...)` and `setReceiveLibrary(...)` to override default libraries
  * `setPeer(...)` to whitelist remote OApp addresses
  * `setDelegate(...)` to assign a different delegate address

<Info>
  A full overview of how to use these adminstrative functions can be found below under [**Deployment & Wiring**](#deployment-and-wiring).
</Info>

### sendString(...)

1. **Update local state (optional)**

   * Before sending, you might update a counter, lock tokens, or perform any onchain action specific to your app.

2. **Encode the message**

   * Use `abi.encode(_message)`, `abi.encodePacked(_message)`, or manual byte shifting/offsets to turn the string into a `bytes` array. LayerZero [packets](../../../concepts/protocol/packet#packet-endpoint) carry raw `bytes`, so you must encode any data type into bytes first.

3. **Call `_lzSend(...)`**
   * `_dstEid` is the destination chain's [Endpoint ID](/v2/concepts/glossary#endpoint-id). LayerZero uses numeric IDs (e.g., `30101` for Ethereum, `30168` for Solana).
   * `_message` is the ABI-encoded string (`bytes memory`).
   * `_options` is a `bytes` array specifying gas or executor instructions for the destination. For example, an `ExecutorLzReceiveOption` tells the destination how much gas to allocate to your receive call.
   * `MessagingFee(msg.value, 0)` pays fees in native gas. If you wanted to pay in ZRO tokens, set the second field instead.
   * `payable(msg.sender)` specifies the refund address for any unused gas. This can be any address (EOA or contract), but if it's a contract, the contract must have a fallback function to receive the refund.

### \_lzReceive(...)

1. **Endpoint verification**

   * Only the LayerZero Endpoint V2 contract can invoke this function. The base `OAppReceiver` enforces that.
   * The call succeeds only if `_origin.sender == peers[_origin.srcEid]`. In other words, the sender's address must match the registered peer for that source chain.

2. **Decode the incoming bytes**

   * Use `abi.decode(_message, (string))` to extract the original string. If you sent a different data type (e.g., a struct), decode with the matching types.
   * Alternatively, you can use `abi.decodePacked()` for packed encoding, or manually splice bytes from specific offsets if you know the exact format of your data structures.

3. **Apply your business logic**
   * In this example, we store the decoded string in `lastMessage`.
   * You could instead:
     * Emit an event (e.g., `emit MessageReceived(_origin.srcEid, decoded)`)
     * Mint or unlock tokens based on the message
     * Call another contract to trigger a downstream workflow

<Tip>
  Always include all five parameters (`_origin`, `_guid`, `_message`, `_executor`, `_extraData`) in your override. Even if you only use `_message`, matching the function signature ensures the Endpoint can call your method correctly.
</Tip>

### (Optional) quoteSendString(...)

You can optionally call the internal `OAppSender._quote(...)` method in a public function to provide accurate estimation for the gas cost of calling `MyOApp.sendString(...)`.

The internal `_quote` method queries the send library selected by the OApp and asks the workers (DVNs and Executor) for fee details for the given encoded message:

1. **Fee estimation before sending**

   * Before calling `sendString(...)`, you need to know how much native gas (or ZRO tokens) to send with your transaction. The `quoteSendString(...)` function provides this cost estimate.

2. **Mirrors send logic**

   * The quote function uses the same message encoding (`abi.encode(_string)`) and option handling (`combineOptions(_dstEid, SEND, _options)`) as the actual send function, ensuring accurate fee estimates.

3. **Enforced options integration**

   * By inheriting `OAppOptionsType3` and using `combineOptions(...)`, the quote function automatically includes any enforced options that the contract owner has configured for the `SEND` message type, plus any additional options provided by the caller.

4. **Flexible payment options**

   * The `_payInLzToken` parameter lets you choose whether to pay fees in the native gas token of the source chain or in ZRO tokens.

     **Example usage:**

   ```solidity wrap theme={null}
   // Get fee estimate first
   MessagingFee memory fee = myOApp.quoteSendString(
       dstEid,
       "Hello World",
       "0x",  // no additional options
       false  // pay in native gas
   );

   // Then send with the estimated fee
   myOApp.sendString{value: fee.nativeFee}(
       dstEid,
       "Hello World",
       "0x"
   );
   ```

***

This section shows you exactly:

* **Where** to update or check local state before sending
* **How** to encode and send your application data over LayerZero
* **Where** to decode incoming data and execute your custom logic

Replace the `string` examples with whatever data structures and state changes your application requires.

## Deployment and Wiring

After you finish writing and testing your `MyOApp` contract, follow these steps to deploy it on each network and wire up the messaging stack.

<Tip>
  We **strongly recommend** using the LayerZero CLI tool to manage your configurations. Our config generator simplifies access to all available deployments across networks and is the preferred method for crosschain messaging. See the [**CLI Guide**](../../../get-started/create-lz-oapp/start) for examples and how to use it in your project.
</Tip>

### 1. Deploy Your OApp Contract

Deploy `MyOApp` on each chain using either the LayerZero CLI (recommended) or manual deployment scripts.

<Tabs>
  <Tab title="LayerZero CLI">
    After running `pnpm compile` at the root level of your example repo, you can deploy your contracts.

    #### Network Configuration

    Before using the CLI, you'll need to configure your networks in `hardhat.config.ts` with LayerZero Endpoint IDs and declare an RPC URL in your `.env` or directly in the config file:

    ```typescript wrap theme={null}
    // hardhat.config.ts
    import { EndpointId } from '@layerzerolabs/lz-definitions'

    // ... rest of hardhat config omitted for brevity
    networks: {
        'optimism-sepolia-testnet': {
            // highlight-next-line
            eid: EndpointId.OPTSEP_V2_TESTNET,
            url: process.env.RPC_URL_OP_SEPOLIA || 'https://optimism-sepolia.gateway.tenderly.co',
            accounts,
        },
        'avalanche-fuji-testnet': {
            // highlight-next-line
            eid: EndpointId.AVALANCHE_V2_TESTNET,
            url: process.env.RPC_URL_FUJI || 'https://avalanche-fuji.drpc.org',
            accounts,
        },
        'arbitrum-sepolia-testnet': {
            // highlight-next-line
            eid: EndpointId.ARBSEP_V2_TESTNET,
            url: process.env.RPC_URL_ARB_SEPOLIA || 'https://arbitrum-sepolia.gateway.tenderly.co',
            accounts,
        },
    }
    ```

    <Info>
      The key addition to a standard `hardhat.config.ts` is the inclusion of LayerZero Endpoint IDs (`eid`) for each network. Check the [Deployments](../../../deployments/deployed-contracts) section for all available endpoint IDs.
    </Info>

    The LayerZero CLI provides automated deployment with built-in endpoint detection based on your `hardhat.config.ts` networks object:

    ```bash wrap theme={null}
    # Deploy using interactive prompts
    npx hardhat lz:deploy
    ```

    The CLI will prompt you to:

    1. **Select chains to deploy to:**

    ```bash wrap theme={null}
    ? Which networks would you like to deploy? ›
    ◉  fuji
    ◉  amoy
    ◉  sepolia
    ```

    2. **Choose deploy script tags:**

    ```bash wrap theme={null}
    ? Which deploy script tags would you like to use? › MyOApp
    ```

    3. **Confirm deployment:**

    ```bash wrap theme={null}
    ✔ Do you want to continue? … yes
    Network: amoy
    Deployer: 0x0000000000000000000000000000000000000000
    Network: sepolia
    Deployer: 0x0000000000000000000000000000000000000000
    Deployed contract: MyOApp, network: amoy, address: 0x0000000000000000000000000000000000000000
    Deployed contract: MyOApp, network: sepolia, address: 0x0000000000000000000000000000000000000000
    ```

    The CLI automatically:

    * Detects the correct LayerZero Endpoint V2 address for each chain
    * Deploys your OApp contract with proper constructor arguments
    * Generates deployment artifacts in `./deployments/` folder
    * Creates network-specific deployment files (e.g., `deployments/sepolia/MyOApp.json`)
  </Tab>

  <Tab title="Manual Foundry">
    For manual deployment using Foundry, create a deployment script that handles endpoint addresses:

    ```solidity wrap theme={null}
    // SPDX-License-Identifier: UNLICENSED
    pragma solidity ^0.8.22;

    import "forge-std/Script.sol";
    import { MyOApp } from "../contracts/MyOApp.sol";

    contract DeployOApp is Script {
        function run() external {
            // Replace these env vars with your own values
            address endpoint = vm.envAddress("ENDPOINT_ADDRESS");
            address owner    = vm.envAddress("OWNER_ADDRESS");

            vm.startBroadcast(vm.envUint("PRIVATE_KEY"));
            MyOApp oapp = new MyOApp(endpoint, owner);
            vm.stopBroadcast();

            console.log("MyOApp deployed to:", address(oapp));
        }
    }
    ```

    Run the deployment script:

    ```bash wrap theme={null}
    # Deploy to testnet
    forge script script/DeployOApp.s.sol --rpc-url $RPC_URL --broadcast --verify

    # Deploy to multiple chains
    forge script script/DeployOApp.s.sol --rpc-url $ETHEREUM_RPC --broadcast --verify
    forge script script/DeployOApp.s.sol --rpc-url $POLYGON_RPC --broadcast --verify
    ```

    You'll need to set the correct LayerZero Endpoint V2 addresses for each chain in your environment variables. Check the [Deployments](../../../deployments/deployed-contracts) section for endpoint addresses.
  </Tab>
</Tabs>

### 2. Wire Messaging Libraries and Configurations

Once your contracts are onchain, you must set up send/receive libraries and DVN/Executor settings so crosschain messages flow correctly.

<Warning>
  **Production deployments should use multiple required DVNs from independent operators.** A single-DVN configuration means a compromise of that one verifier results in unrestricted forged messages on the pathway. See the [Integration Checklist](/v2/tools/integration-checklist#set-security-and-executor-configurations-on-every-pathway) for production DVN guidance.
</Warning>

<Tabs>
  <Tab title="LayerZero CLI">
    The LayerZero CLI automatically handles all wiring via a single configuration file and command:

    #### Configuration File

    In your project root, you can find a `layerzero.config.ts` file:

    ```typescript wrap theme={null}
    import {EndpointId} from '@layerzerolabs/lz-definitions';
    import {ExecutorOptionType} from '@layerzerolabs/lz-v2-utilities';
    import {TwoWayConfig, generateConnectionsConfig} from '@layerzerolabs/metadata-tools';
    import {OAppEnforcedOption, OmniPointHardhat} from '@layerzerolabs/toolbox-hardhat';

    // This contract object defines the OApp deployment on Optimism Sepolia testnet
    // The config references the contract deployment from your ./deployments folder
    const optimismContract: OmniPointHardhat = {
      eid: EndpointId.OPTSEP_V2_TESTNET,
      contractName: 'MyOApp',
    };

    const avalancheContract: OmniPointHardhat = {
      eid: EndpointId.AVALANCHE_V2_TESTNET,
      contractName: 'MyOApp',
    };

    const arbitrumContract: OmniPointHardhat = {
      eid: EndpointId.ARBSEP_V2_TESTNET,
      contractName: 'MyOApp',
    };

    // For this example's simplicity, we will use the same enforced options values for sending to all chains
    // For production, you should ensure `gas` is set to the correct value through profiling the gas usage of calling OApp._lzReceive(...) on the destination chain
    // To learn more, read https://docs.layerzero.network/v2/concepts/applications/oapp-standard#execution-options-and-enforced-settings
    const EVM_ENFORCED_OPTIONS: OAppEnforcedOption[] = [
      {
        msgType: 1,
        optionType: ExecutorOptionType.LZ_RECEIVE,
        gas: 80000,
        value: 0,
      },
    ];

    // To connect all the above chains to each other, we need the following pathways:
    // Optimism <-> Avalanche
    // Optimism <-> Arbitrum
    // Avalanche <-> Arbitrum

    // With the config generator, pathways declared are automatically bidirectional
    // i.e. if you declare A,B there's no need to declare B,A
    // Replace <SECONDARY_DVN> with a non-LayerZero-Labs DVN provider for each pathway.
    // See /v2/deployments/dvn-addresses for the providers available on each chain.
    // Production deployments should use multiple required DVNs from independent operators —
    // a single-DVN configuration means a compromise of that one verifier results in
    // unrestricted forged messages on the pathway.
    const pathways: TwoWayConfig[] = [
      [
        optimismContract, // Chain A contract
        avalancheContract, // Chain B contract
        [['LayerZero Labs', '<SECONDARY_DVN>'], []], // [ requiredDVN[], [ optionalDVN[], threshold ] ]
        [1, 1], // [A to B confirmations, B to A confirmations] — adjust per pathway; production deployments typically use larger values
        [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], // Chain B enforcedOptions, Chain A enforcedOptions
      ],
      [
        optimismContract, // Chain A contract
        arbitrumContract, // Chain C contract
        [['LayerZero Labs', '<SECONDARY_DVN>'], []], // [ requiredDVN[], [ optionalDVN[], threshold ] ]
        [1, 1], // [A to B confirmations, B to A confirmations]
        [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], // Chain C enforcedOptions, Chain A enforcedOptions
      ],
      [
        avalancheContract, // Chain B contract
        arbitrumContract, // Chain C contract
        [['LayerZero Labs', '<SECONDARY_DVN>'], []], // [ requiredDVN[], [ optionalDVN[], threshold ] ]
        [1, 1], // [A to B confirmations, B to A confirmations]
        [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], // Chain C enforcedOptions, Chain B enforcedOptions
      ],
    ];

    export default async function () {
      // Generate the connections config based on the pathways
      const connections = await generateConnectionsConfig(pathways);
      return {
        contracts: [
          {contract: optimismContract},
          {contract: avalancheContract},
          {contract: arbitrumContract},
        ],
        connections,
      };
    }
    ```

    Make sure your contract object's `contractName` matches the named deployment file for the network under `./deployments/`.

    #### Wire Everything

    Run a single command to configure all pathways:

    ```bash wrap theme={null}
    npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts
    ```

    This automatically handles:

    * Fetching the necessary contract addresses for each network from metadata
    * Setting send and receive libraries
    * Configuring DVNs and Executors
    * Setting up peers between contracts
    * Applying enforced options
    * All bidirectional pathways in your config
  </Tab>

  <Tab title="Manual Foundry">
    For manual configuration using Foundry scripts, follow these steps:

    #### Environment Setup

    Here's a comprehensive `.env.example` file showing all the environment variables needed for the different configuration scripts:

    ```bash wrap theme={null}
    # Common variables used across scripts
    ENDPOINT_ADDRESS=0x...        # LayerZero Endpoint V2 address
    OAPP_ADDRESS=0x...           # Your OApp contract address
    SIGNER=0x...                 # Address with permissions to configure/send

    # Library Configuration (SetLibraries.s.sol)
    SEND_LIB_ADDRESS=0x...       # SendUln302 address
    RECEIVE_LIB_ADDRESS=0x...    # ReceiveUln302 address
    DST_EID=30101               # Destination chain EID
    SRC_EID=30110               # Source chain EID
    GRACE_PERIOD=0              # Grace period for library switch (0 for immediate)

    # Send Config (SetSendConfig.s.sol)
    SOURCE_ENDPOINT_ADDRESS=0x... # Chain A Endpoint address
    SENDER_OAPP_ADDRESS=0x...    # OApp on Chain A
    REMOTE_EID=30101            # Endpoint ID for Chain B

    # Peer Configuration (SetPeers.s.sol)
    CHAIN1_EID=30101            # First chain EID
    CHAIN1_PEER=0x...           # OApp address on first chain
    CHAIN2_EID=30110            # Second chain EID
    CHAIN2_PEER=0x...           # OApp address on second chain
    CHAIN3_EID=30111            # Third chain EID
    CHAIN3_PEER=0x...           # OApp address on third chain

    # Message Sending (SendMessage.s.sol)
    MESSAGE="Hello World"        # Message to send crosschain
    ```

    #### 2.1 Set Send and Receive Libraries

    1. **Choose your libraries** (addresses of deployed MessageLib contracts). For standard crosschain messaging, you should use `SendUln302.sol` for `setSendLibrary(...)` and `ReceiveUln302.sol` for `setReceiveLibrary(...)`. You can find the deployments for these contracts under the [Deployments](../../../deployments/deployed-contracts) section.
    2. Call `setSendLibrary(oappAddress, dstEid, sendLibAddress)` on the Endpoint.
    3. Call `setReceiveLibrary(oappAddress, srcEid, receiveLibAddress, gracePeriod)` on the Endpoint.

    ```solidity wrap theme={null}
    // SPDX-License-Identifier: UNLICENSED
    pragma solidity ^0.8.22;

    import "forge-std/Script.sol";
    import { ILayerZeroEndpointV2 } from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/ILayerZeroEndpointV2.sol";

    /// @title LayerZero Library Configuration Script
    /// @notice Sets up send and receive libraries for OApp messaging
    contract SetLibraries is Script {
        function run() external {
            // Load environment variables
            address endpoint = vm.envAddress("ENDPOINT_ADDRESS");    // LayerZero Endpoint address
            address oapp = vm.envAddress("OAPP_ADDRESS");           // Your OApp contract address
            address signer = vm.envAddress("SIGNER");               // Address with permissions to configure

            // Library addresses
            address sendLib = vm.envAddress("SEND_LIB_ADDRESS");    // SendUln302 address
            address receiveLib = vm.envAddress("RECEIVE_LIB_ADDRESS"); // ReceiveUln302 address

            // Chain configurations
            uint32 dstEid = uint32(vm.envUint("DST_EID"));         // Destination chain EID
            uint32 srcEid = uint32(vm.envUint("SRC_EID"));         // Source chain EID
            uint32 gracePeriod = uint32(vm.envUint("GRACE_PERIOD")); // Grace period for library switch

            vm.startBroadcast(signer);

            // Set send library for outbound messages
            ILayerZeroEndpointV2(endpoint).setSendLibrary(
                oapp,    // OApp address
                dstEid,  // Destination chain EID
                sendLib  // SendUln302 address
            );

            // Set receive library for inbound messages
            ILayerZeroEndpointV2(endpoint).setReceiveLibrary(
                oapp,        // OApp address
                srcEid,      // Source chain EID
                receiveLib,  // ReceiveUln302 address
                gracePeriod  // Grace period for library switch
            );

            vm.stopBroadcast();
        }
    }
    ```

    You would need to set up your `.env` file with the appropriate values:

    ```env wrap theme={null}
    ENDPOINT_ADDRESS=0x...
    OAPP_ADDRESS=0x...
    SIGNER=0x...
    SEND_LIB_ADDRESS=0x...    # SendUln302 address
    RECEIVE_LIB_ADDRESS=0x... # ReceiveUln302 address
    DST_EID=30101
    SRC_EID=30110
    GRACE_PERIOD=0           # Set to 0 for immediate switch, or block number for gradual migration
    ```

    #### 2.2 Set Send Config and Receive Config

    If you need non-default DVN or Executor settings (block confirmations, required DVNs, max message size, etc.), call `setConfig(...)` next. To see defaults, use `getConfig(...)`.

    **Send Config (A → B):**

    The send config is set on the source chain (Chain A) and applies to messages being sent from Chain A to Chain B. This config determines the DVN and Executor settings for outbound messages leaving Chain A and destined for Chain B. You must call `setConfig` on the Endpoint contract on Chain A, specifying the remote Endpoint ID for Chain B and the appropriate SendLib address for the A → B pathway.

    ```solidity wrap theme={null}
    // SPDX-License-Identifier: UNLICENSED
    pragma solidity ^0.8.22;

    import "forge-std/Script.sol";
    import { ILayerZeroEndpointV2, SetConfigParam } from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/ILayerZeroEndpointV2.sol";
    import { UlnConfig } from "@layerzerolabs/lz-evm-messagelib-v2/contracts/uln/UlnBase.sol";
    import { ExecutorConfig } from "@layerzerolabs/lz-evm-messagelib-v2/contracts/SendLibBase.sol";

    /// @title LayerZero Send Configuration Script (A → B)
    /// @notice Defines and applies ULN (DVN) + Executor configs for cross‑chain messages sent from Chain A to Chain B via LayerZero Endpoint V2.
    contract SetSendConfig is Script {
        uint32 constant EXECUTOR_CONFIG_TYPE = 1;
        uint32 constant ULN_CONFIG_TYPE = 2;

         /// @notice Broadcasts transactions to set both Send ULN and Executor configurations for messages sent from Chain A to Chain B
        function run() external {
            address endpoint = vm.envAddress("SOURCE_ENDPOINT_ADDRESS"); // Chain A Endpoint
            address oapp      = vm.envAddress("SENDER_OAPP_ADDRESS");    // OApp on Chain A
            uint32 eid        = uint32(vm.envUint("REMOTE_EID"));        // Endpoint ID for Chain B
            address sendLib   = vm.envAddress("SEND_LIB_ADDRESS");      // SendLib for A → B
            address signer    = vm.envAddress("SIGNER");

            /// @notice ULNConfig defines security parameters (DVNs + confirmation threshold) for A → B
            /// @notice Send config requests these settings to be applied to the DVNs and Executor for messages sent from A to B
            /// @dev 0 values will be interpretted as defaults, so to apply NIL settings, use:
            /// @dev uint8 internal constant NIL_DVN_COUNT = type(uint8).max;
            /// @dev uint64 internal constant NIL_CONFIRMATIONS = type(uint64).max;
            UlnConfig memory uln = UlnConfig({
                confirmations:        15,                                      // minimum block confirmations required on A before sending to B
                requiredDVNCount:     2,                                       // number of DVNs required
                optionalDVNCount:     type(uint8).max,                         // optional DVNs count, uint8
                optionalDVNThreshold: 0,                                       // optional DVN threshold
                requiredDVNs:        [address(0x1111...), address(0x2222...)], // sorted list of required DVN addresses
                optionalDVNs:        []                                        // sorted list of optional DVNs
            });

            /// @notice ExecutorConfig sets message size limit + fee‑paying executor for A → B
            ExecutorConfig memory exec = ExecutorConfig({
                maxMessageSize: 10000,                                       // max bytes per crosschain message
                executor:       address(0x3333...)                           // address that pays destination execution fees on B
            });

            bytes memory encodedUln  = abi.encode(uln);
            bytes memory encodedExec = abi.encode(exec);

            SetConfigParam[] memory params = new SetConfigParam[](2);
            params[0] = SetConfigParam(eid, EXECUTOR_CONFIG_TYPE, encodedExec);
            params[1] = SetConfigParam(eid, ULN_CONFIG_TYPE, encodedUln);

            vm.startBroadcast(signer);
            ILayerZeroEndpointV2(endpoint).setConfig(oapp, sendLib, params); // Set config for messages sent from A to B
            vm.stopBroadcast();
        }
    }
    ```

    **Receive Config (B ← A):**

    The receive config is set on the destination chain (Chain B) and applies to messages being received on Chain B from Chain A. This config determines the DVN settings for inbound messages arriving from Chain A. You must call `setConfig` on the Endpoint contract on Chain B, specifying the remote Endpoint ID for Chain A and the appropriate ReceiveLib address for the B ← A pathway.

    ```solidity wrap theme={null}
    // SPDX-License-Identifier: UNLICENSED
    pragma solidity ^0.8.22;

    import "forge-std/Script.sol";
    import { ILayerZeroEndpointV2, SetConfigParam } from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/ILayerZeroEndpointV2.sol";
    import { UlnConfig } from "@layerzerolabs/lz-evm-messagelib-v2/contracts/uln/UlnBase.sol";

    /// @title LayerZero Receive Configuration Script (B ← A)
    /// @notice Defines and applies ULN (DVN) config for inbound message verification on Chain B for messages received from Chain A via LayerZero Endpoint V2.
    contract SetReceiveConfig is Script {
        uint32 constant RECEIVE_CONFIG_TYPE = 2;

        function run() external {
            address endpoint = vm.envAddress("ENDPOINT_ADDRESS");      // Chain B Endpoint
            address oapp      = vm.envAddress("OAPP_ADDRESS");         // OApp on Chain B
            uint32 eid        = uint32(vm.envUint("REMOTE_EID"));      // Endpoint ID for Chain A
            address receiveLib= vm.envAddress("RECEIVE_LIB_ADDRESS");  // ReceiveLib for B ← A
            address signer    = vm.envAddress("SIGNER");

            /// @notice UlnConfig controls verification threshold for incoming messages from A to B
            /// @notice Receive config enforces these settings have been applied to the DVNs for messages received from A
            /// @dev 0 values will be interpretted as defaults, so to apply NIL settings, use:
            /// @dev uint8 internal constant NIL_DVN_COUNT = type(uint8).max;
            /// @dev uint64 internal constant NIL_CONFIRMATIONS = type(uint64).max;
            UlnConfig memory uln = UlnConfig({
                confirmations:      15,                                       // min block confirmations from source (A)
                requiredDVNCount:   2,                                        // required DVNs for message acceptance
                optionalDVNCount:   type(uint8).max,                          // optional DVNs count
                optionalDVNThreshold: 0,                                      // optional DVN threshold
                requiredDVNs:       [address(0x1111...), address(0x2222...)], // sorted required DVNs
                optionalDVNs:       []                                        // no optional DVNs
            });

            bytes memory encodedUln = abi.encode(uln);

            SetConfigParam[] memory params = new SetConfigParam[](1);
            params[0] = SetConfigParam(eid, RECEIVE_CONFIG_TYPE, encodedUln);

            vm.startBroadcast(signer);
            ILayerZeroEndpointV2(endpoint).setConfig(oapp, receiveLib, params); // Set config for messages received on B from A
            vm.stopBroadcast();
        }
    }
    ```

    #### 2.3 Set Peers

    Once you've finished your **OApp Configuration** you can open the messaging channel and connect your OApp deployments by calling `setPeer`.

    A peer is required to be set for each EID (or network). Ideally an OApp (or OFT) will have multiple peers set where one and only one peer exists for one EID.

    The function takes 2 arguments: `_eid`, the destination endpoint ID for the chain our other OApp contract lives on, and `_peer`, the destination OApp contract address in `bytes32` format.

    ```solidity wrap theme={null}
    // SPDX-License-Identifier: UNLICENSED
    pragma solidity ^0.8.22;

    import "forge-std/Script.sol";
    import { MyOApp } from "../contracts/MyOApp.sol";

    /// @title LayerZero OApp Peer Configuration Script
    /// @notice Sets up peer connections between OApp deployments on different chains
    contract SetPeers is Script {
        function run() external {
            // Load environment variables
            address oapp = vm.envAddress("OAPP_ADDRESS");         // Your OApp contract address
            address signer = vm.envAddress("SIGNER");            // Address with owner permissions

            // Example: Set peers for different chains
            // Format: (chain EID, peer address in bytes32)
            (uint32 eid1, bytes32 peer1) = (uint32(vm.envUint("CHAIN1_EID")), bytes32(uint256(uint160(vm.envAddress("CHAIN1_PEER")))));
            (uint32 eid2, bytes32 peer2) = (uint32(vm.envUint("CHAIN2_EID")), bytes32(uint256(uint160(vm.envAddress("CHAIN2_PEER")))));
            (uint32 eid3, bytes32 peer3) = (uint32(vm.envUint("CHAIN3_EID")), bytes32(uint256(uint160(vm.envAddress("CHAIN3_PEER")))));

            vm.startBroadcast(signer);

            // Set peers for each chain
            MyOApp(oapp).setPeer(eid1, peer1);
            MyOApp(oapp).setPeer(eid2, peer2);
            MyOApp(oapp).setPeer(eid3, peer3);

            vm.stopBroadcast();
        }
    }
    ```

    <Warning>
      This function opens your OApp to start receiving messages from the messaging channel, meaning you should configure any application settings you intend on changing prior to calling `setPeer`.
    </Warning>

    <Warning>
      OApps need `setPeer` to be called correctly on both contracts to send messages. The peer address uses `bytes32` for handling non-EVM destination chains.

      If the peer has been set to an incorrect destination address, your messages will not be delivered and handled properly. If not resolved, users can potentially pay gas on source without any corresponding action on destination. You can confirm the peer address is the expected destination OApp address by viewing the `peers` mapping directly.
    </Warning>

    #### 2.4 Set Enforced Options

    Enforced options allow the OApp owner to set mandatory execution parameters that will be applied to all messages of a specific type sent to a destination chain. These options are automatically combined with any caller-provided options when using `OAppOptionsType3`.

    **Why use enforced options?**

    * Ensure sufficient gas is always allocated for message execution on the destination
    * Enforce payment for additional services like PreCrime verification
    * Set consistent execution parameters across all users of your OApp
    * Prevent failed deliveries due to insufficient gas

    ```solidity wrap theme={null}
    // SPDX-License-Identifier: UNLICENSED
    pragma solidity ^0.8.22;

    import "forge-std/Script.sol";
    import { MyOApp } from "../contracts/MyOApp.sol";
    import { EnforcedOptionParam } from "@layerzerolabs/oapp-evm/contracts/oapp/libs/OAppOptionsType3.sol";
    import { OptionsBuilder } from "@layerzerolabs/oapp-evm/contracts/oapp/libs/OptionsBuilder.sol";

    /// @title LayerZero OApp Enforced Options Configuration Script
    /// @notice Sets enforced execution options for specific message types and destinations
    contract SetEnforcedOptions is Script {
        using OptionsBuilder for bytes;

        function run() external {
            // Load environment variables
            address oapp = vm.envAddress("OAPP_ADDRESS");         // Your OApp contract address
            address signer = vm.envAddress("SIGNER");            // Address with owner permissions

            // Destination chain configurations
            uint32 dstEid1 = uint32(vm.envUint("DST_EID_1"));    // First destination EID
            uint32 dstEid2 = uint32(vm.envUint("DST_EID_2"));    // Second destination EID

            // Message type (should match your contract's constant)
            uint16 SEND = 1;  // Message type for sendString function

            // Build options using OptionsBuilder
            bytes memory options1 = OptionsBuilder.newOptions().addExecutorLzReceiveOption(80000, 0);
            bytes memory options2 = OptionsBuilder.newOptions().addExecutorLzReceiveOption(100000, 0);

            // Create enforced options array
            EnforcedOptionParam[] memory enforcedOptions = new EnforcedOptionParam[](2);

            // Set enforced options for first destination
            enforcedOptions[0] = EnforcedOptionParam({
                eid: dstEid1,
                msgType: SEND,
                options: options1
            });

            // Set enforced options for second destination
            enforcedOptions[1] = EnforcedOptionParam({
                eid: dstEid2,
                msgType: SEND,
                options: options2
            });

            vm.startBroadcast(signer);

            // Set enforced options on the OApp
            MyOApp(oapp).setEnforcedOptions(enforcedOptions);

            vm.stopBroadcast();

            console.log("Enforced options set successfully!");
            console.log("Destination 1 EID:", dstEid1, "Gas:", 80000);
            console.log("Destination 2 EID:", dstEid2, "Gas:", 100000);
        }
    }
    ```

    **Environment variables needed:**

    ```env wrap theme={null}
    OAPP_ADDRESS=0x...           # Your deployed MyOApp address
    SIGNER=0x...                 # Address with owner permissions
    DST_EID_1=30101             # First destination endpoint ID
    DST_EID_2=30110             # Second destination endpoint ID
    ```

    **Run the script:**

    ```bash wrap theme={null}
    forge script script/SetEnforcedOptions.s.sol --rpc-url $RPC_URL --broadcast
    ```

    Once set, these enforced options will be automatically applied when using `combineOptions()` in your send functions, ensuring consistent execution parameters across all messages.
  </Tab>
</Tabs>

<br />

## Usage

Once deployed and wired, you can begin sending crosschain messages.

### Calling `send`

<Tabs>
  <Tab title="LayerZero CLI">
    The LayerZero CLI provides a convenient task for sending messages that automatically handles fee estimation and transaction execution.

    #### Using the Send Task

    The CLI includes a built-in `lz:oapp:send` task that:

    1. Quotes the gas cost using your OApp's `quoteSendString()` function
    2. Sends the message with the correct fee
    3. Waits for confirmation and provides tracking links

    **Basic usage:**

    ```bash wrap theme={null}
    npx hardhat lz:oapp:send --dst-eid 30101 --string "Hello ethereum" --network arbitrum-sepolia-testnet
    ```

    **Parameters:**

    * `--dst-eid`: Destination endpoint ID (required)
    * `--string`: Message to send (required)
    * `--network`: Source network name from your hardhat config (required)
    * `--options`: Execution options in hex format (optional, defaults to `0x`)

    **Example output:**

    ```bash wrap theme={null}
    Initiating string send from arbitrum-sepolia-testnet to ethereum-sepolia-testnet
    String to send: "Hello ethereum"
    Destination EID: 30101
    Using signer: 0x1234567890123456789012345678901234567890
    MyOApp contract found at: 0xabcdefabcdefabcdefabcdefabcdefabcdefabcd
    Execution options: 0x
    Quoting gas cost for the send transaction...
      Native fee: 0.001234567890123456 ETH
      LZ token fee: 0 LZ
    Sending the string transaction...
      Transaction hash: 0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef
    Waiting for transaction confirmation...
      Gas used: 123456
      Block number: 1234567
    ✅ SENT_VIA_OAPP: Successfully sent "Hello ethereum" from arbitrum-sepolia-testnet to ethereum-sepolia-testnet
    ✅ TX_HASH: Block explorer link for source chain arbitrum-sepolia-testnet: https://sepolia.arbiscan.io/tx/0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef
    ✅ EXPLORER_LINK: LayerZero Scan link for tracking crosschain delivery: https://testnet.layerzeroscan.com/tx/0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef
    ```

    The task automatically:

    * Finds your deployed `MyOApp` contract
    * Quotes the exact gas fee needed
    * Sends the transaction with proper gas estimation
    * Provides block explorer and LayerZero Scan links for tracking
  </Tab>

  <Tab title="Manual Foundry">
    For manual message sending using Foundry, create a script that handles fee estimation and message transmission:

    ```solidity wrap theme={null}
    // SPDX-License-Identifier: UNLICENSED
    pragma solidity ^0.8.22;

    import "forge-std/Script.sol";
    import { MyOApp } from "../contracts/MyOApp.sol";
    import { MessagingFee } from "@layerzerolabs/oapp-evm/contracts/oapp/OApp.sol";

    /// @title LayerZero OApp Message Sending Script
    /// @notice Demonstrates how to send messages between OApp deployments
    contract SendMessage is Script {
        function run() external {
            // Load environment variables
            address oapp = vm.envAddress("OAPP_ADDRESS");         // Your OApp contract address
            address signer = vm.envAddress("SIGNER");            // Address with permissions to send

            // Destination chain configuration
            uint32 dstEid = uint32(vm.envUint("DST_EID"));      // Destination chain EID

            // Message to send
            string memory message = vm.envString("MESSAGE");     // Your crosschain message
            bytes memory options = vm.envBytes("OPTIONS");       // Execution options (or use "0x" for default)

            // Get the MyOApp contract instance
            MyOApp myOApp = MyOApp(oapp);

            // 1. Quote the gas cost first
            MessagingFee memory fee = myOApp.quoteSendString(
                dstEid,
                message,
                options,
                false  // Pay in native gas, not ZRO tokens
            );

            console.log("Estimated native fee:", fee.nativeFee);
            console.log("Estimated LZ token fee:", fee.lzTokenFee);

            // 2. Send the message with the quoted fee
            vm.startBroadcast(signer);

            myOApp.sendString{value: fee.nativeFee}(
                dstEid,
                message,
                options
            );

            vm.stopBroadcast();

            console.log("Message sent successfully!");
        }
    }
    ```

    **Environment variables needed:**

    ```env wrap theme={null}
    OAPP_ADDRESS=0x...           # Your deployed MyOApp address
    SIGNER=0x...                 # Private key or address with permissions
    DST_EID=30101               # Destination endpoint ID
    MESSAGE="Hello World"        # Message to send
    OPTIONS=0x                   # Execution options (0x for default)
    ```

    **Run the script:**

    ```bash wrap theme={null}
    forge script script/SendMessage.s.sol --rpc-url $RPC_URL --broadcast
    ```
  </Tab>
</Tabs>

## Extensions

The OApp Standard can be extended with various messaging patterns to support complex crosschain applications. Each pattern functions as a distinct omnichain building block, capable of being used independently or in combination.

### ABA (Ping-Pong) Pattern

The **ABA** pattern enables nested messaging where a message sent from Chain A to Chain B triggers another message back to Chain A (`A` → `B` → `A`). This is useful for crosschain authentication, data feeds, or conditional contract execution.

<img src="https://mintcdn.com/layerzero/MzSiOdXt8xlDlEr4/images/learn/ABAlight.svg?fit=max&auto=format&n=MzSiOdXt8xlDlEr4&q=85&s=fa52c0d952ad24d9efa642bee4da991c" alt="Diagram showing ABA messaging pattern: a ping-pong style call where Chain A sends to Chain B, which then sends back to Chain A (A → B → A)" className="block dark:hidden" width="1920" height="517" data-path="images/learn/ABAlight.svg" />

<img src="https://mintcdn.com/layerzero/MzSiOdXt8xlDlEr4/images/learn/ABAdark.svg?fit=max&auto=format&n=MzSiOdXt8xlDlEr4&q=85&s=2fa451ae74341533b88351949651fce8" alt="Diagram showing ABA messaging pattern: a ping-pong style call where Chain A sends to Chain B, which then sends back to Chain A (A → B → A)" className="hidden dark:block" width="1920" height="517" data-path="images/learn/ABAdark.svg" />

#### Implementation

The key is to nest an `_lzSend` call within your `_lzReceive` function:

```solidity wrap theme={null}
function _lzReceive(
    Origin calldata _origin,
    bytes32 /*_guid*/,
    bytes calldata _message,
    address /*_executor*/,
    bytes calldata /*_extraData*/
) internal override {
    // Decode the incoming message
    (string memory data, uint16 msgType, bytes memory returnOptions) = abi.decode(_message, (string, uint16, bytes));

    // Process the message
    lastMessage = data;

    if (msgType == SEND_ABA) {
        // Send response back to origin chain
        _lzSend(
            _origin.srcEid,
            abi.encode("Response from Chain B", SEND),
            returnOptions,
            MessagingFee(msg.value, 0),
            payable(address(this))
        );
    }
}
```

<Tip>
  **ABA Pattern Gas Planning**: When implementing the ABA pattern, consider these important factors:

  1. **Encode return options in your message**: Include the `_options` parameter for the B→A transaction within your A→B message encoding, as shown in the example above with `returnOptions`.

  2. **Calculate total gas costs upfront**: The source OApp (A) needs to know the full transaction cost for the entire A→B→A flow. You should:

     * Quote the cost of the B→A transaction beforehand
     * Include this cost in your `lzReceiveOption` gas allocation for the A→B transaction
     * Ensure sufficient `msg.value` is forwarded to cover both legs of the journey

  3. **Example gas calculation**:

     ```solidity wrap theme={null}
     // Quote B→A cost first
     MessagingFee memory returnFee = quoteBtoA(returnOptions);

     // Include return fee in A→B options
     bytes memory abaOptions = OptionsBuilder.newOptions()
         .addExecutorLzReceiveOption(baseGas + returnGas, returnFee.nativeFee);
     ```

  This ensures your ABA transaction has sufficient gas to complete the full round trip.
</Tip>

### Batch Send

**Batch Send** allows a single transaction to initiate multiple `_lzSend` calls to various destination chains, reducing operational overhead for multi-chain operations.

<img src="https://mintcdn.com/layerzero/MzSiOdXt8xlDlEr4/images/learn/BatchSendLight.svg?fit=max&auto=format&n=MzSiOdXt8xlDlEr4&q=85&s=57095138f498ea83fc8f8614a846b8b3" alt="Diagram showing Batch Send pattern: a single transaction from Chain A initiating multiple _lzSend calls to Chains B, C, and D simultaneously" className="block dark:hidden" width="1920" height="517" data-path="images/learn/BatchSendLight.svg" />

<img src="https://mintcdn.com/layerzero/MzSiOdXt8xlDlEr4/images/learn/BatchSendDark.svg?fit=max&auto=format&n=MzSiOdXt8xlDlEr4&q=85&s=b220135c00ff58f4e29a34bc64e1f1c0" alt="Diagram showing Batch Send pattern: a single transaction from Chain A initiating multiple _lzSend calls to Chains B, C, and D simultaneously" className="hidden dark:block" width="1920" height="517" data-path="images/learn/BatchSendDark.svg" />

#### Key Implementation Points

The batch send pattern includes several important design decisions:

1. **Fee Validation**: Override `_payNative` to change fee check from equivalency to `<` since batch fees are cumulative
2. **Consistent Loop Pattern**: Both `quote` and `send` functions use identical for loops to iterate through destinations for predictable behavior

#### Implementation

```solidity wrap theme={null}
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.22;

import { OApp, MessagingFee, Origin } from "@layerzerolabs/oapp-evm/contracts/oapp/OApp.sol";
import { OAppOptionsType3 } from "@layerzerolabs/oapp-evm/contracts/oapp/libs/OAppOptionsType3.sol";
import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol";

/**
 * @title BatchSendMock contract for demonstrating multiple outbound crosschain calls using LayerZero.
 * @notice THIS IS AN EXAMPLE CONTRACT. DO NOT USE THIS CODE IN PRODUCTION.
 * @dev This contract showcases how to send multiple crosschain calls with one source function call using LayerZero's OApp Standard.
 */
contract BatchSendMock is OApp, OAppOptionsType3 {
    /// @notice Last received message data.
    string public data = "Nothing received yet";

    /// @notice Message types that are used to identify the various OApp operations.
    /// @dev These values are used in things like combineOptions() in OAppOptionsType3 (enforcedOptions).
    uint16 public constant SEND = 1;

    /// @notice Emitted when a message is received from another chain.
    event MessageReceived(string message, uint32 senderEid, bytes32 sender);

    /// @notice Emitted when a message is sent to another chain (A -> B).
    event MessageSent(string message, uint32 dstEid);

    /// @dev Revert with this error when an invalid message type is used.
    error InvalidMsgType();

    /**
     * @dev Constructs a new BatchSend contract instance.
     * @param _endpoint The LayerZero endpoint for this contract to interact with.
     * @param _owner The owner address that will be set as the owner of the contract.
     */
    constructor(address _endpoint, address _owner) OApp(_endpoint, _owner) Ownable(msg.sender) {}

    // Override to change fee check from equivalency to < since batch fees are cumulative
    function _payNative(uint256 _nativeFee) internal override returns (uint256 nativeFee) {
        if (msg.value < _nativeFee) revert NotEnoughNative(msg.value);
        return _nativeFee;
    }

    /**
     * @notice Returns the estimated messaging fee for a given message.
     * @param _dstEids Destination endpoint ID array where the message will be batch sent.
     * @param _msgType The type of message being sent.
     * @param _message The message content.
     * @param _extraSendOptions Extra gas options for receiving the send call (A -> B).
     * Will be summed with enforcedOptions, even if no enforcedOptions are set.
     * @param _payInLzToken Boolean flag indicating whether to pay in LZ token.
     * @return totalFee The estimated messaging fee for sending to all pathways.
     */
    function quote(
        uint32[] memory _dstEids,
        uint16 _msgType,
        string memory _message, // Semantic naming for message content
        bytes calldata _extraSendOptions,
        bool _payInLzToken
    ) public view returns (MessagingFee memory totalFee) {
        bytes memory encodedMessage = abi.encode(_message); // Clear distinction: input vs processed

        for (uint i = 0; i < _dstEids.length; i++) {
            bytes memory options = combineOptions(_dstEids[i], _msgType, _extraSendOptions);
            MessagingFee memory fee = _quote(_dstEids[i], encodedMessage, options, _payInLzToken);
            totalFee.nativeFee += fee.nativeFee;
            totalFee.lzTokenFee += fee.lzTokenFee;
        }
    }

    function send(
        uint32[] memory _dstEids,
        uint16 _msgType,
        string memory _message,
        bytes calldata _extraSendOptions // gas settings for A -> B
    ) external payable {
        // Message type validation for security and extensibility
        if (_msgType != SEND) {
            revert InvalidMsgType();
        }

        // Gas efficiency: calculate total fees upfront (fail-fast pattern)
        MessagingFee memory totalFee = quote(_dstEids, _msgType, _message, _extraSendOptions, false);
        require(msg.value >= totalFee.nativeFee, "Insufficient fee provided");

        // Encodes the message before invoking _lzSend.
        bytes memory _encodedMessage = abi.encode(_message);

        uint256 totalNativeFeeUsed = 0;
        uint256 remainingValue = msg.value;

        for (uint i = 0; i < _dstEids.length; i++) {
            bytes memory options = combineOptions(_dstEids[i], _msgType, _extraSendOptions);
            MessagingFee memory fee = _quote(_dstEids[i], _encodedMessage, options, false);

            totalNativeFeeUsed += fee.nativeFee;
            remainingValue -= fee.nativeFee;

            // Granular fee tracking per destination
            require(remainingValue >= 0, "Insufficient fee for this destination");

            _lzSend(
                _dstEids[i],
                _encodedMessage,
                options,
                fee,
                payable(msg.sender)
            );

            emit MessageSent(_message, _dstEids[i]); // Event emission for tracking
        }
    }

    /**
     * @notice Internal function to handle receiving messages from another chain.
     * @dev Decodes and processes the received message based on its type.
     * @param _origin Data about the origin of the received message.
     * @param message The received message content.
     */
    function _lzReceive(
        Origin calldata _origin,
        bytes32 /*guid*/,
        bytes calldata message,
        address, // Executor address as specified by the OApp.
        bytes calldata // Any extra data or options to trigger on receipt.
    ) internal override {
        string memory _data = abi.decode(message, (string));
        data = _data;

        emit MessageReceived(data, _origin.srcEid, _origin.sender);
    }
}
```

This pattern is particularly useful for **mass updating state from a single call** - allowing you to push data from one chain to many chains efficiently. Common use cases include configuration updates, price feeds, or state synchronization across multiple destination chains.

### Call Composer

**Composed** messaging enables **horizontal composability** where a message triggers external contract calls on the destination chain through `lzCompose`. Unlike vertical composability (multiple calls in a single transaction), horizontal composability processes operations as separate, containerized message packets.

<img src="https://mintcdn.com/layerzero/MzSiOdXt8xlDlEr4/images/learn/Composed-Light.svg?fit=max&auto=format&n=MzSiOdXt8xlDlEr4&q=85&s=3917355397da56862d784598420f6e0c" alt="Diagram showing horizontal composability: OApp receives message via lzReceive, then calls sendCompose to deliver a separate composed message to an external contract via lzCompose (A → B1 → B2)" className="block dark:hidden" width="1920" height="517" data-path="images/learn/Composed-Light.svg" />

<img src="https://mintcdn.com/layerzero/MzSiOdXt8xlDlEr4/images/learn/Composed-Dark.svg?fit=max&auto=format&n=MzSiOdXt8xlDlEr4&q=85&s=38ab6dbf2d791462d654d55dd9846c7a" alt="Diagram showing horizontal composability: OApp receives message via lzReceive, then calls sendCompose to deliver a separate composed message to an external contract via lzCompose (A → B1 → B2)" className="hidden dark:block" width="1920" height="517" data-path="images/learn/Composed-Dark.svg" />

#### Benefits of Horizontal Composability

* **Fault Isolation**: If a composed call fails, it doesn't revert the main token transfer or message
* **Gas Efficiency**: Each step can have independent gas limits and execution options
* **Flexible Workflows**: Complex multi-step operations can be broken into manageable pieces

#### Sending Side

```solidity wrap theme={null}
function sendStringToComposer(
    uint32 _dstEid,
    string memory _string,
    address _composer,
    bytes calldata _extraOptions
) external payable {
    // Include both lzReceive and lzCompose options in enforcedOptions or extraOptions
    bytes memory composedOptions = OptionsBuilder.newOptions()
        .addExecutorLzReceiveOption(65000, 0)        // For the main receive
        .addExecutorLzComposeOption(0, 50000, 0);    // For the compose call

    bytes memory _message = abi.encode(_string, _composer);

    _lzSend(
        _dstEid,
        _message,
        composedOptions,
        MessagingFee(msg.value, 0),
        payable(msg.sender)
    );
}
```

#### Receiving Side

```solidity wrap theme={null}
function _lzReceive(
    Origin calldata _origin,
    bytes32 _guid,
    bytes calldata _message,
    address /*_executor*/,
    bytes calldata /*_extraData*/
) internal override {
    (string memory _string, address composer) = abi.decode(_message, (string, address));

    // Store the message and perform primary logic
    lastMessage = _string;

    // Send composed message to external contract as separate message packet
    endpoint.sendCompose(composer, _guid, 0, _message);
}
```

#### Composer Contract

```solidity wrap theme={null}
import { IOAppComposer } from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppComposer.sol";

contract Composer is IOAppComposer {
    address public immutable endpoint;
    address public immutable trustedOApp;

    constructor(address _endpoint, address _trustedOApp) {
        endpoint = _endpoint;
        trustedOApp = _trustedOApp;
    }

    function lzCompose(
        address _oApp,
        bytes32 /*_guid*/,
        bytes calldata _message,
        address /*_executor*/,
        bytes calldata /*_extraData*/
    ) external payable override {
        // Security checks
        require(msg.sender == endpoint, "!endpoint");
        require(_oApp == trustedOApp, "!oApp");

        // Decode the message payload
        (string memory _string, ) = abi.decode(_message, (string, address));

        // Execute custom business logic
        performCustomAction(_string);
    }

    function performCustomAction(string memory message) internal {
        // Your custom logic here (swap, stake, mint, etc.)
    }
}
```

<Tip>
  **Execution Options for Composed Messages**: You must provide gas for both the main `lzReceive` call and the `lzCompose` call:

  ```solidity wrap theme={null}
  bytes memory options = OptionsBuilder.newOptions()
      .addExecutorLzReceiveOption(baseGas, 0)           // Main message processing
      .addExecutorLzComposeOption(0, composeGas, value); // Composed call (index 0)
  ```

  The `_index` parameter allows multiple composed calls with different gas allocations.
</Tip>

### Message Ordering

LayerZero supports both **unordered** (default) and **ordered** delivery patterns.

#### Ordered Delivery Implementation

```solidity wrap theme={null}
pragma solidity ^0.8.22;

import { OApp, Origin } from "@layerzerolabs/oapp-evm/contracts/oapp/OApp.sol";
import { OptionsBuilder } from "@layerzerolabs/oapp-evm/contracts/oapp/libs/OptionsBuilder.sol";

/**
 * @title OmniChain Nonce Ordered Enforcement Example
 * @dev Implements nonce ordered enforcement for your OApp.
 */
contract OrderedOApp is OApp {
    // Mapping to track the maximum received nonce for each source endpoint and sender
    mapping(uint32 eid => mapping(bytes32 sender => uint64 nonce)) private receivedNonce;

    constructor(address _endpoint, address _owner) OApp(_endpoint, _owner) {}

    /**
     * @dev Public function to get the next expected nonce for a given source endpoint and sender.
     * @param _srcEid Source endpoint ID.
     * @param _sender Sender's address in bytes32 format.
     * @return uint64 Next expected nonce.
     */
    function nextNonce(uint32 _srcEid, bytes32 _sender) public view virtual override returns (uint64) {
        return receivedNonce[_srcEid][_sender] + 1;
    }

    /**
     * @dev Internal function to accept nonce from the specified source endpoint and sender.
     * @param _srcEid Source endpoint ID.
     * @param _sender Sender's address in bytes32 format.
     * @param _nonce The nonce to be accepted.
     */
    function _acceptNonce(uint32 _srcEid, bytes32 _sender, uint64 _nonce) internal virtual override {
        uint64 expectedNonce = receivedNonce[_srcEid][_sender] + 1;
        require(_nonce == expectedNonce, "OApp: invalid nonce");
        receivedNonce[_srcEid][_sender] = _nonce; // Update to the accepted nonce
    }

    /**
     * @dev Override receive function to enforce strict nonce enforcement.
     * @dev This function is internal and should not be public.
     */
    function _lzReceive(
        Origin calldata _origin,
        bytes32 _guid,
        bytes calldata _message,
        address _executor,
        bytes calldata _extraData
    ) internal override {
        // Enforce nonce ordering before processing the message
        _acceptNonce(_origin.srcEid, _origin.sender, _origin.nonce);

        // Process your message logic here
        // Example: string memory receivedMessage = abi.decode(_message, (string));
    }

    // Must include ExecutorOrderedExecutionOption in your send options
    function sendOrdered(uint32 _dstEid, string memory _message) external payable {
        bytes memory options = OptionsBuilder.newOptions()
            .addExecutorLzReceiveOption(200000, 0)
            .addExecutorOrderedExecutionOption(); // Required for ordered execution

        _lzSend(_dstEid, abi.encode(_message), options, MessagingFee(msg.value, 0), payable(msg.sender));
    }
}
```

#### Important Nonce Management Considerations

When implementing ordered delivery, be aware of these critical nonce synchronization issues:

1. **Nonce Validation**: The `_acceptNonce` function must be called in `_lzReceive` to verify the incoming nonce matches the expected sequence before processing any message.

2. **Protocol vs Local Nonce Mismatch**: Functions like `skip()`, `burn()`, and `clear()` advance the protocol's nonce but **do not** automatically update your OApp's local nonce mapping. This creates a dangerous mismatch where:

   * Protocol nonce: 15 (after skipping message 15)
   * OApp mapping: 14 (still expecting message 15)
   * Result: All future messages will be rejected

3. **Solution**: If your OApp needs to use `skip()`, `burn()`, or `clear()`, you must **manually increment your local nonce** to stay synchronized:

```solidity wrap theme={null}
// When skipping a message, update your local tracking
function skipMessage(uint32 _srcEid, bytes32 _sender, uint64 _nonce) external onlyOwner {
    // Skip the message at protocol level
    endpoint.skip(this, _srcEid, _sender, _nonce);

    // Critical: Update local nonce to match protocol
    receivedNonce[_srcEid][_sender] = _nonce;
}
```

**Best Practice**: Only call these recovery functions from within your OApp contract, never externally, to ensure nonce synchronization is maintained.

### Rate Limiting

Control message frequency to prevent spam and ensure controlled crosschain interactions:

```solidity wrap theme={null}
contract RateLimitedOApp is OApp, RateLimiter {
    constructor(
        address _endpoint,
        address _owner,
        RateLimitConfig[] memory _rateLimitConfigs
    ) OApp(_endpoint, _owner) {
        _setRateLimits(_rateLimitConfigs);
    }

    function sendWithRateLimit(
        uint32 _dstEid,
        string memory _message,
        bytes calldata _options
    ) external payable {
        // Check rate limit before sending
        _outflow(_dstEid, 1); // 1 message

        _lzSend(
            _dstEid,
            abi.encode(_message),
            _options,
            MessagingFee(msg.value, 0),
            payable(msg.sender)
        );
    }
}
```

### Further Reading

For detailed implementations and advanced patterns, see:

* [Message Execution Options](../configuration/options) - Options configuration
* [OApp Technical Reference](../../../concepts/technical-reference/oapp-reference) - Deep dive into OApp mechanics
* [Integration Checklist](../../../tools/integration-checklist) - Security considerations and best practices

### Tracing and Troubleshooting

You can follow your testnet and mainnet transaction statuses using [LayerZero Scan](https://layerzeroscan.com/).

Refer to [Debugging Messages](../troubleshooting/debugging-messages) for any unexpected complications when sending a message.

You can also ask for help or follow development in the [Discord](https://discord.com/invite/ktbvm8Nkcr).
