OApp Security Stack and Executor Configuration
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).
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.
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 Stack | Executor | |
---|---|---|
Default Send and Receive A | requiredDVNs: [ Google Cloud, LayerZero Labs ] | LayerZero Labs |
Default Send and Receive B | requiredDVNs: [ Polyhedra, LayerZero Labs ] | LayerZero Labs |
Default Send and Receive C | requiredDVNs: [ Dead DVN, LayerZero Labs ] | LayerZero Labs |
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);
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
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).
Both configurations must be appropriately matched and set across the relevant chains to ensure successful communication and data transfer.
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
:
- ethers
- Foundry
const {ethers} = require('ethers');
// Replace with your actual values
const YOUR_OAPP_ADDRESS = '0xYourOAppAddress';
const YOUR_SEND_LIB_ADDRESS = '0xYourSendLibAddress';
const YOUR_RECEIVE_LIB_ADDRESS = '0xYourReceiveLibAddress';
const YOUR_ENDPOINT_CONTRACT_ADDRESS = '0xYourEndpointContractAddress';
const YOUR_RPC_URL = 'YOUR_RPC_URL';
const YOUR_PRIVATE_KEY = 'YOUR_PRIVATE_KEY';
// Define the remote EID
const remoteEid = 30101; // Replace with your actual EID
// Set up the provider and signer
const provider = new ethers.providers.JsonRpcProvider(YOUR_RPC_URL);
const signer = new ethers.Wallet(YOUR_PRIVATE_KEY, provider);
// Set up the endpoint contract
const endpointAbi = [
'function setSendLibrary(address oapp, uint32 eid, address sendLib) external',
'function setReceiveLibrary(address oapp, uint32 eid, address receiveLib) external',
];
const endpointContract = new ethers.Contract(YOUR_ENDPOINT_CONTRACT_ADDRESS, endpointAbi, signer);
async function setLibraries() {
try {
// Set the send library
const sendTx = await endpointContract.setSendLibrary(
YOUR_OAPP_ADDRESS,
remoteEid,
YOUR_SEND_LIB_ADDRESS,
);
console.log('Send library transaction sent:', sendTx.hash);
await sendTx.wait();
console.log('Send library set successfully.');
// Set the receive library
const receiveTx = await endpointContract.setReceiveLibrary(
YOUR_OAPP_ADDRESS,
remoteEid,
YOUR_RECEIVE_LIB_ADDRESS,
);
console.log('Receive library transaction sent:', receiveTx.hash);
await receiveTx.wait();
console.log('Receive library set successfully.');
} catch (error) {
console.error('Transaction failed:', error);
}
}
setLibraries();
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.22;
// Forge imports
import "forge-std/console.sol";
import "forge-std/Script.sol";
// LayerZero imports
import { ILayerZeroEndpointV2 } from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/ILayerZeroEndpointV2.sol";
contract SetLibraries is Script {
function run(address _endpoint, address _oapp, uint32 _eid, address _sendLib, address _receiveLib, address _signer) external {
// Initialize the endpoint contract
ILayerZeroEndpointV2 endpoint = ILayerZeroEndpointV2(_endpoint);
// Start broadcasting transactions
vm.startBroadcast(_signer);
// Set the send library
endpoint.setSendLibrary(_oapp, _eid, _sendLib);
console.log("Send library set successfully.");
// Set the receive library
endpoint.setReceiveLibrary(_oapp, _eid, _receiveLib);
console.log("Receive library set successfully.");
// Stop broadcasting transactions
vm.stopBroadcast();
}
}
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.
Parameter | Type | Description |
---|---|---|
confirmations | uint64 | The 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. |
requiredDVNCount | uint8 | The quantity of required DVNs that will be paid to send a message from the OApp. |
optionalDVNCount | uint8 | The quantity of optional DVNs that will be paid to send a message from the OApp. |
optionalDVNThreshold | uint8 | The 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. |
requiredDVNs | address[] | An array of addresses for all required DVNs. |
optionalDVNs | address[] | An array of addresses for all optional DVNs. |
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.
Parameter | Type | Description |
---|---|---|
maxMessageSize | uint32 | The maximum size of a message that can be sent cross-chain (number of bytes). |
executor | address | The executor implementation to pay fees to for calling the lzReceive function on the destination chain. |
- ethers
- Foundry
The example below uses ethers.js (^5.7.2
) library to encode the arrays and call the Endpoint contract:
const {ethers} = require('ethers');
// Addresses
const oappAddress = 'YOUR_OAPP_ADDRESS'; // Replace with your OApp address
const sendLibAddress = 'YOUR_SEND_LIB_ADDRESS'; // Replace with your send message library address
// Configuration
const remoteEid = 30101; // Example EID, replace with the actual value
const ulnConfig = {
confirmations: 99, // Example value, replace with actual
requiredDVNCount: 2, // Example value, replace with actual
optionalDVNCount: 0, // Example value, replace with actual
optionalDVNThreshold: 0, // Example value, replace with actual
requiredDVNs: ['0xDvnAddress1', '0xDvnAddress2'], // Replace with actual addresses
optionalDVNs: [], // Replace with actual addresses
};
const executorConfig = {
maxMessageSize: 10000, // Example value, replace with actual
executorAddress: '0xExecutorAddress', // Replace with the actual executor address
};
// Provider and Signer
const provider = new ethers.providers.JsonRpcProvider(YOUR_RPC_URL);
const signer = new ethers.Wallet(YOUR_PRIVATE_KEY, provider);
// ABI and Contract
const endpointAbi = [
'function setConfig(address oappAddress, address sendLibAddress, tuple(uint32 eid, uint32 configType, bytes config)[] setConfigParams) external',
];
const endpointContract = new ethers.Contract(YOUR_ENDPOINT_CONTRACT_ADDRESS, endpointAbi, signer);
// Encode UlnConfig using defaultAbiCoder
const configTypeUlnStruct =
'tuple(uint64 confirmations, uint8 requiredDVNCount, uint8 optionalDVNCount, uint8 optionalDVNThreshold, address[] requiredDVNs, address[] optionalDVNs)';
const encodedUlnConfig = ethers.utils.defaultAbiCoder.encode([configTypeUlnStruct], [ulnConfig]);
// Encode ExecutorConfig using defaultAbiCoder
const configTypeExecutorStruct = 'tuple(uint32 maxMessageSize, address executorAddress)';
const encodedExecutorConfig = ethers.utils.defaultAbiCoder.encode(
[configTypeExecutorStruct],
[executorConfig],
);
// Define the SetConfigParam structs
const setConfigParamUln = {
eid: remoteEid,
configType: 2, // ULN_CONFIG_TYPE
config: encodedUlnConfig,
};
const setConfigParamExecutor = {
eid: remoteEid,
configType: 1, // EXECUTOR_CONFIG_TYPE
config: encodedExecutorConfig,
};
// Send the transaction
async function sendTransaction() {
try {
const tx = await endpointContract.setConfig(
oappAddress,
sendLibAddress,
[setConfigParamUln, setConfigParamExecutor], // Array of SetConfigParam structs
);
console.log('Transaction sent:', tx.hash);
const receipt = await tx.wait();
console.log('Transaction confirmed:', receipt.transactionHash);
} catch (error) {
console.error('Transaction failed:', error);
}
}
sendTransaction();
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.22;
// Forge imports
import "forge-std/console.sol";
import "forge-std/Script.sol";
// LayerZero imports
import { ExecutorConfig } from "@layerzerolabs/lz-evm-messagelib-v2/contracts/SendLibBase.sol";
import { ILayerZeroEndpointV2 } from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/ILayerZeroEndpointV2.sol";
import { OFT } from "@layerzerolabs/oft-evm/contracts/OFT.sol";
import { SetConfigParam } from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/IMessageLibManager.sol";
import { UlnConfig } from "@layerzerolabs/lz-evm-messagelib-v2/contracts/uln/UlnBase.sol";
contract SendConfig is Script {
uint32 public constant EXECUTOR_CONFIG_TYPE = 1;
uint32 public constant ULN_CONFIG_TYPE = 2;
function run(address contractAddress, uint32 remoteEid, address sendLibraryAddress, address signer, UlnConfig calldata ulnConfig, ExecutorConfig calldata executorConfig) external {
OFT myOFT = OFT(contractAddress);
ILayerZeroEndpointV2 endpoint = ILayerZeroEndpointV2(address(myOFT.endpoint()));
SetConfigParam[] memory setConfigParams = new SetConfigParam[](2);
setConfigParams[0] = SetConfigParam({
eid: remoteEid,
configType: EXECUTOR_CONFIG_TYPE,
config: abi.encode(executorConfig)
});
setConfigParams[1] = SetConfigParam({
eid: remoteEid,
configType: ULN_CONFIG_TYPE,
config: abi.encode(ulnConfig)
});
vm.startBroadcast(signer);
endpoint.setConfig(address(myOFT), sendLibraryAddress, setConfigParams);
vm.stopBroadcast();
}
}
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.
Parameter | Type | Description |
---|---|---|
confirmations | uint64 | The minimum number of block confirmations the DVNs must have waited for their verification to be considered valid. |
requiredDVNCount | uint8 | The quantity of required DVNs that must verify before receiving the OApp's message. |
optionalDVNCount | uint8 | The quantity of optional DVNs that must verify before receiving the OApp's message. |
optionalDVNThreshold | uint8 | The 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. |
requiredDVNs | address[] | An array of addresses for all required DVNs to receive verifications from. |
optionalDVNs | address[] | An array of addresses for all optional DVNs to receive verifications from. |
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:
- ethers
- Foundry
The example below uses ethers.js (^5.7.2
) library to encode the arrays and call the Endpoint contract:
const {ethers} = require('ethers');
// Addresses
const oappAddress = 'YOUR_OAPP_ADDRESS'; // Replace with your OApp address
const receiveLibAddress = 'YOUR_RECEIVE_LIB_ADDRESS'; // Replace with your receive message library address
// Configuration
const remoteEid = 30101; // Example EID, replace with the actual value
const ulnConfig = {
confirmations: 99, // Example value, replace with actual
requiredDVNCount: 2, // Example value, replace with actual
optionalDVNCount: 0, // Example value, replace with actual
optionalDVNThreshold: 0, // Example value, replace with actual
requiredDVNs: ['0xDvnAddress1', '0xDvnAddress2'], // Replace with actual addresses
optionalDVNs: [], // Replace with actual addresses
};
// Provider and Signer
const provider = new ethers.providers.JsonRpcProvider(YOUR_RPC_URL);
const signer = new ethers.Wallet(YOUR_PRIVATE_KEY, provider);
// ABI and Contract
const endpointAbi = [
'function setConfig(address oappAddress, address receiveLibAddress, tuple(uint32 eid, uint32 configType, bytes config)[] setConfigParams) external',
];
const endpointContract = new ethers.Contract(YOUR_ENDPOINT_CONTRACT_ADDRESS, endpointAbi, signer);
// Encode UlnConfig using defaultAbiCoder
const configTypeUlnStruct =
'tuple(uint64 confirmations, uint8 requiredDVNCount, uint8 optionalDVNCount, uint8 optionalDVNThreshold, address[] requiredDVNs, address[] optionalDVNs)';
const encodedUlnConfig = ethers.utils.defaultAbiCoder.encode([configTypeUlnStruct], [ulnConfig]);
// Define the SetConfigParam struct
const setConfigParam = {
eid: remoteEid,
configType: 2, // RECEIVE_CONFIG_TYPE
config: encodedUlnConfig,
};
// Send the transaction
async function sendTransaction() {
try {
const tx = await endpointContract.setConfig(
oappAddress,
receiveLibAddress,
[setConfigParam], // This should be an array of SetConfigParam structs
);
console.log('Transaction sent:', tx.hash);
const receipt = await tx.wait();
console.log('Transaction confirmed:', receipt.transactionHash);
} catch (error) {
console.error('Transaction failed:', error);
}
}
sendTransaction();
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.22;
// Forge imports
import "forge-std/console.sol";
import "forge-std/Script.sol";
// LayerZero imports
import { ILayerZeroEndpointV2 } from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/ILayerZeroEndpointV2.sol";
import { OFT } from "@layerzerolabs/oft-evm/contracts/OFT.sol";
import { SetConfigParam } from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/IMessageLibManager.sol";
import { UlnConfig } from "@layerzerolabs/lz-evm-messagelib-v2/contracts/uln/UlnBase.sol";
contract ReceiveConfig is Script {
uint32 public constant RECEIVE_CONFIG_TYPE = 2;
function run(address contractAddress, uint32 remoteEid, address receiveLibraryAddress, address signer, UlnConfig calldata ulnConfig) external {
OFT myOFT = OFT(contractAddress);
ILayerZeroEndpointV2 endpoint = ILayerZeroEndpointV2(address(myOFT.endpoint()));
SetConfigParam[] memory setConfigParams = new SetConfigParam[](1);
setConfigParams[0] = SetConfigParam({
eid: remoteEid,
configType: RECEIVE_CONFIG_TYPE,
config: abi.encode(ulnConfig)
});
vm.startBroadcast(signer);
endpoint.setConfig(address(myOFT), receiveLibraryAddress, setConfigParams);
vm.stopBroadcast();
}
}
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: 15 | confirmations: 15 |
optionalDVNCount: 0 | optionalDVNCount: 0 |
optionalDVNThreshold: 0 | optionalDVNThreshold: 0 |
optionalDVNs: Array(0) | optionalDVNs: Array(0) |
requiredDVNCount: 2 | requiredDVNCount: 2 |
requiredDVNs: Array(DVN1_Address_A, DVN2_Address_A) | requiredDVNs: Array(DVN1_Address_B, DVN2_Address_B) |
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: 5 | confirmations: 15 |
optionalDVNCount: 0 | optionalDVNCount: 0 |
optionalDVNThreshold: 0 | optionalDVNThreshold: 0 |
optionalDVNs: Array(0) | optionalDVNs: Array(0) |
requiredDVNCount: 2 | requiredDVNCount: 2 |
requiredDVNs: Array(DVN1, DVN2) | requiredDVNs: Array(DVN1, DVN2) |
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: 15 | confirmations: 15 |
optionalDVNCount: 0 | optionalDVNCount: 0 |
optionalDVNThreshold: 0 | optionalDVNThreshold: 0 |
optionalDVNs: Array(0) | optionalDVNs: Array(0) |
requiredDVNCount: 1 | requiredDVNCount: 2 |
requiredDVNs: Array(DVN1) | requiredDVNs: Array(DVN1, DVN2) |
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: 15 | confirmations: 15 |
optionalDVNCount: 0 | optionalDVNCount: 0 |
optionalDVNThreshold: 0 | optionalDVNThreshold: 0 |
optionalDVNs: Array(0) | optionalDVNs: Array(0) |
requiredDVNCount: 2 | requiredDVNCount: 2 |
requiredDVNs: Array(DVN1, DVN2) | requiredDVNs: Array(DVN1, DVN_DEAD) |
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.