The last L6 deferral: weight propagation, hop by hop. ~11 min.
L6 said exposure is "a BFS over risk edges, bounded by depth and a threshold." We deferred how. Now we open it — and it's a remarkably small, elegant algorithm answering a question you reason about intuitively: "if token X fails, how exposed is a vault three hops away — and by how much?" The answer is a weight that starts at 1.0 and decays as it travels.
Each node gets an exposure weight to the source: 1.0 means "fully exposed" (the source itself); 0 means "untouched." The weight starts at 1.0 at the source and, every hop, is multiplied by the edge's weight (each edge weight ≤ 1). So influence shrinks multiplicatively with distance:
(Illustrative weights.) A node's RiskUSD exposure is then simply weight × that node's USD
price — how many dollars of it ride on the source's fate.
It's a plain weighted BFS (pkg/risk/bfs.go computeNodeExposure):
// start: the source node is fully exposed to itself queue := []item{{idx: startIdx, depth: 0, weight: 1.0}} visited[startIdx] = true for len(queue) > 0 { cur := queue[0]; queue = queue[1:] // FIFO → breadth-first // record this node's exposure — MAX over paths, not sum entry.RiskScore = math.Max(entry.RiskScore, cur.weight) entry.RiskUSD = math.Max(entry.RiskUSD, cur.weight*node.USDPrice) if cur.depth >= maxDepth { continue } // MaxBFSDepth = 6 for _, e := range forwardEdges { if visited[e.Target] { continue } w := cur.weight * e.Weight // ← multiplicative decay if w < minDelta { continue } // minDelta = 1e-6 → prune visited[e.Target] = true queue = append(queue, item{e.Target, cur.depth+1, w}) } }
Source: pkg/risk/bfs.go (computeNodeExposure, minDelta=1e-6, MaxBFSDepth=6, RiskEdges). The "Bardoscia model with equity normalization" note is the header docstring on the ExposureComputer.
w = cur.weight × e.WeightBecause every edge weight ≤ 1, the product only ever shrinks. That is the attenuation model — distance dilutes exposure automatically, no special distance term needed. Two strong links (0.9 × 0.9 = 0.81) propagate more than one weak one (0.4); a chain of weak links dies fast.
entry.RiskScore = max(entry.RiskScore, cur.weight) — a node's exposure is the weight of its
strongest path to the source, not the sum over all paths. This is the same anti-double-counting choice as
L13's max-over-attack-channels (and why a single position can't blow past
its real value into the ">$1T bug" territory). Sum would inflate; max is the honest worst-single-path.
visited[] is set on first enqueue, so each node is processed once — via its shortest path
(BFS). Normally "first visit" ≠ "best." But because weights only decay, the fewest-hops path carries the
highest weight. So the shortest path is the max-weight path, and single-visit BFS computes the exact
maximum — no need to explore every route. The decay model and the BFS order line up perfectly.
| Bound | Value | Effect |
|---|---|---|
MaxBFSDepth | 6 | Never traverse past 6 hops — risk that far is negligible. |
minDelta | 1e-6 | Drop any path whose weight falls below this — the real tractability lever. |
The minDelta prune is what keeps the frontier bounded: in a graph where a node can have thousands of
edges, most propagation paths decay below 1e-6 within a couple of hops and are abandoned. Without it, BFS would
fan out combinatorially. This is the analytical cousin of L2's "bound
depth, no full scans" — here enforced by the weight itself.
Two scoping choices:
RiskEdges — value-bearing edges: HOLDS, POOL_ASSET,
VAULT_ASSET, LENDING_COLLATERAL, WRAP_UNWRAP, VAULT_ALLOCATION.
Exposure flows along containment / collateral (where value actually sits), not along control
edges like ADMIN_CTRL. Who-admins-what is a different risk (that's the L13 admin channels).ComputeForNodes recomputes only the nodes a
graph delta touched (the "delta propagation" in the model name), and stamps the results back as fields the
rule engine can alert on. Same push-based, incremental discipline as L6/L13.
RiskScore exposure to the source is recorded as the max over arriving paths because…RiskEdges (HOLDS, POOL_ASSET, VAULT_ASSET, …) but NOT over ADMIN_CTRL. Why?MaxBFSDepth + minDelta bounds are the analytical version of which earlier rule?minDelta + depth-6 keep a potentially-exponential traversal bounded.RiskEdges in the forward direction, and why it's incremental.Grounded in: pkg/risk/bfs.go (computeNodeExposure weighted BFS, w = cur.weight*e.Weight, max-over-paths, minDelta=1e-6, MaxBFSDepth=6, RiskEdges, Bardoscia/equity-normalization + delta-propagation docstring), exposure.go (ComputeForNodes incremental entry). Verify against source — the code is the truth.