OAK — OnChain Attack Knowledge

Technique · OAK-T9.016 · emerging

OAK-T9.016 — Contract Accounting / State-Invariant Flaw

Parent Tactics
OAK-T9 (Smart-Contract Exploit)
Adjacent Techniques
OAK-T9.004 (Access-Control Misconfiguration — the sibling: there, someone acted who should not have been allowed to act; here, the caller was entitled to the call and the contract's bookkeeping was wrong), OAK-T9.011 (Precision-Loss / Rounding Attack — the arithmetic sub-case where the error is rounding direction under integer division), OAK-T9.005 (Reentrancy — state read mid-update by a re-entering caller), OAK-T9.001 (Oracle Price Manipulation — the attacker moved an external price; here the contract mis-states its own internal value)
Maturity
emerging
Chains
chain-agnostic (EVM, Solana, Move-family, Starknet — the defect is in contract logic, not the runtime)
First documented
2020-era DeFi at the class level; earliest corpus anchors Uranium Finance (2021-04, swap-invariant constant error) and Compound (2021-09, Comptroller distribution bug)
Aliases
"logic flaw", "accounting bug", "state-machine bug", "invariant violation", "missing solvency check", "contract-correctness bug", "business-logic exploit"
Canonical mitigations
OAK-M16, OAK-M11.

Description

The contract lets a legitimate caller perform a legitimate call, and the call leaves the contract's books wrong in the caller's favour. No role is bypassed and no check of who is asking fails. What fails is the contract's model of how much — a balance, a reserve, a share price, a debt position, a claim record, a state flag — so that the attacker ends the transaction owed, or holding, more than they put in.

T9.016 exists because OAK previously filed these cases under T9.004 (Access-Control Misconfiguration), which described itself as "the residual category" for logic flaws. That grouping made the corpus's most-cited Technique mean two different things and pointed defenders at the wrong control: an access-control finding asks "who may call this?", while every T9.016 case had the right caller and the wrong arithmetic or state transition. The 2026-10 split moved them here.

Recurring sub-shapes in the anchor set:

  • Missing solvency / health check on a value-moving path — an action that should be refused when it leaves the actor undercollateralised is permitted on one path the designers did not guard (Euler 2023 donateToReserves; Platypus 2023 emergency withdrawal; Cypher 2023 aggregate sub-account debt; Rhea 2026 collateral taken from a user-supplied route claim).
  • State not cleared or updated after payout — a record that should be consumed is left live and can be replayed (DoinGud 2026 acceptBid; Dream Health 2026 award reset; Likwid 2026 leverage = 0 path skipping the reserve update).
  • Self-referential or aliasing accounting — the same slot is read before and written after a mutation, or two claims are created on one asset (Miner ERC-X 2024 self-transfer cached balance; Yearn 2023 stale share-token mapping).
  • Invariant check computed wrong — the constant-product or equivalent check exists but is miscalculated, overflows, or reads balances after an internal hook has moved the protocol's own money in (Uranium 2021; KyberSwap 2023 tick boundary; EtherVista 2026 uint112 product; MALT 2026 treasury DAI counted as trader input; GoldPesa 2026 unsettled hook deltas).
  • Edge-case branches in share math — zero liquidity, zero supply, or first-depositor branches that stop being pro-rata (BitBay StableVault 2026).
  • Mispriced internal valuation — the contract's own pricing function values an attacker-shaped input above its backing (Lien Finance 2026).
  • Distribution / reward accounting errors — rewards accrue or distribute against the wrong basis (Compound 2021 Comptroller).

Observed indicators

  • A single transaction (often flash-loan funded) whose net effect is that the caller's withdrawable claim on the protocol rises by more than the value they supplied, through only public entrypoints.
  • The same function called repeatedly in one transaction with the protocol's internal accounting not changing between calls (replayed claims, stale reserves).
  • Calls on rarely-used branches: emergency withdrawal, zero-leverage paths, migration helpers, first-deposit or empty-pool states, self-transfers.
  • Post-event: the root-cause analysis names an invariant ("total claims ≤ total assets", "debt ≤ collateral", "k never decreases") rather than a missing role.

Detection signals

  • Pre-deployment invariant testing / formal verification (OAK-M16): state the conservation invariants explicitly (assets vs. liabilities, k monotonicity, claim consumption) and fuzz every public entrypoint against them, including degenerate states (zero supply, self-transfer, from == to, zero leverage).
  • Per-transaction balance-sheet monitoring: compare protocol assets and liabilities before and after each transaction; alert when a caller's net claim grows faster than their deposit, or when total liabilities exceed assets after a public call.
  • Repeated-call heuristics: the same payout entrypoint invoked more than once per transaction by the same caller against the same record.
  • Rate limits / per-block caps (OAK-M11): do not detect the bug but bound how much one transaction can extract against it.

Real-world examples

Additional incidents

Reference implementations

  • mg-detectors-rs — coverage gap. Balance-sheet delta monitoring per transaction is the generic detector; protocol-specific invariants require per-protocol configuration.

Mitigations

  • OAK-M16 (Pre-Deployment Audit and Formal Verification): highest leverage. Every T9.016 anchor violates an invariant that could have been written down and fuzzed.
  • OAK-M11 (Rate-Limiting and Per-Block Caps): bounds single-transaction extraction.
  • OAK-M34 (Pause-by-Default / Emergency Pause): limits the second and later transactions once monitoring fires.

Discussion

Boundary with T9.004. Ask whether the attacker needed to be someone they were not. If the exploit works only because the contract accepted a caller, signer, account, module or counterparty it should have rejected — including a counterfeit object presented as genuine (Cashio's fake LP account, Crema's fake tick array, Exactly's fake market, FlashLoopAdapter's fake Safe) — it is T9.004. If any caller could have done it through public entrypoints with honest inputs and the contract simply computed the wrong result, it is T9.016. Cases with both (an unrestricted registration function and double-counted shares, as in Redsonic 2026-09) take T9.004 as primary when the missing gate was load-bearing, and cross-reference T9.016.

Boundary with T9.011. Rounding direction under integer division stays in T9.011. Wrong constants, overflow, missing updates and wrong branches are T9.016, even when they are arithmetic.

Maturity. Minted as emerging with seventeen re-homed anchors spanning 2021–2026; it inherits an established class rather than introducing a new one, so promotion is a question of the multi-source attestation bar, not of anchor count.

Parent Tactics

Worked examples (18)