Add yTranche indexing and REST support#445
Draft
murderteeth wants to merge 11 commits into
Draft
Conversation
Adds the Ethereum yTranche deployment to the indexer's configuration. - `yearn/3/tranche/controller` — the TrancheController ABI, registered as a static chain-1 source at its creation block. Its zero-argument state (VAULT, ASSET, reserveVault, tranchesLength, totalClaims, vaultAssets, reserveAssets, backingAssets, ...) is picked up by the automatic snapshot, and its events index through automatic event extraction. - `yearn/3/tranche/vault` — the tranche ABI: the TokenizedStrategy interface unioned with the base and locked tranche implementations, so one ABI covers both deployed variants. Registered as `tranche` things. - `yearn/3/tranche/hook` — a read-only interface for the Hook contract. The Hook owns no thing and no snapshot; the tranche snapshot hook reads it through this interface. Tranches also carry `vault` and `strategy` labels, which the generic `yearn/3/vault` and `yearn/3/strategy` paths would otherwise process. Both filters now exclude things marked `tranche`, so every tranche address has exactly one snapshot owner. The controller and tranche paths are siblings rather than nested under a shared `yearn/3/tranche` hook path: kong's hook resolver applies a parent path's hooks to all child paths, which would cross-wire the two. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
…ting The controller snapshot hook walks `tranchesByPriority(index)` for `tranchesLength()` slots, so tranches are found through the controller with no hardcoded addresses and each entry keeps its priority. For every tranche it reads `tranches()`, `liveAssets()` and `trancheCoverage()` and appends an ordered `tranches` array to the snapshot's hook data. All appended reads use the automatic snapshot's block, so the stored row describes one point in time. Discovery also projects the things the rest of the pipeline needs. Each tranche gets `tranche`, `vault` and `strategy` things sharing one set of defaults: controller, priority, type, asset, main vault, inception and the erc4626 / v3 / yearn flags. `trancheType` records the implementation — base or locked, decided by whether the tranche exposes cooldown configuration — rather than the A/B/E deployment nicknames, which are configuration and not protocol types. Four defaults go beyond the discovery set because existing shared code requires them: `decimals` and `apiVersion` (the tvl and apy calculations parse them off the thing), and `name`/`symbol`, which keep the vault list cache from seeing a null name in the window between a tranche's first thing and its first snapshot. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Hook state is parameterized by tranche, so the automatic snapshot can't reach it — it only captures zero-argument reads, which for a tranche means the raw `hook` address. This hook reads that Hook at the same block with the tranche as target and appends `hookState`: the global gate and rate-limit window, the tranche's deposit limit, its fixed-window deposit and withdrawal counters, and the derived deposit and withdrawal caps. `hook` stays the address and `hookState` stays separate, so a reader can always tell which Hook produced the state. Counters are stored exactly as reported; whether a window has expired depends on the timestamp a consumer is answering for, so that derivation belongs to the consumer. The Hook gets no thing and no snapshot of its own, and no account-scoped state is read. A tranche pointing at a hook that doesn't implement the interface logs and skips enrichment rather than failing the snapshot — the raw address is stored either way, and that's what a reader needs to investigate. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
A vault's asset balance has one authoritative source, and for a tranche it is not the vault: the controller holds the accounting. A tranche's own totalAssets() reports its ERC-4626 balance, while the controller knows what it has accrued and what losses it has absorbed. New `abis/yearn/lib/assets` resolves both cases from a vault thing and a block — totalAssets() normally, `trancheController.liveAssets(vault)` when the thing carries a controller — so tvl, pps and apy can no longer disagree about how much a vault holds. The same module owns the shared pps reader. Ordinary vaults keep reading pricePerShare(); tranches are priced as authoritative assets × share scale ÷ totalSupply, falling back to the share scale when supply is zero. tvl-c, `pps` and every apy observation (current, weekly, monthly, inception) now route through these two functions, so a tranche and an ordinary vault are measured by the same rule at the same blocks. Tranches get `pps`, `apy-bwd-delta-pps` and `tvl-c` hooks and no `tvl` hook: tranche tvl is a claim on the main vault's backing rather than additional protocol assets, and the legacy label is the one naive aggregates sum. Labels, components, pricing, sampling, annualization, compounding and storage are all unchanged, and non-tranche vaults follow exactly the paths they did before — `liveAssets` already excludes pendingExcess, so no pending profit enters pps or tvl. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
The tranche accounting reads decode to fixed tuples that viem types as a union including address[], so the direct cast is not provable. Route both through unknown, as the codebase does elsewhere for multicall tuple decodes. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Two daily series make the controller's accounting historical rather than only current. `tranche-accounting`, one series per tranche: baseline assets, pending excess, live assets, claim, covered, coverage ratio, target rate, excess share and whether accrual is paused. Everything comes from the controller, because the controller — not the tranche's own ERC-4626 view — knows what a tranche has accrued and how much of its claim is actually covered. `tranche-system`, one series at the controller: total claims against vault assets, reserve assets and backing assets, plus their ratio. That ratio is accounting coverage and nothing more — what can be withdrawn right now is a separate question, bounded by hook limits and deliverable vault liquidity. Each observation reads at a single historical block. Blocks before a tranche's registration read as an unregistered tranche rather than reverting, so those early zeros are real; a failed read emits nothing instead, so rpc trouble can't be mistaken for empty accounting. Coverage ratio is left null rather than 1 when there is no claim to cover — no claim is not evidence of full coverage. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
`GET /api/rest/tranche/:chainId` returns the deployment as one document: controller, asset, main vault, reserve vault, system accounting and the tranches in priority order. Each tranche carries its metadata, its raw `hook` address, `hookState`, the controller's accounting for it, coverage, current controller-backed price per share, and deliverable deposit and withdrawal capacity. Amounts are normalized to the asset's decimals, with `asset.decimals` in the payload so raw units can be recovered, and price per share keeps both raw and humanized forms to match the `pps` output. Everything is stamped with the block it came from: system fields from the controller snapshot's block, each tranche from its own, since the two are snapshotted independently. Capacity is reported as the Hook derives it rather than re-derived here, plus effective rate-limit usage: a fixed window has expired once the snapshot's timestamp passes windowStart + rateLimitWindow, at which point nothing is used. That is deliverable capacity, and it is deliberately separate from the coverage ratio, which is an accounting measure. Served from Redis like the other REST resources, with a refresh script wired into the existing refresh-cache workflow. An unset reserve vault reads as the zero address on chain and is reported as absent; a chain with more than one controller keeps the larger deployment and logs the rest rather than dropping it silently. `tranche-accounting` and `tranche-system` join `pps` and `apy-historical` in the timeseries mechanism. Both are narrower than "every vault", so labels now declare their address scope and the refresh scripts give the narrow ones their own pass instead of adding a query per vault. No GraphQL changes. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Fixed-block specs against the deployed Ethereum system, so expected values are reproducible rather than moving targets: - authoritative assets pick totalAssets() for an ordinary vault and liveAssets(vault) for a tranche, each checked against a direct read of the deployed contracts - tranche pps equals authoritative assets × scale ÷ supply, ordinary pps still equals pricePerShare(), and zero supply returns the share scale - apy's current, weekly, monthly and inception observations each equal the shared reader's value at the block apy sampled — apy has no pps path of its own - tvl-c consumes controller-backed assets while keeping its five components, and the tranche path exposes no legacy `tvl` label - controller discovery returns the three deployed tranches in priority order, with their accounting and base/locked implementation type - tranche snapshot enrichment appends hookState with representative limit data, leaves the raw `hook` address alone, and skips enrichment for a missing hook or one that doesn't implement the interface - both accounting series emit their components normalized to asset decimals - the REST payload's units, priority ordering, effective rate-limit usage and pre-snapshot fallbacks, unit-tested off captured row shapes Verifying apy's observations by equality against the reader rather than by mocking it is deliberate: specs share one module registry, so a module mock either leaks or gets bypassed depending on file order. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
New docs/ytranche.md covers what the code can't say on its own: the deployment, why the controller and tranche abi paths are siblings, why a tranche carries three things and what the `tranche` default is protecting, the `hook` vs `hookState` snapshot distinction, and the accounting semantics that are easy to get wrong — pendingExcess is not live NAV, coverage is not withdrawal capacity, target rate is not realized yield, rate limits are fixed windows rather than sliding lookbacks, and tranche claims are not additional protocol TVL. docs/rest.md gains the tranche endpoint with a full response and notes on units, per-tranche block stamping and the capacity-vs-coverage distinction, plus the two new timeseries segments. docs/outputs.md gains both new labels with their components and the tranche pps formula, and its coverage table now records that tranches deliberately emit no legacy `tvl`. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
A tranche controller is bound to one asset class by construction: its ASSET and VAULT are constructor arguments with no setters, so it can never be repointed. A chain therefore holds one controller per asset class — USD, BTC, ETH — and the previous single document per chain could only ever describe one of them. The refresh script papered over this by keeping the controller with the most tranches and logging the rest, which would have silently dropped a whole deployment from REST the moment a second one shipped. Replaces that with a collection and a member: GET /api/rest/tranche/:chainId/controllers every system on the chain GET /api/rest/tranche/:chainId/:controller one system, same shape as before `controllers` is a static route segment, so Next resolves it ahead of the sibling member route and it can never be mistaken for an address — controllers are hex, so the reverse collision is impossible too. The member route accepts either address casing; the cache key lowercases. `/api/rest/tranche/:chainId` is now an unrouted prefix rather than a second URL for the same payload. The dedupe branch is gone: every controller gets its own cache entry and each chain gets a collection entry holding all of them. Nothing else needed changing — discovery, the tranche-system series and the timeseries refresh were already keyed by controller address. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
The controller is a configured source rather than a discovered address, so nothing created a thing for it and it was absent from every thing-shaped surface — including the dashboard's label counts, where the tranches show up but the deployment they belong to did not. It now gets a `trancheController` thing carrying asset, main vault, inception and the v3 / yearn flags. No `reserveVault` default: that one is settable, so its current value belongs to the snapshot rather than to defaults, where it would go stale. The thing is created whether or not any tranches are registered, since a freshly deployed controller with an empty registry still exists. That also removes a duplicated derivation. Both controller lookups in web were reconstructing the set of controllers from the `trancheController` default on the tranches pointing at them, which needed at least one registered tranche to find a deployment at all. They now read the label directly. No abi path claims the `trancheController` label, so the new thing is not fanned out and the controller keeps exactly one snapshot owner. Label counts on the dashboard are grouped straight from the thing table, so it appears with no web changes. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Indexes the yTranche deployment: Kong discovers tranches through the controller,
retains controller state, enriches tranche snapshots with Hook state, generates
controller-backed PPS and performance history, and exposes it over REST.
The controller is the authoritative source for a tranche's assets and price per
share — it holds what the tranche has accrued and what losses it has absorbed,
where the tranche's own
totalAssets()reports its ERC-4626 balance. Sotvl-c,ppsand every APY observation resolve through one shared reader:trancheController.liveAssets(vault)for tranches,totalAssets()andpricePerShare()for ordinary vaults, which keep the paths they already had.Closes #444
How to review
Read the commits in order — each is one self-contained step, and the messages carry
the reasoning that doesn't fit in code comments.
Start with
docs/ytranche.md: it covers the accounting semantics that are easy toget wrong (pendingExcess is assigned profit awaiting a report, coverage is an
accounting measure while capacity is what the Hook will deliver, rate limits are
fixed windows that expire at
windowStart + rateLimitWindow) and thehookvshookStatesnapshot distinction.Then the three places where a wrong call would be expensive:
packages/ingest/abis/yearn/lib/assets.ts— the shared asset/PPS readers, andthe refactor of
lib/tvl.ts,lib/apy.tsand the v2/v3ppshook onto them.This is the only change that reaches non-tranche vaults.
.../tranche/controller/snapshot/hook.ts— discovery and the orderedtranchesaccounting array, all read at the automatic snapshot's block.
packages/web/app/api/rest/tranche/db.ts— payload units and derivations.The two generated
abi.tsfiles are etherscan-verified sources, one line each —skim or skip.
Two deliberate choices worth a second opinion:
vaultandstrategylabels so they reach Kong's existing vaultsurfaces, and a
tranche = truefilter on the genericyearn/3/vaultandyearn/3/strategypaths gives each tranche exactly one snapshot owner.tvl-cfrom controller-backedliveAssets— a claimon the main vault's backing, which the legacy
tvllabel continues to report atthe vault itself, so one number covers those assets.
Test plan
bun --filter ingest test,bun --filter web test,bun --filter lib test.20 new tests. Ingest carries 10 failures that also fail on
main(archive-RPC-dependent specs) — the same set before and after this branch.
through to REST. Three tranches found in priority order; 9 things with correct
base/locked types;
hookStateon each tranche alongside the rawhookaddress; five output labels populated with the legacy
tvllabel appearingfor the main vault alone; both REST routes 200 with matching payloads; unknown
controller 404, bad chainId 400.
tranche-accounting/tranche-systemobservations againstdirect
eth_callat their stored block numbers — exact to the wei, confirmingeach observation is a point-in-time read.
Risk / impact
Runs on the existing schema, reusing the
thing,snapshotandoutputtables.The one change with shared blast radius routes every yearn vault's
tvl-candppsthrough the new readers. Ordinary vaults issue the same
totalAssets()andpricePerShare()calls they did before — the branch is keyed on atrancheControllerdefault that tranches alone carry — and the existingtvlandppsspecs pass unchanged. Rollback is a straight revert; every write lands inexisting tables and columns.
Also adds a
refresh-cacheworkflow step for the tranche cache and widens thetimeseries refresh to controller-addressed series. Both are additive; the tranche
endpoints begin serving once their first refresh runs.
🤖 Generated with Claude Code