Skip to main content
Version: Endpoint V2

Getting Started with LayerZero V2 on Starknet

Any data, whether it's a fungible token transfer, an NFT, or some other smart contract input can be encoded on-chain as bytes and delivered to a destination chain to trigger some action using LayerZero.

Because of this, any blockchain that broadly supports state propagation and events can be connected to LayerZero, including Starknet.

tip

If you're new to LayerZero, we recommend reviewing "What is LayerZero?" before continuing.


LayerZero provides Starknet Cairo Contracts that can communicate with the equivalent Solidity Contract Libraries and Solana Programs deployed on other chains.

These contracts, like their Solidity and Rust counterparts, simplify calling the LayerZero Endpoint, provide message handling, interfaces for protocol configurations, and other utilities for interoperability:

  • Omnichain Fungible Token (OFT): extends OApp with functionality for handling omnichain token transfers using Starknet's ERC20 standard.

  • Omnichain Application (OApp): the base contract utilities for omnichain messaging and configuration.

Each of these contract standards implements common functions for sending and receiving omnichain messages.

Differences from the Ethereum Virtual Machine

The full differences between Solidity/EVM and Cairo/Starknet are significant. For comprehensive guides, see:

info

Skip this section if you already feel comfortable working with Starknet and its account abstraction model.

Comparison Table

AspectEVMStarknet
State ModelStorage slotsFelt-based contract storage
LanguageSolidityCairo
Authorizationmsg.senderget_caller_address()
Cross-ContractExternal callsDispatcher pattern
OApp IdentityContract addressContract address
Balance Typeuint256u256 (struct: two u128)
Fee ModelGas (ETH)Resource bounds (STRK/ETH)
Account ModelEOA + ContractsAccount Abstraction (all accounts are contracts)
DeploymentSingle transactionDeclare + Deploy (two steps)

Account Abstraction (No EOAs)

The most fundamental difference is Starknet's native account abstraction:

EVM:

// EOA (Externally Owned Account) signs and sends transactions directly
// msg.sender is the account that signed the transaction
function transfer() external {
require(msg.sender == owner, "not owner");
}

Starknet:

// All accounts are smart contracts
// Signatures are produced off-chain and validated by the account contract
// get_caller_address() returns the account contract address
fn transfer(ref self: ContractState) {
let caller = get_caller_address();
assert(caller == self.owner.read(), 'not owner');
}

Key Implications:

  • Before deploying any contract, you must have a deployed and funded account contract (wallet)
  • Signatures are produced off-chain by the account owner and validated by the account contract
  • Account contracts handle fee payment and transaction execution for that account
  • Common account implementations include Ready Wallet (formerly Argent), Braavos, and OpenZeppelin Account

Declare Then Deploy Lifecycle

Unlike EVM where you deploy bytecode in a single transaction, Starknet separates code publication from instantiation:

Loading diagram...

Step 1: Declare - Publish your contract code to the network:

sncast declare --contract-name MyOFT
# Returns: class_hash = 0x123abc...

Step 2: Deploy - Create an instance of the declared class:

sncast deploy --class-hash 0x123abc... --network sepolia --arguments '<args>'
# Returns: contract_address = 0x456def...

Key Concepts:

  • class_hash: Unique identifier for your contract's code (like a "template")
  • contract_address: Specific instance of that code with its own state
  • Multiple contracts can share the same class_hash (reusable code)

Dispatcher Pattern vs Inheritance

Starknet uses the dispatcher pattern instead of Solidity's inheritance model.

EVM uses inheritance for contract composition:

// Solidity: Inherit and override
contract MyOFT is OFT {
constructor() OFT("Token", "TKN", endpoint, owner) {}
}

Starknet uses components and dispatchers:

// Cairo: Compose with components
#[starknet::contract]
mod MyOFT {
// Import components
use layerzero::oapps::oapp::oapp_core::OAppCoreComponent;
use openzeppelin::token::erc20::ERC20Component;

// Declare components
component!(path: OAppCoreComponent, storage: oapp_core, event: OAppCoreEvent);
component!(path: ERC20Component, storage: erc20, event: ERC20Event);

// Embed implementations
#[abi(embed_v0)]
impl OAppCoreImpl = OAppCoreComponent::OAppCoreImpl<ContractState>;
}

Cross-contract calls use dispatchers:

// Cairo: Typed dispatcher for cross-contract calls
use layerzero::endpoint::interfaces::endpoint_v2::{IEndpointV2Dispatcher, IEndpointV2DispatcherTrait};

fn call_endpoint(endpoint_address: ContractAddress) {
let endpoint = IEndpointV2Dispatcher { contract_address: endpoint_address };
let fee = endpoint.quote(params, sender); // Type-safe call
}

Constructor Caller Footgun

warning

When deploying via the Universal Deployer Contract (UDC), get_caller_address() in the constructor returns the UDC address, not your account address!

Problem:

#[constructor]
fn constructor(ref self: ContractState, endpoint: ContractAddress) {
// BUG: If deployed via UDC, this sets UDC as owner!
let caller = get_caller_address();
self.ownable.initializer(caller);
}

Solution - Always pass the owner explicitly:

#[constructor]
fn constructor(
ref self: ContractState,
endpoint: ContractAddress,
owner: ContractAddress, // Explicitly pass the intended owner
) {
self.ownable.initializer(owner);
}

Cairo Integer Types

Cairo 1.0 provides native unsigned integer types. For token amounts (ERC20 balances, transfers, allowances), always use u256—matching Solidity's uint256 for cross-chain compatibility.

TypeSizeUse Case
u6464-bitTimestamps, small counters
u128128-bitMedium-sized values
u256256-bitToken amounts (ERC20 balances, transfers)

OpenZeppelin's Cairo ERC20 interface uses u256 for balance_of, total_supply, allowance, and all transfer/approve amounts.

// u256 for token amounts (matches Solidity uint256)
let amount: u256 = 1000000000000000000_u256; // 1 token (18 decimals)

fn balance_of(self: @ContractState, account: ContractAddress) -> u256 {
self.erc20.balance_of(account)
}
note

You'll also encounter felt252 in Cairo—it's Starknet's base field element type used internally (e.g., ContractAddress wraps a felt252). However, don't use it for token amounts; its modular arithmetic can cause unexpected behavior.

Cross-chain encoding: LayerZero messages encode addresses as Bytes32 for compatibility across chains with different address sizes.

Resource Bounds (Gas Model)

Starknet uses resource bounds instead of simple gas limits:

// INVOKE v3 transactions include resource bounds:
// - l1_gas: Max L1 gas willing to pay
// - l2_gas: Max L2 gas (compute) willing to pay
// - l1_data_gas: Max L1 data availability gas

Common error: "Insufficient max fee" - increase your resource bounds (or fee cap in tooling):

sncast deploy --class-hash 0x...

Prerequisites

Before you start building, you'll need to set up your development environment.

1. Deploy and Fund an Account Contract

Unlike EVM, you need an account contract before you can deploy other contracts:

Create a new account (generates keys and computes address)
sncast account create \
--name my_account \
--type oz \
--url <YOUR_SEPOLIA_RPC_URL>
Fund the computed address with STRK/ETH before deploying.

The account create command outputs the computed address. Fund this address using a faucet before running account deploy.

Deploy the account contract
sncast account deploy \
--name my_account \
--url <YOUR_SEPOLIA_RPC_URL>

2. Account File Location

Accounts are stored in ~/.starknet_accounts/starknet_open_zeppelin_accounts.json:

{
"alpha-sepolia": {
"my_account": {
"address": "0x...",
"deployed": true,
"legacy": false,
"private_key": "0x...",
"public_key": "0x...",
"salt": "0x..."
}
}
}

3. Install Scarb (Cairo Build Tool)

Scarb is the official Cairo package manager and build tool:

# Install via asdf
asdf plugin add scarb
asdf install scarb 2.14.0
asdf set scarb 2.14.0

Verify installation:

scarb --version
# scarb 2.14.0 or later

4. Install Starknet Foundry (sncast + snforge)

Starknet Foundry provides sncast (deployment) and snforge (testing):

# Install via asdf
asdf plugin add starknet-foundry
asdf install starknet-foundry 0.53.0
asdf set starknet-foundry 0.53.0

Verify installation:

sncast --version
snforge --version
RPC Version Compatibility

sncast requires a compatible RPC version:

  • Starknet Foundry 0.53.0+ expects RPC v0.9.0 or v0.10.0
  • Starknet Foundry 0.49.0 expects RPC v0.9.0

If you see RPC node uses incompatible version warnings, update your RPC URL to use a compatible version:

# For v0.9.0 (Alchemy)
https://starknet-sepolia.g.alchemy.com/starknet/version/rpc/v0_9/<key>

# For v0.10.0 (Alchemy)
https://starknet-sepolia.g.alchemy.com/starknet/version/rpc/v0_10/<key>

5. Configure snfoundry.toml

Create a snfoundry.toml in your project root to configure sncast defaults:

[sncast.default]
account = "my_account"
url = "https://starknet-sepolia.g.alchemy.com/starknet/version/rpc/v0_9/<YOUR_API_KEY>"
wait-params = { timeout = 300, retry-interval = 10 }
block-explorer = "StarkScan"
show-explorer-links = true
FieldDescription
accountAccount name from your accounts file
urlRPC endpoint URL (must match sncast version; see RPC note above)
wait-paramsTransaction wait timeout and retry settings
block-explorerExplorer for transaction links (StarkScan, Blockchain, Voyager)
show-explorer-linksShow explorer links after transactions

Use sncast account list to see available account names.

6. Install Node.js

For any TypeScript tooling or SDK usage:

# Using nvm (recommended)
nvm install 20
nvm use 20

# Verify
node --version
# v20.x.x

7. Get Testnet STRK/ETH

For testing on Starknet Sepolia testnet:

Project Structure

A typical LayerZero Starknet project structure:

my-oft-project/
├── Scarb.toml # Package manifest (dependencies)
├── snfoundry.toml # Starknet Foundry config
├── src/
│ ├── lib.cairo # Module declarations
│ └── my_oft.cairo # Your OFT contract
└── tests/
└── test_my_oft.cairo # Contract tests

Example Scarb.toml:

[package]
name = "my_oft"
version = "0.1.0"
edition = "2024_07"

[dependencies]
starknet = "2.14.0"
openzeppelin = "2.0.0"
lz_utils = { path = "./node_modules/@layerzerolabs/protocol-starknet-v2/libs/lz_utils" }
layerzero = { path = "./node_modules/@layerzerolabs/protocol-starknet-v2/layerzero" }

[dev-dependencies]
snforge_std = "0.53.0"

[[target.starknet-contract]]
sierra = true
casm = true
Installing LayerZero Packages

Before building, install the LayerZero Cairo contracts:

npm init -y
npm install @layerzerolabs/protocol-starknet-v2

Network Configuration

NetworkEndpoint IDChain ID
Starknet Mainnet30112SN_MAIN
Starknet Sepolia40112SN_SEPOLIA

RPC Endpoints:

  • Mainnet: <YOUR_MAINNET_RPC_URL>
  • Sepolia: <YOUR_SEPOLIA_RPC_URL>
RPC providers

Blast public endpoints are deprecated; use Alchemy, Infura, or another provider that supports Starknet JSON-RPC v0.9+.

Next Steps

Choose your path:

Build an OApp

For custom cross-chain logic:

Build an OFT

For cross-chain tokens:

Understand the Protocol

For protocol-level understanding:

Get Help