starknet/cairo-dev-guide
Starknet Cairo Development Guide
starknetguide👥 Communityconfidence highhealth 100%
v1.0.0·Updated 3/20/2026
Why Starknet?
- ZK Rollup: All transactions are verified by STARK proofs — security comes from math, not game theory
- Native AA: All accounts are contract accounts (no EOA concept)
- Cairo VM: Designed specifically for verifiable computation, independent of the EVM
- Low gas: Compute-intensive tasks are 10-100x cheaper than Ethereum
Cairo Language Basics
// Cairo 2.x syntax (Rust-like)
use starknet::ContractAddress;
#[starknet::contract]
mod ERC20 {
use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess, Map};
#[storage]
struct Storage {
name: ByteArray,
total_supply: u256,
balances: Map<ContractAddress, u256>,
allowances: Map<(ContractAddress, ContractAddress), u256>,
}
#[event]
#[derive(Drop, starknet::Event)]
enum Event {
Transfer: Transfer,
}
#[derive(Drop, starknet::Event)]
struct Transfer {
#[key]
from: ContractAddress,
#[key]
to: ContractAddress,
value: u256,
}
#[abi(embed_v0)]
impl ERC20Impl of super::IERC20<ContractState> {
fn balance_of(self: @ContractState, account: ContractAddress) -> u256 {
self.balances.read(account)
}
fn transfer(ref self: ContractState, recipient: ContractAddress, amount: u256) -> bool {
let sender = starknet::get_caller_address();
let sender_balance = self.balances.read(sender);
assert(sender_balance >= amount, 'Insufficient balance');
self.balances.write(sender, sender_balance - amount);
self.balances.write(recipient, self.balances.read(recipient) + amount);
self.emit(Transfer { from: sender, to: recipient, value: amount });
true
}
}
}
Key Differences from the EVM
| Concept | EVM/Solidity | Starknet/Cairo |
|---|---|---|
| Base type | uint256 | felt252 (252-bit field element) |
| Storage | mapping | Map<K, V> |
| Accounts | EOA + contracts | All contracts |
| Events | event | #[event] + #[derive] |
| Constructor | constructor | #[constructor] |
| Inheritance | is | Composition + interfaces |
| msg.sender | msg.sender | get_caller_address() |
| address | address | ContractAddress |
OpenZeppelin Cairo Components
# Scarb.toml
[dependencies]
openzeppelin = { git = "https://github.com/OpenZeppelin/cairo-contracts.git", tag = "v0.15.0" }
#[starknet::contract]
mod MyToken {
use openzeppelin::token::erc20::{ERC20Component, ERC20HooksEmptyImpl};
use starknet::ContractAddress;
component!(path: ERC20Component, storage: erc20, event: ERC20Event);
#[abi(embed_v0)]
impl ERC20Impl = ERC20Component::ERC20Impl<ContractState>;
#[abi(embed_v0)]
impl ERC20MetadataImpl = ERC20Component::ERC20MetadataImpl<ContractState>;
impl ERC20InternalImpl = ERC20Component::InternalImpl<ContractState>;
#[storage]
struct Storage {
#[substorage(v0)]
erc20: ERC20Component::Storage,
}
#[constructor]
fn constructor(ref self: ContractState, recipient: ContractAddress) {
self.erc20.initializer("MyToken", "MTK");
self.erc20.mint(recipient, 1000000000000000000000000); // 1M tokens
}
}
Deployment Workflow
# Install tooling
curl --proto '=https' --tlsv1.2 -sSf https://docs.swmansion.com/scarb/install.sh | sh
pip install starknet-devnet
# Create a new project
scarb new my_contract
cd my_contract && scarb build
# Local testnet
starknet-devnet --seed 0
# Deploy with sncast
sncast --account my_account declare --contract-name MyContract
sncast --account my_account deploy --class-hash 0x...
Starknet.js Frontend Integration
npm install starknet
import { RpcProvider, Account, Contract, cairo } from "starknet"
// Connect provider
const provider = new RpcProvider({ nodeUrl: "https://starknet-mainnet.public.blastapi.io" })
// Connect wallet (ArgentX / Braavos)
const { account } = await window.starknet.enable()
// Interact with contract
const contract = new Contract(ABI, contractAddress, account)
// Read
const balance = await contract.balanceOf(address)
// Write (invoke)
const tx = await contract.transfer(recipient, cairo.uint256(1000n))
await provider.waitForTransaction(tx.transaction_hash)
Account Abstraction (Native Support)
All accounts on Starknet are contracts, with built-in support for:
- Custom signature schemes (ECDSA / WebAuthn / multisig)
- Session Keys (users grant apps temporary operation permissions)
- Paymaster (gas sponsorship, pay with any token)
- Multicall (execute multiple operations in a single signature)
// Starknet native multicall (no extra SDK required)
const multiCall = await account.execute([
{ contractAddress: tokenA, entrypoint: "approve", calldata: [spender, amount] },
{ contractAddress: dex, entrypoint: "swap", calldata: [tokenA, tokenB, amount] },
])
Tooling Ecosystem
| Tool | Purpose |
|---|---|
| Scarb | Cairo package manager + compiler |
| Starknet Foundry (snforge) | Testing framework (Foundry-like) |
| ArgentX / Braavos | Leading wallets |
| Voyager | Block explorer |
| starknet-devnet | Local testnet |
Starknet Cairo Development Guide
Why Starknet?
- ZK Rollup: All transactions are verified by STARK proofs — security comes from math, not game theory
- Native AA: All accounts are contract accounts (no EOA concept)
- Cairo VM: Designed specifically for verifiable computation, independent of the EVM
- Low gas: Compute-intensive tasks are 10-100x cheaper than Ethereum
Cairo Language Basics
// Cairo 2.x syntax (Rust-like)
use starknet::ContractAddress;
#[starknet::contract]
mod ERC20 {
use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess, Map};
#[storage]
struct Storage {
name: ByteArray,
total_supply: u256,
balances: Map<ContractAddress, u256>,
allowances: Map<(ContractAddress, ContractAddress), u256>,
}
#[event]
#[derive(Drop, starknet::Event)]
enum Event {
Transfer: Transfer,
}
#[derive(Drop, starknet::Event)]
struct Transfer {
#[key]
from: ContractAddress,
#[key]
to: ContractAddress,
value: u256,
}
#[abi(embed_v0)]
impl ERC20Impl of super::IERC20<ContractState> {
fn balance_of(self: @ContractState, account: ContractAddress) -> u256 {
self.balances.read(account)
}
fn transfer(ref self: ContractState, recipient: ContractAddress, amount: u256) -> bool {
let sender = starknet::get_caller_address();
let sender_balance = self.balances.read(sender);
assert(sender_balance >= amount, 'Insufficient balance');
self.balances.write(sender, sender_balance - amount);
self.balances.write(recipient, self.balances.read(recipient) + amount);
self.emit(Transfer { from: sender, to: recipient, value: amount });
true
}
}
}
Key Differences from the EVM
| Concept | EVM/Solidity | Starknet/Cairo |
|---|---|---|
| Base type | uint256 | felt252 (252-bit field element) |
| Storage | mapping | Map<K, V> |
| Accounts | EOA + contracts | All contracts |
| Events | event | #[event] + #[derive] |
| Constructor | constructor | #[constructor] |
| Inheritance | is | Composition + interfaces |
| msg.sender | msg.sender | get_caller_address() |
| address | address | ContractAddress |
OpenZeppelin Cairo Components
# Scarb.toml
[dependencies]
openzeppelin = { git = "https://github.com/OpenZeppelin/cairo-contracts.git", tag = "v0.15.0" }
#[starknet::contract]
mod MyToken {
use openzeppelin::token::erc20::{ERC20Component, ERC20HooksEmptyImpl};
use starknet::ContractAddress;
component!(path: ERC20Component, storage: erc20, event: ERC20Event);
#[abi(embed_v0)]
impl ERC20Impl = ERC20Component::ERC20Impl<ContractState>;
#[abi(embed_v0)]
impl ERC20MetadataImpl = ERC20Component::ERC20MetadataImpl<ContractState>;
impl ERC20InternalImpl = ERC20Component::InternalImpl<ContractState>;
#[storage]
struct Storage {
#[substorage(v0)]
erc20: ERC20Component::Storage,
}
#[constructor]
fn constructor(ref self: ContractState, recipient: ContractAddress) {
self.erc20.initializer("MyToken", "MTK");
self.erc20.mint(recipient, 1000000000000000000000000); // 1M tokens
}
}
Deployment Workflow
# Install tooling
curl --proto '=https' --tlsv1.2 -sSf https://docs.swmansion.com/scarb/install.sh | sh
pip install starknet-devnet
# Create a new project
scarb new my_contract
cd my_contract && scarb build
# Local testnet
starknet-devnet --seed 0
# Deploy with sncast
sncast --account my_account declare --contract-name MyContract
sncast --account my_account deploy --class-hash 0x...
Starknet.js Frontend Integration
npm install starknet
import { RpcProvider, Account, Contract, cairo } from "starknet"
// Connect provider
const provider = new RpcProvider({ nodeUrl: "https://starknet-mainnet.public.blastapi.io" })
// Connect wallet (ArgentX / Braavos)
const { account } = await window.starknet.enable()
// Interact with contract
const contract = new Contract(ABI, contractAddress, account)
// Read
const balance = await contract.balanceOf(address)
// Write (invoke)
const tx = await contract.transfer(recipient, cairo.uint256(1000n))
await provider.waitForTransaction(tx.transaction_hash)
Account Abstraction (Native Support)
All accounts on Starknet are contracts, with built-in support for:
- Custom signature schemes (ECDSA / WebAuthn / multisig)
- Session Keys (users grant apps temporary operation permissions)
- Paymaster (gas sponsorship, pay with any token)
- Multicall (execute multiple operations in a single signature)
// Starknet native multicall (no extra SDK required)
const multiCall = await account.execute([
{ contractAddress: tokenA, entrypoint: "approve", calldata: [spender, amount] },
{ contractAddress: dex, entrypoint: "swap", calldata: [tokenA, tokenB, amount] },
])
Tooling Ecosystem
| Tool | Purpose |
|---|---|
| Scarb | Cairo package manager + compiler |
| Starknet Foundry (snforge) | Testing framework (Foundry-like) |
| ArgentX / Braavos | Leading wallets |
| Voyager | Block explorer |
| starknet-devnet | Local testnet |
| Katana | Local node by the Dojo team |