TL;DR
- Starknet & Cairo is now live as academy Track 05: 10 modules across 4 tiers.
- It opens with an 8-stage Starknet Network Basics reading track that is public, so you can start without an account.
- From there: Cairo foundations, Starknet contracts, then seven security modules covering access control, accounting, state & lifecycle, cross-contract calls, signatures & accounts, denial of service, and upgrades & deployment.
- The thesis of the whole track: a validity proof guarantees the code ran exactly as written. It says nothing about whether it was written safely.
01Why a fifth tree, and why Starknet
Every WEB3PWN track so far has been a different answer to the same question: how do thousands of machines that don't trust each other agree on what happened? Ethereum re-executes everything. Bitcoin verifies spends against scripts. Solana races through explicit account lists. Canton keeps parties private and synchronizes only what they share.
Starknet's answer is the strangest one yet, and arguably the most elegant: don't re-execute at all. Prove it.
Starknet is an Ethereum validity rollup. A sequencer batches and executes transactions off-chain. A prover compresses that execution into a succinct cryptographic proof, a STARK. A verifier contract on Ethereum checks the proof and, if it holds, accepts the new state root. Unlike an optimistic rollup, nothing is assumed correct pending a challenge window. The proof is the guarantee, checked the moment it's submitted.
That's a remarkable property. It also leads security engineers into a dangerous line of thinking: if everything is mathematically proven, what's left to break?
A proof of correct execution is not a proof of a safe contract.
// Starknet Network Basics, Stage 1
The STARK attests that every Cairo instruction ran exactly as written: every storage write, every check, every missing check. If a contract forgets to verify its caller, the proof will faithfully certify that the broken logic executed perfectly. The prover isn't your auditor. That gap between "provably executed" and "safely written" is what this whole track is about.
02The mental model: execute, prove, verify
Before you can attack Starknet contracts you need a clear picture of who does what. The track spends two full theory stages on this, but here's the whole pipeline on one screen:
__validate__ and __execute__.
→ new state + execution trace
The sequencer included it. Fast, but ordering still rests on trust in the sequencer until it's proven.
The batch is proven and verified on Ethereum. Now it inherits Ethereum's security, and L2 → L1 messages become consumable.
What gets proven is the execution trace: a step-by-step record of register values, memory accesses and opcodes as the Cairo machine ran. Checking a proof about that trace is dramatically cheaper than redoing the computation, so Ethereum's cost to accept a batch stays roughly flat no matter how much work the batch contained. Nobody has to trust the prover, either. An invalid trace simply can't produce a proof that verifies.
Keep the two finality levels in mind. Several of the bugs you'll exploit later come from contracts that treat "the sequencer saw it" as if it meant "Ethereum proved it."
03Six EVM instincts that will betray you
If you're coming from Solidity, a lot of your intuition transfers: authorization, state machines, reentrancy-style reasoning about external calls. These six reflexes don't transfer, and each one maps to a module in the track.
| Instinct | On the EVM | On Starknet | Why it bites |
|---|---|---|---|
| Arithmetic | uint256; Solidity ≥ 0.8 reverts on overflow |
felt252 is math mod a prime and wraps silently; u8–u256 are checked |
Accounting on felts can "succeed" into absurd balances |
| Accounts | EOAs with protocol-fixed ECDSA validation | No EOAs. Every account is a contract with __validate__ / __execute__ |
"Who can spend" is whatever the account code says |
| Storage | Sequential slots from declaration order, with packing | Addresses from sn_keccak of the variable name; map keys hashed in with Pedersen |
No proxy slot collisions, but a different set of upgrade footguns |
| Code & upgrades | Proxy contract delegatecalls an implementation |
Declared class + deployed instance; replace_class_syscall swaps the code, keeps the storage |
One unguarded upgrade entrypoint is a full takeover |
| Transactions | Fees deducted by the protocol from the sender's balance | DECLARE, DEPLOY_ACCOUNT, INVOKE; the fee is charged to the sending account contract |
Reverted txs still pay, so any call an attacker can force to revert becomes a griefing lever |
| Cross-domain | Contracts call each other synchronously in one tx | L1 ↔ L2 is asynchronous messaging into #[l1_handler] entrypoints |
No atomicity, and message senders must be authenticated |
Scroll horizontally on small screens →
Three of those rows are worth a closer look, because they produce the most counterintuitive bugs in the track.
04felt252: the integer that isn't
Cairo was designed so that every operation can be proven efficiently, and STARK proofs live in a finite
field. So Cairo's native value type, felt252, isn't a bounded integer. It's a field
element: every add, subtract and multiply happens modulo
P = 2251 + 17·2192 + 1.
There's no overflow and no underflow, only wrapping. A result that lands outside [0, P) is
folded back in, silently, with no revert and no panic. Don't take our word for it. Break it yourself:
accepts 123 · -7 · P · P-5 · 2^251
That is P − 1. In a u256 this would have panicked. As a felt252 it is a perfectly valid, perfectly provable number.
To be fair to Cairo: it also ships u8 through u256, and those are range-checked.
Underflow a u256 and the transaction panics, just like modern Solidity. The trap is that
felt252 is the path of least resistance. It's the native type, hashes and selectors are
made of it, and it looks like a number. Now picture a token contract that stores balances as
felts:
#[storage]
struct Storage {
balances: Map<ContractAddress, felt252>,
}
#[external(v0)]
fn withdraw(ref self: ContractState, amount: felt252) {
let caller = get_caller_address();
let balance = self.balances.read(caller);
// 100 - 101 does not revert. It becomes P - 1.
self.balances.write(caller, balance - amount);
self.send_tokens(caller, amount);
}
#[storage]
struct Storage {
balances: Map<ContractAddress, u256>,
}
#[external(v0)]
fn withdraw(ref self: ContractState, amount: u256) {
let caller = get_caller_address();
let balance = self.balances.read(caller);
// u256 subtraction is checked and panics on underflow;
// the assert turns that into an explicit, readable failure.
assert(balance >= amount, 'insufficient balance');
self.balances.write(caller, balance - amount);
self.send_tokens(caller, amount);
}
Illustrative excerpt: imports and helper implementations omitted
Withdraw one more token than you own and the vulnerable version doesn't fail. It writes a balance of roughly 3.6 × 1075 and sends you the tokens anyway.
The proof will not save you
The STARK for that exploit transaction is perfectly valid. The prover attests that
balance - amount was computed correctly in the field, which it was. Ethereum accepts the
state root. This is exactly the class of bug the Accounting module has you exploit.
05Every account is a contract
Ethereum draws a hard line between externally owned accounts and smart contracts. Starknet erases it. Every account, including the one in your wallet, is a smart contract. That's native account abstraction: it isn't bolted on the way ERC-4337 is, it's simply how accounts work from the base layer up.
For every transaction, the protocol calls two entrypoints on the sending account, in order:
__validate__decides whether the transaction is authorized, typically by checking a signature against the stored public key and the nonce. Fail here and the transaction is rejected before any state changes.__execute__performs the calls the account wants to make. This is where the state changes and side effects happen.
Multisig, session keys, social recovery and sponsored fees all become plain contract code, with no protocol change needed. But "who is allowed to spend from this address" is no longer a rule the protocol enforces. It's whatever the account's code happens to say. Here's one line that separates a wallet from a free-for-all:
#[abi(embed_v0)]
impl SRC6Impl of ISRC6<ContractState> {
fn __execute__(self: @ContractState, calls: Array<Call>) {
// The protocol calls __execute__ with caller address 0.
// Without this check any contract can call it directly,
// skip __validate__ entirely, and act as this account.
let sender = get_caller_address();
assert(sender.is_zero(), 'Account: invalid caller');
execute_calls(calls.span());
}
fn __validate__(self: @ContractState, calls: Array<Call>) -> felt252 {
self.validate_transaction()
}
}
Illustrative excerpt modeled on the common SNIP-6 account pattern
Drop those two highlighted lines and signature validation becomes decoration. Other account bugs in the same family:
- Replayable signatures: a signed payload that doesn't bind the chain ID, the nonce and the account address can be replayed on another network, another account, or the same account twice.
- Sloppy nonce handling: nonces are what stop a valid transaction from being submitted again. If validation code gets them wrong, replay protection goes with them.
- Misread
is_valid_signature: a protocol that asks a caller-chosen contract whether a signature is valid, or accepts any non-zero answer instead of the exact'VALID'magic value, lets attackers approve their own actions.
You'll write that zero-caller check yourself in Starknet Contracts, then exploit the rest of the family in Signatures & Accounts.
06Messages across layers are not function calls
Ethereum and Starknet are separate execution environments, so a contract on one side can't just call a
contract on the other. They talk through a message bridge. An L1 contract sends a message that a Starknet
contract later consumes through an #[l1_handler] entrypoint. Going the other way, an L2
message can only be consumed on Ethereum after its batch reaches ACCEPTED_ON_L1.
The handler receives the L1 sender as its first argument, from_address. Any contract on
Ethereum can send a message to your handler, so that argument is the only thing standing between your
bridge and an unlimited mint:
#[l1_handler]
fn handle_deposit(
ref self: ContractState,
from_address: felt252,
account: ContractAddress,
amount: u256,
) {
// Any L1 contract can send a message to this handler.
// Only the canonical L1 bridge is allowed to mint.
assert(from_address == self.l1_bridge.read(), 'unauthorized L1 sender');
self.mint(account, amount);
}
Illustrative excerpt: storage and mint implementation omitted
Authentication is only half of it. The other half is time:
- A message is not consumed atomically with the L1 transaction that sent it. Code that assumes a deposit has already landed on L2 because the L1 side succeeded is reasoning about a state that doesn't exist yet.
- Withdrawals to L1 wait for proof and verification. Anything that credits users on L1 before
ACCEPTED_ON_L1is trusting the sequencer, not the math. - If your protocol layers its own IDs or nonces on top of messages, tracking what has already been processed is your job, not the bridge's.
Starknet Contracts walks you through a full L1 ↔ L2 message flow. Cross Contract then has you exploit an L1 handler that trusts its payload instead of its sender, and State & Lifecycle drills the consumed-state tracking that stops an operation from being processed twice.
07The path: ten modules, four tiers
The track follows the same skill-tree model as the rest of the academy. Clear a tier and the next one builds directly on it. Here's the whole map:
Foundation
Meet Starknet, then learn the Cairo language and map it onto Starknet contracts.
- 01Starknet
- 02Cairo Foundation
- 03Starknet Contracts
Core Security
Your first Cairo exploits: break caller authorization and field-arithmetic accounting.
- 04Access Control
- 05Accounting
State & Interactions
Abuse lifecycle and state assumptions, then untrusted cross-contract call paths.
- 06State & Lifecycle
- 07Cross Contract
Advanced Security
Replay signatures, grief with denial of service, and hijack upgrades and deployment.
- 08Signatures & Accounts
- 09Denial of Service
- 10Upgrades & Deployment
Start with Network Basics: eight stages, no account needed
The first module is a reading track built to give you the mental model every later lab assumes. Each stage is short, interactive where it helps (yes, the felt252 demo lives there too), and gated by quick quiz checkpoints. Pass one and the next stage unlocks.
- What Is Starknet?Ethereum scaled by proving execution instead of re-running it.
- Cairo & felt252A provable language built on field arithmetic, not bounded integers.
- Accounts & AbstractionEvery account is a contract, so validation logic is programmable.
- Contracts & StorageStorage is a key-value map, addressed explicitly rather than by layout.
- Transactions & FeesThree transaction types, one shared job: reach __validate__ honestly.
- Sequencing & ProvingA sequencer executes, a prover attests, Ethereum only has to check.
- L1 ↔ L2 Messaging & SettlementTwo chains, one asynchronous bridge between them.
- Consensus & the Security ModelWhat Starknet trusts today, and where its bugs actually live.
08What a lab actually feels like
Theory gets you the vocabulary. The labs are where it sticks. Every Cairo security module follows the same loop as the rest of WEB3PWN, with no local toolchain, wallet or devnet to set up:
Read the target
A real Cairo contract, a mission, and a security objective. Find the assumption that doesn't hold.
Write the exploit
Write your attack in the in-browser editor, in Cairo, against the actual vulnerable code.
Execute & verify
It's compiled and run against the target in an isolated, network-less sandbox. Satisfy the objective and it's solved.
Who this track is for
Auditors adding Starknet to their scope, Solidity developers who want to know which instincts to unlearn, and CTF players looking for bug classes that don't exist on the EVM. Having done our Ethereum track helps, but it isn't required: the Foundation tier teaches Cairo from scratch and assumes only general programming experience.
09Start here
Three ways in, depending on how much you want to commit right now:
Read Network Basics
Eight stages from "what is a validity rollup" to where Starknet bugs actually live.
Start stage 1 Five minutesTake the Starknet & Cairo quiz
Drawn from a 100-question bank, easy to very hard. Share your score if you dare.
Test yourself Free accountOpen the full skill tree
All ten modules, sandboxed Cairo labs, points and a spot on the leaderboard.
Enter the trackStuck on a lab, or found a felt252 edge case you want to argue about? Come find us on Discord. See you in the sandbox.