Skip to main content

Message Execution Options

What are message _options?

Because the source chain has no concept of the destination chain's state, you must specify the amount of gas you anticipate will be necessary for executing your lzReceive or lzCompose method on the destination smart contract.

LayerZero provides robust Message Execution Options, which allow you to specify arbitrary logic as part of the message transaction, such as the gas amount and msg.value the Executor pays for message delivery, the order of message execution, or dropping an amount of gas to a destination address.

The most common options you will use when building are lzReceiveOption, lzComposeOption, and lzNativeDropOption.

Options Builders

A Solidity library and off-chain SDK have been provided to build specific Message Options for your application.

Generating Options

You can generate options depending on your OApp's development environment:

  • Remix: for quick testing in Remix, you can deploy locally to the Remix VM a contract using the OptionsBuilder.sol library. See the example provided below.

    Open in RemixWhat is Remix?

  • Foundry: _options can be generated directly in your Foundry unit tests using the OptionsBuilder.sol library. See the OmniCounter Test file as an example for how to properly invoke options.

  • Hardhat: you can also locally declare options in Hardhat via the options.ts file.

All tools use the same method for packing the _options bytes array to simplify your experience when switching between environments.

Step 1: Import Options

All _options tools must be imported into your environment to be used.

Options Library

Import the OptionsBuilder from @layerzerolabs/lz-evm-oapp-v2 into either your Foundry test or smart contract to be deployed locally.

import { OptionsBuilder } from "@layerzerolabs/lz-evm-oapp-v2/contracts/oapp/libs/OptionsBuilder.sol";

Options SDK

Start by importing Options from @layerzerolabs/lz-v2-utilities.

import {Options} from '@layerzerolabs/lz-v2-utilities';

Step 2: Initializing Options

The newOptions method is used to initialize a new bytes array.

const _options = Options.newOptions();

It's a starting point to which you can add specific option configurations.

info

Message Options start with a magic number to identify the options type, followed by type-specific instructions.

TYPE_1 and TYPE_2 are legacy options identifiers for handling Endpoint V1 messages (ie. Relayer Adapter Parameters).

For LayerZero V2, only use TYPE_3 as outlined in this page.

Step 3: Adding Option Types

When generating _options, you will want specific handling for different message types used in your smart contract. For instance, addExecutorLzReceiveOption is a method that can be used to specify how the Executor calls lzReceive on the receiving chain.

const executorGas = 1000000; // Gas limit for the executor
const executorValue = 0; // msg.value for the lzReceive() function on destination in wei

const _options = Options.newOptions().addExecutorLzReceiveOption(executorGas, executorValue);

You can continue appending new Options methods to add more Executor message handling; all packed into a single call.

See below for all Option Types.

Step 4: Passing Options in Send

After generating _options, you will want to test them in a send call.

Options SDK

Using the Options SDK, this can be passed directly into a Hardhat task or unit test depending on your use case.

// Other parameters for the send function
const _dstEid = 'someEndpointId'; // Destination endpoint ID
const message = 'Your message here'; // The message you want to send

// Call the send function on the smart contract
// Convert your options array toHex()
const tx = await yourOAppContract.send(destEndpointId, message, _options.toHex());
await tx.wait();

In this Typescript snippet, the send function is being called on the YourOAppContract contract instance, passing in the destination endpoint ID, the message, and the _options that were constructed:

contract YourOAppContract {
// ... other functions and declarations

function send(uint32 _dstEid, string memory message, bytes memory _options) public payable {
// Logic to handle the sending of the message with the provided options
// This might involve interacting with other contracts or internal logic
bytes memory _payload = abi.encode(message);
_lzSend(
_dstEid, // Destination chain's endpoint ID.
_payload, // Encoded message payload being sent.
_options, // Message execution options (e.g., gas to use on destination).
MessagingFee(msg.value, 0), // Fee struct containing native gas and ZRO token.
payable(msg.sender) // The refund address in case the send call reverts.
);
}

// ... other functions and declarations
}

In this Solidity snippet, the send function takes three parameters: _dstEid, message, and _options. The function's logic would then use those parameters to to send a message cross-chain.

Options Library

Using the OptionsBuilder.sol library, these _options can be directly referenced in your Foundry tests for quick local testing.

import { MyOApp } from "../contracts/oapp/examples/MyOApp.sol";
import { TestHelper } from "../contracts/tests/TestHelper.sol";
import { OptionsBuilder } from "@layerzerolabs/lz-evm-oapp-v2/contracts/oapp/libs/OptionsBuilder.sol";

contract MyOAppTest is TestHelper {
using OptionsBuilder for bytes;
// ... other test setup functions
function test_increment() public {
bytes memory options = OptionsBuilder.newOptions().addExecutorLzReceiveOption(200000, 0);
(uint256 nativeFee, ) = aCounter.quote(bEid, MsgCodec.VANILLA_TYPE, options);
aCounter.increment{ value: nativeFee }(bEid, MsgCodec.VANILLA_TYPE, options);
// ... other test logic
}
}

The above example is taken from the OmniCounter Foundry Test in the LayerZero V2 repo.

info

See TestHelper.sol for full Foundry testing support.

Option Types

There are multiple option types to take advantage of, each controlling specific handling of LayerZero messages.

lzReceive Option

The lzReceive option specifies the gas values the Executor uses when calling lzReceive on the destination chain.

Options.newOptions().addExecutorLzReceiveOption(200000, 0);

It defines the amount of _gas and msg.value to be used in the lzReceive call by the Executor on the destination chain:

OPTION_TYPE_LZRECEIVE contains (uint128 _gas, uint128 _value)

_gas: The amount of gas you'd provide for the lzReceive call in source chain native tokens. 200000 should be enough for most transactions.

_value: The msg.value for the call. This value is often included to fund any operations that need native gas on the destination chain, including sending another nested message.

lzCompose Option

This option allows you to allocate some gas and value to your Composed Message on the destination chain. lzCompose is used when you want to call external contracts from your lzReceive function.

Options.newOptions().addExecutorLzComposeOption(0, 1000000, 0);

OPTION_TYPE_LZCOMPOSE contains (uint16 _index, uint128 _gas, uint128 _value)

_index: The index of the lzCompose() function call. When multiples of this option are added, they are summed PER index by the Executor on the remote chain. This can be useful for defining multiple composed message steps that happen sequentially.

_gas: The gas amount for the lzCompose call varies based on the destination's compose logic and the destination chain's characteristics (e.g., opcode pricing). Although 1000000 is commonly used as a default, it's important to perform tailored testing to determine the optimal gas requirement for your specific transaction needs.

_value: The msg.value for the call.

lzNativeDrop Option

This option contains how much native gas you want to drop to the _receiver, this is often done to allow users or a contract to have some gas on a new chain.

Options.newOptions().addExecutorNativeDropOption(1000000000000, receiverAddressInBytes32);

OPTION_TYPE_LZNATIVEDROP contains (uint128 _amount, bytes32 _receiver)

_amount: The amount of gas in wei to drop for the receiver.

_receiver: The bytes32 representation of the receiver address.

OrderedExecution Option

By adding this option, the Executor will utilize Ordered Message Delivery. This overrides the default behavior of Unordered Message Delivery.

Options.newOptions().addExecutorOrderedExecutionOption(bytes(''));

For example, if nonce 2 transaction fails, all subsequent transactions with this option will not be executed until the previous message has been resolved with.

bytes: The argument should always be initialized as an empty bytes array ("").

caution

These message _options must be combined with in-app contract changes, listed under Ordered Message Delivery.

Duplicate Option Types

Multiple options of the same type can be passed and appended into the same options array. The logic on how multiple options of the same type are summed differs per option type:

bytes memory options = OptionsBuilder.newOptions().addExecutorLzReceiveOption(200000, 0).addExecutorLzComposeOption(0, 1000000, 0).addExecutorLzComposeOption(1, 500000, 0);
  • lzReceive: Both the _gas and _value parameters are summed.

  • lzCompose: Both the _gas and _value parameters are summed by index.

  • lzNativeDrop: The _amount parameter is summed by unique _receiver address.

Make sure that appending multiple options is the intended behavior of your unique OApp.