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

# LayerZero V2 OFT on Starknet

> Create and send Omnichain Fungible Tokens on Starknet with LayerZero V2. Learn OFT variants, deployment, and crosschain token transfers.

The **Omnichain Fungible Token (OFT)** standard enables crosschain token transfers on Starknet. OFTs extend the OApp pattern with built-in token handling, allowing seamless movement of fungible tokens between chains.

## What is an OFT on Starknet?

An OFT on Starknet is a Cairo contract that extends the OApp functionality to enable crosschain token transfers. It integrates with Starknet's native ERC20 system (token contracts, approvals, and metadata) while providing LayerZero's omnichain capabilities.

This guide will walk you through OFT concepts on Starknet; currently **OFTAdapter** and **OFTMintBurnAdapter** are available for deployment. To understand how OFTs integrate with Starknet's ERC20 system and the differences between mint/burn and lock/unlock token management strategies, see [Integration with Starknet ERC20 System](#integration-with-starknet-erc20-system).

## Integration with Starknet ERC20 System

### OFT Types

Starknet provides three OFT variants for different use cases:

| Variant                | Token Ownership              | Use Case                                   |
| ---------------------- | ---------------------------- | ------------------------------------------ |
| **OFT**                | OFT contract owns the token  | New tokens native to LayerZero             |
| **OFTAdapter**         | Wraps existing ERC20         | Bridge existing tokens (lock/unlock)       |
| **OFTMintBurnAdapter** | Delegates to minter contract | Existing tokens with mint/burn permissions |

<Note>
  **Availability**

  At the moment, **OFTAdapter** and **OFTMintBurnAdapter** are available on Starknet. **OFT** is not yet supported for deployment.
</Note>

#### Decision Matrix

```mermaid theme={null}
flowchart TD
    A[Do you have an existing token?] -->|No| B[Use OFT]
    A -->|Yes| C[Can you grant mint/burn to adapter?]
    C -->|Yes| D[Use OFTMintBurnAdapter]
    C -->|No| E[Use OFTAdapter]
```

***

### OFT Adapter

Use when bridging an existing token where you **cannot grant mint/burn permissions** to the adapter.

#### How It Works

* **Send**: Locks tokens in the adapter contract
* **Receive**: Unlocks tokens from the adapter contract
* **Liquidity Required**: Adapter must hold sufficient token balance

**OFTAdapter** class hash: **0x07085790a9702314791b55d7ac1e1202abf152174cc61d8fc3cab36ac4750171** ([View on explorer](https://sepolia.voyager.online/class/0x07085790a9702314791b55d7ac1e1202abf152174cc61d8fc3cab36ac4750171))

***

### OFT Mint/Burn Adapter

Use when bridging an existing token where you **can grant mint/burn permissions** to the adapter.

#### How It Works

* **Send**: Burns tokens via minter contract
* **Receive**: Mints tokens via minter contract
* **No Liquidity Required**: Mint/burn eliminates liquidity constraints

#### Additional Features

The OFTMintBurnAdapter includes:

* **Rate Limiting**: Control transfer volume per chain
* **Fee Collection**: Charge fees on transfers
* **Pausability**: Emergency pause functionality
* **Role-Based Access**: Granular permission control
* **Upgradeability**: Contract upgrade support

**OFTMintBurnAdapter** class hash: **0x07c02E3797d2c7B848FA94820FfB335617820d2c44D82d6B8Cf71c71fbE7dd6E** ([View on explorer](https://sepolia.voyager.online/class/0x07c02E3797d2c7B848FA94820FfB335617820d2c44D82d6B8Cf71c71fbE7dd6E))

#### Role Management

The OFTMintBurnAdapter uses OpenZeppelin's AccessControl with the following roles:

| Role                        | felt252 Value                 | Permissions              |
| --------------------------- | ----------------------------- | ------------------------ |
| `DEFAULT_ADMIN_ROLE`        | `0x0`                         | Grant/revoke other roles |
| `FEE_MANAGER_ROLE`          | `'FEE_MANAGER_ROLE'`          | Withdraw collected fees  |
| `PAUSE_MANAGER_ROLE`        | `'PAUSE_MANAGER_ROLE'`        | Pause/unpause contract   |
| `RATE_LIMITER_MANAGER_ROLE` | `'RATE_LIMITER_MANAGER_ROLE'` | Configure rate limits    |
| `UPGRADE_MANAGER_ROLE`      | `'UPGRADE_MANAGER_ROLE'`      | Upgrade contract         |

<Tip>
  **Role Constants**

  Roles are defined as short strings (felt252). To grant a role via sncast, use the string's felt252 encoding. For example, `'FEE_MANAGER_ROLE'` encodes to `0x4645455f4d414e414745525f524f4c45`.
</Tip>

```bash theme={null}
# Grant FEE_MANAGER_ROLE to an address
sncast --account <ACCOUNT_NAME> invoke \
  --contract-address 0x<OFT/OFT_ADAPTER> \
  --function grant_role \
  --url <RPC_URL> \
  --arguments '0x4645455f4d414e414745525f524f4c45, <RECIPIENT_ADDRESS>'
```

## Deployment

Before building an OFT, install the required dependencies.

<Tip>
  **New to Starknet?**

  If you haven't used Starknet before, start with [Getting Started on Starknet](/v2/developers/starknet/getting-started) to understand the account model, tooling, and development basics.
</Tip>

**Prerequisites**:

* Scarb and Starknet Foundry installed (see [Getting Started](/v2/developers/starknet/getting-started))
* Node.js and npm for installing LayerZero packages
* A funded Starknet account and RPC URL for deployment (see [Getting Started](/v2/developers/starknet/getting-started))

**Deployment workflow for OFTMintBurnAdapter:**

1. Deploy `ERC20MintBurnUpgradeable` as your token
2. Deploy `OFTMintBurnAdapter` with the token address as both `erc20_token` and `minter_burner`
3. Grant the adapter's address permission to mint/burn on the token contract

### Step 1: Deploy ERC20MintBurnUpgradeable

For OFTMintBurnAdapter deployments, LayerZero provides a reference ERC20 token implementation with built-in mint/burn permissions:

This contract:

* Implements the `IMintableToken` interface
* Supports role-based access for mint/burn permissions
* Is upgradeable via OpenZeppelin's `UpgradeableComponent`

The **ERC20MintBurnUpgradeable** class has been declared and has been verified - [view on explorer](https://sepolia.voyager.online/class/0x01bea3900ebe975f332083d441cac55f807cf5de7b1aa0b7ccbda1de53268500).

```bash theme={null}
# Deploy
sncast --account <ACCOUNT_NAME> deploy \
  --class-hash 0x01bea3900ebe975f332083d441cac55f807cf5de7b1aa0b7ccbda1de53268500 \
  --url <RPC_URL> \
  --arguments '"MyToken", "MTK", 18, <DEFAULT_ADMIN_ADDRESS>'
```

Constructor parameters:

* `name` (ByteArray) - Token name (use quoted string)
* `symbol` (ByteArray) - Token symbol (use quoted string)
* `decimals` (u8) - Token decimals (e.g., 18)
* `default_admin` (ContractAddress) - Address granted `DEFAULT_ADMIN_ROLE`. You can set this to your address.

Running the above successfully would return an output like:

```
Success: Deployment completed

Contract Address: <CONTRACT_ADDRESS>
Transaction Hash: <TXN_HASH>

To see deployment details, visit:
contract: https://sepolia.starkscan.co/contract/<CONTRACT_ADDRESS>
transaction: https://sepolia.starkscan.co/tx/<TXN_HASH>
```

Copy the Contract Address and set it aside for use in the next step.

### Step 2: Deploy OFTMintBurnAdapter

```bash theme={null}
sncast --account <ACCOUNT_NAME> deploy \
  --class-hash 0x07c02E3797d2c7B848FA94820FfB335617820d2c44D82d6B8Cf71c71fbE7dd6E \
  --url <RPC_URL> \
  --arguments '<ERC20_TOKEN_ADDRESS>, <MINTER_BURNER_ADDRESS>, 0x0316d70a6e0445a58c486215fac8ead48d3db985acde27efca9130da4c675878, <OWNER_ADDRESS>, 0x04718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d, <SHARED_DECIMALS>'
```

Constructor parameters:

* `erc20_token`: ERC20 token contract address
* `minter_burner`: Minter/burner contract address (use the token address)
* `lz_endpoint`: LayerZero Endpoint address (`0x0316d70a6e0445a58c486215fac8ead48d3db985acde27efca9130da4c675878` for Sepolia, `0x524e065abff21d225fb7b28f26ec2f48314ace6094bc085f0a7cf1dc2660f68` for Mainnet)
* `owner`: Contract owner address (your deployer account)
* `native_token`: Fee payment token (STRK token address shown above)
* `shared_decimals`: Shared decimals across chains (u8, e.g., 6)

<Info>
  **STRK Token Address**

  The address `0x04718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d` is the STRK token contract on Starknet (same on both Sepolia testnet and Mainnet). This is used to pay LayerZero messaging fees.
</Info>

<Tip>
  **Network addresses**

  For endpoint IDs and LayerZero contract addresses, see [Deployed Contracts](/v2/deployments/deployed-contracts).
</Tip>

***

## Deployment for Custom OFT

If you need to build a custom OFT contract (e.g., with additional logic or modifications), follow these steps to set up your project before declaring and deploying. You can use the OFTMintBurnAdapter as a starting point.

### Step 1: Install LayerZero Cairo Contracts

The LayerZero Cairo packages are currently published on NPM.

```bash theme={null}
# Create your project directory
mkdir my-oft-project && cd my-oft-project

# Initialize npm and install LayerZero Starknet packages
npm init -y
```

<Tabs>
  <Tab title="OFTMintBurnAdapter (Recommended)">
    ```bash theme={null}
    npm install @layerzerolabs/protocol-starknet-v2 @layerzerolabs/oft-mint-burn-starknet
    ```
  </Tab>

  <Tab title="OFTAdapter">
    ```bash theme={null}
    npm install @layerzerolabs/protocol-starknet-v2 @layerzerolabs/oft-adapter-starknet
    ```
  </Tab>
</Tabs>

### Step 2: Copy the contracts

Copy the contracts into your project's directory:

<Tabs>
  <Tab title="OFTMintBurnAdapter (Recommended)">
    ```bash theme={null}
    cp -R node_modules/@layerzerolabs/oft-mint-burn-starknet/contracts/oft_mint_burn/. .
    ```
  </Tab>

  <Tab title="OFTAdapter">
    ```bash theme={null}
    cp -R node_modules/@layerzerolabs/oft-adapter-starknet/contracts/oft_adapter/. .
    ```
  </Tab>
</Tabs>

### Step 3: Modify dependency paths

In the `Scarb.toml`, remove the parent directory references in the `path` fields:

```
- lz_utils = { path = "../../node_modules/@layerzerolabs/protocol-starknet-v2/libs/lz_utils" }
- layerzero = { path = "../../node_modules/@layerzerolabs/protocol-starknet-v2/layerzero" }
+ lz_utils = { path = "node_modules/@layerzerolabs/protocol-starknet-v2/libs/lz_utils" }
+ layerzero = { path = "node_modules/@layerzerolabs/protocol-starknet-v2/layerzero" }
```

### Step 4: Make your customizations

Modify the contract as necessary.

### Step 5: Build, Declare, and Deploy

```bash theme={null}
# Build the contract
scarb build

# Declare the contract class
sncast declare --contract-name MyCustomOFT

# Deploy with your constructor arguments
sncast deploy \
  --class-hash <YOUR_CLASS_HASH> \
  --arguments '<CONSTRUCTOR_ARGS>'
```

***

## Core Operations

### Sending Tokens

#### Step 1: Quote

```rust wrap theme={null}
// Get fee and receipt estimates
let send_param = SendParam {
    dst_eid: 30101,  // Ethereum Mainnet
    to: recipient_bytes32,
    amount_ld: 1000000000000000000_u256,  // 1 token
    min_amount_ld: 900000000000000000_u256,  // 0.9 token minimum
    extra_options: build_options(200000),
};

let quote = oft.quote_oft(send_param);
// quote.receipt.amount_sent_ld = actual amount debited
// quote.receipt.amount_received_ld = amount received on destination
```

#### Step 2: Get Messaging Fee

```rust wrap theme={null}
let messaging_fee = oft.quote_send(send_param, false);
// messaging_fee.native_fee = STRK/ETH needed for LayerZero
```

#### Step 3: Send

```rust wrap theme={null}
// Approve tokens if using OFTAdapter
if oft.approval_required() {
    token.approve(oft_address, send_param.amount_ld);
}

// Approve native token for messaging fee
native_token.approve(oft_address, messaging_fee.native_fee);

// Send tokens
let result = oft.send(send_param, messaging_fee, refund_address);
// result.message_receipt.guid = unique message ID
// result.oft_receipt = actual amounts sent/received
```

***

## Decimal Precision

### Token Amounts and u256

All token amounts in Starknet OFT contracts use **`u256`**, matching Solidity's `uint256` and OpenZeppelin's Cairo ERC20 interface for crosschain compatibility.

```rust wrap theme={null}
// OFT amounts are always u256
let amount_ld: u256 = 1000000000000000000_u256;  // 1 token (18 decimals)
let min_amount_ld: u256 = 900000000000000000_u256;  // 0.9 token minimum
```

### Local vs Shared Decimals

OFTs use two decimal representations:

| Type                | Description                       | Typical Value |
| ------------------- | --------------------------------- | ------------- |
| **Local Decimals**  | Token decimals on this chain      | 18            |
| **Shared Decimals** | Common decimals across all chains | 6             |

```rust wrap theme={null}
// Shared decimals = 6 means max precision of 6 decimal places
// A token with 18 local decimals has conversion rate of 10^12

const SHARED_DECIMALS: u8 = 6;
let local_decimals: u8 = 18;
let conversion_rate = 10_u256.pow((local_decimals - SHARED_DECIMALS).into()); // 10^12
```

### Dust Removal

When converting from local to shared decimals, precision is lost ("dust"):

```rust wrap theme={null}
// Sending 1.123456789012345678 tokens (18 decimals)
// Shared representation: 1.123456 (6 decimals)
// Dust lost: 0.000000789012345678

fn _remove_dust(self: @ComponentState, amount_ld: u256) -> u256 {
    let conversion_rate = self.OFTCore_decimal_conversion_rate.read();
    (amount_ld / conversion_rate) * conversion_rate
}
```

<Tip>
  Always use `quote_oft` before sending to see the exact amounts after dust removal and fees.
</Tip>

***

## Configuration

After deployment, configure your OFT to enable crosschain transfers. Use the SDK for DVN/executor config and set peers last.

<Warning>
  **Critical order**

  Configure security settings before setting peers. Setting peers opens the pathway.
</Warning>

<Info>
  **Endpoint IDs**

  For endpoint IDs and LayerZero contract addresses, see [Deployed Contracts](/v2/deployments/deployed-contracts).
</Info>

### SDK Setup

Install the SDK dependencies:

```bash theme={null}
npm install starknet @layerzerolabs/lz-v2-protocol-starknet @layerzerolabs/lz-v2-utilities @layerzerolabs/lz-definitions
npm install -D tsx
```

Create `config.ts` (or equivalent) and load your compiled artifact from `target/release/*.contract_class.json`:

```typescript wrap theme={null}
import {readFileSync} from 'node:fs';
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';

async function main() {
  const RPC_URL = process.env.RPC_URL!;
  const ACCOUNT_ADDRESS = process.env.ACCOUNT_ADDRESS!;
  const PRIVATE_KEY = process.env.PRIVATE_KEY!;
  const OFT_ADDRESS = process.env.OFT_ADDRESS!;
  const OFT_ARTIFACT_PATH = process.env.OFT_ARTIFACT_PATH!;

  const compiledArtifact = JSON.parse(readFileSync(OFT_ARTIFACT_PATH, 'utf8'));
  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;

  // Configuration code goes here...
}

main().catch(console.error);
```

Run the script:

```bash theme={null}
RPC_URL=... \
ACCOUNT_ADDRESS=0x... \
PRIVATE_KEY=0x... \
OFT_ADDRESS=0x... \
OFT_ARTIFACT_PATH=./target/release/my_oft_OFT.contract_class.json \
npx tsx config.ts
```

### Prerequisite: Set Delegate (required if configuring via external account)

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 = oapp.populateTransaction.set_delegate(DELEGATE_ADDRESS);
await account.execute([setDelegateCall]);
```

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

### Step 1: Set Message Libraries (optional)

Use custom send/receive libraries when defaults are unavailable for your EID.

```typescript wrap theme={null}
const setSendLibCall = endpoint.populateTransaction.set_send_library(
  OFT_ADDRESS,
  remoteEid,
  SEND_LIB_ADDRESS,
);
const setReceiveLibCall = endpoint.populateTransaction.set_receive_library(
  OFT_ADDRESS,
  remoteEid,
  RECEIVE_LIB_ADDRESS,
  0, // Grace period in blocks
);
await account.execute([setSendLibCall, setReceiveLibCall]);

const sendLibAddress = (await endpoint.get_send_library(OFT_ADDRESS, remoteEid)).lib;
const receiveLibAddress = (await endpoint.get_receive_library(OFT_ADDRESS, remoteEid)).lib;
```

### Step 2: Configure DVNs (recommended)

Configure ULN settings for both send and receive. DVN addresses must be sorted ascending.

```typescript wrap theme={null}
const sendUlnConfig = 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,
});

const setSendConfigCall = endpoint.populateTransaction.set_send_configs(
  OFT_ADDRESS,
  sendLibAddress,
  [
    {
      eid: remoteEid,
      config_type: MessageLibConfigType.ULN, // 2
      config: sendUlnConfig,
    },
  ],
);
await account.execute([setSendConfigCall]);

const receiveUlnConfig = 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,
});

const setReceiveConfigCall = endpoint.populateTransaction.set_receive_configs(
  OFT_ADDRESS,
  receiveLibAddress,
  [
    {
      eid: remoteEid,
      config_type: MessageLibConfigType.ULN, // 2
      config: receiveUlnConfig,
    },
  ],
);
await account.execute([setReceiveConfigCall]);
```

### Step 3: Configure Executor (recommended)

Executor settings apply to send direction.

```typescript wrap theme={null}
const executorConfig = encodeExecutorConfig({
  max_message_size: 10000,
  executor: EXECUTOR_ADDRESS,
});

const setExecutorConfigCall = endpoint.populateTransaction.set_send_configs(
  OFT_ADDRESS,
  sendLibAddress,
  [
    {
      eid: remoteEid,
      config_type: MessageLibConfigType.EXECUTOR, // 1
      config: executorConfig,
    },
  ],
);
await account.execute([setExecutorConfigCall]);
```

### Step 4: Set Enforced Options (optional)

All OFT variants include the `OAppOptionsType3Component` for managing execution options. Use it to set minimum gas requirements per destination chain. If `getOAppContract` does not expose `set_enforced_options`, load your compiled artifact from `target/dev/*.contract_class.json` as shown in the SDK setup.

```typescript wrap theme={null}
const options = Options.newOptions()
  .addExecutorLzReceiveOption(200000, 0) // 200k gas for lz_receive
  .toBytes();

const setOptionsCall = oappOptions.populateTransaction.set_enforced_options([
  {
    eid: remoteEid,
    msg_type: 1, // SEND
    options,
  },
]);
await account.execute([setOptionsCall]);
```

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

```bash theme={null}
# Set enforced options for SEND message type (type 1)
# Options format: 0x0003 (type 3 header) + executor options
sncast --account <ACCOUNT_NAME> invoke \
  --contract-address <OFT_ADDRESS> \
  --function set_enforced_options \
  --url <RPC_URL> \
  --arguments 'array![layerzero::oapps::common::oapp_options_type_3::structs::EnforcedOptionParam { eid: <DST_EID>, msg_type: 1, options: <OPTIONS_BYTEARRAY> }]'
```

For `<OPTIONS_BYTEARRAY>`, pass a ByteArray expression (see the Starknet Foundry calldata transformation docs). With `--arguments`, use a raw ByteArray struct literal for arbitrary bytes, e.g. `core::byte_array::ByteArray { data: array![0x..., 0x...], pending_word: 0x..., pending_word_len: 0 }` (data are 31-byte chunks; pending\_word\_len is 0-30).

Example: lzReceive gas = 120000, value = 0:

```bash theme={null}
sncast --account <ACCOUNT_NAME> invoke \
  --contract-address <OFT_ADDRESS> \
  --function set_enforced_options \
  --url <RPC_URL> \
  --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_u8 } }]'
```

### Step 5: Set Peer (required, last)

Set the remote peer after security configuration. EVM addresses must be left-padded to 32 bytes.

```typescript wrap theme={null}
const peerBytes32 = {value: BigInt('0x000000000000000000000000' + EVM_OFT_ADDRESS.slice(2))};
const setPeerCall = oapp.populateTransaction.set_peer(remoteEid, peerBytes32);
await account.execute([setPeerCall]);
```

**Configuration order**:

1. Set delegate (required if configuring via external account)
2. Set message libraries (optional)
3. Configure DVNs (recommended)
4. Configure executor (recommended)
5. Set enforced options (optional)
6. Set peer (required, last)

See the [Configuration Guide](/v2/developers/starknet/configuration/dvn-executor-config) for detailed options encoding, DVN ordering, and gas recommendations.

***

## Events

### OFT-Specific Events

```rust wrap theme={null}
// Tokens sent to another chain
#[derive(Drop, starknet::Event)]
pub struct OFTSent {
    #[key]
    pub guid: Bytes32,
    pub dst_eid: u32,
    pub from: ContractAddress,
    pub amount_sent_ld: u256,
    pub amount_received_ld: u256,
}

// Tokens received from another chain
#[derive(Drop, starknet::Event)]
pub struct OFTReceived {
    #[key]
    pub guid: Bytes32,
    pub src_eid: u32,
    pub to: ContractAddress,
    pub amount_received_ld: u256,
}
```

***

## Best Practices & Deployment Checklist

1. **Install dependencies** - npm install LayerZero packages
2. **Choose OFT variant** based on your token situation
3. **Build contract** via `scarb build`
4. **Declare contract** via `sncast declare`
5. **Deploy contract** via `sncast deploy` with `--arguments`
6. **Verify contract** via `sncast verify` using Voyager or Walnut
7. **Configure DVNs and executor** for security (see [Configuration Guide](/v2/developers/starknet/configuration/dvn-executor-config))
8. **Set enforced options** for minimum gas
9. **Set peers last** on both chains (bidirectional)
10. **Test on testnet** before mainnet deployment

***

## Next Steps

* [Configuration Guide](/v2/developers/starknet/configuration/dvn-executor-config) - DVN and security setup
* [Protocol Overview](/v2/developers/starknet/protocol-overview) - Message lifecycle
* [Technical Reference](/v2/developers/starknet/technical-reference/starknet-guidance) - Deployment tooling
* [Troubleshooting](/v2/developers/starknet/troubleshooting/common-errors) - Common errors
