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

# DVN and Executor Configuration on Starknet

> Configure Decentralized Verifier Networks (DVNs), Executors, and message libraries for LayerZero on Starknet.

This guide explains how to configure Decentralized Verifier Networks (DVNs), Executors, and message libraries for your Starknet OApp or OFT using the SDK.

<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. See the [Integration Checklist](/v2/tools/integration-checklist#set-security-and-executor-configurations-on-every-pathway) for production DVN guidance.
</Warning>

## Overview

Configuration is done through the SDK provided by `@layerzerolabs/lz-v2-protocol-starknet`. All examples use SDK methods to interact with the Endpoint and message library contracts.

**Configuration flow**:

1. Set delegate (required if configuring via external account)
2. Set message libraries (optional)
3. Configure DVNs for send/receive (optional but recommended)
4. Set enforced options (optional)
5. Set peer addresses (required - opens pathway, call last!)

<Warning>
  **Critical Order**

  Always configure in this order. Setting peers last ensures the pathway isn't opened until all security settings are in place.
</Warning>

## SDK Setup

```typescript wrap theme={null}
import {RpcProvider, Account} from 'starknet';
import {
  getEndpointV2Contract,
  getOAppContract,
  getUltraLightNodeContractWithAddress,
  encodeUlnConfig,
  encodeExecutorConfig,
  MessageLibConfigType,
} from '@layerzerolabs/lz-v2-protocol-starknet';
import {EndpointId, ChainName, Environment} from '@layerzerolabs/lz-definitions';

// Setup provider and account
const provider = new RpcProvider({nodeUrl: 'YOUR_RPC_URL'});
const account = new Account({provider, address: accountAddress, signer: privateKey});

// Get contract instances
const oappContract = await getOAppContract(oappAddress, provider);
const endpointContract = await getEndpointV2Contract(
  ChainName.STARKNET,
  Environment.TESTNET,
  provider,
);

// If you already have the Endpoint address, you can use:
// const endpointContract = await getEndpointV2ContractWithAddress(ENDPOINT_ADDRESS, provider);
// This is recommended when running outside the monorepo.
```

## Prerequisite: Set Delegate

Endpoint configuration calls (`set_send_library`, `set_receive_library`, `set_send_configs`, `set_receive_configs`) require the caller to be the OApp itself or an authorized delegate. If you're configuring from an external account, set a delegate first (owner-only):

```typescript wrap theme={null}
const setDelegateCall = oappContract.populateTransaction.set_delegate(accountAddress);
await account.execute([setDelegateCall]);
```

Use the address of the account that will submit the endpoint configuration transactions.

## Default Configuration

LayerZero provides sensible defaults. If you don't configure custom settings:

| Setting         | Default                 |
| --------------- | ----------------------- |
| DVN             | LayerZero Labs DVN      |
| Executor        | LayerZero Labs Executor |
| Send Library    | ULN302                  |
| Receive Library | ULN302                  |

You can query the default configuration via the Endpoint or check [LayerZero Deployments](/v2/deployments/deployed-contracts).

***

## Configuration Methods

### Set Peer

```typescript wrap theme={null}
import {getOAppContract} from '@layerzerolabs/lz-v2-protocol-starknet';
import {EndpointId} from '@layerzerolabs/lz-definitions';

const oapp = await getOAppContract(oappAddress, provider);

// Peer address as Bytes32 (left-padded for EVM addresses)
const peerBytes32 = {value: BigInt('0x000000000000000000000000' + evmAddress.slice(2))};

const call = oapp.populateTransaction.set_peer(
  EndpointId.ETHEREUM_V2_MAINNET, // Remote chain endpoint ID
  peerBytes32,
);

await account.execute([call]);
```

**Address format**: Use Bytes32 for all peers. EVM addresses (20 bytes) must be left-padded with zeros to 32 bytes.

### Set Message Libraries

```typescript wrap theme={null}
import {getEndpointV2Contract} from '@layerzerolabs/lz-v2-protocol-starknet';
import {EndpointId, ChainName, Environment} from '@layerzerolabs/lz-definitions';

const endpoint = await getEndpointV2Contract(ChainName.STARKNET, Environment.TESTNET, provider);

// Set custom send library
const setSendLibCall = endpoint.populateTransaction.set_send_library(
  oappAddress,
  EndpointId.ETHEREUM_V2_MAINNET,
  messageLibAddress,
);

// Set custom receive library (with grace period)
const setReceiveLibCall = endpoint.populateTransaction.set_receive_library(
  oappAddress,
  EndpointId.ETHEREUM_V2_MAINNET,
  messageLibAddress,
  0, // Grace period in blocks (0 = immediate)
);

await account.execute([setSendLibCall, setReceiveLibCall]);
```

**Default**: Uses Endpoint defaults if not configured. If you see `DEFAULT_SEND_LIB_UNAVAILABLE` or `UNSUPPORTED_EID`, set the send/receive libraries explicitly and ensure the EID is supported by the ULN.

```typescript wrap theme={null}
import {getUltraLightNodeContractWithAddress} from '@layerzerolabs/lz-v2-protocol-starknet';

const uln = await getUltraLightNodeContractWithAddress(ulnAddress, provider);
const canSend = await uln.is_supported_send_eid(remoteEid);
const canReceive = await uln.is_supported_receive_eid(remoteEid);
```

***

## DVN Configuration

### Configure Send DVN (Outbound)

```typescript wrap theme={null}
import {
  getEndpointV2Contract,
  encodeUlnConfig,
  MessageLibConfigType,
} from '@layerzerolabs/lz-v2-protocol-starknet';
import {EndpointId, ChainName, Environment} from '@layerzerolabs/lz-definitions';

const endpoint = await getEndpointV2Contract(ChainName.STARKNET, Environment.TESTNET, provider);
const remoteEid = EndpointId.ETHEREUM_V2_MAINNET;

// Get the current send library address
const sendLibResponse = await endpoint.get_send_library(oappAddress, remoteEid);
const sendLibAddress = sendLibResponse.lib;

// Encode ULN configuration
const ulnConfig = encodeUlnConfig({
  confirmations: 15,
  has_confirmations: true,
  required_dvns: [LAYERZERO_DVN_ADDRESS, PARTNER_DVN_ADDRESS], // Must be sorted ascending
  has_required_dvns: true,
  optional_dvns: [],
  optional_dvn_threshold: 0,
  has_optional_dvns: false,
});

// Set send config
const call = endpoint.populateTransaction.set_send_configs(oappAddress, sendLibAddress, [
  {
    eid: remoteEid,
    config_type: MessageLibConfigType.ULN, // 2
    config: ulnConfig,
  },
]);

await account.execute([call]);
```

### Configure Receive DVN (Inbound)

```typescript wrap theme={null}
// Get the current receive library address
const receiveLibResponse = await endpoint.get_receive_library(oappAddress, remoteEid);
const receiveLibAddress = receiveLibResponse.lib;

// Encode ULN configuration for receive
const ulnConfig = encodeUlnConfig({
  confirmations: 15,
  has_confirmations: true,
  required_dvns: [LAYERZERO_DVN_ADDRESS],
  has_required_dvns: true,
  optional_dvns: [],
  optional_dvn_threshold: 0,
  has_optional_dvns: false,
});

// Set receive config
const call = endpoint.populateTransaction.set_receive_configs(oappAddress, receiveLibAddress, [
  {
    eid: remoteEid,
    config_type: MessageLibConfigType.ULN, // 2
    config: ulnConfig,
  },
]);

await account.execute([call]);
```

### ULN Configuration Structure

| Field                    | Type       | Description                                        |
| ------------------------ | ---------- | -------------------------------------------------- |
| `confirmations`          | `number`   | Block confirmations required before verification   |
| `has_confirmations`      | `boolean`  | Set `true` to use custom value                     |
| `required_dvns`          | `string[]` | DVN addresses (all must verify) - sorted ascending |
| `has_required_dvns`      | `boolean`  | Set `true` to use custom DVNs                      |
| `optional_dvns`          | `string[]` | Optional DVN addresses - sorted ascending          |
| `optional_dvn_threshold` | `number`   | How many optional DVNs must verify                 |
| `has_optional_dvns`      | `boolean`  | Set `true` to use custom optional DVNs             |

<Info>
  **has\_* Fields*\*

  The `has_*` fields indicate whether the corresponding value should override the default configuration. Set them to `true` when you want to use custom values, otherwise the protocol defaults will be used.
</Info>

<Warning>
  **DVN Ordering**

  DVN addresses in `required_dvns` and `optional_dvns` **must be sorted in ascending order**. The contract will revert if unsorted or duplicate DVNs are provided.
</Warning>

**Config types**: `MessageLibConfigType.EXECUTOR` = 1, `MessageLibConfigType.ULN` = 2

***

## Configuring Executor

The Executor delivers messages on the destination chain.

```typescript wrap theme={null}
import {
  getEndpointV2Contract,
  encodeExecutorConfig,
  MessageLibConfigType,
} from '@layerzerolabs/lz-v2-protocol-starknet';
import {ChainName, Environment} from '@layerzerolabs/lz-definitions';

const endpoint = await getEndpointV2Contract(ChainName.STARKNET, Environment.TESTNET, provider);

// Encode executor configuration
const executorConfig = encodeExecutorConfig({
  max_message_size: 10000,
  executor: EXECUTOR_ADDRESS,
});

// Set executor config (only for send direction)
const call = endpoint.populateTransaction.set_send_configs(oappAddress, sendLibAddress, [
  {
    eid: remoteEid,
    config_type: MessageLibConfigType.EXECUTOR, // 1
    config: executorConfig,
  },
]);

await account.execute([call]);
```

### Executor Config Structure

| Field              | Type     | Description                   |
| ------------------ | -------- | ----------------------------- |
| `max_message_size` | `number` | Maximum message size in bytes |
| `executor`         | `string` | Executor contract address     |

***

## Setting Enforced Options

Enforced options set **minimum** execution parameters that users cannot override.

This requires the OAppOptionsType3 component (included in OFT contracts). If your ABI doesn't expose `set_enforced_options`, load your compiled contract artifact and call the entrypoint directly.

```typescript wrap theme={null}
import {Contract} from 'starknet';
import {Options} from '@layerzerolabs/lz-v2-utilities';
import compiledArtifact from './path/to/contract_class.json';

// Use your compiled ABI for OFT/OAppOptionsType3 since getOAppContract
// only exposes the base OApp interface.
const oappAbi = compiledArtifact.abi; // Load from target/dev/*.contract_class.json
const oapp = new Contract({abi: oappAbi, address: oappAddress, provider}).typedv2(oappAbi);

// Build options with minimum gas requirements
const options = Options.newOptions()
  .addExecutorLzReceiveOption(200000, 0) // 200k gas for lz_receive
  .toBytes();

// Set enforced options for SEND message type (1)
const call = oapp.populateTransaction.set_enforced_options([
  {
    eid: remoteEid,
    msg_type: 1,
    options,
  },
]);

await account.execute([call]);
```

If you prefer `sncast`, call the entrypoint directly:

```bash theme={null}
# lzReceive gas = 120000, value = 0 (no compose)
sncast invoke \
  --contract-address <OFT_ADDRESS> \
  --function set_enforced_options \
  --network sepolia \
  --arguments 'array![layerzero::oapps::common::oapp_options_type_3::structs::EnforcedOptionParam { eid: <DST_EID>, msg_type: 1, options: core::byte_array::ByteArray { data: array![], pending_word: 0x0003010011010000000000000000000000000001d4c0, pending_word_len: 22 } }]'
```

### Message Types

| Type   | Value | Description           |
| ------ | ----- | --------------------- |
| `SEND` | 1     | Standard OFT transfer |

***

## Complete Configuration Example

```typescript wrap theme={null}
import {RpcProvider, Account, Contract} from 'starknet';
import {
  getEndpointV2Contract,
  getOAppContract,
  encodeUlnConfig,
  encodeExecutorConfig,
  MessageLibConfigType,
} from '@layerzerolabs/lz-v2-protocol-starknet';
import {Options} from '@layerzerolabs/lz-v2-utilities';
import {EndpointId, ChainName, Environment} from '@layerzerolabs/lz-definitions';
import compiledArtifact from './path/to/contract_class.json';

async function configureOApp() {
  const provider = new RpcProvider({nodeUrl: RPC_URL});
  const account = new Account({provider, address: ACCOUNT_ADDRESS, signer: PRIVATE_KEY});

  const endpoint = await getEndpointV2Contract(ChainName.STARKNET, Environment.TESTNET, provider);
  const oapp = await getOAppContract(OFT_ADDRESS, provider);
  const oappOptions = new Contract({
    abi: compiledArtifact.abi,
    address: OFT_ADDRESS,
    provider,
  }).typedv2(compiledArtifact.abi);
  const remoteEid = EndpointId.ETHEREUM_V2_MAINNET;
  const ULN_ADDRESS = '0x...'; // ULN302 send library address

  // Authorize the account to configure endpoint settings (owner-only)
  const setDelegateCall = oapp.populateTransaction.set_delegate(ACCOUNT_ADDRESS);

  // Set libraries (required if defaults aren't configured for the EID)
  const setSendLibCall = endpoint.populateTransaction.set_send_library(
    OFT_ADDRESS,
    remoteEid,
    ULN_ADDRESS,
  );
  const setReceiveLibCall = endpoint.populateTransaction.set_receive_library(
    OFT_ADDRESS,
    remoteEid,
    ULN_ADDRESS,
    0,
  );

  // 1. Configure send DVN + Executor
  const sendConfigCall = endpoint.populateTransaction.set_send_configs(OFT_ADDRESS, ULN_ADDRESS, [
    {
      eid: remoteEid,
      config_type: MessageLibConfigType.ULN,
      config: encodeUlnConfig({
        confirmations: 15,
        has_confirmations: true,
        required_dvns: [LAYERZERO_DVN, PARTNER_DVN],
        has_required_dvns: true,
        optional_dvns: [],
        optional_dvn_threshold: 0,
        has_optional_dvns: false,
      }),
    },
    {
      eid: remoteEid,
      config_type: MessageLibConfigType.EXECUTOR,
      config: encodeExecutorConfig({
        max_message_size: 10000,
        executor: EXECUTOR_ADDRESS,
      }),
    },
  ]);

  // 2. Configure receive DVN
  const receiveConfigCall = endpoint.populateTransaction.set_receive_configs(
    OFT_ADDRESS,
    ULN_ADDRESS,
    [
      {
        eid: remoteEid,
        config_type: MessageLibConfigType.ULN,
        config: encodeUlnConfig({
          confirmations: 15,
          has_confirmations: true,
          required_dvns: [LAYERZERO_DVN],
          has_required_dvns: true,
          optional_dvns: [],
          optional_dvn_threshold: 0,
          has_optional_dvns: false,
        }),
      },
    ],
  );

  // 3. Set enforced options
  const enforcedOptionsCall = oappOptions.populateTransaction.set_enforced_options([
    {
      eid: remoteEid,
      msg_type: 1, // SEND message type
      options: Options.newOptions().addExecutorLzReceiveOption(200000, 0).toBytes(),
    },
  ]);

  // 4. Set peer (LAST!)
  const setPeerCall = oapp.populateTransaction.set_peer(remoteEid, {
    value: BigInt('0x000000000000000000000000' + EVM_OFT_ADDRESS.slice(2)),
  });

  // Execute all in atomic transaction
  await account.execute([
    setDelegateCall,
    setSendLibCall,
    setReceiveLibCall,
    sendConfigCall,
    receiveConfigCall,
    enforcedOptionsCall,
    setPeerCall,
  ]);

  console.log('Configuration complete!');
}
```

***

## Reading Configuration

### Get Current Send Config

```typescript wrap theme={null}
import {getUltraLightNodeContractWithAddress} from '@layerzerolabs/lz-v2-protocol-starknet';

const ulnContract = await getUltraLightNodeContractWithAddress(sendLibAddress, provider);

// Get executor config
const executorConfig = await ulnContract.get_raw_oapp_executor_config(oappAddress, remoteEid);
console.log('Max message size:', executorConfig.max_message_size);
console.log('Executor:', executorConfig.executor);

// Get ULN config
const ulnConfig = await ulnContract.get_raw_oapp_uln_send_config(oappAddress, remoteEid);
console.log('Confirmations:', ulnConfig.confirmations);
console.log('Required DVNs:', ulnConfig.required_dvns);
```

### Get Current Receive Config

```typescript wrap theme={null}
const ulnConfig = await ulnContract.get_raw_oapp_uln_receive_config(oappAddress, remoteEid);
console.log('Confirmations:', ulnConfig.confirmations);
console.log('Required DVNs:', ulnConfig.required_dvns);
```

### Get Peer

```typescript wrap theme={null}
const oapp = await getOAppContract(oappAddress, provider);
const peer = await oapp.get_peer(remoteEid);
console.log('Peer:', peer.value.toString(16));
```

***

## Gas Recommendations

| Operation           | Recommended Gas | Notes                  |
| ------------------- | --------------- | ---------------------- |
| `lz_receive` (OApp) | 200,000         | Basic message handling |
| `lz_receive` (OFT)  | 200,000         | Token credit operation |

<Tip>
  Always test your specific use case on testnet to determine accurate gas requirements.
</Tip>

***

## Next Steps

* [Protocol Overview](/v2/developers/starknet/protocol-overview) - Message lifecycle
* [Technical Reference](/v2/developers/starknet/technical-reference/starknet-guidance) - Deployment guide
* [Troubleshooting](/v2/developers/starknet/troubleshooting/common-errors) - Configuration errors
