Skip to main content
Version: Endpoint V2

OApp Security Stack and Executor Configuration

You may recall that you can configure send and receive settings for your OApp per pathway set.

LayerZero defines a pathway as any configuration where any two points (OApp on Chain A and OApp on Chain B), have each called the setPeer function and enabled messaging to and from each contract instance.

Every LayerZero Endpoint can be used to send and receive messages. Because of that, each Endpoint has a separate Send and Receive Configuration, which an OApp can configure per target Endpoint (i.e., sending to that target, receiving from that target).

Protocol V2 Light Protocol V2 Dark

For a configuration to be considered correct, the Send Library configurations on Chain A must match Chain B's Receive Library configurations for filtering messages.

info

In the diagram above, the Source OApp has added the DVN's source chain address to the Send Library configuration.

The Destination OApp has added the DVN's destination chain address to the Receive Library configuration.

The DVN can now read from the source chain, and deliver the message to the destination chain.

Checking Default Configuration

For commonly travelled pathways, LayerZero provides a default pathway configuration. If you provide no configuration prior to setting peers, the protocol will fallback to the default configuration.

The default configuration varies from pathway to pathway, based on the unique properties of each chain, and which decentralized verifier networks or executors listen for those networks.

A default pathway configuration, at the time of writing, will always have one of the following set within SendULN302.sol and ReceiveUlN302.sol as a Preset Configuration:

Security StackExecutor
Default Send and Receive ArequiredDVNs: [ Google Cloud, LayerZero Labs ]LayerZero Labs
Default Send and Receive BrequiredDVNs: [ Polyhedra, LayerZero Labs ]LayerZero Labs
Default Send and Receive CrequiredDVNs: [ Dead DVN, LayerZero Labs ]LayerZero Labs

info

What is a Dead DVN?

Since LayerZero allows for anyone to permissionlessly run DVNs, the network may occassionally add new chain Endpoints before the default providers (Google Cloud or Polyhedra) support every possible pathway to and from that chain.

A default configuration with a Dead DVN will require you to either configure an available DVN provider for that Send or Receive pathway, or run your own DVN if no other security providers exist, before messages can safely be delivered to and from that chain.


Other default configuration settings, like source and destination block confirmations, will vary per chain pathway based on recommendations provided by each chain.

To read the default configuration, you can call the LayerZero Endpoint's getConfig method to return the default send and receive configuration for that target Endpoint.

/**
* @notice This function is used to retrieve configuration data for a specific OApp using a LayerZero Endpoint on the same chain.
*
* @param _oapp Address of the OApp for which the configuration is being retrieved.
* @param _lib Address of the library (send or receive) used by the OApp at the specified endpoint.
* @param _eid Endpoint ID (EID) of the target endpoint on the other side of the pathway. The EID filters
* the configurations specifically for the target endpoint, which is crucial for ensuring that messages are
* sent and received correctly and securely between the configured endpoints (pathways).
* @param _configType Type of configuration to retrieve (e.g., executor configuration, ULN configuration).
* This parameter specifies the format and data of the returned configuration.
*
* @return config Returns the configuration data as bytes, which can be decoded into the respective
* configuration structure as per the requested _configType.
*/
function getConfig(
address _oapp,
address _lib,
uint32 _eid,
uint32 _configType
) external view returns (bytes memory config);
tip

The create-lz-oapp npx package also provides a faster CLI command to return every default configuration for each pathway in your project!

npx hardhat lz:oapp:config:get:default

The example below uses defaultAbiCoder from the ethers.js (^5.7.2) library to decode the bytes arrays returned by an OApp using the Ethereum Mainnet Endpoint:

import * as ethers from 'ethers';

// Define provider
const provider = new ethers.providers.JsonRpcProvider('YOUR_RPC_PROVIDER_HERE');

// Define the smart contract address and ABI
const ethereumLzEndpointAddress = '0x1a44076050125825900e736c501f859c50fE728c';
const ethereumLzEndpointABI = [
'function getConfig(address _oapp, address _lib, uint32 _eid, uint32 _configType) external view returns (bytes memory config)',
];

// Create a contract instance
const contract = new ethers.Contract(ethereumLzEndpointAddress, ethereumLzEndpointABI, provider);

// Define the addresses and parameters
const oappAddress = '0xEB6671c152C88E76fdAaBC804Bf973e3270f4c78';
const sendLibAddress = '0xbB2Ea70C9E858123480642Cf96acbcCE1372dCe1';
const receiveLibAddress = '0xc02Ab410f0734EFa3F14628780e6e695156024C2';
const remoteEid = 30102; // Example target endpoint ID, Binance Smart Chain
const executorConfigType = 1; // 1 for executor
const ulnConfigType = 2; // 2 for UlnConfig

async function getConfigAndDecode() {
try {
// Fetch and decode for sendLib (both Executor and ULN Config)
const sendExecutorConfigBytes = await contract.getConfig(
oappAddress,
sendLibAddress,
remoteEid,
executorConfigType,
);
const executorConfigAbi = ['tuple(uint32 maxMessageSize, address executorAddress)'];
const executorConfigArray = ethers.utils.defaultAbiCoder.decode(
executorConfigAbi,
sendExecutorConfigBytes,
);
console.log('Send Library Executor Config:', executorConfigArray);

const sendUlnConfigBytes = await contract.getConfig(
oappAddress,
sendLibAddress,
remoteEid,
ulnConfigType,
);
const ulnConfigStructType = [
'tuple(uint64 confirmations, uint8 requiredDVNCount, uint8 optionalDVNCount, uint8 optionalDVNThreshold, address[] requiredDVNs, address[] optionalDVNs)',
];
const sendUlnConfigArray = ethers.utils.defaultAbiCoder.decode(
ulnConfigStructType,
sendUlnConfigBytes,
);
console.log('Send Library ULN Config:', sendUlnConfigArray);

// Fetch and decode for receiveLib (only ULN Config)
const receiveUlnConfigBytes = await contract.getConfig(
oappAddress,
receiveLibAddress,
remoteEid,
ulnConfigType,
);
const receiveUlnConfigArray = ethers.utils.defaultAbiCoder.decode(
ulnConfigStructType,
receiveUlnConfigBytes,
);
console.log('Receive Library ULN Config:', receiveUlnConfigArray);
} catch (error) {
console.error('Error fetching or decoding config:', error);
}
}

// Execute the function
getConfigAndDecode();

The getConfig function will return you an array of values from both the SendLib and ReceiveLib's configurations.

The logs below show the output from the Ethereum Endpoint for SendLib302.sol when sending messages to Binance Smart Chain:

Send Library Executor Config:
executorAddress: "0x173272739Bd7Aa6e4e214714048a9fE699453059"
maxMessageSize: 10000

Send Library ULN Config:
confirmations: {_hex: '0x0f', _isBigNumber: true} // this is just big number 15
optionalDVNCount: 0
optionalDVNThreshold: 0
optionalDVNs: Array(0)
requiredDVNCount: 2
requiredDVNs: Array(2)
0: "0x589dEDbD617e0CBcB916A9223F4d1300c294236b" // LZ Ethereum DVN Address
1: "0xD56e4eAb23cb81f43168F9F45211Eb027b9aC7cc" // Google Cloud Ethereum DVN Address

And when the Ethereum Endpoint uses ReceiveLib302.sol to receive messages from Binance Smart Chain:

Receive Library ULN Config

confirmations: {_hex: '0x0f', _isBigNumber: true} // this is just big number 15
optionalDVNCount: 0
optionalDVNThreshold: 0
optionalDVNs: Array(0)
requiredDVNCount: 2
requiredDVNs: Array(2)
0: "0x589dEDbD617e0CBcB916A9223F4d1300c294236b" // LZ Ethereum DVN Address
1: "0xD56e4eAb23cb81f43168F9F45211Eb027b9aC7cc" // Google Cloud Ethereum DVN Address
info

The important takeaway is that every LayerZero Endpoint can be used to send and receive messages. Because of that, each Endpoint has a separate Send and Receive Configuration, which an OApp can configure by the target destination Endpoint.

In the above example, the default Send Library configurations control how messages emit from the Ethereum Endpoint to the BNB Endpoint.

The default Receive Library configurations control how the Ethereum Endpoint filters received messages from the BNB Endpoint.

For a configuration to be considered correct, the Send Library configurations on Chain A must match Chain B's Receive Library configurations for filtering messages.

Challenge: Confirm that the BNB Endpoint's Send Library ULN configuration matches the Ethereum Endpoint's Receive Library ULN Configuration using the methods above.

Custom Configuration

To use non-default protocol settings, the delegate (should always be OApp owner) should call setSendLibrary, setReceiveLibrary, and setConfig from the OApp's Endpoint.

When setting your OApp's config, ensure that the Send Configuration for the OApp on the sending chain (Chain A) matches the Receive Configuration for the OApp on the receiving chain (Chain B).

// Configure the sending OApp on Chain A
const tx_send = await endpointContractChainA.setConfig(oappAddressA, sendLibAddressA, [
setConfigParamUln,
setConfigParamExecutor,
]);

// Configure the receiving OApp on Chain B
const tx_receive = await endpointContractChainB.setConfig(oappAddressB, receiveLibAddressB, [
setConfigParamUln,
]);

// Ensure both transactions are confirmed before proceeding
await tx_send.wait();
await tx_receive.wait();

Both configurations must be appropriately matched and set across the relevant chains to ensure successful communication and data transfer.

The examples below will explain how setConfig manages your OApp's configurations, and how to build the transactions necessary to add custom parameters.

info

The setDelegate function in LayerZero's OApp allows the contract owner to appoint a delegate who can manage configurations for both the Executor and ULN. This delegate, once set, has the authority to modify configurations on behalf of the OApp owner. We strongly recommend you always make sure owner and delegate are the same address.

Setting Send and Receive Libraries

Before changing any OApp Send or Receive configurations, you should first setSendLibrary and setReceiveLibrary to the intended library. At the time of writing, the latest library for Endpoint V2 is SendULN302.sol and ReceiveULN302.sol:

const sendTx = await endpointContract.setSendLibrary(oappAddress, eid, sendLibAddress);
await sendTx.wait();

const receiveTx = await endpointContract.setReceiveLibrary(oappAddress, eid, receiveLibAddress);
await receiveTx.wait();
info

Why do you need to set a sendLibrary and receiveLibrary?

LayerZero uses Appendable Message Libraries. This means that while existing versions will always be immutable and available to configure, updates can still be added by deploying new Message Libraries as separate contracts and having applications manually select the new version.

If an OApp had NOT called setSendLibrary or setReceiveLibrary, the LayerZero Endpoint will fallback to the default configuration, which may be different than the MessageLib you have configured.

Explicitly setting the sendLibrary and receiveLibrary ensures that your configurations will apply to the correct library version, and will not fallback to any new library versions released.

Setting Send Config

You will call the same function in the Endpoint to set your sendConfig and receiveConfig:

/// @dev authenticated by the _oapp
function setConfig(
address _oapp,
address _lib,
SetConfigParam[] calldata _params
) external onlyRegistered(_lib) {
_assertAuthorized(_oapp);

IMessageLib(_lib).setConfig(_oapp, _params);
}

The SetConfigParam struct defines how to set custom parameters for a given configType and the remote chain's eid (endpoint ID):

struct SetConfigParam {
uint32 dstEid;
uint32 configType;
bytes config;
}

The ULN and Executor have separate config types, which change how the bytes array is structured:

CONFIG_TYPE_ULN = 2; // Security Stack and block confirmation config

CONFIG_TYPE_EXECUTOR = 1; // Executor and max message size config

Based on the configType, the MessageLib will expect one of the following structures for the config bytes array:

const configTypeUlnStruct =
'tuple(uint64 confirmations, uint8 requiredDVNCount, uint8 optionalDVNCount, uint8 optionalDVNThreshold, address[] requiredDVNs, address[] optionalDVNs)';

const configTypeExecutorStruct = 'tuple(uint32 maxMessageSize, address executorAddress)';

Each config is encoded and passed as an ordered bytes array in your SetConfigParam struct.

Send Config Type ULN (Security Stack)

The SendConfig describes how messages should be emitted from the source chain. See DVN Addresses for the list of available DVNs.

ParameterTypeDescription
confirmationsuint64The number of block confirmations to wait before a DVN should listen for the payloadHash. This setting can be used to ensure message finality on chains with frequent block reorganizations.
requiredDVNCountuint8The quantity of required DVNs that will be paid to send a message from the OApp.
optionalDVNCountuint8The quantity of optional DVNs that will be paid to send a message from the OApp.
optionalDVNThresholduint8The minimum number of verifications needed from optional DVNs. A message is deemed Verifiable if it receives verifications from at least the number of optional DVNs specified by the optionalDVNsThreshold, plus the required DVNs.
requiredDVNsaddress[]An array of addresses for all required DVNs.
optionalDVNsaddress[]An array of addresses for all optional DVNs.
caution

If you set your block confirmations too low, and a reorg occurs after your confirmation, it can materially impact your OApp or OFT.

Send Config Type Executor

See Deployed LZ Endpoints and Addresses for every chain's Executor address.

ParameterTypeDescription
maxMessageSizeuint32The maximum size of a message that can be sent cross-chain (number of bytes).
executoraddressThe executor implementation to pay fees to for calling the lzReceive function on the destination chain.

The example below uses ethers.js (^5.7.2) library to encode the arrays and call the Endpoint contract:

// Using ethers v5

const oappAddress = 'YOUR_OAPP_ADDRESS'; // Replace with your OApp address
const sendLibAddress = 'YOUR_SEND_LIB_ADDRESS'; // Replace with your send message library address

const provider = new ethers.providers.JsonRpcProvider(YOUR_RPC_URL);
const signer = new ethers.Wallet(YOUR_PRIVATE_KEY, provider);
const endpointContract = new ethers.Contract(YOUR_ENDPOINT_CONTRACT_ADDRESS, YOUR_ENDPOINT_ABI, signer);

const setConfigParamUln = [...]; // Array of SetConfigParam structs, with config type 2 and configTypeUlnStruct
const setConfigParamExecutor = [...]; // Array of SetConfigParam structs, with config type 1 and configTypeExecutorStruct

const tx = await endpointContract.setConfig(oappAddress, sendLibAddress, [
setConfigParamUln,
setConfigParamExecutor,
]);

await tx.wait();

Setting Receive Config

You will still call the setConfig function described above, but because ReceiveLib302.sol only enforces the DVN and block confirmation configurations, you do not need to set an Executor configuration.

CONFIG_TYPE_ULN = 2; // Security Stack and block confirmation config
const configTypeUlnStruct =
'tuple(uint64 confirmations, uint8 requiredDVNCount, uint8 optionalDVNCount, uint8 optionalDVNThreshold, address[] requiredDVNs, address[] optionalDVNs)';

Receive Config Type ULN (Security Stack)

The ReceiveConfig describes how to enforce and filter messages when receiving packets from the remote chain. See DVN Addresses for the list of available DVNs.

ParameterTypeDescription
confirmationsuint64The minimum number of block confirmations the DVNs must have waited for their verification to be considered valid.
requiredDVNCountuint8The quantity of required DVNs that must verify before receiving the OApp's message.
optionalDVNCountuint8The quantity of optional DVNs that must verify before receiving the OApp's message.
optionalDVNThresholduint8The minimum number of verifications needed from optional DVNs. A message is deemed Verifiable if it receives verifications from at least the number of optional DVNs specified by the optionalDVNsThreshold, plus the required DVNs.
requiredDVNsaddress[]An array of addresses for all required DVNs to receive verifications from.
optionalDVNsaddress[]An array of addresses for all optional DVNs to receive verifications from.
caution

If you set your block confirmations too low, and a reorg occurs after your confirmation, it can materially impact your OApp or OFT.


Use the ULN config type and the struct definition to form your configuration for the call:

// Using ethers v5

const oappAddress = 'YOUR_OAPP_ADDRESS'; // Replace with your OApp address
const receiveLibAddress = 'YOUR_RECEIVE_LIB_ADDRESS'; // Replace with your receive message library address

const provider = new ethers.providers.JsonRpcProvider(YOUR_RPC_URL);
const signer = new ethers.Wallet(YOUR_PRIVATE_KEY, provider);
const endpointContract = new ethers.Contract(YOUR_ENDPOINT_CONTRACT_ADDRESS, YOUR_ENDPOINT_ABI, signer);

const setConfigParamUln = [...]; // Array of SetConfigParam structs, with config type 2 and configTypeUlnStruct

const tx = await endpointContract.setConfig(oappAddress, receiveLibAddress, [
setConfigParamUln
]);

await tx.wait();

Resetting Configurations

To erase your configuration and fallback to the default configurations, simply pass null values as your configuration params and call setConfig again:

// ULN Configuration Reset Params
const confirmations = 0;
const optionalDVNCount = 0;
const requiredDVNCount = 0;
const optionalDVNThreshold = 0;
const requiredDVNs = [];
const optionalDVNs = [];

const ulnConfigData = {
confirmations,
requiredDVNCount,
optionalDVNCount,
optionalDVNThreshold,
requiredDVNs,
optionalDVNs,
};

const ulnConfigEncoded = ethersV5.utils.defaultAbiCoder.encode(
[configTypeUlnStruct],
[ulnConfigData],
);

const resetConfigParamUln = {
eid: DEST_CHAIN_ENDPOINT_ID, // Replace with the target chain's endpoint ID
configType: configTypeUln,
config: ulnConfigEncoded,
};

// Executor Configuration Reset Params
const maxMessageSize = 0; // Representing no limit on message size
const executorAddress = '0x0000000000000000000000000000000000000000'; // Representing no specific executor address

const configTypeExecutorStruct = 'tuple(uint32 maxMessageSize, address executorAddress)';
const executorConfigData = {
maxMessageSize,
executorAddress,
};

const executorConfigEncoded = ethers.utils.defaultAbiCoder.encode(
[executorConfigStructType],
[executorConfigData],
);

const resetConfigParamExecutor = {
eid: DEST_CHAIN_ENDPOINT_ID, // Replace with the target chain's endpoint ID
configType: configTypeExecutor,
config: executorConfigEncoded,
};

After defining the null values in your config params, call setConfig:

const messageLibAddresses = ['sendLibAddress', 'receiveLibAddress'];

let resetTx;

// Call setConfig on the send and receive lib
for (const messagelibAddress of messageLibAddresses) {
resetTx = await endpointContract.setConfig(oappAddress, messagelibAddress, [
resetConfigParamUln,
resetConfigParamExecutor,
]);

await resetTx.wait();
}

Debugging Configurations

A correct OApp configuration example:

SendUlnConfig (A to B)ReceiveUlnConfig (B to A)
confirmations: 15confirmations: 15
optionalDVNCount: 0optionalDVNCount: 0
optionalDVNThreshold: 0optionalDVNThreshold: 0
optionalDVNs: Array(0)optionalDVNs: Array(0)
requiredDVNCount: 2requiredDVNCount: 2
requiredDVNs: Array(DVN1_Address_A, DVN2_Address_A)requiredDVNs: Array(DVN1_Address_B, DVN2_Address_B)
tip

The sending OApp's SendLibConfig (OApp on Chain A) and the receiving OApp's ReceiveLibConfig (OApp on Chain B) match!

Block Confirmation Mismatch

An example of an incorrect OApp configuration:

SendUlnConfig (A to B)ReceiveUlnConfig (B to A)
confirmations: 5confirmations: 15
optionalDVNCount: 0optionalDVNCount: 0
optionalDVNThreshold: 0optionalDVNThreshold: 0
optionalDVNs: Array(0)optionalDVNs: Array(0)
requiredDVNCount: 2requiredDVNCount: 2
requiredDVNs: Array(DVN1, DVN2)requiredDVNs: Array(DVN1, DVN2)
danger

The above configuration has a block confirmation mismatch. The sending OApp (Chain A) will only wait 5 block confirmations, but the receiving OApp (Chain B) will not accept any message with less than 15 block confirmations.

Messages will be blocked until either the sending OApp has increased the outbound block confirmations, or the receiving OApp decreases the inbound block confirmation threshold.

DVN Mismatch

Another example of an incorrect OApp configuration:

SendUlnConfig (A to B)ReceiveUlnConfig (B to A)
confirmations: 15confirmations: 15
optionalDVNCount: 0optionalDVNCount: 0
optionalDVNThreshold: 0optionalDVNThreshold: 0
optionalDVNs: Array(0)optionalDVNs: Array(0)
requiredDVNCount: 1requiredDVNCount: 2
requiredDVNs: Array(DVN1)requiredDVNs: Array(DVN1, DVN2)
danger

The above configuration has a DVN mismatch. The sending OApp (Chain A) only pays DVN 1 to listen and verify the packet, but the receiving OApp (Chain B) requires both DVN 1 and DVN 2 to mark the packet as verified.

Messages will be blocked until either the sending OApp has added DVN 2's address on Chain A to the SendUlnConfig, or the receiving OApp removes DVN 2's address on Chain B from the ReceiveUlnConfig.

Dead DVN

This configuration includes a Dead DVN:

SendUlnConfig (A to B)ReceiveUlnConfig (B to A)
confirmations: 15confirmations: 15
optionalDVNCount: 0optionalDVNCount: 0
optionalDVNThreshold: 0optionalDVNThreshold: 0
optionalDVNs: Array(0)optionalDVNs: Array(0)
requiredDVNCount: 2requiredDVNCount: 2
requiredDVNs: Array(DVN1, DVN2)requiredDVNs: Array(DVN1, DVN_DEAD)
danger

The above configuration has a Dead DVN. Similar to a DVN Mismatch, the sending OApp (Chain A) pays DVN 1 and DVN 2 to listen and verify the packet, but the receiving OApp (Chain B) has currently set DVN 1 and a Dead DVN to mark the packet as verified.

Since a Dead DVN for all practical purposes should be considered a null address, no verification will ever match the dead address.

Messages will be blocked until the receiving OApp removes or replaces the Dead DVN from the ReceiveUlnConfig.