Lesson 13 · The Risk Math · Deeper Track

How the risk numbers are actually computed

The formulas behind the fields — opening the box deferred in L6. ~13 min.

Builds on: L6 · L12 Anchor: DeFi attack value New: severity weights New: max vs noisy-OR

L6 told you risk fields exist; L12 showed rules fire on them. Now we open the box: how is admin_risk_usd = $8.4M actually computed? The answer is satisfyingly concrete — and it turns on one core decision the system makes twice, two different ways: how do you combine many partial risks into one number?

1 · Two quantities, two combination rules

The engine computes two kinds of risk number, and they're combined differently because they mean different things:

QuantityUnitCombined byWhy
Extractable value (admin_risk_usd)USDmax over channelsAn attacker picks their best single attack — worst case = the most they could steal.
Node risk (node_risk_score)0..1noisy-OR over channelsIndependent risk factors compound — more weak spots = higher probability something goes wrong.
The one idea to carry
Dollars combine by max (worst single path); probabilities combine by noisy-OR (compounding). If you remember nothing else, remember that the unit dictates the combinator. We'll see both below.

2 · Severity weights: not all admin power is equal

Before the channels, the ingredient they all use. An admin role carries a severity — a minter that can inflate supply is catastrophic; a pauser is mild. Four tiers map to weights (pkg/types/role_severity.go):

1.0Critical
DEFAULT_ADMIN, minter
0.7High
upgrader, curator
0.3Moderate
0.1Low
pauser-ish

These weights gate and scale the attack channels: a minter (critical, 1.0) is treated as controlling 100% of supply; a "rug pull" channel only counts admins with severity ≥ 0.7; the lending channel needs ≥ 0.3. Severity is how the graph's ADMIN_CTRL edges (L2/L5) get a dollar teeth.

Source: pkg/types/role_severity.go (SeverityWeight: critical 1.0 / high 0.7 / moderate 0.3 / low 0.1), with role→severity registrations (DEFAULT_ADMIN_ROLE = critical, …) carefully kept at parity with the Python regexes.

3 · Extractable value: the threat model (your DeFi anchor)

admin_risk_usd answers a question you ask instinctively: "if this key turns evil, what's the most it could steal of this token?" For each (focus_token, wallet) the computer evaluates 6 attack channels (pkg/risk/admin_risk.go, "matches extractable.py"):

#ChannelExtractable USD =
1Direct holdingsHOLDS(token→wallet) USD — what it simply owns
2Admin controlADMIN_CTRL to token × severity; minter ⇒ 100% of supply
3Pool drainco-token minter in a shared pool
4Rug pullpool/vault admin with severity ≥ 0.7
5Oracle manipulationoracle admin (mis-price → drain)
6Lending controllending-market admin with severity ≥ 0.3
admin_risk_usd (per token, per wallet) admin_risk_usd = max( ch₁, ch₂, ch₃, ch₄, ch₅, ch₆ )
Why max, not sum — a modeling choice you'll appreciate
You can't both rug the vault and mint infinite supply and manipulate the oracle for fully additive value — the channels overlap and an attacker realistically executes their single best path. So the model takes the worst single channel, not the sum. Summing would double-count the same underlying TVL and produce the "> $1T position" nonsense CLAUDE.md warns about. Max is the conservative, non-double-counting choice.

4 · Node risk: compounding probabilities (noisy-OR)

node_risk_score is a 0..1 likelihood-flavoured score from component risks like governance_risk and longevity_risk. Here the real code does not take a max or a sum — it combines them as independent failure probabilities (node_risk_score.go):

composite node_risk_score composite = 1 − (1−gov)·(1−lon)·(1−audit)·(1−firewall)

This is noisy-OR: "the probability something is wrong = 1 − probability everything is fine." It's monotonic (any factor ↑ pushes the score ↑), bounded in 0..1, and rewards being clean on all axes. governance_risk itself blends admin-role severity + multisig threshold + key-rotation history; longevity_risk penalises young / unverified / unaudited contracts. EOAs get governance_risk = 0 — they are the key, not a thing controlled by one.

⚠️ Code > docs (a real discrepancy worth seeing)
The field catalog (rule-fields.json) describes node_risk_score as "max across 6 channels." The code is the noisy-OR product above over four terms — and right now audit and firewall are hardcoded 0.5 placeholders. So today: composite = 1 − 0.25·(1−gov)·(1−lon). This is exactly why this course's refrain is "verify against source — the code is the truth": the human-facing doc is a simplification, and two channels are stubs awaiting implementation. A deep reader checks the formula, not the description.

5 · Concentration: the HHI (a clean, classic formula)

vault_concentration asks the curator's question: is this vault's capital dangerously piled into one strategy? It uses the Herfindahl-Hirschman Index over each strategy's USD share (vault_concentration.go computeHHI):

concentration HHI = Σ shareᵢ²   ·   EffectiveStrategies = 1 / HHI

If all capital sits in one strategy, every other share is 0 and HHI = 1²= 1 (max concentration); spread evenly across n, each share is 1/n and HHI = n·(1/n)² = 1/n. The reciprocal 1/HHI is beautifully intuitive: the effective number of equally-sized strategies. HHI 0.5 → "effectively 2 strategies"; 0.1 → "effectively 10." One number, instantly readable.

Source: pkg/risk/vault_concentration.go (computeHHI = sum of squared shares; EffectiveStrategies = 1/HHI; top-strategies list capped at 20 for JSON, scalars cover all priced strategies).

6 · Where the math runs: partial graphs, parity-locked

Two structural facts tie this back to the architecture:

The risk math, in one breath
Severity-weight the ADMIN_CTRL edges → run each focus token's neighbourhood through attack channels (max for USD extractable) and risk factors (noisy-OR for 0..1 scores) plus HHI for concentration → stamp the fields → the rules engine fires on them. Threat-model math over an anchored subgraph, locked to Python parity.

Check yourself

1. Why is admin_risk_usd the max over attack channels, not the sum?
2. A minter role on a focus token contributes which extractable value in the admin-control channel?
3. The composite node_risk_score = 1 − (1−gov)(1−lon)(1−audit)(1−firewall) is which kind of combination?
4. rule-fields.json calls node_risk_score "max across 6 channels," but the code is a 4-term noisy-OR with audit/firewall = 0.5 stubs. What's the lesson?
5. Severity weights (critical 1.0 / high 0.7 / moderate 0.3 / low 0.1) are used to…
6. A vault with HHI = 0.25 has roughly how many "effective" equally-sized strategies?
7. The risk computers run per focus token on a LoadPartialGraph neighbourhood because…
8. Why do the comments insist each computer "matches extractable.py / node_risk_score.py exactly"?
↳ Ask your teacher
Try: "Show me the admin-control channel code in admin_risk.go," · "How is governance_risk's multisig-threshold term computed?" · "What's a real parity test for these formulas?" · "Walk me through exposure BFS weight propagation (the L6 deferral)."

What you can now do

Layer complete — L6 fully opened
You no longer just know that the risk engine produces fields — you know how each number is built, why the combinators differ, and where the formulas can mislead if you trust the docs over the code. The risk engine is now fully transparent, from cell decomposition (L6) to the per-field arithmetic (here).

Grounded in: pkg/risk/admin_risk.go (6 attack channels, max extractable, "matches extractable.py"), pkg/risk/node_risk_score.go (noisy-OR composite, audit/firewall=0.5 stubs, EOA governance=0), pkg/risk/vault_concentration.go (computeHHI = Σ share², EffectiveStrategies=1/HHI), pkg/types/role_severity.go (severity weights 1.0/0.7/0.3/0.1), docs/rule-fields.json. Verify against source — the code is the truth.