Starknet FAQ
Frequently asked questions about developing LayerZero applications on Starknet.
General
Why does Starknet use account contracts instead of EOAs?
Starknet implements native account abstraction, meaning all accounts are smart contracts. This provides:
- Flexible signature validation: Support for different signature schemes
- Custom transaction logic: Batching, session keys, social recovery
- Gas abstraction: Pay fees in different tokens
- Enhanced security: Multi-sig, spending limits, etc.
Before you can deploy any contract, you must first deploy and fund an account contract (for example, Ready Wallet, formerly Argent; Braavos; or OpenZeppelin Account).
What's the difference between class_hash and contract_address?
| Concept | Description | Analogy |
|---|---|---|
| class_hash | Unique identifier for contract code | Like a class/template |
| contract_address | Unique identifier for contract instance | Like an object/instance |
class_hash = 0x123... (the "OFT" code template)
│
├── contract_address = 0xaaa... (OFT instance for Token A)
├── contract_address = 0xbbb... (OFT instance for Token B)
└── contract_address = 0xccc... (OFT instance for Token C)
Multiple contracts can share the same class_hash but have different addresses and state.
Do I need to deploy my own Endpoint?
No. LayerZero deploys and maintains the Endpoint contract on each supported chain. You only need to:
- Deploy your OApp/OFT contract
- Reference the existing Endpoint address in your constructor
- Configure your security settings (DVNs, peers, etc.)
Check LayerZero Deployments for the official Endpoint address.
What's the difference between STRK and ETH on Starknet?
Starknet supports two fee tokens:
| Token | Purpose | Notes |
|---|---|---|
| STRK | Native Starknet token | Primary fee token on Starknet |
| ETH | Bridged Ethereum | Still supported as a fee token |
LayerZero messaging fees are paid in the native token configured by your OApp (usually STRK on Starknet).
Development
How do I encode addresses for cross-chain messages?
LayerZero uses Bytes32 for addresses to support different address sizes across VMs:
// Starknet address (felt252) → Bytes32
let starknet_addr: ContractAddress = ...;
let as_bytes32: Bytes32 = starknet_addr.into();
// EVM address (20 bytes) → Bytes32 (left-padded with zeros)
// 0xABCDEF... becomes 0x000000000000000000000000ABCDEF...
let evm_peer = Bytes32 {
value: 0x000000000000000000000000_<20_BYTE_EVM_ADDRESS>
};
How do I batch multiple configuration calls?
Multicall is implemented at the account-contract level: a single INVOKE can execute multiple calls atomically when the account supports it. Most major account implementations (Ready Wallet, formerly Argent; Braavos; OpenZeppelin Account) expose multicall by default. If an account contract does not implement multicall, batching is not available for that account.
// Using starknet.js
const calls = [
{ contractAddress: oft, entrypoint: 'set_enforced_options', calldata: [...] },
{ contractAddress: oft, entrypoint: 'set_dvn_config', calldata: [...] },
{ contractAddress: oft, entrypoint: 'set_peer', calldata: [...] }, // Last!
];
await account.execute(calls); // All execute atomically
This is useful for configuring all settings in one transaction, ensuring the pathway isn't opened until everything is ready.
What is the shared decimals limit?
OFTs use shared decimals (default: 6) to maintain consistency across chains with different token decimal precision:
| Local Decimals | Shared Decimals | Conversion Rate | Max Precision |
|---|---|---|---|
| 18 | 6 | 10^12 | 0.000001 |
| 8 | 6 | 10^2 | 0.000001 |
| 6 | 6 | 1 | 0.000001 |
Implications:
- Amounts smaller than the conversion rate become "dust" and are removed
- Always use
quote_oftto see exact received amounts before sending
Why is my constructor setting the wrong owner?
When deploying via the Universal Deployer Contract (UDC), get_caller_address() returns the UDC address, not your account.
Wrong:
#[constructor]
fn constructor(ref self: ContractState, endpoint: ContractAddress) {
self.ownable.initializer(get_caller_address()); // Returns UDC!
}
Correct:
#[constructor]
fn constructor(
ref self: ContractState,
endpoint: ContractAddress,
owner: ContractAddress, // Pass explicitly
) {
self.ownable.initializer(owner);
}
How do I check my OApp configuration?
Query configuration via the Endpoint or your OApp:
// Check peer
let peer = oapp.get_peer(eid);
// Check delegate
let delegate = endpoint.get_delegate(oapp_address);
// Check library
let send_lib = endpoint.get_send_library(oapp_address, dst_eid);
// Check enforced options
let options = oapp.get_enforced_options(dst_eid, msg_type);
Or use block explorers like Voyager or Starkscan.
Deployment
What tooling do I use to deploy Starknet contracts?
Use Starknet Foundry (sncast):
# Declare (publish code)
sncast declare --contract-name MyOFT
# Deploy (create instance)
sncast deploy --class-hash 0x... --arguments '...'
Can I use Hardhat or Foundry (EVM) for Starknet?
No. Starknet uses Cairo, not Solidity. You need Starknet-specific tools:
| EVM Tool | Starknet Equivalent |
|---|---|
| Hardhat | Scarb + sncast |
| Foundry | Starknet Foundry (sncast + snforge) |
| Remix | N/A (use Scarb locally) |
How do I verify my contract?
Use sncast verify or block explorers:
Using sncast (recommended):
sncast verify \
--class-hash <CLASS_HASH> \
--contract-name MyOFT \
--verifier voyager \
--network sepolia
Using block explorers:
- Voyager: Go to contract → "Verify & Publish"
- Starkscan: Go to contract → "Verify Contract"
Upload your source files or provide a GitHub link.
Cross-Chain
How long do cross-chain transfers take?
Transfer time depends on:
- Source chain finality: Time for DVNs to verify
- DVN verification: Usually 1-5 minutes after finality
- Executor delivery: Near-instant after verification
Typical Starknet → EVM: 10-30 minutes
Typical EVM → Starknet: 5-15 minutes
Track your transfer on LayerZero Scan.
Why did my cross-chain message fail?
Common reasons:
- Insufficient gas: Increase enforced options
- Peer not set: Configure bidirectional peers
- Contract paused: Unpause the destination contract
- Rate limit exceeded: Check OFTMintBurnAdapter limits
- Application error: Bug in
_lz_receivelogic
Check LayerZero Scan for the LzReceiveAlert event details.
Can I retry a failed message?
If a message failed execution (not verification), you can:
- Fix the issue on the destination contract
- Use recovery operations if needed:
clear: Clear a blocking messagenilify: Reset verificationskip: Skip unverified message
See Protocol Overview - Recovery Operations.
Security
What are the default security settings?
If you don't configure custom settings:
| Setting | Default |
|---|---|
| DVN | LayerZero Labs DVN |
| Executor | LayerZero Labs Executor |
| Send Library | ULN302 |
| Receive Library | ULN302 |
Should I use multiple DVNs?
Recommended for production. Multiple DVNs provide:
- Increased security (multiple independent verifiers)
- Resilience (no single point of failure)
- Trust minimization
Example dual DVN setup using the TypeScript SDK:
import {encodeUlnConfig} from '@layerzerolabs/lz-v2-protocol-starknet';
const config = encodeUlnConfig({
confirmations: 15,
has_confirmations: true,
required_dvns: [LAYERZERO_DVN, PARTNER_DVN], // Sorted ascending
has_required_dvns: true,
optional_dvns: [],
optional_dvn_threshold: 0,
has_optional_dvns: false,
});
What's the difference between owner and delegate?
| Role | Permissions | Use Case |
|---|---|---|
| Owner | Full control (peers, ownership) | Long-term custody |
| Delegate | Configuration only (DVNs, options) | Operational management |
Delegates can manage day-to-day configuration without having power to set peers or transfer ownership.
Costs
What fees are involved in cross-chain transfers?
- Source chain gas: Pay for the
sendtransaction - LayerZero fees: DVN and Executor fees (quoted via
quote_send) - Destination gas: Paid by Executor, funded by step 2
Use quote_send to get the total LayerZero fee before sending.
Why are my fees higher than expected?
Fee factors:
- Options: Higher gas limits = higher fees
- Message size: Larger payloads cost more
- Destination chain: Different chains have different costs
- DVN count: More DVNs = higher verification costs
Optimize by:
- Using appropriate gas limits (not excessive)
- Minimizing message payload size
- Using efficient encoding
Troubleshooting
Where can I get help?
- Documentation: You're here! Check other sections.
- LayerZero Scan: Track and debug transactions
- Discord: LayerZero Community
- GitHub: Report issues on the relevant repository
How do I debug a transaction?
- Get transaction hash from sncast output
- Check status:
sncast tx-status <HASH> - View on explorer: Voyager or Starkscan
- Check events: Look for error events
- Simulate locally: Use
snforge test