Context
Event-derived analytics give flow but not current contract state. Blend's per-pool TVL, utilisation and APY live in reserve storage, not in any event, so /v1/lending/pools could only serve a window net-flow proxy. The lake captures every contract_data entry (key_xdr and entry_xdr) in stellar.ledger_entry_changes (ADR-0038).
Decision
Add a read path that, for a known contract and storage key, fetches the latest contract_data entry from the lake and decodes it with a per-protocol decoder.
- Read-time, not materialised. No new worker, table or backfill. The reader builds the exact
key_xdr(aswasm_lake_reader.instanceKeyXDRdoes) and probes the lake's current-state projectionledger_entries_currenton its(entry_type, key_xdr)sort key. It does not scanledger_entry_changes. - Decoders live with the source. Blend's
internal/sources/blend/storage.godecodesReserveData,ReserveConfigandPoolConfigby field name (scval.MapField). The interest-rate model is ported from the contract'sinterest.rsandreserve.rsintointernal/sources/blend/interest.goand checked ininterest_test.goagainst vectors from the contract's own tests. - Current-state figures. TVL (supply and borrow underlying times price), utilisation, and borrow and supply APY are computed from certified state. The backstop take rate comes from the pool's
PoolConfig. - Honest provenance. A figure from contract storage is labelled current-state. The event-derived window proxy (
net_supplied_30d,net_borrowed_30d) stays separate and is never blended with it.
Invariant
- Current-state fields and the window net-flow proxy are distinct fields.
internal/api/v1/lending_reserve_decimals_test.goguards that a reserve with unknown decimals is excluded fromtvl_usdand the total is flaggedlower_bound. - A protocol's storage layout is decoded in that protocol's source package, by field name and never by position.
- Fixed-point rounding matches the contract:
internal/sources/blend/interest.gouses ceil or floor exactly wheresoroban-fixed-point-mathdoes. - Money in the reader and the rate model is
*big.Intor*big.Rat, neverfloat64(AGENTS.md, ADR-0003).
Consequences
- Blend gains current-state TVL, utilisation and APY without a backfill. The pattern generalises to any protocol whose state is wanted: build the key, probe the lake, decode.
- Coverage is the live contract-storage capture window. A reserve untouched since capture began is not found. Historical TVL series would need a contract-state backfill, which is deferred.
- A pool with many reserves fans out one lookup per reserve. Results are cached, and a materialised projection is the escape hatch.
Evidence
internal/storage/clickhouse/blend_pool_state_reader.go, internal/sources/blend/storage.go, internal/sources/blend/interest.go, internal/sources/blend/interest_test.go, internal/api/v1/lending.go.