Skip to main content
Version: Endpoint V2

Getting Started with Omnichain Messaging

LayerZero is a smart contract protocol that allows smart contracts on different blockchain networks to communicate with each other.

Any data, whether it's a fungible token transfer (ERC20), the metadata of an NFT (ERC721), or Token Bound Accounts (ERC6551) can be encoded on-chain as a byte array, delivered to a destination chain, and decoded to trigger some action.

LayerZero offers Contract Standards that simplify calling the LayerZero Endpoint, provide message handling, interfaces for protocol configurations, and other utilities for interoperability:

Each of these contract standards implement common functions for sending and receiving omnichain messages.

  • _lzSend: calls the LayerZero Endpoint to send a message.
info

This code snippet is already implemented in the Remix example below. Simply review this code to understand how it works internally.

/**
* @dev Internal function to interact with the LayerZero EndpointV2.send() for sending a message.
* @param _dstEid The destination endpoint ID.
* @param _message The message payload.
* @param _options Additional options for the message.
* @param _fee The calculated LayerZero fee for the message.
* - nativeFee: The native fee.
* - lzTokenFee: The lzToken fee.
* @param _refundAddress The address to receive any excess fee values sent to the endpoint.
* @return receipt The receipt for the sent message.
* - guid: The unique identifier for the sent message.
* - nonce: The nonce of the sent message.
* - fee: The LayerZero fee incurred for the message.
*/
function _lzSend(
uint32 _dstEid,
bytes memory _message,
bytes memory _options,
MessagingFee memory _fee,
address _refundAddress
) internal virtual returns (MessagingReceipt memory receipt) {
// @dev Push corresponding fees to the endpoint, any excess is sent back to the _refundAddress from the endpoint.
uint256 messageValue = _payNative(_fee.nativeFee);
if (_fee.lzTokenFee > 0) _payLzToken(_fee.lzTokenFee);

return
// solhint-disable-next-line check-send-result
endpoint.send{ value: messageValue }(
MessagingParams(_dstEid, _getPeerOrRevert(_dstEid), _message, _options, _fee.lzTokenFee > 0),
_refundAddress
);
}
  • _lzReceive: handles what to do after receiving a message from the LayerZero Endpoint.
info

This code snippet is already implemented in the Remix example below. Simply review this code to understand how it works internally.

/**
* @dev Entry point for receiving messages or packets from the endpoint.
* @param _origin The origin information containing the source endpoint and sender address.
* - srcEid: The source chain endpoint ID.
* - sender: The sender address on the src chain.
* - nonce: The nonce of the message.
* @param _guid The unique identifier for the received LayerZero message.
* @param _message The payload of the received message.
* @param _executor The address of the executor for the received message.
* @param _extraData Additional arbitrary data provided by the corresponding executor.
*
* @dev Entry point for receiving msg/packet from the LayerZero endpoint.
*/
function lzReceive(
Origin calldata _origin,
bytes32 _guid,
bytes calldata _message,
address _executor,
bytes calldata _extraData
) public payable virtual {
// Ensures that only the endpoint can attempt to lzReceive() messages to this OApp.
if (address(endpoint) != msg.sender) revert OnlyEndpoint(msg.sender);

// Ensure that the sender matches the expected peer for the source endpoint.
if (_getPeerOrRevert(_origin.srcEid) != _origin.sender) revert OnlyPeer(_origin.srcEid, _origin.sender);

// Call the internal OApp implementation of lzReceive.
_lzReceive(_origin, _guid, _message, _executor, _extraData);
}

This method of encoding send parameters and decoding them on the destination chain is the basis for how all OApps work.

Example Omnichain Application

The OApp Standard contains both a send and receive interface.

info

This code snippet is already implemented in the Remix example below. Simply review this code to understand how it works internally.

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.20;

// @dev Import the 'MessagingFee' and 'MessagingReceipt' so it's exposed to OApp implementers
// solhint-disable-next-line no-unused-import
import { OAppSender, MessagingFee, MessagingReceipt } from "./OAppSender.sol";
// @dev Import the 'Origin' so it's exposed to OApp implementers
// solhint-disable-next-line no-unused-import
import { OAppReceiver, Origin } from "./OAppReceiver.sol";
import { OAppCore } from "./OAppCore.sol";

/**
* @title OApp
* @dev Abstract contract serving as the base for OApp implementation, combining OAppSender and OAppReceiver functionality.
*/
abstract contract OApp is OAppSender, OAppReceiver {

/**
* @dev Constructor to initialize the OApp with the provided endpoint and owner.
* @param _endpoint The address of the LOCAL LayerZero endpoint.
* @param _delegate The delegate capable of making OApp configurations inside of the endpoint.
*/
constructor(address _endpoint, address _delegate) OAppCore(_endpoint, _delegate) {}

/**
* @notice Retrieves the OApp version information.
* @return senderVersion The version of the OAppSender.sol implementation.
* @return receiverVersion The version of the OAppReceiver.sol implementation.
*/
function oAppVersion()
public
pure
virtual
override(OAppSender, OAppReceiver)
returns (uint64 senderVersion, uint64 receiverVersion)
{
return (SENDER_VERSION, RECEIVER_VERSION);
}
}

You can use the Remix IDE to see how OAppSender and OAppReceiver work together for sending and receiving any arbitrary data to supported destination chains.

OAppSender.sol

Open in RemixWhat is Remix?

OAppReceiver.sol

Open in RemixWhat is Remix?

Prerequisites

  1. You should first be familiar with writing and deploying contracts to your desired blockchains. This involves understanding the specific smart contract language and the deployment process for those chains.

  2. A wallet set up and funded for the chains you'll be working with.

Deploying Your Contracts

We'll deploy the Source Contract on Sepolia, and the Destination Contract on Optimism Sepolia:

info

This example can be used with any EVM-compatible blockchain that LayerZero supports.


  1. Open MetaMask and select the Ethereum Sepolia network. Make sure you have native gas in the wallet connected.

  2. In Remix under the Deploy & Run Transactions tab, select Injected Provider - MetaMask in the Environment list.

  3. Under the Deploy section, fill in the Endpoint Address for your current chain.

Sepolia Endpoint Address

0x6edce65403992e310a62460808c4b910d972f10f

Optimism Sepolia Endpoint Address

0x6edce65403992e310a62460808c4b910d972f10f
  1. Click deploy, follow the MetaMask prompt to confirm the transaction, and wait for the contract address to appear under Deployed Contracts.

  2. Repeat the above steps for any other chains you plan to deploy to and connect.

Connecting Your Contracts

To connect your OApp deployments together, you will need to call setPeer on both the Ethereum Sepolia and Optimism Sepolia OApp.

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.

// @dev must-have configurations for standard OApps
function setPeer(uint32 _eid, bytes32 _peer) public virtual onlyOwner {
peers[_eid] = _peer; // Array of peer addresses by destination.
emit PeerSet(_eid, _peer); // Event emitted each time a peer is set.
}
tip

Remember, an EVM address is a bytes20 value. That means you will need to convert your address to bytes32 when calling setPeer. This can easily be done by Zero Padding the address until it is 32 bytes in length.

LayerZero uses bytes32 for broad compatibility with non-EVM chains.


Pass the address of your destination contract as a bytes32 value, as well as the destination endpoint ID.

  • To send to Ethereum Sepolia, the Endpoint ID is: 40161.

  • To send to Optimism Sepolia, the Endpoint ID is: 40232.

caution

You'll need to repeat this wiring on both contracts in order to send and receive messages. That means calling setPeer on both your Ethereum Sepolia and Optimism Sepolia contracts. Remember to switch networks in MetaMask.

If successful, you now should be setup to start sending cross-chain messages!

Estimating Fees

The LayerZero Protocol gas fees can vary based on your source chain, destination chain, and the payload you're attempting to send, which is why we recommend estimating fees before sending your first transaction.

To do this, we'll use the quote function.

This function invokes the _quote function to estimate the fees associated with a particular LayerZero transaction using four inputs:

  • _dstEid: This is the identifier of the destination chain's endpoint where the transaction is intended to go.

  • _message: This is the arbitrary message you intend to send to your destination chain and contract.

  • _options: A bytes array that contains serialized execution options that tell the protocol the amount of gas to for the Executor to send when calling lzReceive, as well as other function call related settings.

  • _payInLzToken: A boolean which determines whether to return the fee estimate in the native gas token or in ZRO token.

note

In this tutorial, you will deliver 50000 wei for the lzReceive call by passing 0x0003010011010000000000000000000000000000c350 as your _options. You will be quoted 50000 wei on the source chain, which the Executor will convert to the destination gas token and use in the call. See Message Execution Options for all possible execution settings.

Sending Your Message

To use the send function, simply input a string into the message field that you wish to send to your destination chain.

Contract A

Remember to pass the msg.value we quoted using quote in Remix, as we still need to pay gas fees on the source and destination, as well as for the Security Stack and Executor who authenticate and deliver the messages. Once you've successfully sent your transaction, call the data field from your destination contract to see your first omnichain message!

Contract B

Your message may take a few minutes to appear in the destination block explorer, depending on which chains you deploy to.

Tracking Your Message

Finally, let's see what's happening in our transaction. Take your transaction hash and paste it into: https://testnet.layerzeroscan.com/

You should see Status: Delivered, confirming your message has been delivered to its destination using LayerZero.

Congrats, you just sent your first omnichain message! 🥳

Further Reading

Now that you understand the basics for how OApps work, you should explore setting up your development environment and diving deeper into the omnichain contract standards!