Skip to main content
Version: Endpoint V2 Docs

Design Patterns & Extensions

Each design pattern functions as a distinct omnichain building block, capable of being used independently or in conjunction with others.

Message PatternDescription
ABAa nested send call from Chain A to Chain B that sends back again to the source chain (A -> B -> A)
Batch Senda single send that calls multiple destination chains
Composeda message that transfers from a source to destination chain and calls an external contract (A -> B1 -> B2)
Composed ABAtransfers data from a source to destination, calls an external contract, and then calls back to the source (A -> B1 -> B2 -> A)
Message Orderingenforce the ordered delivery of messages on execution post verification
Rate Limitrate limit the number of send calls for a given amount of messages or tokens transferred

This modularity allows for the seamless integration and combination of patterns to suit specific developer requirements.

ABA

AB messaging refers to a one way send call from a source to destination blockchain.

OFT Example OFT Example

In the Getting Started Guide, we use this design pattern to send a string from Chain A to store on Chain B (A -> B).

The ABA message pattern extends this simple logic by nesting another _lzSend call within the destination contract's _lzReceive function. You can think of this as a ping-pong style call, pinging a destination chain and ponging back to the original source (A -> B -> A).

ABA Light ABA Dark


This is particularly useful when actions on one blockchain depend on the state or confirmation of another, such as:

  • Conditional Execution of Contracts: A smart contract on chain A will only execute a function if a condition on chain B is met. It sends a message to chain B to check the condition and then receives a confirmation back before proceeding.

  • Omnichain Data Feeds: A contract on Chain A can fetch data from the destination (Chain B) to complete a process back on the source.

  • Cross-chain Authentication: A user or contract might authenticate on chain A, ping chain B to process something that requires this authentication, and then receive back a token or confirmation that the process was successful.

Code Example

This pattern demonstrates vertical composability, where the nested message contains both handling for the message receipt, as well as additional logic for a subsequent call that must all happen within one atomic transaction.

// 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 ABA contract for demonstrating LayerZero messaging between blockchains.
* @notice THIS IS AN EXAMPLE CONTRACT. DO NOT USE THIS CODE IN PRODUCTION.
* @dev This contract showcases a PingPong style call (A -> B -> A) using LayerZero's OApp Standard.
*/
contract ABA 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.
uint16 public constant SEND = 1;
uint16 public constant SEND_ABA = 2;

/// @notice Emitted when a return message is successfully sent (B -> A).
event ReturnMessageSent(string message, uint32 dstEid);

/// @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 PingPong 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) {}

function encodeMessage(string memory _message, uint16 _msgType, bytes memory _extraReturnOptions) public pure returns (bytes memory) {
// Get the length of _extraReturnOptions
uint256 extraOptionsLength = _extraReturnOptions.length;

// Encode the entire message, prepend and append the length of extraReturnOptions
return abi.encode(_message, _msgType, extraOptionsLength, _extraReturnOptions, extraOptionsLength);
}

/**
* @notice Returns the estimated messaging fee for a given message.
* @param _dstEid Destination endpoint ID where the message will be sent.
* @param _msgType The type of message being sent.
* @param _message The message content.
* @param _extraSendOptions Gas options for receiving the send call (A -> B).
* @param _extraReturnOptions Additional gas options for the return call (B -> A).
* @param _payInLzToken Boolean flag indicating whether to pay in LZ token.
* @return fee The estimated messaging fee.
*/
function quote(
uint32 _dstEid,
uint16 _msgType,
string memory _message,
bytes calldata _extraSendOptions,
bytes calldata _extraReturnOptions,
bool _payInLzToken
) public view returns (MessagingFee memory fee) {
bytes memory payload = encodeMessage(_message, _msgType, _extraReturnOptions);
bytes memory options = combineOptions(_dstEid, _msgType, _extraSendOptions);
fee = _quote(_dstEid, payload, options, _payInLzToken);
}


/**
* @notice Sends a message to a specified destination chain.
* @param _dstEid Destination endpoint ID for the message.
* @param _msgType The type of message to send.
* @param _message The message content.
* @param _extraSendOptions Options for sending the message, such as gas settings.
* @param _extraReturnOptions Additional options for the return message.
*/
function send(
uint32 _dstEid,
uint16 _msgType,
string memory _message,
bytes calldata _extraSendOptions, // gas settings for A -> B
bytes calldata _extraReturnOptions // gas settings for B -> A
) external payable {
// Encodes the message before invoking _lzSend.
require(bytes(_message).length <= 32, "String exceeds 32 bytes");

if (_msgType != SEND && _msgType != SEND_ABA) {
revert InvalidMsgType();
}

bytes memory options = combineOptions(_dstEid, _msgType, _extraSendOptions);

_lzSend(
_dstEid,
encodeMessage(_message, _msgType, _extraReturnOptions),
options,
// Fee in native gas and ZRO token.
MessagingFee(msg.value, 0),
// Refund address in case of failed source message.
payable(msg.sender)
);

emit MessageSent(_message, _dstEid);
}

function decodeMessage(bytes calldata encodedMessage) public pure returns (string memory message, uint16 msgType, uint256 extraOptionsStart, uint256 extraOptionsLength) {
extraOptionsStart = 256; // Starting offset after _message, _msgType, and extraOptionsLength
string memory _message;
uint16 _msgType;

// Decode the first part of the message
(_message, _msgType, extraOptionsLength) = abi.decode(encodedMessage, (string, uint16, uint256));

return (_message, _msgType, extraOptionsStart, extraOptionsLength);
}

/**
* @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, uint16 _msgType, uint256 extraOptionsStart, uint256 extraOptionsLength) = decodeMessage(message);
data = _data;

if (_msgType == SEND_ABA) {

string memory _newMessage = "Chain B says goodbye!";

bytes memory _options = combineOptions(_origin.srcEid, SEND, message[extraOptionsStart:extraOptionsStart + extraOptionsLength]);

_lzSend(
_origin.srcEid,
abi.encode(_newMessage, SEND),
// Future additions should make the data types static so that it is easier to find the array locations.
_options,
// Fee in native gas and ZRO token.
MessagingFee(msg.value, 0),
// Refund address in case of failed send call.
// @dev Since the Executor makes the return call, this contract is the refund address.
payable(address(this))
);

emit ReturnMessageSent(_newMessage, _origin.srcEid);
}

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


receive() external payable {}

}
info

This message pattern can also be considered an ABC type call (A -> B -> C), as the nested _lzSend can send to any new destination chain.

Batch Send

The Batch Send design pattern, where a single transaction can initiate multiple _lzSend calls to various destination chains, is highly efficient for operations that need to propagate an action across several blockchains simultaneously.

Batch Send Light Batch Send Dark

This can significantly reduce the operational overhead associated with performing the same action multiple times on different blockchains. It streamlines omnichain interactions by bundling them into a single transaction, making processes more efficient and easier to manage for example:

  • Simultaneous Omnichain Updates: When a system needs to update the same information across multiple chains (such as a change in governance parameters or updating oracle data), Batch Send can propagate the updates in one go.

  • DeFi Strategies: For DeFi protocols that operate on multiple chains, rebalancing liquidity pools or executing yield farming strategies can be done in batch to maintain parity across ecosystems.

  • Aggregated Omnichain Data Posting: Oracles or data providers that supply information to smart contracts on multiple chains can use Batch Send to post data such as price feeds, event outcomes, or other updates in a single transaction.

Code Example

// 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 BatchSend contract for demonstrating multiple outbound cross-chain calls using LayerZero.
* @notice THIS IS AN EXAMPLE CONTRACT. DO NOT USE THIS CODE IN PRODUCTION.
* @dev This contract showcases how to send multiple cross-chain calls with one source function call using LayerZero's OApp Standard.
*/
contract BatchSend 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) {}

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,
bytes calldata _extraSendOptions,
bool _payInLzToken
) public view returns (MessagingFee memory totalFee) {
bytes memory encodedMessage = abi.encode(_message);

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 {
if (_msgType != SEND) {
revert InvalidMsgType();
}

// Calculate the total messaging fee required.
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;

// Ensure the current call has enough allocated fee from msg.value.
require(remainingValue >= 0, "Insufficient fee for this destination");

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

emit MessageSent(_message, _dstEids[i]);
}
}

/**
* @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);
}
}

Composed

A composed message refers to an application that invokes the Endpoint method, sendCompose, to deliver a composed call to a destination contract via lzCompose.

Composed Light Composed Dark

This pattern demonstrates horizontal composability, which differs from vertical composability in that the external call is now containerized as a new message packet; enabling the application to ensure that certain receipt logic remains separate from the external call itself.

info

Since each composable call is created as a separate message packet via lzCompose, this pattern can be extended for as many steps as your application needs (B1 -> B2 -> B3, etc).


This pattern can be particularly powerful for orchestrating complex interactions and processes on the destination chain that need contract logic to be handled in independent steps, such as:

  • Omnichain DeFi Strategies: A smart contract could trigger a token transfer on the destination chain and then automatically interact with a DeFi protocol to lend, borrow, provide liquidity, stake, etc. executing a series of financial strategies across chains.

  • NFT Interactions: An NFT could be transferred to another chain, and upon arrival, it could trigger a contract to issue a license, register a domain, or initiate a subscription service linked to the NFT's ownership.

  • DAO Coordination: A DAO could send funds to another chain's contract and compose a message to execute specific DAO-agreed upon investments or funding of public goods.


Composing an OApp

There are 3 relevant contract interactions when composing an OApp:

  1. Source OApp: the OApp sending a cross-chain message via _lzSend to a destination.

  2. Destination OApp(s): the OApp receiving a cross-chain message via _lzReceive and calling sendCompose.

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

Sending Message

The sending OApp is required to pass specific Composed Message Execution Options (more on this below) for the sendCompose call, but is not required to pass any input parameters for the call itself (however this pattern may be useful depending on what arbitrary action you wish to trigger when composing).

For example, this send function packs the destination _composedAddress for the destination OApp to decode and use for the actual composed call.

/// @notice Sends a message from the source to destination chain.
/// @param _dstEid Destination chain's endpoint ID.
/// @param _message The message to send.
/// @param _composedAddress The contract you wish to deliver a composed call to.
/// @param _options Message execution options (e.g., for sending gas to destination).
function send(
uint32 _dstEid,
string memory _message,
address _composedAddress, // the destination contract implementing ILayerZeroComposer
bytes calldata _options
) external payable returns(MessagingReceipt memory receipt) {
// Encodes the message before invoking _lzSend.
bytes memory _payload = abi.encode(_message, _composedAddress);
receipt = _lzSend(
_dstEid,
_payload,
_options,
// Fee in native gas and ZRO token.
MessagingFee(msg.value, 0),
// Refund address in case of failed source message.
payable(msg.sender)
);
}

Sending Compose

The receiving OApp invokes the LayerZero Endpoint's sendCompose method as part of your OApp's _lzReceive business logic.

The sendCompose method takes the following inputs:

  1. _to: the contract implementing the ILayerZeroComposer receive interface.

  2. _guid: the global unique identifier of the source message (provided standard by lzReceive).

  3. _index: the index of the composed message (used for pricing different gas execution amounts along different composed legs of the transaction).

/// @dev the Oapp sends the lzCompose message to the endpoint
/// @dev the composer MUST assert the sender because anyone can send compose msg with this function
/// @dev with the same GUID, the Oapp can send compose to multiple _composer at the same time
/// @dev authenticated by the msg.sender
/// @param _to the address which will receive the composed message
/// @param _guid the message guid
/// @param _message the message
function sendCompose(address _to, bytes32 _guid, uint16 _index, bytes calldata _message) external {
// must have not been sent before
if (composeQueue[msg.sender][_to][_guid][_index] != NO_MESSAGE_HASH) revert Errors.ComposeExists();
composeQueue[msg.sender][_to][_guid][_index] = keccak256(_message);
emit ComposeSent(msg.sender, _to, _guid, _index, _message);
}

This means that when a packet is received (_lzReceive) by the Destination OApp, it will send (sendCompose) a new composed packet via the destination LayerZero Endpoint.

/// @dev Called when data is received from the protocol. It overrides the equivalent function in the parent contract.
/// Protocol messages are defined as packets, comprised of the following parameters.
/// @param _origin A struct containing information about where the packet came from.
/// @param _guid A global unique identifier for tracking the packet.
/// @param payload Encoded message.
function _lzReceive(
Origin calldata _origin,
bytes32 _guid,
bytes calldata payload,
address, // Executor address as specified by the OApp.
bytes calldata // Any extra data or options to trigger on receipt.
) internal override {
// Decode the payload to get the message
(string memory _message, address _composedAddress) = abi.decode(payload, (string, address));
// Storing data in the destination OApp
data = _message;
// Send a composed message[0] to a composed receiver
endpoint.sendCompose(_composedAddress, _guid, 0, payload);
}
info

The above sendCompose call hardcodes _index to 0 and simply forwards the same payload as _lzReceive to lzCompose, however these inputs can also be dynamically adjusted depending on the number and type of composed calls you wish to make.

Composed Message Execution Options

You can decide both the _gas and msg.value that should be used for the composed call(s), depending on the type and quantity of messages you intend to send.

Your configured Executor will use the _options provided in the original _lzSend call to determine the gas limit and amount of msg.value to include per message _index:

// addExecutorLzComposeOption(uint16 _index, uint128 _gas, uint128 _value)
Options.newOptions()
.addExecutorLzReceiveOption(50000, 0)
.addExecutorLzComposeOption(0, 30000, 0)
.addExecutorLzComposeOption(1, 30000, 0);

It's important to remember that gas costs may vary depending on the destination chain. For example, all new Ethereum transactions cost 21000 wei, but other chains may have lower or higher opcode costs, or entirely different gas mechanisms.

You can read more about generating _options and the role of _index in Message Execution Options.

Receiving Compose

The destination must implement the ILayerZeroComposer interface to handle receiving the composed message.

From there, you can decide any additional composed business logic to execute within lzCompose, as shown below:

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

import { ILayerZeroComposer } from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/ILayerZeroComposer.sol";

/// @title ComposedReceiver
/// @dev A contract demonstrating the minimum ILayerZeroComposer interface necessary to receive composed messages via LayerZero.
contract ComposedReceiver is ILayerZeroComposer {

/// @notice Stores the last received message.
string public data = "Nothing received yet";

/// @notice Store LayerZero addresses.
address public immutable endpoint;
address public immutable oApp;

/// @notice Constructs the contract.
/// @dev Initializes the contract.
/// @param _endpoint LayerZero Endpoint address
/// @param _oApp The address of the OApp that is sending the composed message.
constructor(address _endpoint, address _oApp) {
endpoint = _endpoint;
oApp = _oApp;
}

/// @notice Handles incoming composed messages from LayerZero.
/// @dev Decodes the message payload and updates the state.
/// @param _oApp The address of the originating OApp.
/// @param /*_guid*/ The globally unique identifier of the message.
/// @param _message The encoded message content.
function lzCompose(
address _oApp,
bytes32 /*_guid*/,
bytes calldata _message,
address,
bytes calldata
) external payable override {
// Perform checks to make sure composed message comes from correct OApp.
require(_oApp == oApp, "!oApp");
require(msg.sender == endpoint, "!endpoint");

// Decode the payload to get the message
(string memory message, ) = abi.decode(_message, (string, address));
data = message;
}
}

Further Reading

For more advanced implementations of sendCompose and lzCompose:

  • Review the OmniCounter.sol for sending composed messages to the same OApp implementation.
  • Read the OFT Composing section to see how to implement composed business logic into your OFTs.

Composed ABA

The Composed ABA design pattern enables sophisticated omnichain communication by allowing for an operation to be performed as part of the receive logic on the destination chain (B1), a follow-up action or call containerized as an independent step within lzCompose (B2), which then sends back to the source chain (A).

ComposedABA Light ComposedABA Dark

info

This message pattern can also be considered a Composed ABC type call (A -> B1 -> B2 -> C), as the nested _lzSend can send to any new destination chain.


This pattern demonstrates a complex, multi-step, process across blockchains where each step requires its own atomic logic to execute without depending on separate execution logic. Here are some use cases that could benefit from a Composed ABA design pattern:

  • Omnichain Data Verification: Chain A sends a request to chain B to verify a set of data. Once verified, a contract on chain B executes an action based on this data and sends a signal back to chain A to either proceed with the next step or record the verification.

  • Omnichain Collateral Management: When collateral on chain A is locked or released, a corresponding contract on chain B could be called to issue a loan or unlock additional funds. Confirmation of the action is then sent back to chain A to complete the process.

  • Multi-Step Contract Interaction for Games and Collectibles: In a gaming scenario, an asset (like an NFT) could be sent from chain A to B, triggering a contract on B that could unlock a new level or feature in a game, with a confirmation or reward then sent back to chain A.

Message Ordering

LayerZero offers both unordered delivery and ordered delivery, providing developers with the flexibility to choose the most appropriate transaction ordering mechanism based on the specific requirements of their application.

Unordered Delivery

By default, the LayerZero protocol uses unordered delivery, where transactions can be executed out of order if all transactions prior have been verified.

If transactions 1 and 2 have not been verified, then transaction 3 cannot be executed until the previous nonces have been verified.

Once nonces 1, 2, 3 have been verified:

  • If nonce 2 failed to execute (due to some gas or user logic related issue), nonce 3 can still proceed and execute.

Lazy Nonce Enforcement Light Lazy Nonce Enforcement Dark

This is particularly useful in scenarios where transactions are not critically dependent on the execution of previous transactions.

Ordered Delivery

Developers can configure the OApp contract to use ordered delivery.

Strict Nonce Enforcement Light Strict Nonce Enforcement Dark

In this configuration, if you have a sequence of packets with nonces 1, 2, 3, and so on, each packet must be executed in that exact, sequential order:

  • If nonce 2 fails for any reason, it will block all subsequent transactions with higher nonces from being executed until nonce 2 is resolved.

Strict Nonce Enforcement Fail Light Strict Nonce Enforcement Fail Dark

Strict nonce enforcement can be important in scenarios where the order of transactions is critical to the integrity of the system, such as any multi-step process that needs to occur in a specific sequence to maintain consistency.

info

In these cases, strict nonce enforcement can be used to provide consistency, fairness, and censorship-resistance to maintain system integrity.

Code Example

To implement strict nonce enforcement, you need to implement the following:

  • a mapping to track the maximum received nonce.

  • override _acceptNonce and nextNonce.

  • add ExecutorOrderedExecutionOption in _options when calling _lzSend.

caution

If you do not pass an ExecutorOrderedExecutionOption in your _lzSend call, the Executor will attempt to execute the message despite your application-level nonce enforcement, leading to a message revert.

Append to your Message Options an ExecutorOrderedExecutionOption in your _lzSend call:

// appends "01000104", the ExecutorOrderedExecutionOption, to your options bytes array
_options = OptionsBuilder.newOptions().addExecutorLzReceiveOption(200000, 0).addExecutorOrderedExecutionOption();

Implement strict nonce enforcement via function override:

pragma solidity ^0.8.20;

import { OApp } from "@layerzerolabs/oapp-evm/contracts/oapp/OApp.sol"; // Import OApp and other necessary contracts/interfaces

/**
* @title OmniChain Nonce Ordered Enforcement Example
* @dev Implements nonce ordered enforcement for your OApp.
*/
contract MyNonceEnforcementExample 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;

/**
* @dev Constructor to initialize the omnichain contract.
* @param _endpoint Address of the LayerZero endpoint.
* @param _owner Address of the contract owner.
*/
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 {
receivedNonce[_srcEid][_sender] += 1;
require(_nonce == receivedNonce[_srcEid][_sender], "OApp: invalid nonce");
}

// @dev Override receive function to enforce strict nonce enforcement.
function _lzReceive(
Origin calldata _origin,
bytes32 _guid,
bytes calldata _message,
address _executor,
bytes calldata _extraData
) public payable virtual override {
_acceptNonce(_origin.srcEid, _origin.sender, _origin.nonce);
// Call the internal function with the correct parameters
super._lzReceive(_origin, _guid, _message, _executor, _extraData);
}
}

Rate Limiting

The RateLimiter.sol is used to control the number of cross-chain messages that can be sent within a certain time window, ensuring that the OApp is not spammed by too many transactions at once. It's particularly useful for:

  • Preventing Denial of Service Attacks: By setting thresholds on the number of messages that can be processed within a given timeframe, the RateLimiter acts as a safeguard against DoS attacks, where malicious actors might attempt to overload an OApp with a flood of transactions. This protection ensures that the OApp remains accessible and functional for legitimate users, even under attempted attacks.

  • Regulatory Compliance: Some applications may need to enforce limits to comply with legal or regulatory requirements.

The RateLimiter is only useful under specific application use cases. It will not be necessary for most OApps and can even be counterproductive for more generic applications:

  • Low Traffic Applications: If your application doesn't expect high volumes of traffic, implementing a rate limiter might be unnecessary overhead.

  • Critical Systems Requiring Immediate Transactions: For systems where transactions need to be processed immediately without delay, rate limiting could hinder performance.

Installation

To begin working with LayerZero contracts, you can install the OApp npm package to an existing project:

npm install @layerzerolabs/lz-evm-oapp-v2

Usage

Import the RateLimiter.sol contract into your OApp contract file and inherit the contract:

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.22;

import { OApp } from "@layerzerolabs/oapp-evm/contracts/oapp/OApp.sol";
import { RateLimiter } from "@layerzerolabs/oapp-evm/contracts/oapp/utils/RateLimiter.sol";

contract MyOApp is OApp, RateLimiter {
// ...contract
}

Initializing Rate Limits

In the constructor of your contract, initialize the rate limits using _setRateLimits with an array of RateLimitConfig structs.

Example:

constructor(
RateLimitConfig[] memory _rateLimitConfigs,
address _lzEndpoint,
address _delegate
) OApp(_lzEndpoint, _delegate) {
_setRateLimits(_rateLimitConfigs);
}
// ...Rest of contract code

RateLimitConfig Struct:

struct RateLimitConfig {
uint32 dstEid; // destination endpoint ID
uint256 limit; // arbitrary limit of messages/tokens to transfer
uint256 window; // window of time before limit resets
}

Setting Rate Limits

Provide functions to set or update rate limits dynamically. This can include a function to adjust individual or multiple rate limits and a mechanism to authorize who can make these changes (typically restricted to the contract owner or a specific role).

/**
* @dev Sets the rate limits based on RateLimitConfig array. Only callable by the owner or the rate limiter.
* @param _rateLimitConfigs An array of RateLimitConfig structures defining the rate limits.
*/
function setRateLimits(
RateLimitConfig[] calldata _rateLimitConfigs
) external {
if (msg.sender != rateLimiter && msg.sender != owner()) revert OnlyRateLimiter();
_setRateLimits(_rateLimitConfigs);
}

Checking Rate Limits During Send Calls

Before processing transactions, use _checkAndUpdateRateLimit to ensure the transaction doesn't exceed the set limits. This function should be called in any transactional functions, such as message passing or token transfers.

Message Passing

function send(
uint32 _dstEid,
string memory _message,
bytes calldata _options
) external payable {
_checkAndUpdateRateLimit(_dstEid, 1); // updating the rate limit per message sent
bytes memory _payload = abi.encode(_message); // Encodes message as bytes.
_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.
);
}

Token Transfers

/**
* @dev Checks and updates the rate limit before initiating a token transfer.
* @param _amountLD The amount of tokens to be transferred.
* @param _minAmountLD The minimum amount of tokens expected to be received.
* @param _dstEid The destination endpoint identifier.
* @return amountSentLD The actual amount of tokens sent.
* @return amountReceivedLD The actual amount of tokens received.
*/
function _debit(
uint256 _amountLD,
uint256 _minAmountLD,
uint32 _dstEid
) internal virtual override returns (uint256 amountSentLD, uint256 amountReceivedLD) {
_checkAndUpdateRateLimit(_dstEid, _amountLD);
return super._debit(_amountLD, _minAmountLD, _dstEid);
}