> ## Documentation Index
> Fetch the complete documentation index at: https://docs.layerzero.network/llms.txt
> Use this file to discover all available pages before exploring further.

# EVM DVN and Executor Configuration

> Step-by-step guide to evm dvn and executor configuration using LayerZero V2. Build and deploy omnichain applications with crosschain messaging. Follow step-...

Before setting your DVN and Executor Configuration, you should review the [Security Stack Core Concepts](../../../concepts/modular-security/security-stack-dvns).

<Warning>
  **Production deployments should use multiple required DVNs from independent operators.** A single-DVN configuration means a compromise of that one verifier results in unrestricted forged messages on the pathway. The configuration examples on this page that show `requiredDVNCount: 1` are illustrative only — production pathways should set `requiredDVNCount >= 2` with DVNs from different operators. See the [Integration Checklist](../../../tools/integration-checklist#set-security-and-executor-configurations-on-every-pathway) for production DVN guidance.
</Warning>

You can manually configure your EVM OApp's Send and Receive settings by:

* **Reading Defaults:** Use the `getConfig` method to see default configurations.

* **Setting Libraries:** Call `setSendLibrary` and `setReceiveLibrary` to choose the correct Message Library version.

* **Setting Configs:** Use the `setConfig` function to update your custom DVN and Executor settings.

For both Send and Receive configurations, make sure that for a given [channel](../../../concepts/glossary#channel--lossless-channel):

* **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.

<Tip>
  ### Use the LayerZero CLI

  The LayerZero CLI has abstracted these calls for every supported chain. See the [**CLI Setup Guide**](../../../get-started/create-lz-oapp/start) to easily deploy, configure, and send messages using LayerZero.
</Tip>

### Self-Validation with `cast`

<a id="self-validation-with-cast" />

The recipes throughout this page use [Foundry's `cast`](https://book.getfoundry.sh/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](../../../deployments/deployed-contracts) — the value shown is the current Ethereum mainnet address and most chains share it, but a handful do not.

<Note>
  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](../../../developers).
</Note>

```bash wrap theme={null}
# LayerZero V2 — environment for self-validation snippets
# Fill in for the pathway you are auditing.

# Source chain (A) — where messages are sent FROM
export RPC_A=https://...                      # JSON-RPC endpoint for chain A
export ENDPOINT_A=0x1a44076050125825900e736c501f859c50fE728c   # EndpointV2 on chain A
export OAPP_A=0x...                           # Your OApp address on chain A
export EID_B=30106                            # Destination endpoint ID (chain B)

# Destination chain (B) — where messages are RECEIVED
export RPC_B=https://...
export ENDPOINT_B=0x1a44076050125825900e736c501f859c50fE728c
export OAPP_B=0x...
export EID_A=30101                            # Source endpoint ID (chain A)

# Resolve libraries once per pathway
export SEND_LIB_A=$(cast call "$ENDPOINT_A" "getSendLibrary(address,uint32)(address)" "$OAPP_A" "$EID_B" --rpc-url "$RPC_A")
export RECV_LIB_B=$(cast call "$ENDPOINT_B" "getReceiveLibrary(address,uint32)(address,bool)" "$OAPP_B" "$EID_A" --rpc-url "$RPC_B" | head -1)
```

<Note>
  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`](../technical-reference/api#ulnconfig). Older deployments may use different library versions — confirm against the ABI for the library address you resolved above.
</Note>

### Getting the Default Config

You can easily fetch and decode your OApp’s current Send/Receive settings via `endpoint.getConfig(...)`. Below are two options:

<CodeGroup>
  ```solidity wrap Foundry theme={null}
  // SPDX-License-Identifier: UNLICENSED
  pragma solidity ^0.8.22;

  import "forge-std/Script.sol";
  import { console } from "forge-std/console.sol";
  import { ILayerZeroEndpointV2 } from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/ILayerZeroEndpointV2.sol";
  import { UlnConfig } from "@layerzerolabs/lz-evm-messagelib-v2/contracts/uln/UlnBase.sol";
  import { ExecutorConfig } from "@layerzerolabs/lz-evm-messagelib-v2/contracts/SendLibBase.sol";

  /// @title GetConfigScript
  /// @notice Retrieves and logs the current configuration for the OApp.
  contract GetConfigScript is Script {
      /// @notice Calls getConfig on the specified LayerZero Endpoint.
      /// @dev Decodes the returned bytes as a UlnConfig. Logs some of its fields.
      /// @param rpcUrl The RPC URL for the target chain.
      /// @param endpoint The LayerZero Endpoint address.
      /// @param oapp The address of your OApp.
      /// @param lib The address of the Message Library (send or receive).
      /// @param eid The remote endpoint identifier.
      /// @param configType The configuration type (1 = Executor, 2 = ULN).
      function getConfig(
          string memory _rpcUrl,
          address _endpoint,
          address _oapp,
          address _lib,
          uint32 _eid,
          uint32 _configType
      ) external {
          // Create a fork from the specified RPC URL.
          vm.createSelectFork(_rpcUrl);
          vm.startBroadcast();

          // Instantiate the LayerZero endpoint.
          ILayerZeroEndpointV2 endpoint = ILayerZeroEndpointV2(_endpoint);
          // Retrieve the raw configuration bytes.
          bytes memory config = endpoint.getConfig(_oapp, _lib, _eid, _configType);

          if (_configType == 1) {
              // Decode the Executor config (configType = 1)
              ExecutorConfig memory execConfig = abi.decode(config, (ExecutorConfig));
              // Log some key configuration parameters.
              console.log("Executor Type:", execConfig.maxMessageSize);
              console.log("Executor Address:", execConfig.executor);
          }

          if (_configType == 2) {
              // Decode the ULN config (configType = 2)
              UlnConfig memory decodedConfig = abi.decode(config, (UlnConfig));
              // Log some key configuration parameters.
              console.log("Confirmations:", decodedConfig.confirmations);
              console.log("Required DVN Count:", decodedConfig.requiredDVNCount);
              for (uint i = 0; i < decodedConfig.requiredDVNs.length; i++) {
                  console.logAddress(decodedConfig.requiredDVNs[i]);
              }
              console.log("Optional DVN Count:", decodedConfig.optionalDVNCount);
              for (uint i = 0; i < decodedConfig.optionalDVNs.length; i++) {
                  console.logAddress(decodedConfig.optionalDVNs[i]);
              }
              console.log("Optional DVN Threshold:", decodedConfig.optionalDVNThreshold);

          }
          vm.stopBroadcast();
      }
  }
  ```

  ```typescript wrap Ethers V5 theme={null}
  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();
  ```
</CodeGroup>

### Setting the Send and Receive Libraries

<CodeGroup>
  ```solidity wrap Foundry theme={null}
  // SPDX-License-Identifier: UNLICENSED
  pragma solidity ^0.8.22;

  import "forge-std/Script.sol";
  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 {
          ILayerZeroEndpointV2 endpoint = ILayerZeroEndpointV2(_endpoint);

          vm.startBroadcast(_signer);
          endpoint.setSendLibrary(_oapp, _eid, _sendLib);
          console.log("Send library set successfully.");
          endpoint.setReceiveLibrary(_oapp, _eid, _receiveLib);
          console.log("Receive library set successfully.");
          vm.stopBroadcast();
      }
  }
  ```

  ```typescript wrap Ethers V5 theme={null}
  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();
  ```
</CodeGroup>

<h3 id="asymmetric-library-config">
  Asymmetric Library Configuration
</h3>

Pin the send and receive libraries on **both** sides of every pathway. If one side calls `setSendLibrary` / `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 **and** `EndpointV2.setReceiveLibrary(oapp, srcEid, recvLib, gracePeriod)` on the destination side for the same pathway.
* Pin the same library version on both sides — for example, `SendUln302` on the sender, `ReceiveUln302` on 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.

**Don't:**

* 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 `getSendLibrary` returning a non-zero address means the library is pinned — it falls through to `defaultSendLibrary` if the OApp has not set its own.

<Warning>
  Default libraries are not a contract — they are a setting LayerZero Labs controls. If a default migration ships while only one side of your pathway is implicit, the explicit and implicit sides drift, and messages already in flight may stop verifying until you `setConfig` against the new library address.
</Warning>

#### How to check

```bash wrap theme={null}
# Compare each OApp's effective library against the endpoint's default for that EID.
APP_SEND_LIB=$(cast call "$ENDPOINT_A" "getSendLibrary(address,uint32)(address)" "$OAPP_A" "$EID_B" --rpc-url "$RPC_A")
DEFAULT_SEND_LIB=$(cast call "$ENDPOINT_A" "defaultSendLibrary(uint32)(address)" "$EID_B" --rpc-url "$RPC_A")
[ "$APP_SEND_LIB" = "$DEFAULT_SEND_LIB" ] && echo "A→B sender is on DEFAULT library" || echo "A→B sender pinned: $APP_SEND_LIB"

APP_RECV_LIB=$(cast call "$ENDPOINT_B" "getReceiveLibrary(address,uint32)(address,bool)" "$OAPP_B" "$EID_A" --rpc-url "$RPC_B" | head -1)
DEFAULT_RECV_LIB=$(cast call "$ENDPOINT_B" "defaultReceiveLibrary(uint32)(address)" "$EID_A" --rpc-url "$RPC_B")
[ "$APP_RECV_LIB" = "$DEFAULT_RECV_LIB" ] && echo "B receive is on DEFAULT library" || echo "B receive pinned: $APP_RECV_LIB"
```

If exactly one of the two prints `DEFAULT`, the pathway is asymmetric — pin both sides to the same explicit library.

<Note>
  `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`.
</Note>

### Setting Custom Send Config (DVN & Executor)

<a id="send-config-type-executor" />

<a id="send-config-type-uln-security-stack" />

In this example, we configure both the ULN (DVN settings) and Executor settings on the sending chain.

<CodeGroup>
  ```solidity wrap Foundry theme={null}
  // SPDX-License-Identifier: UNLICENSED
  pragma solidity ^0.8.22;

  import "forge-std/Script.sol";
  import { ILayerZeroEndpointV2, SetConfigParam } from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/ILayerZeroEndpointV2.sol";
  import { UlnConfig } from "@layerzerolabs/lz-evm-messagelib-v2/contracts/uln/UlnBase.sol";
  import { ExecutorConfig } from "@layerzerolabs/lz-evm-messagelib-v2/contracts/SendLibBase.sol";

  /// @title LayerZero Send Configuration Script
  /// @notice Defines and applies ULN (DVN) + Executor configs for cross‑chain messaging via LayerZero Endpoint V2.
  contract SetSendConfig is Script {
      uint32 constant EXECUTOR_CONFIG_TYPE = 1;
      uint32 constant ULN_CONFIG_TYPE = 2;

       /// @notice Broadcasts transactions to set both Send ULN and Executor configurations
      function run() external {
          address endpoint = vm.envAddress("SOURCE_ENDPOINT_ADDRESS");
          address oapp      = vm.envAddress("SENDER_OAPP_ADDRESS");
          uint32 eid        = uint32(vm.envUint("REMOTE_EID"));
          address sendLib   = vm.envAddress("SEND_LIB_ADDRESS");
          address signer    = vm.envAddress("SIGNER");

          /// @notice ULNConfig defines security parameters (DVNs + confirmation threshold)
          /// @notice Send config requests these settings to be applied to the DVNs and Executor
          /// @dev 0 values will be interpretted as defaults, so to apply NIL settings, use:
          /// @dev uint8 internal constant NIL_DVN_COUNT = type(uint8).max;
          /// @dev uint64 internal constant NIL_CONFIRMATIONS = type(uint64).max;
          UlnConfig memory uln = UlnConfig({
              confirmations:        15,                                      // minimum block confirmations required
              requiredDVNCount:     2,                                       // number of DVNs required
              optionalDVNCount:     type(uint8).max,                         // optional DVNs count, uint8
              optionalDVNThreshold: 0,                                       // optional DVN threshold
              requiredDVNs:        [address(0x1111...), address(0x2222...)], // sorted list of required DVN addresses
              optionalDVNs:        []                                        // sorted list of optional DVNs
          });

          /// @notice ExecutorConfig sets message size limit + fee‑paying executor
          ExecutorConfig memory exec = ExecutorConfig({
              maxMessageSize: 10000,                                       // max bytes per crosschain message
              executor:       address(0x3333...)                           // address that pays destination execution fees
          });

          bytes memory encodedUln  = abi.encode(uln);
          bytes memory encodedExec = abi.encode(exec);

          SetConfigParam[] memory params = new SetConfigParam[](2);
          params[0] = SetConfigParam(eid, EXECUTOR_CONFIG_TYPE, encodedExec);
          params[1] = SetConfigParam(eid, ULN_CONFIG_TYPE, encodedUln);

          vm.startBroadcast(signer);
          ILayerZeroEndpointV2(endpoint).setConfig(oapp, sendLib, params);
          vm.stopBroadcast();
      }
  }
  ```

  ```typescript wrap Ethers V5 theme={null}
  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
  // UlnConfig controls verification threshold for incoming messages
  // Receive config enforces these settings have been applied to the DVNs and Executor
  // 0 values will be interpretted as defaults, so to apply NIL settings, use:
  // uint8 internal constant NIL_DVN_COUNT = type(uint8).max;
  // uint64 internal constant NIL_CONFIRMATIONS = type(uint64).max;
  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, must be in alphabetical order
    optionalDVNs: [], // Replace with actual addresses, must be in alphabetical order
  };

  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();
  ```
</CodeGroup>

### 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).

<Warning>
  This config enforces all of the configuration settings from the source chain. Ensure that the DVNs in this config object match the sender side of the channel, otherwise messages will be blocked.

  Blocked messages can be caused by:

  * **Mismatch of block confirmations:** if source block confirmations are less than the destination

  * **Mismatch of DVNs:** the source DVNs do not match the threshold requirements of the destination

  A mismatch will result in a config error, and in some cases can result in a loss of funds if not caught.
</Warning>

<Info>
  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**](../../../tools/integration-checklist#enforce-msgvalue-in-_lzreceive-and-lzcompose) for more details.
</Info>

<br />

<CodeGroup>
  ```solidity wrap Foundry theme={null}
  // SPDX-License-Identifier: UNLICENSED
  pragma solidity ^0.8.22;

  import "forge-std/Script.sol";
  import { ILayerZeroEndpointV2, SetConfigParam } from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/ILayerZeroEndpointV2.sol";
  import { UlnConfig } from "@layerzerolabs/lz-evm-messagelib-v2/contracts/uln/UlnBase.sol";

  /// @title LayerZero Receive Configuration Script
  /// @notice Defines and applies ULN (DVN) config for inbound message verification via LayerZero Endpoint V2.
  contract SetReceiveConfig is Script {
      uint32 constant RECEIVE_CONFIG_TYPE = 2;

      function run() external {
          address endpoint = vm.envAddress("ENDPOINT_ADDRESS");
          address oapp      = vm.envAddress("OAPP_ADDRESS");
          uint32 eid        = uint32(vm.envUint("REMOTE_EID"));
          address receiveLib= vm.envAddress("RECEIVE_LIB_ADDRESS");
          address signer    = vm.envAddress("SIGNER");

          /// @notice UlnConfig controls verification threshold for incoming messages
          /// @notice Receive config enforces these settings have been applied to the DVNs and Executor
          /// @dev 0 values will be interpretted as defaults, so to apply NIL settings, use:
          /// @dev uint8 internal constant NIL_DVN_COUNT = type(uint8).max;
          /// @dev uint64 internal constant NIL_CONFIRMATIONS = type(uint64).max;
          UlnConfig memory uln = UlnConfig({
              confirmations:      15,                                       // min block confirmations from source
              requiredDVNCount:   2,                                        // required DVNs for message acceptance
              optionalDVNCount:   type(uint8).max,                          // optional DVNs count
              optionalDVNThreshold: 0                                       // optional DVN threshold
              requiredDVNs:       [address(0x1111...), address(0x2222...)], // sorted required DVNs
              optionalDVNs:       []                                        // no optional DVNs
          });

          bytes memory encodedUln = abi.encode(uln);

          SetConfigParam[] memory params = new SetConfigParam[](1);
          params[0] = SetConfigParam(eid, RECEIVE_CONFIG_TYPE, encodedUln);

          vm.startBroadcast(signer);
          ILayerZeroEndpointV2(endpoint).setConfig(oapp, receiveLib, params);
          vm.stopBroadcast();
      }
  }
  ```

  ```typescript wrap Ethers V5 theme={null}
  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, must be in alphabetical order
    optionalDVNs: [], // Replace with actual addresses, must be in alphabetical order
  };

  // 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();
  ```
</CodeGroup>

## 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) |

<Tip>
  The sending OApp's **SendLibConfig** (OApp on Chain A) and the receiving OApp's **ReceiveLibConfig** (OApp on Chain B) match!
</Tip>

### 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) |

<Warning>
  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.
</Warning>

<Note>
  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.
</Note>

#### DVN Mismatch

<a id="dvn-mismatch" />

<a id="blocking-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)** |

<Warning>
  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.
</Warning>

A DVN mismatch is **blocking** when the receiver's required DVNs are not a subset of the sender's effective DVN set, or when the worst-case overlap is smaller than the receiver's threshold — messages cannot accumulate the attestations they need and the channel halts at the next nonce.

#### Non-Blocking DVN Mismatch

<a id="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, call `setConfig` using the same recipe as [Asymmetric DVN Configuration](#asymmetric-dvn-config).
* 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)` against `getUlnConfig(receive)` for every pathway.

**Don't:**

* 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.

<Warning>
  The effective security of a non-blocking mismatch is whichever side is **stricter**, not the union of the two configs. An auditor inspecting only the send config will overcount or undercount the DVNs your messages actually require, depending on which side has the wider set.
</Warning>

##### How to check

```bash wrap theme={null}
# Merged config (defaults filled in) is what messages actually enforce.
echo "Send-side (A→B) merged:"
cast call "$SEND_LIB_A" \
  "getUlnConfig(address,uint32)((uint64,uint8,uint8,uint8,address[],address[]))" \
  "$OAPP_A" "$EID_B" --rpc-url "$RPC_A"

echo "Receive-side (B from A) merged:"
cast call "$RECV_LIB_B" \
  "getUlnConfig(address,uint32)((uint64,uint8,uint8,uint8,address[],address[]))" \
  "$OAPP_B" "$EID_A" --rpc-url "$RPC_B"

# Compare requiredDVNs / optionalDVNs / optionalDVNThreshold by hand.
# If sets differ but the sender still satisfies the receiver's required-subset
# and threshold, this is non-blocking — messages flow, but observed posture
# differs from enforced posture. Align both sides intentionally.
```

<h4 id="asymmetric-dvn-config">
  Asymmetric DVN Configuration
</h4>

Pin DVNs explicitly on **both** sides of every pathway. If one side calls `setConfig` 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 **and** `EndpointV2.setConfig(oapp, recvLib, [...])` on the receive side with matching `UlnConfig` values for every pathway.
* Pin `requiredDVNs`, `optionalDVNs`, and `optionalDVNThreshold` on 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.

**Don't:**

* Rely on `getUlnConfig` (the **merged** view) for parity checks — it hides asymmetry by filling in defaults on both sides. Use `getAppUlnConfig` to see RAW values.
* Treat an empty `requiredDVNs: []` and `requiredDVNCount: 0` as "no DVNs" — it means "use the default."

<Warning>
  Defaults are **mutable**. LayerZero Labs can change the default DVN set for any EID at any time. An OApp on the default for one direction and explicit for the other will silently shift to a new posture when the default updates; messages already in flight may stop verifying until the asymmetric side is brought into alignment.
</Warning>

##### How to check

```bash wrap theme={null}
# `getAppUlnConfig` returns RAW values (zeros where the OApp hasn't set anything).
# `getUlnConfig` returns the MERGED values (defaults filled in).
# Asymmetry = one side has zeros, the other side has non-zeros, for the same field.

# Send side (what chain A's OApp pays for)
cast call "$SEND_LIB_A" \
  "getAppUlnConfig(address,uint32)((uint64,uint8,uint8,uint8,address[],address[]))" \
  "$OAPP_A" "$EID_B" --rpc-url "$RPC_A"

# Receive side (what chain B's OApp requires)
cast call "$RECV_LIB_B" \
  "getAppUlnConfig(address,uint32)((uint64,uint8,uint8,uint8,address[],address[]))" \
  "$OAPP_B" "$EID_A" --rpc-url "$RPC_B"

# Interpret: if either side returns
# `(0, 0, 0, 0, [], [])` while the mirror returns non-zero, the configuration is asymmetric.
# Resolve by calling setConfig on the side with zeros to pin the same DVNs as the mirror.
```

#### [Dead DVN](../../../concepts/glossary#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)** |

<Warning>
  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.
</Warning>

## Summary

* **Retrieve defaults:** Use `getConfig` if you need to review existing settings.

* **Set Libraries:** Choose your Message Library version by calling `setSendLibrary` and `setReceiveLibrary`.

* **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.
