Deploying Smart Contracts in StarkNet
StarkNet is a ZK Rollup from StarkWare running on top of Ethereum. Unlike zkSync Era with its EVM compatibility, StarkNet uses its own Cairo language and CASM virtual machine. You cannot directly port a Solidity contract to StarkNet — you need to rewrite it from scratch or use the Warp transpiler (which only works with a limited subset of Solidity). This must be factored into effort estimation.
Cairo: StarkNet's Contract Language
Cairo 1.x vs Cairo 0.x
Historically, Cairo was written in a procedural style with explicit hint management, which was non-trivial. Starting with Cairo 1.0 (2023), the syntax has been redesigned to be Rust-like, with contracts now written as #[starknet::contract] modules:
#[starknet::contract]
mod Counter {
use starknet::ContractAddress;
#[storage]
struct Storage {
count: u128,
owner: ContractAddress,
}
#[event]
#[derive(Drop, starknet::Event)]
enum Event {
Incremented: Incremented,
}
#[derive(Drop, starknet::Event)]
struct Incremented {
new_count: u128,
}
#[constructor]
fn constructor(ref self: ContractState, owner: ContractAddress) {
self.owner.write(owner);
self.count.write(0);
}
#[abi(embed_v0)]
fn increment(ref self: ContractState) {
let current = self.count.read();
self.count.write(current + 1);
self.emit(Incremented { new_count: current + 1 });
}
}
The compiler is scarb (a package manager and build tool for Cairo, similar to Cargo). Contracts compile to Sierra (Safe Intermediate Representation), then to CASM for execution.
Key Differences from Solidity
-
Storage — Cairo storage works differently. There are no dynamic arrays in storage directly: use
Vec<T>throughStorageVecor mappings throughMap<K, V>. Structures are automatically decomposed into separate storage slots. -
Felt252 — the basic data type, a 252-bit field. Familiar
uint256is implemented as a struct of twofelt252. U256 arithmetic is more expensive in gas than working withfelt252. -
Account Abstraction is Native — StarkNet has no EOA in the Ethereum sense. Each account is a contract implementing the
IAccountinterface. This means multicall, session keys, social recovery — all are "out of the box" at the account level.
Deployment Infrastructure
Scarb and Project Initialization
# Install Scarb
curl --proto '=https' --tlsv1.2 -sSf https://docs.swmansion.com/scarb/install.sh | sh
# New project
scarb new my_contract
cd my_contract
# Scarb.toml
[package]
name = "my_contract"
version = "0.1.0"
[dependencies]
starknet = ">=2.6.0"
[[target.starknet-contract]]
sierra = true
casm = true
# Compilation
scarb build
# Result: target/dev/my_contract_Counter.contract_class.json (Sierra)
# target/dev/my_contract_Counter.compiled_contract_class.json (CASM)
Deployment via starkli
starkli is the primary CLI tool for interacting with StarkNet:
# Install
curl https://get.starkli.sh | sh
# Setup keystore
starkli signer keystore new ~/.starkli-wallets/deployer/keystore.json
# Declare contract class — unique to StarkNet
# First the class is registered on the network, then an instance is deployed
starkli declare \
target/dev/my_contract_Counter.contract_class.json \
--account ~/.starkli-wallets/deployer/account.json \
--keystore ~/.starkli-wallets/deployer/keystore.json \
--rpc https://starknet-mainnet.public.blastapi.io
# Deploy instance with constructor arguments
starkli deploy \
CLASS_HASH \
OWNER_ADDRESS \
--account ~/.starkli-wallets/deployer/account.json \
--keystore ~/.starkli-wallets/deployer/keystore.json \
--rpc https://starknet-mainnet.public.blastapi.io
The two-step declare/deploy model is a StarkNet architectural feature. A single class_hash can have thousands of instances — this saves space on the network.
Deployment via starknet-rs / starknet.js
For programmatic deployment via TypeScript:
import { RpcProvider, Account, Contract, json, stark } from 'starknet'
import fs from 'fs'
const provider = new RpcProvider({
nodeUrl: 'https://starknet-mainnet.public.blastapi.io',
})
const account = new Account(
provider,
ACCOUNT_ADDRESS,
PRIVATE_KEY,
'1' // Cairo version
)
// Declare
const compiledSierra = json.parse(
fs.readFileSync('./target/dev/my_contract_Counter.contract_class.json').toString('ascii')
)
const compiledCasm = json.parse(
fs.readFileSync('./target/dev/my_contract_Counter.compiled_contract_class.json').toString('ascii')
)
const declareResponse = await account.declare({
contract: compiledSierra,
casm: compiledCasm,
})
await provider.waitForTransaction(declareResponse.transaction_hash)
// Deploy
const deployResponse = await account.deployContract({
classHash: declareResponse.class_hash,
constructorCalldata: [OWNER_ADDRESS],
})
await provider.waitForTransaction(deployResponse.transaction_hash)
Verification and Voyager
StarkNet explorer — Voyager (voyager.online) and Starkscan. Verification via Voyager accepts Cairo sources and Scarb.toml. API for programmatic verification:
curl -X POST https://api.voyager.online/beta/contract/verify \
-H "Content-Type: application/json" \
-d '{
"contractAddress": "0x...",
"files": { "src/lib.cairo": "..." },
"scarbVersion": "2.6.0"
}'
Effort Estimation
| Scenario | Timeline |
|---|---|
| Simple contract (counter, storage) from scratch | 4–8 h |
| ERC-20 / ERC-721 migration from Solidity | 1 d (OpenZeppelin Cairo already implements standards) |
| Custom DeFi logic (rewrite from Solidity) | 1–2 d + tests |
If a contract is already written in Solidity and there's no fundamental need for StarkNet-specific features (native AA, ZK-proofs for custom logic) — I recommend first considering zkSync Era with its EVM compatibility. The move to Cairo is justified when you need capabilities that Cairo provides natively.







