Skip to main content
Version: Endpoint V2

Configuring Pathways

The LayerZero Protocol allows your OApp contract owner to configure various different decentralized verifier networks (DVNs) to verify your cross-chain messages using unique validation techniques, Executors to automatically deliver messages, and direct control over the amount of destination gas units used for different calls within your destination OApp.

Dark only image

To configure your OApp, you will need to change your layerzero.config.ts for your desired pathways.

Adding Configurations

Inside your create-lz-oapp repo, head to the layerzero.config.ts and scroll down to module.exports.

info

This tutorial assumes you've already added your desired OApp contracts to your layerzero.config.ts.

If not, review the Project Configuration section before continuing.

As explained previously, the CLI Toolkit organizes your OApp's configurations on a per-pathway basis:

module.exports = {
// Define the contracts to be deployed on each network
// Each contract is associated with a specific blockchain.
contracts: [
{
contract: sepoliaContract,
},
{
contract: bscContract,
},
],
// Define the pathway between each contract.
// This allows for cross-chain communication using LayerZero.
connections: [
{
from: bscContract,
to: sepoliaContract,
},
{
from: sepoliaContract,
to: bscContract,
},
],
};

To add a specific pathway configuration, add a config: {} to your connection:

module.exports = {
// Define the contracts to be deployed on each network
// Each contract is associated with a specific blockchain.
contracts: [
{
contract: sepoliaContract,
},
{
contract: bscContract,
},
],
// Define the pathway between each contract.
// This allows for cross-chain communication using LayerZero.
connections: [
{
from: bscContract,
to: sepoliaContract,
config: {},
},
{
from: sepoliaContract,
to: bscContract,
},
],
};

Each pathway contains a config, containing multiple configuration structs for changing how your OApp sends and receives messages, specifically for the chain your OApp is sending from:

NameTypeDescription
sendLibraryAddressThe message library used for configuring all sent messages from this chain. (e.g., SendUln302.sol)
receiveLibraryConfigStructA struct containing the receive message library address (e.g., ReceiveUln302.sol), and an optional BigInt, gracePeriod, the time to wait before updating to a new MessageLib version during version migration. Controls how the from chain receives messages.
receiveLibraryTimeoutConfigStructAn optional param, defining when the old receive library (lib) will expire (expiry) during version migration.
sendConfigStructControls how the OApp sends from this pathway, containing two more structs: executorConfig and ulnConfig (DVNs).
receiveConfigStructControls how the OApp (from) receives messages, specifically the ulnConfig (DVNs).
enforcedOptionsStructControls the minimum destination gas sent to the destination, per message type (e.g., _lzReceive, lzCompose, etc.) in your OApp.
tip

When adding a config, consider that connections moves in a bidirectional, two-way path:

  • The sendConfig applies to all message sent from Chain A and received by the to address, Chain B.

  • The receiveConfig applies to all messages received by Chain A (from), sent from Chain B (the to contract).

For example, this config: {} applies only to how the bscContract sends messages to the sepoliaContract, and how the bscContract receives messages from the sepoliaContract.

Adding sendLibrary

Every configuration should start by adding a sendLibrary.

connections: [
{
// Sets the peer `from -> to`. Optional, you do not have to connect all pathways.
from: bscContract,
to: sepoliaContract,
// Optional Configuration
config: {
// Required Send Library Address on BSC
sendLibrary: "0x0000000000000000000000000000000000000000",
},
},
],

Each MessageLib contains the available configuration options for the protocol, and so must be set by the application owner to prevent unintended updates.

info

You should use the sendLibrary address for the chain you're sending from (i.e., SendUln302.sol on BSC).

info

The MessageLib Registry is append only, meaning that old Message Libraries will always be available for OApps. Locking your Library is only necessary to prevent updates.

Adding receiveLibrary

Every configuration should also add a receiveLibrary. Similar to the sendLibrary, the OApp owner must also set the Receive Library to ensure that your configured application settings will be locked.

To do this, add a receiveLibraryConfig:

connections: [
{
// Sets the peer `from -> to`. Optional, you do not have to connect all pathways.
from: bscContract,
to: sepoliaContract,
// Optional Configuration
config: {
// Required Send Library Address on BSC
sendLibrary: "0x0000000000000000000000000000000000000000",
receiveLibraryConfig: {
// Required Receive Library Address on BSC
receiveLibrary: "0x0000000000000000000000000000000000000000",
// Optional Grace Period for Switching Receive Library Address on BSC
gracePeriod: BigInt(0),
},
// Optional Receive Library Timeout for when the Old Receive Library Address will no longer be valid on BSC
receiveLibraryTimeoutConfig: {
lib: "0x0000000000000000000000000000000000000000",
expiry: BigInt(0),
},
},
},
],

The Receive Library also provides two additional parameters to help future-proof OApp's for migrating MessageLib versions:

  • gracePeriod: the time to wait before updating to a new MessageLib version during version migration. If the grace period is 0, it will delete the timeout configuration.

  • expiry: the time at which messages in-flight from the old library will be considered invalid. This is mainly for handling messages that are in-flight during the migration.

In most cases, setting the gracePeriod to 0 will be sufficient.

Adding sendConfig

Your sendConfig controls what DVN addresses and Executor addresses should be paid to verify and execute when a message is sent.

info

Each DVN and Executor contains both on-chain and off-chain component. When sending a message, you pay the DVNs and Executors contracts on the source chain, and they relay the message to the equivalent contracts on the destination chain.

For your sendConfig, use the DVNs and Executor contract addresses on the same chain as your sending OApp.

tip

The DVNs you use do not need to be the same on all pathways of an OApp. The DVNs just need to be the same for a given chain pathway. You could technically have one set of DVNs verifying transactions from Arbitrum to Base and Base to Arbitrum, and a separate set of DVNs verifying transactions from Arbitrum to Avalanche and Avalanche to Arbitrum.

connections: [
{
// Sets the peer `from -> to`. Optional, you do not have to connect all pathways.
from: bscContract,
to: sepoliaContract,
// Optional Configuration
config: {
// Required Send Library Address on BSC
sendLibrary: "0x0000000000000000000000000000000000000000",
// Required Receive Library Config
receiveLibraryConfig: {
// Required Receive Library Address on BSC
receiveLibrary: "0x0000000000000000000000000000000000000000",
// Optional Grace Period for Switching Receive Library Address on BSC
gracePeriod: BigInt(0),
},
// Optional Receive Library Timeout for when the Old Receive Library Address will no longer be valid on BSC
receiveLibraryTimeoutConfig: {
lib: "0x0000000000000000000000000000000000000000",
expiry: BigInt(0),
},
// Optional Send Configuration
// @dev Controls how the `from` chain sends messages to the `to` chain.
sendConfig: {
executorConfig: {
maxMessageSize: 99,
// The configured Executor address on BSC
executor: "0x0000000000000000000000000000000000000000",
},
ulnConfig: {
// The number of block confirmations to wait on BSC before emitting the message from the source chain (BSC).
confirmations: BigInt(42),
// The address of the DVNs you will pay to verify a sent message on the source chain (BSC).
// The destination tx will wait until ALL `requiredDVNs` verify the message.
requiredDVNs: [],
// The address of the DVNs you will pay to verify a sent message on the source chain (BSC).
// The destination tx will wait until the configured threshold of `optionalDVNs` verify a message.
optionalDVNs: [
"0x_POLYHEDRA_DVN_ADDRESS_ON_BSC",
"0x_LAYERZERO_DVN_ADDRESS_ON_BSC",
],
// The number of `optionalDVNs` that need to successfully verify the message for it to be considered Verified.
optionalDVNThreshold: 2,
},
},
},
},
],

Adding receiveConfig

The receive configuration controls what DVN addresses your OApp expects to have verified the message in-flight.

tip

For example, if BSC is receiving messages from Sepolia, you should use the DVN contract addresses on BSC for each DVN provider you have in your sendConfig.

connections: [
{
// Sets the peer `from -> to`. Optional, you do not have to connect all pathways.
from: bscContract,
to: sepoliaContract,
// Optional Configuration
config: {
// Required Send Library Address on BSC
sendLibrary: "0x0000000000000000000000000000000000000000",
// Required Receive Library Config
receiveLibraryConfig: {
// Required Receive Library Address on BSC
receiveLibrary: "0x0000000000000000000000000000000000000000",
// Optional Grace Period for Switching Receive Library Address on BSC
gracePeriod: BigInt(0),
},
// Optional Receive Library Timeout for when the Old Receive Library Address will no longer be valid on BSC
receiveLibraryTimeoutConfig: {
lib: "0x0000000000000000000000000000000000000000",
expiry: BigInt(0),
},
// Optional Send Configuration
// @dev Controls how the `from` chain sends messages to the `to` chain.
sendConfig: {
executorConfig: {
maxMessageSize: 99,
// The configured Executor address on BSC
executor: "0x0000000000000000000000000000000000000000",
},
ulnConfig: {
// The number of block confirmations to wait on BSC before emitting the message from the source chain (BSC).
confirmations: BigInt(42),
// The address of the DVNs you will pay to verify a sent message on the source chain (BSC).
// The destination tx will wait until ALL `requiredDVNs` verify the message.
requiredDVNs: [],
// The address of the DVNs you will pay to verify a sent message on the source chain (BSC).
// The destination tx will wait until the configured threshold of `optionalDVNs` verify a message.
optionalDVNs: [
"0x_POLYHEDRA_DVN_ADDRESS_ON_BSC",
"0x_LAYERZERO_DVN_ADDRESS_ON_BSC",
],
// The number of `optionalDVNs` that need to successfully verify the message for it to be considered Verified.
optionalDVNThreshold: 2,
},
},
// Optional Receive Configuration
// @dev Controls how the `from` chain receives messages from the `to` chain.
receiveConfig: {
ulnConfig: {
// The number of block confirmations to expect from the `to` chain (Sepolia).
confirmations: BigInt(42),
// The address of the DVNs your `receiveConfig` expects to receive verifications from on the `from` chain (BSC).
// The `from` chain's OApp will wait until the configured threshold of `requiredDVNs` verify the message.
requiredDVNs: [],
// The address of the `optionalDVNs` you expect to receive verifications from on the `from` chain (BSC).
// The destination tx will wait until the configured threshold of `optionalDVNs` verify the message.
optionalDVNs: [
"0x_POLYHEDRA_DVN_ADDRESS_ON_BSC",
"0x_LAYERZERO_DVN_ADDRESS_ON_BSC",
],
// The number of `optionalDVNs` that need to successfully verify the message for it to be considered Verified.
optionalDVNThreshold: 2,
},
},
},
},
],

Adding enforcedOptions

You can specify both a minimum destination gas and msg.value that users must pay for both your contract's lzReceive and `lzCompose logic to execute as intended.

The CLI Toolkit enables you to configure your message options in a human-readable format, provided that your OApp has added an Enforced Option Message Type.

info

The Omnichain Fungible Token (OFT) Standard by default already has Enforced Options added to the contract, with two message types available:

// @dev execution types to handle different enforcedOptions
uint16 internal constant SEND = 1; // a standard token transfer via lzReceive
uint16 internal constant SEND_AND_CALL = 2; // a token transfer, followed by a composable call via lzCompose
connections: [
{
// Sets the peer `from -> to`. Optional, you do not have to connect all pathways.
from: bscContract,
to: sepoliaContract,
// Optional Configuration
config: {
// Required Send Library Address on BSC
sendLibrary: "0x0000000000000000000000000000000000000000",
receiveLibraryConfig: {
// Required Receive Library Address on BSC
receiveLibrary: "0x0000000000000000000000000000000000000000",
// Optional Grace Period for Switching Receive Library Address on BSC
gracePeriod: BigInt(0),
},
// Optional Receive Library Timeout for when the Old Receive Library Address will no longer be valid on BSC
receiveLibraryTimeoutConfig: {
lib: "0x0000000000000000000000000000000000000000",
expiry: BigInt(0),
},
// Optional Send Configuration
// @dev Controls how the `from` chain sends messages to the `to` chain.
sendConfig: {
executorConfig: {
maxMessageSize: 99,
// The configured Executor address on BSC
executor: "0x0000000000000000000000000000000000000000",
},
ulnConfig: {
// The number of block confirmations to wait on BSC before emitting the message from the source chain (BSC).
confirmations: BigInt(42),
// The address of the DVNs you will pay to verify a sent message on the source chain (BSC).
// The destination tx will wait until ALL `requiredDVNs` verify the message.
requiredDVNs: [],
// The address of the DVNs you will pay to verify a sent message on the source chain (BSC).
// The destination tx will wait until the configured threshold of `optionalDVNs` verify a message.
optionalDVNs: [
"0x_POLYHEDRA_DVN_ADDRESS_ON_BSC",
"0x_LAYERZERO_DVN_ADDRESS_ON_BSC",
],
// The number of `optionalDVNs` that need to successfully verify the message for it to be considered Verified.
optionalDVNThreshold: 2,
},
},
// Optional Receive Configuration
// @dev Controls how the `from` chain receives messages from the `to` chain.
receiveConfig: {
ulnConfig: {
// The number of block confirmations to expect from the `to` chain (Sepolia).
confirmations: BigInt(42),
// The address of the DVNs your `receiveConfig` expects to receive verifications from on the `from` chain (BSC).
// The `from` chain's OApp will wait until the configured threshold of `requiredDVNs` verify the message.
requiredDVNs: [],
// The address of the `optionalDVNs` you expect to receive verifications from on the `from` chain (BSC).
// The destination tx will wait until the configured threshold of `optionalDVNs` verify the message.
optionalDVNs: [
"0x_POLYHEDRA_DVN_ADDRESS_ON_BSC",
"0x_LAYERZERO_DVN_ADDRESS_ON_BSC",
],
// The number of `optionalDVNs` that need to successfully verify the message for it to be considered Verified.
optionalDVNThreshold: 2,
},
},
// Optional Enforced Options Configuration
// @dev Controls how much gas to use on the `to` chain, which the user pays for on the source `from` chain.
enforcedOptions: [
{
msgType: 1,
optionType: ExecutorOptionType.LZ_RECEIVE,
gas: 60000,
value: 1,
},
{
msgType: 1,
optionType: ExecutorOptionType.NATIVE_DROP,
amount: 1,
receiver: "0x0000000000000000000000000000000000000000",
},
{
msgType: 2,
optionType: ExecutorOptionType.LZ_RECEIVE,
index: 0,
gas: 60000,
value: 1,
},
{
msgType: 2,
optionType: ExecutorOptionType.COMPOSE,
index: 1,
gas: 50000,
value: 1,
},
],
},
},
],

Review the Transaction Pricing section and the Execution Options to better understand how you should add your execution gas settings.

Final Config

Your final config should look like:

connections: [
{
// Sets the peer `from -> to`. Optional, you do not have to connect all pathways.
from: bscContract,
to: sepoliaContract,
// Optional Configuration
config: {
// Required Send Library Address on BSC
sendLibrary: "0x0000000000000000000000000000000000000000",
receiveLibraryConfig: {
// Required Receive Library Address on BSC
receiveLibrary: "0x0000000000000000000000000000000000000000",
// Optional Grace Period for Switching Receive Library Address on BSC
gracePeriod: BigInt(0),
},
// Optional Receive Library Timeout for when the Old Receive Library Address will no longer be valid on BSC
receiveLibraryTimeoutConfig: {
lib: "0x0000000000000000000000000000000000000000",
expiry: BigInt(0),
},
// Optional Send Configuration
// @dev Controls how the `from` chain sends messages to the `to` chain.
sendConfig: {
executorConfig: {
maxMessageSize: 99,
// The configured Executor address on BSC
executor: "0x0000000000000000000000000000000000000000",
},
ulnConfig: {
// The number of block confirmations to wait on BSC before emitting the message from the source chain (BSC).
confirmations: BigInt(42),
// The address of the DVNs you will pay to verify a sent message on the source chain (BSC).
// The destination tx will wait until ALL `requiredDVNs` verify the message.
requiredDVNs: [],
// The address of the DVNs you will pay to verify a sent message on the source chain (BSC).
// The destination tx will wait until the configured threshold of `optionalDVNs` verify a message.
optionalDVNs: [
"0x_POLYHEDRA_DVN_ADDRESS_ON_BSC",
"0x_LAYERZERO_DVN_ADDRESS_ON_BSC",
],
// The number of `optionalDVNs` that need to successfully verify the message for it to be considered Verified.
optionalDVNThreshold: 2,
},
},
// Optional Receive Configuration
// @dev Controls how the `from` chain receives messages from the `to` chain.
receiveConfig: {
ulnConfig: {
// The number of block confirmations to expect from the `to` chain (Sepolia).
confirmations: BigInt(42),
// The address of the DVNs your `receiveConfig` expects to receive verifications from on the `from` chain (BSC).
// The `from` chain's OApp will wait until the configured threshold of `requiredDVNs` verify the message.
requiredDVNs: [],
// The address of the `optionalDVNs` you expect to receive verifications from on the `from` chain (BSC).
// The destination tx will wait until the configured threshold of `optionalDVNs` verify the message.
optionalDVNs: [
"0x_POLYHEDRA_DVN_ADDRESS_ON_BSC",
"0x_LAYERZERO_DVN_ADDRESS_ON_BSC",
],
// The number of `optionalDVNs` that need to successfully verify the message for it to be considered Verified.
optionalDVNThreshold: 2,
},
},
// Optional Enforced Options Configuration
// @dev Controls how much gas to use on the `to` chain, which the user pays for on the source `from` chain.
enforcedOptions: [
{
msgType: 1,
optionType: ExecutorOptionType.LZ_RECEIVE,
gas: 60000,
value: 1,
},
{
msgType: 1,
optionType: ExecutorOptionType.NATIVE_DROP,
amount: 1,
receiver: "0x0000000000000000000000000000000000000000",
},
{
msgType: 2,
optionType: ExecutorOptionType.LZ_RECEIVE,
index: 0,
gas: 60000,
value: 1,
},
{
msgType: 2,
optionType: ExecutorOptionType.COMPOSE,
index: 1,
gas: 50000,
value: 1,
},
],
},
},
],

Applying Changes

Wiring your contracts will set the peer address for your OApp or OFT and initialize the desired configuration in your layerzero.config.ts. If unfamiliar with this concept, review the OApp Quickstart.

The CLI Tool makes this one step easier by enabling you to wire and configure your contract pathways with a single command:

$ npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts

Before wiring your contracts, you should review your layerzero.config.ts to ensure that you have specified accurately the configuration you want to set.