Skip to main content
  1. A user calls a smart contract OApp on the source chain and pays a fee to send a crosschain message to the Endpoint.
  2. The Endpoint check the validity of the crosschain message and assigns each job to the OApp configured DVNs (Decentralized Verifier Networks) and Executor to execute the crosschain message.
  3. The DVNs verify the message on the destination chain. After the required and optional DVNs have verified the message, the message is to be inserted (committed) in the message channel of the Endpoint on the destination chain.
  4. After the message has been inserted in the Endpoint’s message channel, the Executor calls Endpoint.lzReceive to trigger the execution of the crosschain message on the destination chain.
  5. The Endpoint calls the payable ReceiverOApp.lzReceive to pass the message and execute the internal receive logic. You can modify the internal execution logic inside ReceiverOApp._lzReceive to trigger any intended outcome from the crosschain message.

You can find all of the above contracts by visiting Supported Chains and Supported DVNs.

Send Overview

The OApp calls EndpointV2.send to send the crosschain message and pays a fee to each configured DVN and Executor.

EndpointV2.sol

Inside the send call:
  • emit event to each DVN and Executor according to the OApp send configuration for the crosschain message. Also calculate and record the fee that should be paid to each DVN and Executor.
  • check whether the fees the user is willing to pay can cover the fees required by the DVNs and Executor.
  • transfer fee to _sendLibrary (which records fee allocation).
Inside the internal _send call:
  • get the nonce of this packet according to the path: [sender, destination chain, receiver].
  • generate guid of the packet (global unique identifier).
  • get the _sendLibrary of the OApp (OApp can set their specific send library of each destination chain).
  • call _sendLibrary to emit events to notify Executor and DVNs, also calculate and record the fee that should be paid to each.
The guid is generated using the following parameters:

SendUln302.sol

Next, the message is handled by the OApp selected Send Library. For example, SendUln302.send:
  • pay workers (DVNs and Executor) and treasury. In the send process, the fee is not directly paid to the workers, but recorded in the send library (SendUln302.sol) for workers to claim later.
  • call DVNs and Executor’s contract to emit event to notify them to send crosschain message.
Inside the SendUln302._payWorkers, the contract:
  • splits options to get executorOptions (Executor) and validationOptions (DVN).
  • get the OApp set Executor and corresponding maxMessageSize (If not set, then a default maxMessageSize of 10000 bytes is used), and checks that the size of the message to send is less than than the max.
  • calls _payExecutor to assign job to corresponding Executor and record the fee paid.
  • calls _payVerifier to assign job to specified DVNs and record fee paid.
Inside the SendUln302._payExecutor:
  • calls Executor (default or set by OApp) to assign job and calculate the fee needed.
  • record the Executor’s fee inside the send library.
Inside the SendUln302._payVerifier:
  • calculate payloadHash and payload, which will be used to emit event to notify DVN to send the crosschain message.
    • payloadHash is a digest including information about the version and path of the crosschain message;
    • payload includes information of the guid and the body of the crosschain message.
  • get the sender OApp config about which DVNs to use.
  • assign job for each DVN, including both required and optional.
Inside the SendUln302._assignJobs:
  • call each required and optional DVN to notify them to verify the crosschain message on the destination chain.
  • update each DVN’s fee.
  • return the totalFee used by all DVNs.

Assign Job to Executor

Executor.assignJob calls ExecutorFeeLib.getFeeOnSend to calculate the fee that should be paid to the Executor, and emit an event to notify. In the ExecutorFeeLib.getFeeOnSend, it will check the msg.value specified by the message sender and enforce that it should be smaller than the DstConfig.nativeCap of the destination chain. This is because the supply of native tokens (e.g., Ether) must be maintained by the Executor, and is not controlled by the OApp unless running a custom Executor.

Assign Job to DVNs

DVN.assignJob calls DVNFeeLib.getFeeOnSend to calculate the fee that should be paid to the DVNs, and emit events to notify them.

Send Limitations

Max Message Bytes Size

The maxMessageSize depends on the Send Library. In SendUln302, the default max is 10000 bytes, but this value can be configured per OApp.

Max Native Gas Token Requests

In the ExecutorFeeLib._decodeExecutorOptions, it limits the maximum native gas token amount that can be requested from the Executor for the destination chain transaction. This config is set in Executor.dstConfig:

Verification Workflow

After the crosschain message has been sent on the source chain (event has been emitted to notify DVNs and Executor), DVN will first verify the message on the destination chain, after which Executor will execute the message.

DVN Verification

DVNs call ReceiveUln302.verify to submit their witness of the source crosschain message using the _payloadHash.

Commit Verification

After the OApp’s required DVNs have all verified, and the threshold of optional DVNs has been reached, ReceiveUln302.commitVerification can be called by any address to commit the verification to the Endpoint’s message channel.
_verifyAndReclaimStorage verifies that the required and optional DVNs have submitted witness.

Insert Hash to Endpoint’s Message Channel

Inside the verify:
  • check msg.sender is valid ReceiveLibrary configured by the OApp.
  • get the lazyNonce of the OApp.
  • check the crosschain message path is valid for the receiver.
  • check the message represented by the nonce has not been executed before.
  • insert the message into the Endpoint’s message channel.
lazyNonce is the latest executed message’s nonce. To execute a transaction, LayerZero requires all messages before the current message has been verified. So all messages before the message with lazyNonce has been verified.
isValidReceiveLibrary checks whether the ReceiveLib is the expected ReceiveLib of the receiver. If not, then check whether there has been a Timeout set for the current ReceiveLib. Timeout is used to help improve the UX of updating a ReceiveLib. For example, if OApp decides to switch the ReceiveLib, it can update the address on the destination chain, but some crosschain messages may already be in-flight and not inserted in the destination chain Endpoint’s message channel before the switch. Those messages depend on the previous ReceiveLib, so Timeout provides a grace period to ensure already in-flight messages have successful execution.
_initializable is used to check whether the crosschain message path is valid for the receiver. _lazyInboundNonce greater than 0 suggests a message has already been executed successfully, so no need to call _receiver to check the path again, which helps save gas. Otherwise, call _receiver.allowInitializePath to check (the OApp standard inherits OAppReceiver which has already implemented allowInitializePath).
_verifiable checks that the nonce / message has not been executed before.
  • If _origin.nonce > _lazyInboundNonce, then the nonce / message has not been executed before, otherwise _lazyInboundNonce_origin.nonce.
  • If _origin.nonce_lazyInboundNonce, then the nonce / message has been verified. If the payload hash is empty, which means the nonce / message has been executed (because the Endpoint will clear the payload hash of the nonce after successful execution), it cannot be executed again.
_inbound inserts the message into the channel (inboundPayloadHash).

Receive Workflow

Endpoint Execution

After the crosschain message has been inserted into the channel (Endpoint.inboundPayloadHash), Executor will try to call Endpoint.lzReceive to execute the message.
  • clear the payload first to prevent reentrancy and double execution.
  • call ILayerZeroReceiver.lzReceive to execute the message.
Inside the _clearPayload:
  • update the lazyInboundNonce.
  • verify payload provided by Executor.
  • delete message in the channel to prevent double execution.

OApp Execution

By default, the OApp standard inherits OAppReceiver which implements lzReceive called by Endpoint to execute message.
  • check msg.sender is Endpoint.
  • check the path is valid.
  • call internal _lzReceive to execute logic (developer should override to add specific use).
In the original _getPeerOrRevert implementation, it can only assign one valid sender for each source chain, but developers can override this to allow multiple senders on one source chain.
Developers should also override OAppReceiver.allowInitializePath so that the message can be successfully inserted into the Endpoint’s message channel (the Endpoint will call to check whether the path is valid).
Special thanks to community member SennHanami for their contribution to this documentation page. You can read their full deep-dive at: Decode LayerZero V2.