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.
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:
Skip this section if you already feel comfortable working with Starknet and its account abstraction model.
Comparison Table
| Aspect | EVM | Starknet |
|---|---|---|
| State Model | Storage slots | Felt-based contract storage |
| Language | Solidity | Cairo |
| Authorization | msg.sender | get_caller_address() |
| Cross-Contract | External calls | Dispatcher pattern |
| OApp Identity | Contract address | Contract address |
| Balance Type | uint256 | u256 (struct: two u128) |
| Fee Model | Gas (ETH) | Resource bounds (STRK/ETH) |
| Account Model | EOA + Contracts | Account Abstraction (all accounts are contracts) |
| Deployment | Single transaction | Declare + 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:
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
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.
| Type | Size | Use Case |
|---|---|---|
u64 | 64-bit | Timestamps, small counters |
u128 | 128-bit | Medium-sized values |
u256 | 256-bit | Token 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)
}
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
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
| Field | Description |
|---|---|
account | Account name from your accounts file |
url | RPC endpoint URL (must match sncast version; see RPC note above) |
wait-params | Transaction wait timeout and retry settings |
block-explorer | Explorer for transaction links (StarkScan, Blockchain, Voyager) |
show-explorer-links | Show 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:
- Starknet Faucet - Get testnet STRK
- Starkgate Bridge - Bridge ETH from Ethereum Sepolia
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
Before building, install the LayerZero Cairo contracts:
npm init -y
npm install @layerzerolabs/protocol-starknet-v2
Network Configuration
| Network | Endpoint ID | Chain ID |
|---|---|---|
| Starknet Mainnet | 30112 | SN_MAIN |
| Starknet Sepolia | 40112 | SN_SEPOLIA |
RPC Endpoints:
- Mainnet:
<YOUR_MAINNET_RPC_URL> - Sepolia:
<YOUR_SEPOLIA_RPC_URL>
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:
- OApp Overview - Architecture and patterns
- Protocol Overview - Deep technical dive
- Technical Overview - Starknet fundamentals
Build an OFT
For cross-chain tokens:
- OFT Overview - Token architecture
- Configuration Guide - Security and DVN setup
Understand the Protocol
For protocol-level understanding:
- Technical Overview - Cairo architecture and patterns
- Protocol Overview - Complete message workflows
Get Help
- Troubleshooting - Common issues
- FAQ - Frequently asked questions
- Discord - Community support