How a focus token's price feed becomes cells — the gnarliest of the six construction paths. ~13 min.
L19 named the six cell-construction paths but treated them as a black box: "decompose the token,
out come cells." Now we open one — the oracle path — all the way down. It's the richest, because a single
target_role = "oracle" hides two completely different attacks, and because the most
counter-intuitive idea in the whole engine lives here: an oracle attack usually doesn't steal the focus token at all.
target_roleBoth emit cells with target_role = "oracle", but they're different threat models with different code paths,
different extractable math, and different value pools:
emitOracleCellsViaToken
Oracle overprices T → a lending market that holds T as collateral lets you borrow other assets against the inflated value → you walk off with the other asset, not T.
Needs: a market that BOTH consumes the oracle AND holds T as collateral.
emitValueDefiningOracleCells
For RWA tokens (BUIDL, USTBL, USYC…) whose price is a NAV feed. Publish a bad NAV → redeem/mint T against the issuer's reserve at the wrong rate → drain the whole supply.
Consumer is the token itself — no third-party market needed.
emitOracleCellsViaToken walks every oracle attached to the focus token, then finds eligible markets —
the ones that both consume that oracle and hold T as collateral:
for oracleID := range obs.OraclesPerToken[tok.Addr] { for _, marketAddr := range oracleConsumerMarkets[oracleID] { info := lending[tok.Addr][marketAddr] if info == nil { continue } // market doesn't hold T → not eligible stake := math.Max(info.Avail, info.MaxBorrow) // at_stake = borrowable surface ... } if len(eligible) == 0 { /* honest-zero sentinel */ } }
For each eligible market it emits two cells — the same admin/contract pairing you saw in L19:
ADMIN_CTRL into the oracle (failure_source = admin) — "someone who controls the oracle key".failure_source = contract) — "the oracle contract is exploited / buggy". Carries discovery_gap = true if no admin was found at all.Each carries scope_market = <market>, the per-market oracle_ltv, and oracle_other_borrowable_usd — the inputs the extractable step needs.
min(at_stake × LTV, other_borrowable)// at_risk_extractable.go — the Phase 2.8.b branch switch { case scopeMarket == "": ex, reason = 0, "oracle_no_consumer_market" case ltv <= 0: ex, reason = 0, "oracle_market_no_borrow_ltv" // e-mode / collateral-only default: ex = math.Min(at*ltv, otherBorrowable) // borrow surface ∧ actual cash in market }
Two real ceilings, multiplied and clamped: at_stake × LTV is the legitimate borrow the fake price unlocks;
other_borrowable is the actual other-asset cash sitting in the market to walk away with. You can't borrow more
than the LTV allows, and you can't take more than is actually there.
emitValueDefiningOracleCells fires off a single node attribute — T.attrs.value_defining_oracle — stamped by
the enrichment layer (navlink_refresher.go). The cell shape is simpler: the consumer is the token, so
at_stake = T.SupplyUSD (the whole supply), scope_market = nil, contributing_venues = [token_addr],
and observed = true unconditionally (the attack surface is live as long as the token has supply).
// extractable for a value-defining (NAV) cell if exitTotal > 0 { ex = math.Min(at, exitTotal) reason = "nav_oracle_onchain_redemption_capped" } else { ex = 0 reason = "nav_oracle_offchain_redemption_not_modeled" // BUIDL/WTGXX: bank-wire KYC redemption }
else. For tokens like BUIDL or iBENJI, redemption is off-chain (KYC bank wire) — the on-chain
metric genuinely can't measure it, so it reports $0 with an explicit reason rather than silently emitting
nothing. Before #88 these tokens produced no oracle cell at all; the surface was invisible. The honest-zero sentinel
in model A (at_stake = 0 when no eligible market exists) is the same philosophy: a visible $0 beats a silent
gap. (at_risk_cells.go / at_risk_extractable.go.)
Downstream code must know which model a cell came from. The single source of truth is isValueDefiningOracleCell
(at_risk_cells.go), a pure predicate on cell shape:
func isValueDefiningOracleCell(c *Cell) bool { return c.TargetRole == TargetRoleOracle && c.ScopeMarket == nil && // model A pins a market; model B doesn't len(c.ContributingVenues) == 1 && c.ContributingVenues[0] != c.TargetID // venue is the TOKEN, not the oracle (≠ sentinel) }
And here's the subtle part from L19's dedup story: oracle cells use a 5-tuple key that omits target_id
(at_risk_types.go CellKey doc). That's deliberate — it consolidates model A's per-(oracle,market)
cells with model B's value-defining cells so the same oracle isn't counted twice. And in the rollup, NAV cells collapse to
value_pool_id = "focus_token:<addr>" — the same pool as a focus-token admin compromise, because both ultimately
drain the issuer's reserve. Max-within-pool (L19) then refuses to double-count those dollars.
value_pool_id. Model A's oracle cells scope to the market; model B's NAV cells scope
to the token. Both routes to the same dollars are collapsed by MAX. The oracle path is precisely where the overlap
problem is worst — and precisely why the collapse machinery exists.
at_stake = T.SupplyUSD (whole supply) for a value-defining NAV cell, but only the market's borrowable surface for a model-A cell?isValueDefiningOracleCell keys on scope_market == nil and contributing_venues[0] != target_id. Why centralize this predicate?value_pool_id = "focus_token:<addr>" — the same pool as a focus-token admin compromise. The effect in the rollup is…target_role = "oracle": DeFi-borrow (model A) and NAV-redemption (model B).min(at_stake×LTV, other_borrowable) vs min(at_stake, exit_total) / honest-$0.isValueDefiningOracleCell, the 5-tuple key, and the focus_token value-pool keep the two models from double-counting (ties to L19).Grounded in: pkg/risk/at_risk_cells.go (emitOracleCellsViaToken Phase 2.8.b, emitValueDefiningOracleCells #88, isValueDefiningOracleCell, eligibility = consume∧hold, admin+contract cell pairing, honest-zero sentinel), at_risk_extractable.go (oracle branch: min(at×ltv, other_borrowable); NAV: min(at, exit_total) / nav_oracle_offchain_redemption_not_modeled), at_risk_types.go (oracle 5-tuple CellKey), at_risk_value_pool.go (NAV → focus_token pool). Verify against source — the code is the truth.