-
Reading Defaults: Use the
getConfigmethod to see default configurations. -
Setting Libraries: Call
setSendLibraryandsetReceiveLibraryto choose the correct Message Library version. -
Setting Configs: Use the
setConfigfunction to update your custom DVN and Executor settings.
- Send (Chain A) settings match the Receive (Chain B) settings.
- DVN addresses are provided in alphabetical order.
- Block confirmations are correctly set to avoid mismatches.
Self-Validation with cast
The recipes throughout this page use Foundry’s cast so you can read the exact on-chain values that govern message delivery on each pathway. Fill in the environment variables below once per pathway and the snippets in later sections will pick them up. Verify the EndpointV2 address for your chain at Deployed Contracts — the value shown is the current Ethereum mainnet address and most chains share it, but a handful do not.
These snippets are EVM-only. Solana, Aptos, Sui, TON, Starknet, Stellar, and Tron OApps must use their respective tooling — see the per-VM configuration pages under Developers.
The
UlnConfig tuple signature used in subsequent recipes is (uint64 confirmations, uint8 requiredDVNCount, uint8 optionalDVNCount, uint8 optionalDVNThreshold, address[] requiredDVNs, address[] optionalDVNs) and matches the struct defined in @layerzerolabs/lz-evm-protocol-v2. Older deployments may use different library versions — confirm against the ABI for the library address you resolved above.Getting the Default Config
You can easily fetch and decode your OApp’s current Send/Receive settings viaendpoint.getConfig(...). Below are two options:
Setting the Send and Receive Libraries
Asymmetric Library Configuration
Pin the send and receive libraries on both sides of every pathway. If one side callssetSendLibrary / setReceiveLibrary and the mirror leaves the library implicit, the implicit side will silently inherit whatever LayerZero Labs ships as that EID’s default — and that default can change.
Do:
- Call
EndpointV2.setSendLibrary(oapp, dstEid, sendLib)on the source side andEndpointV2.setReceiveLibrary(oapp, srcEid, recvLib, gracePeriod)on the destination side for the same pathway. - Pin the same library version on both sides — for example,
SendUln302on the sender,ReceiveUln302on the receiver. - Re-run the validation snippet below any time you add a new chain, migrate to a new library version, or rotate the OApp’s delegate.
- Leave one side on the default library because it “works today.” Defaults are mutable; LayerZero Labs may publish a new library version and roll the default forward without your involvement.
- Assume
getSendLibraryreturning a non-zero address means the library is pinned — it falls through todefaultSendLibraryif the OApp has not set its own.
How to check
DEFAULT, the pathway is asymmetric — pin both sides to the same explicit library.
getSendLibrary returning the same address as defaultSendLibrary does not prove the OApp is on the default. The OApp may have explicitly called setSendLibrary with the default’s address, in which case the side is pinned even though the equality test reports DEFAULT. To disambiguate, read sendLibrary[oapp][eid] directly from the endpoint: a value equal to the DEFAULT_LIB sentinel means implicit, any other address (including one that happens to equal the current default) means explicitly pinned. The same caveat applies to getReceiveLibrary / defaultReceiveLibrary.Setting Custom Send Config (DVN & Executor)
In this example, we configure both the ULN (DVN settings) and Executor settings on the sending chain.Setting Custom Receive Config (DVN Only)
On the receiving chain, only the ULN (DVN) configuration is needed since the Executor is not enforced on destination (i.e., the call can be made by anyone without permission).Since anyone can call
endpoint.lzReceive(...) for a verified LayerZero message, if you require specific execution requirements you will need to enforce them in your child contract’s internal _lzReceive(...). See the Integration Checklist for more details.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) |
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 reverse case — send confirmations higher than receive (
send-confirmations-higher) — does not block delivery: messages still verify. The sender simply waits more confirmations than the receiver requires, adding latency with no security gain. Resolve it by lowering the send-side confirmations to the receiver’s value (or raising the receiver’s to match, if you want the extra finality) via setConfig on the ULN.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) |
Non-Blocking DVN Mismatch
Pin identical DVN sets on both sides of every pathway. Some asymmetries do not block delivery — they still let messages verify — but they leave the on-chain enforced posture stricter than the documented send posture, which auditors and on-call engineers reading the send config will get wrong. A non-blocking DVN mismatch occurs when the send and receive DVN sets are not identical, but in every adversarial pick of send’s optional DVNs the receiver’s required-subset and optional threshold are still satisfied. Messages flow. Observed posture differs from enforced posture. Do:- Compare the merged (
getUlnConfig) configurations on both sides and align them intentionally. To bring the differing side into line, callsetConfigusing the same recipe as Asymmetric DVN Configuration. - If the sides differ on purpose (for example, the sender pays an additional optional DVN that the receiver does not require, in order to publish extra attestations downstream observers can read), document the rationale next to the deployment artifact.
- Treat any silent drift between sides as a regression — promote a CI check that diffs
getUlnConfig(send)againstgetUlnConfig(receive)for every pathway.
- Read only the send config and infer the application’s security posture from it — the receive side is the enforcement boundary.
- Add a new optional DVN on one side without mirroring on the other.
- Assume “messages are still delivering” means the configuration is correct; non-blocking mismatches deliver until the next default rotation changes the merged set.
How to check
Asymmetric DVN Configuration
Pin DVNs explicitly on both sides of every pathway. If one side callssetConfig with explicit requiredDVNs and the mirror leaves the value implicit (the OApp has never called setConfig for that field), the implicit side inherits the chain’s default DVN set — and LayerZero Labs can change that default at any time without notice. A pathway that is symmetric today can become asymmetric overnight when the default rotates.
The same drift applies to optionalDVNs and optionalDVNThreshold: an implicit threshold of 0 follows the default, not whatever the mirror has pinned.
Do:
- Call
EndpointV2.setConfig(oapp, sendLib, [...])on the send side andEndpointV2.setConfig(oapp, recvLib, [...])on the receive side with matchingUlnConfigvalues for every pathway. - Pin
requiredDVNs,optionalDVNs, andoptionalDVNThresholdon both sides — even if the explicit value happens to match today’s default. - After every LayerZero default migration, re-run the validation snippet below for every pathway you operate; one side flipping from implicit to a new default is exactly the asymmetry this finding catches.
- Rely on
getUlnConfig(the merged view) for parity checks — it hides asymmetry by filling in defaults on both sides. UsegetAppUlnConfigto see RAW values. - Treat an empty
requiredDVNs: []andrequiredDVNCount: 0as “no DVNs” — it means “use the default.”
How to check
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) |
Summary
-
Retrieve defaults: Use
getConfigif you need to review existing settings. -
Set Libraries: Choose your Message Library version by calling
setSendLibraryandsetReceiveLibrary. -
Set Configurations: Update your DVN (ULN) and Executor settings with
setConfig. - Ensure matching configurations: The Send settings on one chain must match the Receive settings on the other chain.