Add yTranche indexing and REST support - #445
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) <noreply@anthropic.com>
…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) <noreply@anthropic.com>
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) <noreply@anthropic.com>
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) <noreply@anthropic.com>
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) <noreply@anthropic.com>
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) <noreply@anthropic.com>
`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) <noreply@anthropic.com>
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) <noreply@anthropic.com>
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) <noreply@anthropic.com>
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) <noreply@anthropic.com>
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) <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
LGTM!
Logic and rationale for how the system works looks sound. Expected output and data about the tranches and overall system look comprehensive and clear. Well documented and as easy to understand as possible for 2 complicated systems coming together. Nice work!
Below is the automated codex review with a few minor comments that I will let you determine whether are worth picking up before merge.
Automated Review
Summary
Adds yTranche controller discovery, tranche and Hook-state snapshots, controller-backed asset/PPS calculations, accounting time series, and REST endpoints. Two correctness
issues remain around null accounting semantics and stale Hook state.
Issues
-
packages/web/app/api/rest/timeseries/db.ts:75 — The new accounting series cannot preserve the required coverageRatio: null when there is no claim. Both daily
aggregation queries convert an all-null bucket to zero with COALESCE(AVG(NULLIF(value, 0)), 0).-
Change: For coverageRatio in tranche-accounting and tranche-system, preserve SQL NULL in both getFullTimeseries and getRecentTimeseries; keep the existing aggregation
and zero behavior for every pre-existing label and component. -
Keep: Controller/tranche address scoping, route segments, legacy timeseries responses, and aggregation for other components must remain unchanged.
-
Done when: Latest and historical REST responses return coverageRatio: null for zero-claim observations in both new labels, with focused tests covering both queries.
-
-
packages/ingest/abis/yearn/3/tranche/vault/snapshot/hook.ts:39 — Removing a Hook or switching to an incompatible Hook retains the previous hookState. The hook returns
{}, while upsertSnapshot merges that over the existing state; REST then pairs the current raw Hook address—including the zero address—with stale limits and capacities.-
Change: Return an explicit hookState: null when the raw Hook is absent, zero, or cannot be enriched, and ensure the snapshot loader preserves that null as a clearing
value. -
Keep: Preserve the raw automatic hook address, keep enrichment failures non-fatal, and do not create a Hook thing or standalone Hook snapshot.
-
Done when: A valid snapshot followed by a zero-address or incompatible-Hook snapshot persists hookState: null, and REST returns null Hook state and capacity rather than
the previous values.
-
Suggestions
- Consider extracting the duplicated scoped-series traversal from refresh.ts and refresh-historical.ts into a shared helper, while retaining their respective readers, Redis
keys, and payload shapes.
Coverage this review did not reach:
- Controller-hook side effects that create/update controller, tranche, vault, strategy, and ERC-20 things—including a controller with zero tranches.
- ABI/config integration, automatic event extraction, sibling hook resolution, and generic snapshot-owner exclusions.
- End-to-end REST database, Redis, collection/member route, multi-controller, and 200/400/404 behavior.
- Executable coverage of scoped timeseries discovery and cache population.
- Non-tranche regression coverage across the broader V2/V3 and yield-bearing asset/PPS paths.
Verdict
COMMENT
———
How This Was Reviewed
Reviewed with the review-pr-workflow skill (https://github.com/yearn/webops/blob/main/skills/review-pr-workflow/SKILL.md) —
5 review lenses, each material finding independently verified by fresh native Codex session agents. 0 candidate findings were refuted and dropped.
Verification and preview
-
Lint: 0 errors, 34 existing/current warnings. The repository wrapper required unsupported terminal behavior, so its underlying package lint commands were run directly.
-
Web tests: 9/9 passed.
-
Ingest selection: 20/20 passed.
-
Production build: passed.
-
No UI files changed, so Playwright visual verification was skipped.
-
No source changes were made.
| const addresses = await extractTranchesByPriority(chainId, address, snapshot.tranchesLength, blockNumber) | ||
| const tranches = await extractTrancheAccounting(chainId, address, addresses, blockNumber) | ||
|
|
||
| await projectControllerThing(chainId, address, snapshot) |
There was a problem hiding this comment.
if we can do a promise.all here would be small tiny ways to make it faster.
| @@ -0,0 +1,98 @@ | |||
| import { EvmAddressSchema, Thing } from 'lib/types' | |||
There was a problem hiding this comment.
the file contains mostly "tranche" related things, im wondering if doesnt make sense rename to be under lib/traches/assets maybe?
| block_number AS "blockNumber", | ||
| block_time AS "blockTime" | ||
| FROM snapshot | ||
| WHERE chain_id = $1 AND lower(address) = lower($2) |
There was a problem hiding this comment.
| WHERE chain_id = $1 AND lower(address) = lower($2) | |
| WHERE chain_id = $1 AND address = $2 |
and make sure controller is passed throug getAdress() from viem. All the addressess have been normalized already
|
|
||
| const assetRows = assetAddress | ||
| ? await db.query( | ||
| 'SELECT defaults FROM thing WHERE chain_id = $1 AND lower(address) = lower($2) AND label = $3', |
matheus1lva
left a comment
There was a problem hiding this comment.
Review
Verdict: COMMENT
No blockers. Four confirmed issues (two extend the prior automated review with new detail, two are new), three non-blocking suggestions.
Issues
-
packages/ingest/abis/yearn/3/tranche/vault/snapshot/hook.ts:39— When a tranche's Hook is absent, zeroed, or unreadable, the hook returns{};upsertSnapshotshallow-merges (packages/ingest/load/index.ts:104-108), so the previoushookStateis retained. REST (tranche/db.ts:254,:280-281,:302-310) then serves the stale limits stamped with the current snapshot's blockNumber/blockTime, and deriveseffectiveUsed/remaining/capacityfrom them.setHookmakes this reachable. Spec task 4 requires hookState read at the snapshot's block.- Change: Return
{ hookState: null }for the absent/zero/incompatible cases so the merge overwrites the stored value (notundefined— it can be dropped in queue serialization), and havebuildTrancheSystemtreat null as absent (hookState: null,capacity: null, whichdb.spec.ts:166-183already exercises for the no-snapshot case). Add a test: populated state followed by a zero/incompatible-Hook snapshot persists null. - Keep: Raw
hookaddress preserved; enrichment failures non-fatal; no second top-levelhookfield. - Done when: After a snapshot where the Hook is zero/unreadable, the stored row's
hook.hookStateis null and REST returnshookState: null/capacity: nullinstead of an older block's values.
- Change: Return
-
packages/web/app/api/rest/timeseries/db.ts:75(and:103) —COALESCE(AVG(NULLIF(value, 0)), 0)collapses an all-NULL daily bucket to 0. The SQL predates this PR, but #445 newly routestranche-accounting/tranche-systemthrough it, so "no claim to cover" is served as "0% covered" — the exact inversion spec task 5 forbids, and this PR's own docs (docs/outputs.md, docs/ytranche.md, docs/rest.md's?components=coverageRatio) promise null. Ingest is correct (value: claim > 0n ? div(covered, claim) : undefinedpersists as NULL); the aggregation destroys it.- Change: Preserve NULL through aggregation for these series —
AVG(value)in bothgetFullTimeseriesandgetRecentTimeseries; if the COALESCE must stay for existing vault labels, branch on the label. - Keep: Existing vault labels (
pps,apy-bwd-delta-pps,apr-oracle,tvl-c) keep current bucket values and shape; daily bucketing and the historical/latest merge unchanged. - Done when:
GET /api/rest/timeseries/tranche-accounting/1/<tranche>?components=coverageRatioreturnsvalue: nullfor a zero-claim day, and likewise fortranche-system.
- Change: Preserve NULL through aggregation for these series —
-
packages/ingest/abis/yearn/3/tranche/controller/snapshot/hook.ts:132—tranches.map((address, priority) => …)derivespriorityfrom the caller's array index, not from the controller.tranche-accounting/hook.ts:46callsextractTrancheAccounting(chainId, controller, [address], blockNumber)with a single address, so every tranche comes backpriority: 0. The spec cements the wrong behavior:hook.spec.ts:93-96passes[a, e]and asserts[0, 1]while the discovery test above provesesits at index 2. Currently latent (the accounting hook doesn't consumepriority), but the field lies for any subset caller.- Change: Take
{ address, priority }[](the controller snapshot knows the real index fromextractTranchesByPriority) and havetranche-accounting/hook.ts:46pass the tranche's storeddefaults.priority. Updatehook.spec.ts:96to expect real priorities. - Keep: The controller snapshot's
tranchesarray stays priority-ordered with the same field names/values. - Done when:
extractTrancheAccountingcannot produce a priority disagreeing withtranchesByPriority(index); the[a, e] -> [0, 1]assertion is replaced by[0, 2].
- Change: Take
-
packages/ingest/abis/yearn/3/tranche/controller/snapshot/hook.ts:171(and:69) —estimateCreationBlock— a bytecode binary search (~25 archive calls, only a 10s cache;packages/lib/blocks.ts:98-127) — runs unconditionally for the controller and every tranche on every snapshot cycle. The repo's convention guards it:yearn/3/vault/snapshot/hook.ts:119runs it onlyif (! await things.exist(...)). Three tranches ≈ ~100 archivegetBytecodecalls per controller snapshot for values that never change.- Change: Guard both calls with
things.exist(chainId, address, label)and omitinceptBlock/inceptTimefromdefaultswhen the thing exists —upsertThingDefaults(packages/ingest/db.ts:124) is a right-wins merge, so omitting keys preserves stored values. - Keep: Tranche things still re-project
priority,trancheType, and the rest ofdefaultseach snapshot; only incept fields are skipped. - Done when: A repeat snapshot of an already-discovered controller issues no
estimateCreationBlockcalls and stored incept values are unchanged.
- Change: Guard both calls with
Suggestions
.github/workflows/refresh-cache.yml:31— The refresh-cache step and the widening ofrefresh.ts/refresh-historical.tsto tranche/controller-addressed series are not requested in #444 (task 7 scopes to the two REST resources and existing timeseries REST). They are necessary plumbing — the routes read only Redis, so without them every tranche route 404s — and the PR body discloses them. Worth an explicit scope acknowledgment since they touch infrastructure shared with the vault caches.packages/web/app/api/rest/tranche/db.ts:302—capacityre-emits threehookStatefields verbatim under different names (depositCap,depositLimit,withdrawCap) and ignoresremaining, the rate-limit-aware number computed just above intoRateLimit(db.ts:122). Either dropcapacity, or make it carry only derived deliverable numbers (e.g.Math.min(cap, remaining)) and stop restatinglimit.packages/web/app/api/rest/timeseries/refresh-historical.ts:52— The 34-line scoped-refresh pass is duplicated byte-for-byte withrefresh.ts:52-82, differing only in fetcher and key function. Extract onerefreshScopedSeries(fetch, key)helper so adding a fourth scoped label edits one place, not two.
Coverage this review did not reach (non-blocking): controller-discovery side effects (process/projectControllerThing/projectTrancheThings are never called by any spec, so zero-tranche controllers and thing/default projection are untested); ABI/config wiring (abis.yaml, sibling-path isolation, snapshot ownership, automatic event extraction); the non-tranche regression surface of lib/assets.ts/tvl.ts/apy.ts and the v2 PPS hook (no legacy-fallback or failed-read pinning); tranche-path historical APY (apy-shared-pps.spec uses an ordinary v2 vault); the REST DB layer beyond the pure payload builder (getTrancheControllers/getTrancheSystems, multi-controller, case handling); the two routes and cache refresh (no 200/400/404, header, or Redis-failure tests); timeseries label routing/refresh plumbing regression; and docs-vs-code cross-check of the negative-scope criteria.
How This Was Reviewed
Reviewed with the review-pr-workflow skill —
5 review lenses (skeptic / architect / minimalist models per agent-and-subagents rules),
each material finding independently verified (auto panel: codex gpt-5.6-terra). 2 candidate findings were refuted and dropped.
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.
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