New Track Starknet Cairo Track 05

Starknet & Cairo Is Live.
Break the Provable VM.

WEB3PWN's fifth security tree takes you from STARK proofs and felt252 field math to exploiting account abstraction, cross-contract calls and upgrade paths. Every lab runs in a Cairo sandbox, right in your browser.

A felt252 subtraction wrapping from 0 to P minus 1 around the Cairo prime field, beside Starknet's sequencer, prover and L1 verifier pipeline vulnerable.cairo 1 2 3 4 5 let balance: felt252 = 100; let next = balance - 101; // no revert. no panic. // next == P - 1 // = 3618502788…5872020480 SEQUENCER PROVER L1 VERIFIER execute prove verify ACCEPTED_ON_L1 · state root verified on Ethereum 0 P − 1 felt252 mod P P = 2^251 + 17·2^192 + 1

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:

off-chain · L2 Sequencer Orders pending transactions and runs each one's __validate__ and __execute__. → new state + execution trace
off-chain · L2 Prover Turns the entire execution trace into one succinct STARK proof. → proof + state root
on-chain · Ethereum L1 Verifier Checks the proof without ever seeing individual transactions. → state root accepted as final
ACCEPTED_ON_L2

The sequencer included it. Fast, but ordering still rests on trust in the sequencer until it's proven.

ACCEPTED_ON_L1

The batch is proven and verified on Ethereum. Now it inherits Ethereum's security, and L2 → L1 messages become consumable.

Fig. 1: One Starknet batch, from mempool to Ethereum finality

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; u8u256 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:

felt252 playground P = 2251 + 17·2192 + 1

accepts 123 · -7 · P · P-5 · 2^251

a − b (mod P) field wrapped · no revert
3618502788666131213697322783095070105623107215331596699973092056135872020480

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:

cairo

#[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);
}

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:

account.cairo cairo

#[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:

bridge.cairo cairo

#[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_L1 is 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:

Tier 01

Foundation

Meet Starknet, then learn the Cairo language and map it onto Starknet contracts.

  • 01StarknetNetwork Basics · public
  • 02Cairo FoundationLanguage
  • 03Starknet ContractsContracts
Tier 02

Core Security

Your first Cairo exploits: break caller authorization and field-arithmetic accounting.

  • 04Access ControlExploit lab
  • 05AccountingExploit lab
Tier 03

State & Interactions

Abuse lifecycle and state assumptions, then untrusted cross-contract call paths.

  • 06State & LifecycleExploit lab
  • 07Cross ContractExploit lab
Tier 04

Advanced Security

Replay signatures, grief with denial of service, and hijack upgrades and deployment.

  • 08Signatures & AccountsExploit lab
  • 09Denial of ServiceExploit lab
  • 10Upgrades & DeploymentExploit lab

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.

  1. What Is Starknet?Ethereum scaled by proving execution instead of re-running it.
  2. Cairo & felt252A provable language built on field arithmetic, not bounded integers.
  3. Accounts & AbstractionEvery account is a contract, so validation logic is programmable.
  4. Contracts & StorageStorage is a key-value map, addressed explicitly rather than by layout.
  5. Transactions & FeesThree transaction types, one shared job: reach __validate__ honestly.
  6. Sequencing & ProvingA sequencer executes, a prover attests, Ethereum only has to check.
  7. L1 ↔ L2 Messaging & SettlementTwo chains, one asynchronous bridge between them.
  8. 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:

01

Read the target

A real Cairo contract, a mission, and a security objective. Find the assumption that doesn't hold.

02

Write the exploit

Write your attack in the in-browser editor, in Cairo, against the actual vulnerable code.

03

Execute & verify

It's compiled and run against the target in an isolated, network-less sandbox. Satisfy the objective and it's solved.

compiling exploit
running against target in sandbox
security objective satisfied
validation completed in sandbox
lab solved · points added to your rank

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:

Stuck on a lab, or found a felt252 edge case you want to argue about? Come find us on Discord. See you in the sandbox.

WEB3PWN Team

We build the labs at WEB3PWN, the hands-on Web3 security academy by ResearchZero.

The Proof Is Valid.
The Contract Isn't.

Go find out why. Start with the free Network Basics stages, then work your way up to hijacking upgrades.