Lesson 40 · Risk-Graph Math · Deeper Track

Where the BFS weights come from

The last gap in the risk engine: each edge's exposure weight is a real economic quantity. ~12 min.

Builds on: L16 · L21 · L39 Anchor: LTV & vault allocation share New: weight = transmission coefficient New: one quantity, three roles

L16 taught exposure as a weight that starts at 1.0 and is multiplied by each edge's weight (≤1) every hop — and then took those per-edge weights as a given. This lesson closes that gap, and the answer is the most satisfying tie-up in the whole risk strand: an edge's weight isn't a tuning knob. It's the real on-chain quantity that says what fraction of a shock crosses that edge — and you've already met both of the important ones.

Your anchor: not all edges transmit equally
Intuitively, if a token a vault holds drops 50%, how much does the vault feel? It depends on the edge. If the vault put 30% of its capital there, it feels 30% of the shock. If a lending market accepts that token at 80% LTV, 80% of the value transmits into borrow exposure. A pure "this pool contains that token" link, by contrast, transmits nothing on its own. The BFS weight is exactly that per-edge transmission fraction — made of numbers you already know.

1 · The assignment — extractWeight, one switch

Every edge's weight is set at graph-load time by a single function (graph.go extractWeight), switching on edge type:

switch edgeType {
case EdgeLendingCollateral:  return g.extractLTV(r)        // the market's LTV (continuous)
case EdgeVaultAllocation:    return sharePct(r)           // the allocation fraction (continuous; %/100 if >1)
case EdgePoolAsset, EdgeVaultAsset, EdgeWrapUnwrap:
                            return 0.0                  // structural carriers — block propagation
default:                    return 1.0                  // e.g. HOLDS — full transmission
}
Edge typeWeightMeaning
LENDING_COLLATERALLTV (≈0.0–0.9)fraction of collateral value that becomes borrow exposure
VAULT_ALLOCATIONshare_pct (0–1)fraction of the vault's capital riding on that target
HOLDS & most others1.0full transmission — a direct value link, no attenuation
POOL_ASSET / VAULT_ASSET / WRAP_UNWRAP0.0structural carriers; topology only, transmit no exposure
So the "decay" is real, but selective
L16's multiplicative decay only actually attenuates on the continuous edges. Cross a HOLDS (×1.0) and the weight is unchanged; cross a VAULT_ALLOCATION at 30% (×0.30) and it drops to a third; hit a structural carrier (×0.0) and propagation dies on the spot (w = cur.weight × 0 < minDelta → dropped). The genuine hop-by-hop fade L16 described comes from the LTV and allocation-share edges; the 1.0 edges pass it through; the 0.0 edges stop it.

2 · One quantity, three roles (the tie-up)

Here's why this is satisfying. The two continuous weights aren't new numbers invented for the BFS — they're the exact same quantities you've already studied driving other parts of the engine:

QuantityIn at_risk…In concentration…In exposure-BFS…
LTVbounds oracle extractable: min(at_stake×LTV, …) (L20); the multi-field LTV behind L21's $2.85B bugthe LENDING_COLLATERAL edge weight (here)
allocation sharethe per-strategy share squared into HHI (L39)the VAULT_ALLOCATION edge weight (here)

And the LTV extraction is literally the same multi-field dance: extractLTV reads ltv / lltv / cf / bcf / max_ltv / lt — the very field-name priority that, when it was a single hardcoded ltv, caused L21's $2.85B Spark phantom. One correctly-extracted economic coefficient, reused three ways.

A worked hop
Token T fails. A vault allocates 40% to a market that lends against T at 75% LTV; T is in a pool too (structural):

1.0T fails
×0.75LENDING
COLLATERAL (LTV)
0.75market
×0.40VAULT_
ALLOCATION (share)
0.30vault
×0.0POOL_
ASSET
stops
The vault carries weight 0.30 of T's shock; RiskUSD = 0.30 × vault price (L16). The pool link transmits nothing.

3 · Direction is also per-edge-type

One companion detail: alongside the weight, edgeDirection[edgeType] sets which way risk flows. Most edges propagate dst_to_src (e.g. LENDING_COLLATERAL, ORACLE_DEP, VAULT_ALLOCATION — risk flows from the thing-depended-on back to the dependent); HOLDS and a few others flow src_to_dst. The loader stamps each edge into the Forward adjacency in its risk-direction, so the BFS (L16) just walks Forward and the direction is already baked in. Weight says how much crosses; direction says which way.

L16 is now fully closed — and so is the risk engine
Exposure propagation had exactly one unexplained input: the per-edge weight. It's the edge's economic transmission coefficient — LTV, allocation share, 1.0 for direct value links, 0.0 for structural carriers — sourced from the same on-chain quantities that drive extractable (L20/L21) and concentration (L39). Bardoscia contagion (L16) modelled not with invented constants but with the protocol's own risk parameters. There is no remaining black box in the risk math.

Check yourself

1. What is an edge's exposure-BFS weight, conceptually?
2. What weight does a LENDING_COLLATERAL edge carry?
3. A VAULT_ALLOCATION edge's weight is the allocation share_pct. Where have you seen that exact quantity before?
4. POOL_ASSET / VAULT_ASSET / WRAP_UNWRAP edges get weight 0.0. What's the effect on propagation?
5. L16 said exposure decays multiplicatively each hop. Given the weights, where does the genuine attenuation come from?
6. extractLTV reads ltv / lltv / cf / bcf / max_ltv / lt rather than a single ltv field. Why the multi-field read?
7. A vault allocates 40% to a market lending against T at 75% LTV. What exposure weight does the vault carry to T's failure?
8. Besides the weight, edgeDirection is set per edge type. What does it determine?
↳ Ask your teacher
Try: "Show me extractPropagationLTV's field-priority order." · "Why does HOLDS transmit at 1.0 but POOL_ASSET at 0.0 — aren't both 'value-bearing'?" · "How is edgeDirection used to build Forward vs Reverse adjacency?" · "Could a malformed share_pct > 1 inflate exposure, and what guards it?" · "Where do DebtRank / Impact reuse this same weighted graph?"

What you can now do

Risk-math deep dive: complete
Three lessons closed the bucket: node_risk_score's governance noisy-OR and its stubs (L38), HHI concentration and its accounting (L39), and now the BFS edge weights (L40) — which also closed L16's last deferral. Combined with L6, L13, L16, and L19–L23, every number the risk engine produces is now derivable to its source, with no parameter left unexplained.

Grounded in: pkg/risk/graph.go (extractWeight — LENDING_COLLATERAL→extractLTV, VAULT_ALLOCATION→share_pct (%/100 if >1), POOL_ASSET/VAULT_ASSET/WRAP_UNWRAP→0.0, default→1.0; extractLTVextractPropagationLTV(ltv,lltv,cf,bcf,max_ltv,lt); edgeDirection per-edge-type risk-flow → Forward/Reverse adjacency; RiskEdge.Weight consumed by L16's computeNodeExposure), edge_weight_parity_test.go (structural edges 0.0, defaults 1.0). Verify against source — the code is the truth.