Lesson 51 · Trace Drill · Application

Trace a pool drain

Same shape as L50, a different attack — foregrounding extractable vs at-stake. ~14 min.

Builds on: L50 · L19 · L21 Anchor: drain a DEX pool New: the cash-out ceiling, live

You've run the trace once (L50). This is a drill on a different attack — and it surfaces a distinction L50 glossed: when something is drained, how much can the attacker actually keep? A pool drain is the cleanest place to see at_stake and extractable diverge, and to watch the system act before the drain (as standing exposure) and during it (as an event). Same pipeline, new emphasis.

The scenario
Pool P is a Balancer-style weighted pool holding $800M of a focus token FT (plus counter-assets). Its admin is a weak 2-of-3 Safe. An attacker compromises it and calls a privileged function that sweeps the pool's reserves out to their own wallet. We'll watch the system both quantify the risk in advance and record the drain as it lands.

Before the attack — the standing-exposure cell

This is the half L50 under-played. At build-time the system already discovered Pool P (factory seeder, classified as a pool, L24), the HOLDS edge FT→P at $800M (kept fresh by the LP refresher, L24/L36), and P's admin (L25). So at_risk already emits a Path-3 pool cell (L19):

FieldValueWhy
target_rolepoolP holds FT via HOLDS and is a pool by subtype (Path 3, L19)
at_stake_usd$800Mthe FT value exposed if P's admin/contract fails
extractable_usdmin($800M, available exit liquidity)what the attacker can actually cash out — bounded by L21, self-subtracting P itself
The new emphasis: at-stake ≠ extractable
$800M is at stake, but a pool drain's value has to be sold or borrowed back out somewhere, and that "somewhere" is finite (L21). If FT's other pools, wrapped-bridge routes, and lending-borrow depth total only ~$300M — and the engine self-subtracts Pool P itself (you can't cash out through the venue you just drained) — then extractable ≈ $300M, capped at $800M. The honest "how bad is this really" number isn't the headline TVL; it's the exit-bounded figure. That gap is the whole reason exit-liquidity v2 exists.

The rest of the standing picture: the "pool drain" admin attack channel feeds admin_risk_usd (L13); exposure-BFS propagates FT's risk through the pool weighted by the edge (L16/L40); and a customer rule on FT's venues could already be alerting on this $300M extractable exposure — before any attack. The risk engine is preventive first.

The trace — the drain lands at block N

1
The drain executes — big Transfers out of Pool P. L1 · L7
sweeping the reserves emits ERC-20 Transfer(from: P, to: attacker, …) logs; block N is ingested and streamed.
Idempotency/RPC: at-least-once delivery; block fetched once.
2
Filter — Pool P is monitored. L1 · L2
P was added to the monitored set at discovery, so its Transfers survive the ~95% drop.
3
Decode the canonical event. L3 · L44
this is the everyday ERC-20 Transfer — note the shared-topic0 gotcha (ERC-20 = 3 topics + 32-byte data; ERC-721 = 4 topics) the decoder guards against. from/to from indexed topics, amount from data.
Indexed-topic + fixed-data decode shapes together (L44).
4
Write — P's HOLDS balance collapses. L4 · L9 · L36 · L41
the handler updates the FT→P HOLDS edge: P's balance drops toward zero. The balance cache is updated (plausibility-gated, L36); the write rides the recorder → lone graph-writer.
Idempotency: MERGE + monotonic guard; replaying block N is a no-op.
5
The attacker is promoted into the graph. L2 · L24
the recipient just received a focus-token transfer worth ≫ $1M → the promotion rule adds them to the monitored set; discovery enriches them. The thief is now a tracked, large FT holder.
L2's "$1M transfer promotes a new address" — exactly this path.
6
Conservation confirms: it moved, it didn't vanish. L29 · L32
verify_balance_conservation still finds Σ HOLDS(FT) ≈ totalSupply — the $800M left P and arrived at the attacker; supply is conserved. The LP refresher reconciles P's now-tiny reserves against chain.
The audit layer distinguishes a drain (value relocated) from a data bug (value lost).
7
at_risk recomputes — the target moves. L19 · L21 · L23
next cycle, P's pool cell at-stake collapses (it's empty now), while the attacker's wallet becomes a large direct-holdings cell. The exposure didn't disappear — it relocated from "drainable from P" to "held by the thief."
Combinator: still max-within-pool / sum-across; the dollars are re-attributed, not duplicated.
8
A rule fires. L12 · L45
either the standing extractable-exposure rule that was already ACTIVE (pre-drain), or a balance-change / admin-event rule now tripping → one AlertEvent via the firing state machine.
Idempotency: fire-once debounce — the ongoing exposure doesn't re-alert every cycle.
9
The alert reaches a human. L15 · L18
AlertStream → msg-id dedup → OpenSearch → notifier → the customer's on-call.
Two clocks — the lesson of this trace
A pool drain shows the system has two jobs. Preventive: long before block N, at_risk quantified "$300M extractable from Pool P" and a rule could have alerted, so a customer de-risks first — the exit-bounded number, not the $800M headline, is the actionable one. Recording: when the drain lands, the event pipeline detects it, relocates the exposure to the thief, promotes them, and conservation proves value moved rather than vanished. L50 was mostly the preventive clock; here you see both.

How this trace differs from L50

L50 (signer added to NAV-oracle Safe)L51 (pool drain)
at_risk pathvalue-defining oracle cell (Path 2b / model B)HOLDS-based pool cell (Path 3)
headline numberat_stake = full supply (NAV redemption)extractable ≪ at_stake — exit-liquidity-bounded
decodersafeAddedOwner (Safe-specific)canonical ERC-20 Transfer (+ topic0 gotcha)
discovery twistsigner pulled in as related addrattacker promoted by a ≥$1M transfer (L2)
clockmostly preventive (standing risk)preventive and recording (the drain lands)
You can now run the trace cold
Two attacks, same machine, different emphases — and you narrated each hop with its subsystem and lens. That's the skill the trace drill builds: hand it any attack, and you can walk it end to end, predict which cells/edges move, and know what the honest (exit-bounded) number is. Run a few more on your own and it becomes reflex.

Run it yourself

1. Pool P holds $800M of FT. Why is its cell's extractable_usd likely well below $800M?
2. What target_role does the at_risk cell for Pool P carry, and via which construction path?
3. The drain emits ERC-20 Transfer logs. What decoding gotcha applies (L44)?
4. At step 5 the attacker's wallet is added to the monitored set. Which rule pulls it in?
5. At step 6, verify_balance_conservation still finds Σ HOLDS(FT) ≈ totalSupply after the drain. What does that tell the system?
6. After the drain, what happens to the $800M of exposure in the risk model (step 7)?
7. The lesson frames a pool drain as showing the system's "two clocks." What are they?
8. How does this trace's headline number differ from L50's, and why?
↳ Ask your teacher
Try: "Trace a proxy upgrade, or an oracle mispricing on a DeFi-borrow market (L20 model A)." · "If exit liquidity were near zero, would the standing rule still alert?" · "How fast after the drain would the recording-clock alert actually fire?" · "Quiz me cold: random attack, name the cells and edges that move." · "Where would the attacker's other-asset proceeds show up, if at all?"

What you can now do

The trace is a reusable instrument
Two traces in, the pattern is yours: take any attack, find the standing-exposure cell it perturbs, follow the on-chain event through ingest → decode → write → discovery → recompute → rule → alert, and read off the honest exit-bounded number. This is how you'd reason about a new protocol's risk, or whether a code change moves a number it shouldn't — the working form of "understanding it end to end."

Applies: block-ingest/stream (L1/L7), monitored filter + ≥$1M promotion (L1/L2), ERC-20 Transfer decode + topic0 gotcha (L3/L44), HOLDS write + balance cache + recorder/writer (L4/L9/L36/L41), discovery promotion (L24), conservation + LP refresher (L29/L32/L24), Path-3 pool cell + at_stake/extractable + exit-liquidity self-subtract (L19/L21), admin_risk pool-drain channel + exposure (L13/L16/L40), rule firing + alert delivery (L12/L45/L15/L18); woven with the four syntheses (L46–L49). Verify against source — the code is the truth.