| name | generating-dependency-graphs |
| description | Procedure for authoring contract dependency graph YAML files (reports/graph/<slug>.yaml) from a completed risk assessment report. |
| allowed-tools | Read Write Edit Grep Glob Bash(npm:*) Bash(git:*) |
Generating contract dependency graphs
Each risk report in reports/report/ documents a protocol's contracts and their relationships in prose. A dependency-graph YAML in reports/graph/ makes those relationships visual at /graph/<slug>/ on the website (linked from /report/<slug>/ automatically when a YAML exists).
This skill turns a finished risk report into the corresponding YAML.
Follow the project workflow in CLAUDE.md: pull latest master before starting and open a draft PR when the graph task is ready.
When to invoke
- After a risk report at
reports/report/<slug>.md is finalized and the next natural step is to publish its graph.
- To backfill an existing report that has no graph YAML yet.
- Skip if
reports/graph/<slug>.yaml already exists — unless explicitly updating it (e.g. after a reassessment changed the strategy mix).
Inputs
The corresponding reports/report/<slug>.md. The sections worth mining:
- Contract Addresses tables — the primary node list.
- Funds Management — strategy allocation percentages, the underlying protocol each strategy targets.
- Centralization & Control → Governance — multisigs, timelocks, role assignments, the access-control root contract.
- External Dependencies — the protocols sitting underneath the vault's strategies.
- Appendix: Contract Architecture (when present) — pre-drawn architecture diagram is often the cleanest source of edges.
You should not need to make new onchain calls or open Etherscan — the report has already verified everything.
File layout
- One YAML per report slug at
reports/graph/<slug>.yaml.
- Same slug as the
.md file. The URL /graph/<slug>/ is generated by src/pages/graph/[slug].astro via getStaticPaths().
- The report page automatically renders a "View dependency graph →" link when
hasGraph(slug) returns true — no code change needed.
Schema
The schema is enforced at build time by the validator in src/lib/graph.ts. Authoritative field list (mirror of the TypeScript types in that file):
| Top-level | Required | Type | Notes |
|---|
slug | yes | string | Must match the filename and the report slug. |
title | no | string | Shown in the page header. Defaults to the slug. |
chain | yes | string | Default chain for nodes. ethereum, polygon, base, arbitrum, sonic, katana, hyperevm. |
categories | yes | list | Subset of the fixed 5 categories below. Only declare what the graph uses. |
nodes | yes | list | See node fields. |
edges | yes | list | See edge fields. |
Node fields:
| Field | Required | Type | Notes |
|---|
id | yes | string | Unique within the file. Slug-style, e.g. susds-lender. |
label | yes | string | Card title. Keep ≤ ~40 chars; it wraps to 2 lines. |
category | yes | enum | One of the 5 below. |
address | no | string | EVM address. Used to build the Etherscan link on click. |
chain | no | string | Per-node chain override; falls back to top-level chain. |
note | no | string | Free-form context shown in tooltips (future use); useful when address is absent. |
Edge fields:
| Field | Required | Type | Notes |
|---|
from | yes | string | Must match a node id. |
to | yes | string | Must match a node id. |
kind | yes | enum | One of the 10 below — drives edge color/style and legend grouping. |
label | no | string | Optional inline label. Only allocates-to always carries one (the % share); other kinds carry labels only when the value adds information. |
Minimal example (from reports/graph/yearn-yvusdc.yaml):
slug: yearn-yvusdc
title: yvUSDC-1 — Contract Dependency Graph
chain: ethereum
categories:
- { id: vault, label: Vault }
- { id: strategy, label: Strategy }
- { id: dependency, label: External Dependency }
nodes:
- id: yvUSDC
label: yvUSDC-1
address: "0xBe53A109B494E5c9f97b9Cd39Fe969BE68BF6204"
category: vault
- id: susds-lender
label: USDC → sUSDS Lender (97.15%)
address: "0x7130570BCEfCedBe9d15B5b11A33006156460f8f"
category: strategy
- id: sky-susds
label: Sky / sUSDS (Savings Rate)
address: "0xa3931d71877C0E7a3148CB7Eb4463524FEc27fbD"
category: dependency
edges:
- { from: yvUSDC, to: susds-lender, kind: allocates-to, label: "97.15%" }
- { from: susds-lender, to: sky-susds, kind: deposits-into, label: "USDS → sUSDS" }
Vocabulary
These enums are fixed — extending them requires coordinated styling changes in src/pages/graph/[slug].astro (the .cat-* CSS rules and the flow/role/gov sets in the client script). Don't invent new values ad-hoc.
Categories (5)
id | Use for |
|---|
vault | The user-facing vault token(s) and the receipt/share tokens. The "what the user holds" layer. |
strategy | An active investment leg — Yearn V3 strategies, InfiniFi farms, looper positions. |
governance | Multisigs, timelocks, access-control root contracts, role-managers, governance tokens with control rights. |
infra | Internal protocol machinery that isn't itself governance — mint/redeem controllers, debt allocators, keepers, accountants the graph still benefits from showing. |
dependency | External protocols the strategies sit on top of — Sky/sUSDS, Spark, Morpho vaults, Maple pools, Midas tokens, escrow counterparties. |
Edge kinds (11)
Grouped by the visual style they get (this matches the legend on the rendered page). Three of them — allocates-to, deposits-into, routes-through — are flow kinds, which have extra effects (see "Flow kinds" below the tables).
Money flow — solid teal lines, prominent:
kind | Meaning | Label renders on page? |
|---|
allocates-to | Vault → strategy. The vault holds debt in this strategy. | Yes — always set it to the % share. |
deposits-into | Strategy → underlying protocol. The strategy parks USDC into this venue. | Yes — optional but useful (e.g. "USDC → aUSDC"). |
Mint authority — red dashed lines, highest-trust signal:
kind | Meaning | Label renders on page? |
|---|
mints | Account/contract holds privileged authority to create new supply of the target token. Direction: minter → token. Pair the minter with a node (typically infra for protocol contracts, governance if a multisig holds the mint role directly). One mints edge per role-holder per token. The absence of mints edges in a graph is itself meaningful — it means mint is permissionless (e.g. open ERC-4626) or gated only by collateral deposit. | No (documentation-only). Use the label to record the role name (e.g. RECEIPT_TOKEN_MINTER, MINTER_ROLE). |
Role / control — amber dashed lines:
kind | Meaning | Label renders on page? |
|---|
holds-role | An account holds a specific privileged role on the target. | No (documentation-only). Still write the role name in YAML — it helps reviewers reading the file. |
controls | One contract administers another (e.g. timelock → access-control root). | No (documentation-only). |
manages | A registry/factory manages the target contract. | No (documentation-only). |
Governance signaling — violet dotted lines:
kind | Meaning | Label renders on page? |
|---|
proposes-on | Multisig → timelock (PROPOSER role). | No (documentation-only). |
cancels-on | Multisig → timelock (CANCELLER only, no proposer). | No (documentation-only). |
Incidental wiring — thin gray lines:
kind | Meaning | Label renders on page? |
|---|
routes-through | A → B, where B is a hop in the flow (PSM, exchanger, hook). | No (documentation-only). |
routes-fees-to | Vault → accountant → fee recipient. | No (documentation-only). |
deploys | Factory → deployed contract. | No (documentation-only). |
Heads-up on labels: only allocates-to and deposits-into render their label on the canvas. For every other kind, the label is captured in the YAML for the reviewer who reads the file — but it won't appear on the rendered graph. Use labels generously in YAML (they document intent), but don't expect them on screen for non-flow kinds.
Flow kinds
allocates-to, deposits-into, and routes-through form the flow set. Three behaviors depend on it:
- Hover-chain highlight — hovering a card walks the backward chain via flow edges only. So if you want a contract A to appear when a reader hovers contract B downstream of it, connect them via a flow kind. Governance/role edges never participate in the chain.
- Cross-graph downstream expansion — when another graph cross-links to a node in yours, the inlined subgraph in their page traces forward through your flow edges only. Use flow kinds for everything that represents capital movement so the downstream view is complete.
- Visual styling — flow edges render solid and slightly thicker/greener than other kinds.
The corollary: don't use manages or routes-through interchangeably. routes-through participates in flow; manages doesn't.
Selection rules
Most reports have 20–40 contracts; a readable graph has 15–30 nodes. The rules below say which to keep and which to drop.
Include
- The user-facing vault token(s) — for InfiniFi, that's
siUSD + iUSD + the locking position. For yvUSDC, just the vault itself.
- Every multisig and timelock that sits on the governance path to the vault. Also the access-control root contract if one exists (e.g.
InfiniFiCore, Yearn RoleManager).
- Every active strategy/farm carrying meaningful debt. Cut-off: roughly ≥1% of TVL or ≥1% of vault debt. Always include any "queued but unfunded" strategy if the report flags it as material context.
- The external protocol each included strategy deposits into. If the report only names the underlying without a contract address, include the node without
address (e.g. "Spark Lend USDC Market" in yvUSDC).
Skip
- Helper / hook contracts that don't shape the dependency structure: accountants, fee dumpers, oracle factories, profit smoothers, after-mint / before-redeem hooks, matured-farm cleaners. They clutter the graph without adding information.
- Dust farms (<1% of TVL) and matured/inactive farms (
activation = 0). Mention them in the report; don't draw them.
- Internal role-spaghetti. When an access-control root contract grants roles to 10+ contracts, don't draw 10 edges from it. Pick one symbolic edge to a key managed contract (typically the vault or main entry proxy) and rely on the report's role table for the full picture.
- Yearn V3 infrastructure that's identical across all V3 vaults (Vault Factory, Tokenized Strategy implementation) — include only if the report singles them out for risk reasons.
Group
When two contracts function as a single dependency, represent them as one node with a note. Examples:
- An RWA escrow EOA + its onchain rate manager = one
RWA Escrow Counterparty node with note: "EOA receiver + rate manager".
- A Yearn strategy that uses a PSM hop and an exchanger to reach the same destination = one edge
routes-through to the destination, not two routes-through edges through the intermediaries (unless either intermediary is itself a risk surface worth showing).
Authoring procedure
- Read the report end-to-end. Especially the Contract Addresses tables, the Centralization → Governance section, the Funds Management allocation table, and the External Dependencies block.
- List candidate contracts from the Contract Addresses tables. For each one, decide include/skip/group per the rules above.
- Pick categories for every included contract. The 5-category enum is the only allowed source.
- Draft nodes. Use slug-style
ids, address from the report, label kept short (≤40 chars including any percentage suffix).
- Draft edges in this order so you don't miss any:
- Governance flow — multisig → timelock(s) → access-control root → vault. Add any direct multisig → vault edges for non-timelocked roles (PAUSE, EMERGENCY_WITHDRAWAL).
- Internal vault flow — Gateway/mint/redeem controllers → token, staked-token → receipt-token, locking → receipt-token.
- Allocation fan — vault (or receipt token) → each strategy with the
% label.
- Dependency fan — each strategy → its external dependency.
- Verify locally (see next section). Iterate on labels and skips until the graph reads cleanly left-to-right.
Verification
- From the repo root, start the dev server:
npm run dev
(Astro 6 needs Node ≥ 22.12. If nvm defaults to an older version, prefix the command with the right PATH.)
- Open
http://localhost:4321/graph/<slug>/. Confirm:
- Every node from the YAML appears as a card.
- No console errors.
- The graph reads left-to-right: governance on the left, vault / strategies in the middle, external dependencies on the right.
- Allocation
% labels are visible on the vault → strategy edges.
- Clicking a node opens the explorer in a new tab.
- Open
http://localhost:4321/report/<slug>/. Confirm the "View dependency graph →" link appears next to the GitHub link.
npm run build — confirms the validator (getGraphBySlug in src/lib/graph.ts) accepts the file. The build will fail with a clear error if any edge endpoint doesn't match a node id or any node uses an unknown category.
Worked examples
reports/graph/yearn-yvusdc.yaml — single-vault, single-token, hierarchical governance. The cleanest starting point when copying.
reports/graph/infinifi.yaml — multi-token (siUSD / iUSD / liUSD), 9-strategy fan-out, dual timelock, includes an offchain EOA counterparty node. Reference this when the protocol has more than one user-facing token or a long farm list.
Cross-graph linking
When a node's address matches a non-dependency node in another graph (i.e. that contract is the vault / strategy / governance / infra of another graph), three things happen automatically at build time. No new YAML fields are needed — matching is purely address-based, computed in getGraphIndex() in src/lib/graph.ts.
What you get for free
- Score-color tinted card. The cross-linked card gets a background gradient and border in the linked report's risk-tier color (
scoreColor() from src/lib/colors.ts). Low-risk dependencies show light green; high-risk would show orange/red. A reviewer reading the host graph sees risk at a glance without leaving the page.
- Risk pill on the card. A small badge with the linked report's
Final Score and tier ("2.4 · Low") sits next to the truncated address.
- Whole card click navigates to
/graph/<linked-slug>/ instead of opening the block explorer. A tiny ↗ icon in the corner keeps the explorer reachable for that specific case.
- Downstream subgraph is inlined. The linked graph's nodes reachable from the matching node via flow edges only (
allocates-to → deposits-into → routes-through) are pulled into the host graph at build time. They appear at ~78 % opacity to mark the boundary, and they keep their original categories. Their IDs are namespaced (<linked-slug>::<original-id>) so they never collide with native IDs.
Example. InfiniFi's dep-stcusd node and Cap's stcUSD node both carry 0x88887bE…D8888. On the InfiniFi page:
- The stcUSD card tints green (Cap = Low Risk).
- Clicking it navigates to
/graph/cap-stcusd/.
- Behind stcUSD, you see cUSD → USDC FRV / Lender / wWTGXX FRV → Morpho / Aave / Operators / Symbiotic / wWTGXX inlined automatically — about 9 extra cards.
Scope is one layer deep
If your graph references protocol X, and protocol X's graph references protocol Y, only X's downstream is inlined — Y's is not. This is intentional; recursive expansion would make every graph balloon to the sum of all linked graphs. Visit /graph/X/ if you need to drill further.
Implications for authors
- Be careful with addresses. A typo or chain mismatch breaks the cross-link silently — no error, just no badge or expansion. When authoring a dep node that corresponds to a protocol that has its own graph, copy the address exactly (checksummed form is fine; matching is case-insensitive but verify the digits).
- Flow kinds matter for the other graph too. If your graph may be referenced by others as a dependency, the flow edges (
allocates-to / deposits-into / routes-through) you draw are what shows up inlined in those graphs. Keep your flow chain clean and complete — governance edges won't make it across, but a missing deposits-into will leave a dependency invisible.
- Don't manually duplicate the linked graph's nodes in your YAML. The expansion adds them for you. If you do duplicate, you'll get two cards: one native and one inlined.
Suppressing a cross-link
Omit address from the node. Without an address the matcher can't fire — the card behaves as a plain leaf. Useful when you want a dependency to open Etherscan instead of navigating away (rare).
Hover & chain highlight
Hovering any card walks the backward money-flow chain from that node — recursively, following only flow kinds (allocates-to, deposits-into, routes-through) — and highlights every edge and ancestor card in blue. Governance edges (holds-role, controls, manages, proposes-on, cancels-on) and incidental edges (routes-fees-to, deploys) are not traversed.
Example on InfiniFi: hovering the inlined cUSD (Cap Stablecoin) lights up:
cUSD ← stcUSD ← (CapFarm + SwapFarm) ← iUSD ← (siUSD + LockingController)
— the full backward chain. Mint/Redeem controllers connect to iUSD via manages edges, so they stay un-highlighted; that's correct — the chain is for capital movement.
Implications for authors
- Edge kind affects traceability. If you want a relationship to participate in the chain (and in cross-graph expansion), use a flow kind. If the relationship is structural but not capital movement (a role grant, a registry pointer), use a non-flow kind so it doesn't pollute the chain.
- Make every node reachable through flow edges from a source node. Otherwise hovering it produces an empty chain. This is rarely a problem for vaults/strategies/dependencies — they live on the flow path by construction — but worth checking for any infra node you decide to include.
Common pitfalls
- Duplicate node
id — build fails with [graph:<slug>] duplicate node id '...'.
- Edge endpoint doesn't match any node — build fails with
[graph:<slug>] edge.from '...' does not match any node (or .to). Most common cause: typo in an id or an edge to a contract you decided to skip.
- Unknown category — build fails with
[graph:<slug>] node '...' has unknown category '...'. Stick to the 5-category enum.
- Unknown / typo'd edge kind — build fails with
[graph:<slug>] edge '<from>' → '<to>' has unknown kind '<kind>'. The error lists all allowed kinds. Validated against ALLOWED_EDGE_KINDS in src/lib/graph.ts, which is the canonical source of truth for the 10-key enum in the Edge kinds table above.
- Address mismatch breaks cross-linking silently. If your dependency node's
address doesn't match the linked graph's vault address exactly (case-insensitive, but every digit must be right), the cross-link and downstream expansion just don't happen — no build error, no warning. When in doubt, copy the address from the source graph or the report's contract-addresses table verbatim.
- Wrong edge kind hides a relationship from chains/expansion. A
manages or holds-role edge that should have been allocates-to will still render, but it won't participate in the hover chain or in cross-graph downstream expansion. Use flow kinds for anything that represents capital movement.
- Visual noise from over-labeling. Labels render only for
allocates-to and deposits-into. Labels on other kinds are documentation-only (visible in YAML, not on canvas) — fine to include for reviewer context, but don't expect them on screen.
- Invented bright colors / new categories — if a contract doesn't fit one of the 5, it's almost always
infra (internal machinery) or dependency (external). Don't add a new category without updating src/pages/graph/[slug].astro too.