All articles
Shadow ArenaMay 25, 20269 min read

33% of Bugs in 6 Real Audit Targets Are 'Missed State Updates'

Across 63 documented findings in Zealynx Academy's Shadow Arena, the dominant bug class isn't reentrancy or oracle manipulation. It's missed state updates in conditional paths.

By Carlos (Bloqarl)

TL;DR

  • We classified all 63 documented bugs across the 6 audit targets in Zealynx Academy's Shadow Arena (Basin, ElasticSwap, Velodrome, Flux Finance, Canto v2, Venus, totaling 13,712 SLOC).
  • The single largest category, about 33% of all findings, is "missed state update in a conditional path". Function does the right thing in the happy path, then skips a key state write in an early-return, a revert-recovery, or a special-case branch.
  • This bug class has the lowest "interesting writeup" energy and the highest production impact. No clever exploit math, no novel primitive. Just a state variable that should have been written when condition X was true, and wasn't.
  • It is also the most under-detected class in real audits. Static analyzers can't tell which writes are required without semantic understanding of the protocol. Manual auditors miss it because the bug is invisible by definition: it's about what's not written.
  • AI auditors miss it for the same reason. The pattern that fires their detectors (an unsafe write) is absent. The bug is in the gap.

Why this matters

Most security training, most CTFs, and most public writeups foreground a particular set of attack archetypes: reentrancy, oracle manipulation, signature replay, flash-loan attacks, share inflation. These are the bugs with cinematic explanations and dollar-amount headlines. They're also a small minority of what actually breaks in production audits of forked DeFi.

When we built the Shadow Arena training set, we did something different from CTFs. Instead of constructing isolated puzzles around famous vulnerability classes, we picked six real, line-for-line forks of production protocols and used the actual auditor-found bug list as the answer key. The result is a dataset that reflects what auditors actually find when they sit down with a real Compound or AMM fork, not what makes for the best Capture-the-Flag round.

When you classify those bugs by root cause, the most common category isn't even close. Missed state updates dominate.

If you're training to audit real protocols, this matters. The skills that catch missed-state bugs are different from the skills that catch reentrancy. Reentrancy training tells you to look for call.value followed by state writes. Missed-state training tells you to follow every variable's lifecycle through every branch and ask whether each branch updates everything it should.

That's a much harder discipline. It's also what separates auditors who find real bugs from auditors who run a checklist and ship a clean report.

What "missed state update" actually means

The pattern is straightforward to describe, hard to spot in practice. The function has multiple execution paths. Some paths update state variable X. Some paths don't. The paths that don't should, but the developer didn't notice the omission because the happy path looks correct in isolation.

A simplified example, based on a real Shadow Arena finding pattern:

function harvest(uint256 strategyId) external {
    Strategy storage strat = strategies[strategyId];

    if (block.timestamp < strat.lastHarvest + COOLDOWN) {
        return;  // early-return, no state changes
    }

    uint256 yield = computeYield(strat);
    if (yield == 0) {
        strat.lastHarvest = block.timestamp;  // updated here
        return;
    }

    // happy path
    strat.accumulatedYield += yield;
    strat.lastHarvest = block.timestamp;  // updated here
    _distributeYield(yield);
}

Three paths, three different state-update behaviors. The first early-return doesn't update lastHarvest. The second early-return updates only lastHarvest. The happy path updates both accumulatedYield and lastHarvest. Is this correct?

In the first early-return, the user gets to call harvest again on the next block. That might be fine, or it might let them spam the function before the cooldown expires. The point is: you can't tell whether the missing write is a bug without understanding the protocol's intended semantics.

That ambiguity is why static analysis fails on this class. The compiler sees three different write sets across three branches and has no way to decide which is correct. Only a human (or an AI agent with deep protocol context) can answer the question.

The Shadow Arena distribution

Across the 63 documented bugs in our 6 targets, the bug-class breakdown is roughly:

ClassApprox shareNotable
Missed state update in conditional path33%Dominates. Velodrome alone has 6 of these.
Token-handling pathology (FoT, donation/inflation, ERC777, reverting tokens)13%Fee-on-transfer breaks 3 of 6 protocols. cToken donation appears in flux-finance and venus.
Forked-protocol parameter/units mistakes10%venus blocksPerYear (15s vs 3s), canto per-year vs per-block, canto 1 vs 1e18 mantissa.
Locked funds / permanent reward loss10%velodrome H-02, M-07, M-08 all permanently lock bribe rewards.
Oracle integrity (staleness, hardcoded caps, missed updates)8%basin H-01 fails to update pumps in shift()/sync(); venus M-02 uses stale prices.
Access control / initialization guards8%canto-v2 has 3 of these.
Frontrunning / MEV / missing deadlines6%venus M-11 missing deadline on PancakeSwap, basin M-07 CREATE2 frontrun.
Bit-shift / off-by-one / low-level6%basin pump-encoding has 3 of these.
Signature replay2%flux-finance M-04 KYC EIP-712 missing nonce.

The locked-funds category overlaps with missed-state. Most of the locked-funds bugs (Velodrome's H-02, M-07, M-08) trigger because a state update gets skipped in an epoch-transition branch, leaving rewards stranded. If you bucket those together with the dedicated missed-state findings, the share rises closer to 40%.

Why static and AI tools miss it

Three structural reasons:

  1. The bug is in the absence. Pattern matchers fire on what's there. call.value followed by a write is detectable. Reentrancy-locked functions are detectable. Missing writes have no pattern to detect, only a semantic expectation that the auditor has to construct from the protocol's invariants.

  2. The required-write set is protocol-specific. A function that updates lastHarvest in one protocol might correctly leave it untouched in a different protocol with different semantics. There is no universal rule. Generic linters can't decide.

  3. Existing AI auditors run on individual functions. They look at a function in isolation and ask "is this safe?" instead of "across all the call paths through this function, are all the protocol's required state writes performed?" The latter is a graph-of-paths question, not a single-function question.

The Krait kill-gate pattern (one of the architectural decisions taught in Academy's AI Auditor Builder pillar) addresses this with a "spec auditor" mindset: an explicit pass that asks, for each state variable, whether every code path that should update it actually does. We've documented that pattern as one of the 12 steps to building a precision-first AI auditor.

How to train your eye

Three habits, in order of impact:

Habit 1: Trace every state variable, not every function. Most auditors read code function by function. Reverse it. Pick a state variable, list every place it's read, list every place it's written. Then ask: in every branch of every reading function, is the variable in the state it needs to be? If not, who writes it, and is that path always taken?

Habit 2: Enumerate paths explicitly. When you read a function, write down every distinct execution path: happy path, each early-return, each revert-recovery, each special case. For each path, write the set of state variables modified. If two paths semantically should leave state in equivalent forms, but their write sets differ, that's a finding candidate.

Habit 3: Treat conditional branches as suspect by default. The happy path almost always updates state correctly because the developer tested it. The conditional branches (early-returns, error paths, special-case skips) are where the developer's attention dropped. That is where the bugs live.

This is exactly the skill Shadow Arena is designed to train. The targets are large enough that path enumeration is non-trivial. The known-bug list is detailed enough that you can grade your own findings against the ground truth.

What real losses look like

Public losses from this class don't make headlines because they're invisible until the cumulative effect is large. A protocol that under-pays rewards by 0.3% per epoch loses real users over time but doesn't appear on a "top 10 hacks" list. A lending protocol that mis-tracks accrued interest because of a missed update during liquidation runs slightly under-collateralized, and the gap surfaces only during a market stress event.

The bugs in our Velodrome dataset are good examples. H-02 strands bribes permanently. M-07 strands them on epoch transition. M-08 accumulates a rounding-induced lock over many epochs. None of these are "the protocol got drained for $50M" stories. All of them silently transfer value from honest participants to the contract's stuck balance. That's the actual texture of most production bugs.

Related questions

What is a "missed state update" bug? A function has multiple execution paths (happy path, early-returns, revert recovery, conditional branches). One or more of those paths skips a state-variable write that the protocol's invariants require. The bug is the absence of the write in that path.

Why don't static analyzers catch it? Static analyzers (Slither, Mythril, etc.) detect unsafe writes (e.g., reentrancy patterns, unchecked external calls). They cannot reason about required writes without knowing the protocol's intended semantics. The bug is invisible without that knowledge.

Are these bugs typically high or low severity? Severity varies. Many are Medium (silent reward under-payment, stale-state edge cases). A meaningful subset are High (permanent fund lock, exploitable accounting drift). Across our 6-target dataset, missed-state bugs span the full High/Medium severity range.

How do I get better at finding them? Three habits: trace state variables not functions, enumerate execution paths explicitly, treat conditional branches as suspect by default. Practice on real codebases like Shadow Arena, not on toy CTFs.

Does the dominance generalize beyond these 6 targets? Our dataset is too small to claim a universal share, but the pattern is consistent with what Code4rena's public retrospectives and large audit-firm reports describe. Reentrancy-style bugs make headlines; state-update bugs make audit reports.

Where to start

If you want to train this skill on real code, the Velodrome target in Shadow Arena is the densest practice ground. It has 13 documented findings concentrated in the bribe and reward-distribution subsystem, and most of them require following multi-branch state lifecycles. Pick the target, read the code without looking at the answer key, then grade your findings.

For a smaller surface, start with Basin (1,145 SLOC, 14 findings). Half the findings are in a single subsystem (the pump-encoding code), which gives you concentrated repetitions of the core skill in a constrained scope.

Either way, expect to feel slow at first. This isn't a pattern you spot in 30 seconds the way you spot a missing reentrancy guard. It's a discipline that compounds over many hours of code-tracing.

Tagged

Shadow ArenaAudit MethodologySmart Contract Security