From 67aa84d6c8d9047563e8f38e49bf2a510c081011 Mon Sep 17 00:00:00 2001 From: TokenBrice Date: Sat, 5 Sep 2026 20:36:19 +0200 Subject: [PATCH 1/4] fix(reserves): recover verified reports and on-chain observations Refresh USDGO to the examined July Solana reserve report and retire the obsolete June Avalanche check. Rebind verified USDRIF implementations, refresh each adapter clock, and tolerate unused River manager slots while preserving required reads. Plan: agents/2026-09-05-night-watch/README.md Tasks: R1, B1 --- docs/live-reserves.md | 7 +- .../refresh-independent-assurance-reports.ts | 33 ++--- .../independent-assurance/usdgo.json | 51 ++++--- shared/data/stablecoins/coins/usdgo-osl.json | 20 +-- shared/data/stablecoins/coins/usdrif-rif.json | 14 +- .../domains/reserves/usdgo-osl.json | 32 ++--- .../lib/live-reserve-adapter-param-schemas.ts | 6 - .../live-reserve-adapter-declarations.ts | 4 +- .../cron/__tests__/sync-live-reserves.test.ts | 25 ++++ .../fixtures/usdgo-transparency.html | 3 +- .../__tests__/independent-assurance.test.ts | 2 +- .../__tests__/river-protocol-info.test.ts | 6 +- .../__tests__/usdgo-transparency.test.ts | 75 +++------- .../reserve-adapters/river-protocol-info.ts | 1 + .../reserve-adapters/usdgo-transparency.ts | 136 +----------------- worker/src/cron/sync-live-reserves.ts | 2 +- 16 files changed, 143 insertions(+), 274 deletions(-) diff --git a/docs/live-reserves.md b/docs/live-reserves.md index 9470bc57b2..0841a88b12 100644 --- a/docs/live-reserves.md +++ b/docs/live-reserves.md @@ -177,6 +177,7 @@ Warnings now carry both a display `severity` and an execution `effect`: 3. Resolves an adapter from `worker/src/cron/reserve-adapters/index.ts`. 4. Builds a breaker key as `live-reserves:${breakerScope ?? adapter}`. 5. Checks the per-source circuit breaker before each coin fetch. + Each primary or fallback adapter attempt receives its own start time as `nowSec`, so time spent earlier in the queue cannot make current chain heads appear to be in the future. 6. Persists either a fresh snapshot (`reserve_composition`) plus sync-state row, or an error/degraded/skipped sync-state row. The `reserve-recovery` lane retains its five-minute trigger. In `recover` mode it fences any in-flight attempt, claims the checkpoint's ready pointer, and replays the suffix; a crash before terminal checkpoint finalization is therefore retried within one recovery tick. `off` keeps the sweep but skips recovery scans. The checkpoint pointer is the sole run-level resume mechanism; the cache table is not used for reserve cursor ownership. @@ -526,15 +527,15 @@ the `re-metrics` adapter parses Re Protocol's official metrics page and now extr `fx` now publishes f(x) protocol pool debt balances as conservative live proxy redemption capacity for `fxusd-f-x-protocol`; the configured score-grade path reads the reviewed Ethereum WBTC and wstETH pool collateral/debt totals directly on-chain. `fxsave-f-x-protocol` uses the generic `erc4626-single-asset` path to publish the vault's idle fxSP balance as live-direct capacity for its fxSP/router exit route. The same adapter can also attach reviewed ERC-4626 liquidity sidecars: Morpho V1/V2 vault configs use Morpho V2 `liquidity` or V1 `liquidity.underlying` as same-run API capacity after validating the vault address, chain id, listed status, and underlying asset; Yearn V3 configs use the vault's same-run default withdrawal queue, measuring `totalIdle()` plus each funded strategy's `min(currentDebt, convertToAssets(maxRedeem(vault)))`; `sbold-k3-capital` uses `sbold-sp-withdrawable` to read the BOLD amount returned by the vault's `calcFragments()` liquidity view, representing compounded Stability Pool deposits that the vault can withdraw; and `sfrxusd-frax` uses `fraxtal-hop-withdrawable` because Ethereum-local withdrawal is disabled. Two coin-specific paths override the generic idle-balance assumption: `eearn-ember` pins the vault, validator, and protocol-config proxy/implementation identities and reports zero immediate capacity for its operator-batched queue while exposing pause, queue, and current-fee diagnostics; `sdusd-dtrinity` pins the dSTAKE router and exact strategy/adapter set, then uses the current dLEND `maxWithdraw` cross-checked against dUSD available liquidity and the current unstaking fee. Required identity or state-read failures abort those snapshots rather than falling back to idle assets or full NAV. The sfrxUSD observer pins finalized Ethereum and Fraxtal blocks, all six proxy implementations, direct-contract code, peers, tokens, oracles, inventory, quote components, and return-message funding. It publishes the accepted packet for diagnostics and bounded capacity, but the V9 route remains non-scoreable while Ethereum transaction gas and a measured or primary-source settlement upper bound are missing. Morpho V2 `forceDeallocatableLiquidity` is persisted for context but not used as scoring capacity. Yearn V3 funded-strategy probe failures, sBOLD liquidity-read failures, and any sfrxUSD identity/state drift degrade instead of falling back to full NAV or the disabled local withdrawal path. `asymmetry` now publishes USDaf protocol supply from the timestamped stats API as live direct redemption capacity alongside branch collateral slices. `jupusd` consumes Jupiter's public transparency API and latest snapshot timestamp, grouping USDC/USDtb holdings into reserve slices while emitting whitelisted-primary live redemption capacity and route status from the public oracle endpoint. -`solstice-attestation` and `river-protocol-info` are proof-class reserve-sync adapters. They make current issuer/protocol telemetry visible on reserve detail and status surfaces, but their registry evidence class is `weak-live-probe`, so they do not become V9 Backing reserve evidence. Solstice remains proof-class until its aggregate solvency feed exposes timestamped asset-category composition; River remains proof-class because its protocol-info endpoint exposes aggregate TVL/circulating-supply telemetry rather than asset-level collateral composition. River snapshots degrade when the aggregate TVL is below circulating satUSD, and timestampless protocol-info payloads remain freshness-unverified. +`solstice-attestation` and `river-protocol-info` are proof-class reserve-sync adapters. They make current issuer/protocol telemetry visible on reserve detail and status surfaces, but their registry evidence class is `weak-live-probe`, so they do not become V9 Backing reserve evidence. Solstice remains proof-class until its aggregate solvency feed exposes timestamped asset-category composition; River remains proof-class because its protocol-info endpoint exposes aggregate TVL/circulating-supply telemetry rather than asset-level collateral composition. River snapshots degrade when the aggregate TVL is below circulating satUSD, and timestampless protocol-info payloads remain freshness-unverified. River's bounded redemption probe tolerates reverts in speculative manager slots beyond the on-chain count; every manager inside that count and its branch reads remain mandatory. `audx-independent-assurance` uses the reviewed July 31, 2026 report: AUD 3,231,371.79 in designated bank accounts against AUD 3,208,057.00 issued supply. Aura Partners signed its limited-assurance report on August 20, 2026; the posting date is unknown. The manifest retains the report's printed AEDT timestamp and exact PDF hash, with freshness measured from the examined period rather than the signature or fetch date. -`usdgo-transparency` is now independent: the Deloitte examination is the authoritative composition/liability source through the independent-assurance manifest (exact PDF SHA-256 binding), with the Avalanche BUIDL balance pinned on-chain and the issuer transparency API demoted to a cross-check that raises informational warnings on later-period drift. +`usdgo-transparency` uses the Deloitte examination as its authoritative composition/liability source through the independent-assurance manifest (exact PDF SHA-256 binding), with the issuer transparency API retained as a cross-check that raises informational warnings on later-period drift. The July 31, 2026 report supersedes June and locates BUIDL on Solana. Config version 3 retires the obsolete June Avalanche balance check; current report hash, latest-report detection, asset/liability reconciliation, and same-period issuer disagreement checks remain mandatory. Pharos does not claim an independent replay of the report-time Solana balance. The offline compiler verifies the report's zero Morph liabilities before omitting that empty chain from positive liability rows. `anzen-usdz` is now independent: it reads USDz `totalPooledSPCT()` and actual SPCT holdings on-chain and reconciles them against bridge-adjusted USDz liabilities across the five issuance chains, failing closed on shortfall instead of trusting global SPCT supply. -`moc-v3-buckets` is a narrow Money-on-Chain V3 adapter (USDRIF): pinned Rootstock block/header and proxy implementation hashes, bucket-identity and sole-token revert checks, liability/supply reconciliation, wallet-accounting exclusions, and protocol-vs-market price divergence guards; it emits no redemption telemetry. +`moc-v3-buckets` is a narrow Money-on-Chain V3 adapter (USDRIF): pinned Rootstock block/header and proxy implementation hashes, bucket-identity and sole-token revert checks, liability/supply reconciliation, wallet-accounting exclusions, and protocol-vs-market price divergence guards; it emits no redemption telemetry. The September 5 review at Rootstock block 9214021 binds both upgraded buckets to the verified MocCARC20 implementation `0x5316384be99310fdea0da0b3cc0d0bb9e7f0887f` and rechecks the RIFUSD peg-container identity. `zephyr-scanner` consumes Zephyr's reserve snapshot API for `zsd-zephyr-protocol`, preserving the snapshot capture timestamp, ZEPH reserve value, ZSD supply, reserve ratio, moving-average reserve ratio, and ZSD yield-reserve metadata. It is proof-class because the feed is protocol-published native-chain telemetry over volatile ZEPH collateral rather than independently verified asset-level reserve evidence. diff --git a/scripts/maintenance/refresh-independent-assurance-reports.ts b/scripts/maintenance/refresh-independent-assurance-reports.ts index 3084e57e09..aab3c0c3b4 100644 --- a/scripts/maintenance/refresh-independent-assurance-reports.ts +++ b/scripts/maintenance/refresh-independent-assurance-reports.ts @@ -143,11 +143,11 @@ function profile(product: IndependentAssuranceProduct): CompilerProfile { product, profile: "usdgo-v1", officialIndexUrl: "https://www.anchorage.com/platform/usdgo-reserve-attestations", - reportUrl: "https://learn.anchorage.com/06.30.26_USDGO-Stablecoin-Attestation-Report.pdf", - reportDate: "2026-06-30", - reportAsOf: "2026-06-30T23:59:59Z", + reportUrl: "https://learn.anchorage.com/07.31.26_USDGO-Stablecoin-Attestation-Report-signed.pdf", + reportDate: "2026-07-31", + reportAsOf: "2026-07-31T23:59:59Z", reportTimeZone: "UTC", - reportIssuedAt: "2026-07-28T23:59:00Z", + reportIssuedAt: "2026-08-28T23:59:00Z", attestor: "Deloitte & Touche LLP", engagement: "Independent accountant's examination under AICPA attestation standards", conclusion: "unmodified", @@ -165,18 +165,12 @@ function profile(product: IndependentAssuranceProduct): CompilerProfile { code: "solana", label: "Solana USDGO redeemable tokens", // eslint-disable-next-line security/detect-unsafe-regex -- anchored per-line pattern over an offline reviewed PDF text dump; bounded digit runs, no nested quantifier ambiguity. - pattern: /^\s*a\.\s+Total USDGO natively minted tokens\s+([0-9][0-9,]*(?:\.[0-9]+)?)\s+[0-9][0-9,]*(?:\.[0-9]+)?\s+[0-9][0-9,]*(?:\.[0-9]+)?\s*$/im, - }, - { - code: "morph", - label: "Morph USDGO redeemable tokens", - // eslint-disable-next-line security/detect-unsafe-regex -- anchored per-line pattern over an offline reviewed PDF text dump; bounded digit runs, no nested quantifier ambiguity. - pattern: /^\s*a\.\s+Total USDGO natively minted tokens\s+[0-9][0-9,]*(?:\.[0-9]+)?\s+([0-9][0-9,]*(?:\.[0-9]+)?)\s+[0-9][0-9,]*(?:\.[0-9]+)?\s*$/im, + pattern: /^\s*a\.\s+Total USDGO natively minted tokens\s+([0-9][0-9,]*(?:\.[0-9]+)?)\s+(?:[0-9][0-9,]*(?:\.[0-9]+)?|-)\s+[0-9][0-9,]*(?:\.[0-9]+)?\s*$/im, }, ], requiredText: [ { label: "AICPA attestation standards", pattern: /American Institute of Certi(?:f|fi)ied Public Accountants[\s\S]*AICPA/i }, - { label: "USDGO June 2026 report date", pattern: /June 30, 2026[\s\S]*11:59:59 PM Coordinated Universal Time/i }, + { label: "USDGO July 2026 report date", pattern: /July 31, 2026[\s\S]*11:59:59 PM Coordinated Universal Time/i }, { label: "favorable examination conclusion", pattern: /fairly stated, in all material respects/i }, { label: "USDGO Schedule I", pattern: /Schedule I: Total USDGO Natively Minted Tokens/i }, { label: "USDGO Schedule II", pattern: /Schedule II: Composition of Reserve Assets/i }, @@ -186,13 +180,16 @@ function profile(product: IndependentAssuranceProduct): CompilerProfile { { label: "qualified/adverse/disclaimed conclusion", pattern: /qualified opinion|adverse opinion|disclaimer of opinion|except for/i }, ], reportedTotals: [ + // The reviewed dash is zero; omit the empty chain from positive liability rows. + // eslint-disable-next-line security/detect-unsafe-regex -- anchored per-line pattern over the reviewed offline report. + { label: "USDGO Morph liabilities", expected: "0", pattern: /^\s*a\.\s+Total USDGO natively minted tokens\s+[0-9][0-9,]*(?:\.[0-9]+)?\s+([0-9][0-9,]*(?:\.[0-9]+)?|-)\s+[0-9][0-9,]*(?:\.[0-9]+)?\s*$/im, }, // eslint-disable-next-line security/detect-unsafe-regex -- anchored per-line pattern over an offline reviewed PDF text dump; bounded digit runs, no nested quantifier ambiguity. - { label: "USDGO redeemable token total", expected: "859224943", pattern: /^\s*Total USDGO redeemable tokens outstanding\s+\$?([0-9][0-9,]*(?:\.[0-9]+)?)(?:\s+\(Schedule I\))?\s*$/im }, - { label: "USDGO reserve asset total", expected: "861072523", pattern: usdgoScheduleAmountPattern("Schedule II:", "Total") }, + { label: "USDGO redeemable token total", expected: "1112640495", pattern: /^\s*Total USDGO redeemable tokens outstanding\s+\$?([0-9][0-9,]*(?:\.[0-9]+)?)(?:\s+\(Schedule I\))?\s*$/im }, + { label: "USDGO reserve asset total", expected: "1116301304", pattern: usdgoScheduleAmountPattern("Schedule II:", "Total") }, ], - reportedAssetTotal: "861072523", - computedAssetTotal: "861072523", - reportedLiabilityTotal: "859224943", + reportedAssetTotal: "1116301304", + computedAssetTotal: "1116301304", + reportedLiabilityTotal: "1112640495", }; case "XSGD": return straitsxProfile(product, "SGD", "23,674,708", "23,661,169"); @@ -276,7 +273,7 @@ function straitsxProfile( function amountFromMatch(match: RegExpMatchArray | null, label: string): string { const raw = match?.[1]; if (!raw) throw new Error(`offline assurance compiler: could not extract ${label}`); - return raw.replace(/[$,]/g, ""); + return raw === "-" ? "0" : raw.replace(/[$,]/g, ""); } function assertProfileText(text: string, config: CompilerProfile): void { diff --git a/shared/data/live-reserves/independent-assurance/usdgo.json b/shared/data/live-reserves/independent-assurance/usdgo.json index bf097a2c2b..852e58ffce 100644 --- a/shared/data/live-reserves/independent-assurance/usdgo.json +++ b/shared/data/live-reserves/independent-assurance/usdgo.json @@ -3,34 +3,53 @@ "product": "USDGO", "profile": "usdgo-v1", "officialIndexUrl": "https://www.anchorage.com/platform/usdgo-reserve-attestations", - "reportUrl": "https://learn.anchorage.com/06.30.26_USDGO-Stablecoin-Attestation-Report.pdf", - "reportSha256": "5b2b26b92345ba87f12aa2405cfa9d619adf593ca18dbbb6180ef23ee9dc2eb0", - "reportByteLength": 425848, - "reportDate": "2026-06-30", - "reportAsOf": "2026-06-30T23:59:59Z", + "reportUrl": "https://learn.anchorage.com/07.31.26_USDGO-Stablecoin-Attestation-Report-signed.pdf", + "reportSha256": "27a8c25bd1d269892c8a58bb24c040042e51f5f6932796de3ecf6469870718fa", + "reportByteLength": 406396, + "reportDate": "2026-07-31", + "reportAsOf": "2026-07-31T23:59:59Z", "reportTimeZone": "UTC", - "reportIssuedAt": "2026-07-28T23:59:00Z", + "reportIssuedAt": "2026-08-28T23:59:00Z", "attestor": "Deloitte & Touche LLP", "engagement": "Independent accountant's examination under AICPA attestation standards", "conclusion": "unmodified", "unit": "USD", "assets": [ - { "code": "cash", "label": "Cash", "amount": "9410255" }, - { "code": "buidl", "label": "BUIDL at fair value", "amount": "170977843" }, - { "code": "stbxx", "label": "STBXX money market fund (CUSIP 38151N205)", "amount": "99111018" }, - { "code": "jltxx", "label": "JLTXX money market fund (CUSIP 46655R119)", "amount": "581573407" } + { + "code": "cash", + "label": "Cash", + "amount": "11061357" + }, + { + "code": "buidl", + "label": "BUIDL at fair value", + "amount": "312429373" + }, + { + "code": "stbxx", + "label": "STBXX money market fund (CUSIP 38151N205)", + "amount": "99400695" + }, + { + "code": "jltxx", + "label": "JLTXX money market fund (CUSIP 46655R119)", + "amount": "693409879" + } ], "liabilities": [ - { "code": "solana", "label": "Solana USDGO redeemable tokens", "amount": "859224936" }, - { "code": "morph", "label": "Morph USDGO redeemable tokens", "amount": "7" } + { + "code": "solana", + "label": "Solana USDGO redeemable tokens", + "amount": "1112640495" + } ], - "reportedAssetTotal": "861072523", - "computedAssetTotal": "861072523", - "reportedLiabilityTotal": "859224943", + "reportedAssetTotal": "1116301304", + "computedAssetTotal": "1116301304", + "reportedLiabilityTotal": "1112640495", "extraction": { "tool": "Poppler pdftotext -layout", "parserVersion": "26.08.0", - "normalizedTextSha256": "ed69623b9649a90e8c36ec2890b62a991c71485b53da9fccdac89ee484de398f", + "normalizedTextSha256": "67189de8b2c6d2b7b7bc37aeea06d6ff6949f3daf6e9e67666043b408275c1fe", "pageCount": 7 } } diff --git a/shared/data/stablecoins/coins/usdgo-osl.json b/shared/data/stablecoins/coins/usdgo-osl.json index 3268abea28..baae8f5f18 100644 --- a/shared/data/stablecoins/coins/usdgo-osl.json +++ b/shared/data/stablecoins/coins/usdgo-osl.json @@ -18,17 +18,17 @@ "cadence": "monthly", "attestorJurisdiction": "United States", "latestReport": { - "periodEnd": "2026-06-30", - "publishedAt": "2026-07-28", + "periodEnd": "2026-07-31", + "publishedAt": "2026-08-28", "assuranceMethod": "examination", "scope": "assets-and-liabilities", "liabilityReconciliation": "full", - "reviewer": "PoR verification sweep 2026-08 (June report PDF read; composition review advanced in lockstep)", + "reviewer": "Pharos night watch 2026-09-05 (July report and composition reviewed together)", "confidence": "verified", "sources": [ { - "label": "Anchorage USDGO Reserve Report - June 30, 2026", - "url": "https://learn.anchorage.com/06.30.26_USDGO-Stablecoin-Attestation-Report.pdf" + "label": "Anchorage USDGO Reserve Report - July 31, 2026", + "url": "https://learn.anchorage.com/07.31.26_USDGO-Stablecoin-Attestation-Report-signed.pdf" } ] } @@ -64,7 +64,7 @@ "governanceQuality": "regulated-entity", "liveReservesConfig": { "adapter": "usdgo-transparency", - "version": 2, + "version": 3, "semantics": "attestation-mix", "breakerScope": "usdgo-osl", "display": { @@ -82,13 +82,7 @@ "profile": "usdgo-v1", "indexHost": "www.anchorage.com", "reportHosts": ["learn.anchorage.com"], - "issuerCrossCheckUrl": "https://www.usdgo.com/api/lark-bitable", - "avalancheRpcUrl": "https://api.avax.network/ext/bc/C/rpc", - "avalancheBuidlToken": "0x53fc82f14f009009b440a706e31c9021e1196a2f", - "avalancheBuidlWallet": "0xc1d56e817d8f6c53d42ed50ed0d789eeb1495b5e", - "avalancheBuidlBlock": 89166720, - "avalancheBuidlBlockHash": "0xf39651e0ea42f8f78d0d375fa39ddd531896083e3f5c1daef7e7efa987ee7939", - "expectedBuidlCodeHash": "0xee8a105971995661291a9f284262a87abf2381b3cdc93b2c8fbeffe4cd636dd9" + "issuerCrossCheckUrl": "https://www.usdgo.com/api/lark-bitable" } }, "yieldConfig": { diff --git a/shared/data/stablecoins/coins/usdrif-rif.json b/shared/data/stablecoins/coins/usdrif-rif.json index 5e84f1f5e7..65239db986 100644 --- a/shared/data/stablecoins/coins/usdrif-rif.json +++ b/shared/data/stablecoins/coins/usdrif-rif.json @@ -89,18 +89,18 @@ "rifBucket": { "address": "0xa27024ed70035e46dba712609fc2afa1c97aa36a", "expectedProxyCodeHash": "0x146015dd2944fa6e6d87c95119399290c52ddea7e7a09f62ef54f04ee300463a", - "expectedImplementationAddress": "0x7d41de4fe6a9c57f13032b4d5489a8b7690d3fdc", - "expectedImplementationCodeHash": "0xe74d8993933d2fa17f50aeadcdd26df50dc9d609463cd0987df6bdbc8bbdc252", + "expectedImplementationAddress": "0x5316384be99310fdea0da0b3cc0d0bb9e7f0887f", + "expectedImplementationCodeHash": "0xdaf1fdd739f2a2030fe5e92db559631b2c0dabf28e0d97abf6e48446f73da78e", "collateralToken": "0x2acc95758f8b5f583470ba265eb685a8f45fc9d5", "collateralDecimals": 18, - "expectedPegContainerProvider": "0x6a5b2c84e63b5c1330bf4cccff1ad6f23116cc14", + "expectedPegContainerProvider": "0xafb1b8c320acc776c1279bcdb24ab8f84ab727a4", "expectedPriceProvider": "0x6a343488338b944c6fcc89906646fac1e8e91ce5" }, "docBucket": { "address": "0x697535055aa7afd2c280523c7b062b1f05284661", "expectedProxyCodeHash": "0x5dcd44a0907ccf27e9d5f19c9466dac15c4dba10b0f30776d531269a30cfcde3", - "expectedImplementationAddress": "0x2f5b77f2ed6d6947917e4d9746108e26275b3b66", - "expectedImplementationCodeHash": "0xdf1ccdda002e33133f8169380e689969709477a5bc10f297fab924ac94359a3c", + "expectedImplementationAddress": "0x5316384be99310fdea0da0b3cc0d0bb9e7f0887f", + "expectedImplementationCodeHash": "0xdaf1fdd739f2a2030fe5e92db559631b2c0dabf28e0d97abf6e48446f73da78e", "collateralToken": "0xe700691da7b9851f2f35f8b8182c69c53ccad9db", "collateralDecimals": 18, "expectedPegContainerProvider": "0x6a343488338b944c6fcc89906646fac1e8e91ce5", @@ -108,7 +108,9 @@ }, "sourceUrls": [ "https://github.com/money-on-chain/stable-protocol-roc-v2/tree/64734b2599fd89658d62c15eda805acec7c9e054", - "https://rootstock.blockscout.com/address/0xa27024ed70035e46dba712609fc2afa1c97aa36a" + "https://rootstock.blockscout.com/address/0xa27024ed70035e46dba712609fc2afa1c97aa36a", + "https://rootstock.blockscout.com/address/0x5316384be99310fdea0da0b3cc0d0bb9e7f0887f?tab=contract", + "https://rootstock.blockscout.com/block/9214021" ] } }, diff --git a/shared/data/stablecoins/domains/reserves/usdgo-osl.json b/shared/data/stablecoins/domains/reserves/usdgo-osl.json index cb9b5da975..713f026174 100644 --- a/shared/data/stablecoins/domains/reserves/usdgo-osl.json +++ b/shared/data/stablecoins/domains/reserves/usdgo-osl.json @@ -3,7 +3,7 @@ "reserves": [ { "name": "Goldman Sachs STBXX (CUSIP 38151N205)", - "pct": 11.510182, + "pct": 8.904468, "risk": "low", "assetClass": "money-market-fund", "issuerOrObligor": "Goldman Sachs Financial Square Government Fund", @@ -18,7 +18,7 @@ }, { "name": "JPMorgan JLTXX (CUSIP 46655R119)", - "pct": 67.540584, + "pct": 62.116731, "risk": "low", "assetClass": "money-market-fund", "issuerOrObligor": "JPMorgan Liquidity Funds", @@ -33,7 +33,7 @@ }, { "name": "BUIDL (BlackRock USD Institutional Digital Liquidity Fund)", - "pct": 19.856381, + "pct": 27.987907, "risk": "low", "coinId": "buidl-blackrock", "depType": "collateral", @@ -49,7 +49,7 @@ }, { "name": "Cash at FDIC-insured depository institutions", - "pct": 1.092853, + "pct": 0.990893, "risk": "very-low", "assetClass": "bank-deposit", "issuerOrObligor": "Undisclosed major commercial banks", @@ -62,22 +62,22 @@ } ], "reserveReview": { - "reviewedAt": "2026-08-08", - "reviewer": "PoR verification sweep 2026-08 (lockstep coherent refresh: report and composition review updated together from the same artefact)", + "reviewedAt": "2026-09-05", + "reviewer": "Pharos night watch primary-source report review", "confidence": "verified", "sources": [ { - "label": "Anchorage USDGO Reserve Report - June 30, 2026", - "url": "https://learn.anchorage.com/06.30.26_USDGO-Stablecoin-Attestation-Report.pdf" + "label": "Anchorage USDGO Reserve Report - July 31, 2026", + "url": "https://learn.anchorage.com/07.31.26_USDGO-Stablecoin-Attestation-Report-signed.pdf" }, { "label": "Anchorage USDGO reserve attestations", "url": "https://www.anchorage.com/platform/usdgo-reserve-attestations" } ], - "rationale": "Deloitte's June 30, 2026 examination (report dated 2026-07-28, PDF read this pass): reserves $861,072,523 against 859,224,943 redeemable USDGO (Solana + Morph) - money market funds $680,684,425 (CUSIP 38151N205 $99,111,018; CUSIP 46655R119 $581,573,407), BUIDL at fair value $170,977,843 (Avalanche, self-custodied; private unregistered BVI fund with no secondary market), and $9,410,255 cash at FDIC-insured institutions ($9,160,255 uninsured). Surplus $1,847,580.", - "compositionBasis": "Deloitte examination of the USDGO Reserve Report as of June 30, 2026", - "compositionAsOf": "2026-06-30", + "rationale": "Deloitte's July 31, 2026 examination, issued August 28, reports reserves of $1,116,301,304 against 1,112,640,495 redeemable USDGO on Solana; Morph liabilities are zero. Reserves comprise $99,400,695 STBXX, $693,409,879 JLTXX, $312,429,373 BUIDL on Solana in segregated Anchorage custody, and $11,061,357 cash at FDIC-insured banks ($10,811,357 uninsured). Surplus is $3,660,809. BUIDL is a private unregistered BVI fund without a secondary market.", + "compositionBasis": "Deloitte examination of the USDGO Reserve Report as of July 31, 2026", + "compositionAsOf": "2026-07-31", "scope": "full-composition", "knownUnknownExposure": "The two money market fund CUSIPs are disclosed but their issuer names and underlying portfolio composition are not identified in the report.", "knownUnknownExposurePct": 0 @@ -94,13 +94,13 @@ "segregation": "segregated", "bankruptcyRemoteness": "structured", "rehypothecation": "unknown", - "reviewedAt": "2026-07-17", - "reviewer": "Kimi FIAT-CTRL V9 R4 evidence review", + "reviewedAt": "2026-09-05", + "reviewer": "Pharos night watch primary-source report review", "confidence": "verified", "sources": [ { - "label": "Anchorage USDGO Reserve Report - May 31, 2026", - "url": "https://learn.anchorage.com/05.31.26_USDGO_Stablecoin_Attestation_Report_signed.pdf" + "label": "Anchorage USDGO Reserve Report - July 31, 2026", + "url": "https://learn.anchorage.com/07.31.26_USDGO-Stablecoin-Attestation-Report-signed.pdf" }, { "label": "USDGO transparency page (names reserve fund issuers, accessed 2026-07-17)", @@ -115,6 +115,6 @@ "url": "https://am.jpmorgan.com/us/en/asset-management/adv/products/jpmorgan-onchain-liquidity-token-money-market-fund-token-class-46655r119" } ], - "uncertainty": "The report says reserve assets are held in segregated fiduciary trust accounts for token holders and identifies Anchorage as the issuer and qualified custodian. It does not state a general rehypothecation prohibition. It also does not name the FDIC-insured banks holding the $1.6M cash slice or any money-market-fund subcustodian; the issuer transparency page (accessed 2026-07-17) identifies the two money market funds as the Goldman Sachs Stablecoin Reserves Fund and the JPMorgan OnChain Liquidity-Token Money Market Fund, but their underlying fund custodians remain undisclosed. Anchorage self-custodies the BUIDL slice in segregated wallets (Avalanche 0xC1d56E817d8f6C53D42eD50Ed0D789eEb1495b5e per the May 31, 2026 report)." + "uncertainty": "The July report states that reserve assets are held in segregated fiduciary trust accounts for token holders and identifies Anchorage as issuer and qualified custodian. It does not state a general rehypothecation prohibition or name the banks holding $11,061,357 cash. Fund custodians remain undisclosed. Anchorage self-custodies the $312,429,373 BUIDL slice in segregated Solana wallets; the report identifies 9yGxs2pSFFBZhJVrwRAgbKHaLQnuHa7BpmCRsHuzG968. The report-time Solana token balance has not been independently replayed by Pharos." } } diff --git a/shared/lib/live-reserve-adapter-param-schemas.ts b/shared/lib/live-reserve-adapter-param-schemas.ts index e03be6b70c..c3962b85c2 100644 --- a/shared/lib/live-reserve-adapter-param-schemas.ts +++ b/shared/lib/live-reserve-adapter-param-schemas.ts @@ -112,12 +112,6 @@ const usdgoAssuranceParamsSchema = z profile: z.literal("usdgo-v1"), ...assuranceParamsShape, issuerCrossCheckUrl: z.literal("https://www.usdgo.com/api/lark-bitable"), - avalancheRpcUrl: z.literal("https://api.avax.network/ext/bc/C/rpc"), - avalancheBuidlToken: z.literal("0x53fc82f14f009009b440a706e31c9021e1196a2f"), - avalancheBuidlWallet: z.literal("0xc1d56e817d8f6c53d42ed50ed0d789eeb1495b5e"), - avalancheBuidlBlock: z.literal(89166720), - avalancheBuidlBlockHash: z.literal("0xf39651e0ea42f8f78d0d375fa39ddd531896083e3f5c1daef7e7efa987ee7939"), - expectedBuidlCodeHash: z.literal("0xee8a105971995661291a9f284262a87abf2381b3cdc93b2c8fbeffe4cd636dd9"), }) .strict(); diff --git a/shared/types/live-reserve-adapter-declarations.ts b/shared/types/live-reserve-adapter-declarations.ts index 5d1b0c1e11..ab499948a7 100644 --- a/shared/types/live-reserve-adapter-declarations.ts +++ b/shared/types/live-reserve-adapter-declarations.ts @@ -932,7 +932,9 @@ export const LIVE_RESERVE_ADAPTER_DESCRIPTOR_DECLARATIONS = { allowedFreshnessModes: VERIFIED_ONLY_FRESHNESS, }, }, - "usdgo-transparency": declareAdapter("usdgoAssurance", HTTP_DISCLOSURE_ATTESTATION_V2), + "usdgo-transparency": declareAdapter("usdgoAssurance", HTTP_DISCLOSURE_ATTESTATION_V2, { + configValidation: configPolicy(["attestation-mix"], [3]), + }), "usdh-native-markets": { primaryInputKinds: ["http-html"], paramsSchema: "none", diff --git a/worker/src/cron/__tests__/sync-live-reserves.test.ts b/worker/src/cron/__tests__/sync-live-reserves.test.ts index c4dd405ade..52f5cfb3e3 100644 --- a/worker/src/cron/__tests__/sync-live-reserves.test.ts +++ b/worker/src/cron/__tests__/sync-live-reserves.test.ts @@ -373,6 +373,31 @@ describe("syncLiveReserves", () => { expect(recordOutcomeSafeMock).toHaveBeenCalledTimes(uniqueBreakerKeyCount); }); + it("refreshes the adapter clock after time spent earlier in the reserve queue", async () => { + const startedMs = Date.now(); + const clock = vi.spyOn(Date, "now").mockReturnValue(startedMs); + const fetch = mockAdapterRegistry(async () => ({ + slices: [{ name: "Cash", pct: 100, risk: "low" as const }], + })); + fetch.mockImplementationOnce(async () => { + clock.mockReturnValue(startedMs + 180_000); + return { + slices: [{ name: "Cash", pct: 100, risk: "low" as const }], + metadata: { freshnessMode: "not-applicable" as const }, + }; + }); + try { + const { syncLiveReserves } = await import("../sync-live-reserves"); + await syncLiveReserves(mockD1(), new AbortController().signal, {}); + expect(fetch).toHaveBeenCalledWith( + expect.anything(), expect.anything(), expect.anything(), + expect.objectContaining({ nowSec: Math.floor(startedMs / 1_000) + 180 }), + ); + } finally { + clock.mockRestore(); + } + }); + it("reuses identical shared HTTP reserve sources within a run", async () => { const adapterFetch = mockAdapterRegistry(async () => ({ diff --git a/worker/src/cron/reserve-adapters/__tests__/fixtures/usdgo-transparency.html b/worker/src/cron/reserve-adapters/__tests__/fixtures/usdgo-transparency.html index b6c5a5ea69..1174a14794 100644 --- a/worker/src/cron/reserve-adapters/__tests__/fixtures/usdgo-transparency.html +++ b/worker/src/cron/reserve-adapters/__tests__/fixtures/usdgo-transparency.html @@ -1,4 +1,4 @@ - +
@@ -11,4 +11,5 @@

Reserve attestations

Apr May Jun + Jul
diff --git a/worker/src/cron/reserve-adapters/__tests__/independent-assurance.test.ts b/worker/src/cron/reserve-adapters/__tests__/independent-assurance.test.ts index 5d7360a6a7..4021ceb785 100644 --- a/worker/src/cron/reserve-adapters/__tests__/independent-assurance.test.ts +++ b/worker/src/cron/reserve-adapters/__tests__/independent-assurance.test.ts @@ -273,7 +273,7 @@ describe("independent-assurance manifest framework", () => { it("still fails closed when the USDGO family has two reports for the reviewed latest date", async () => { const fixture = "usdgo-transparency.html"; const ambiguous = readIndexFixture(fixture) + - 'Jun revised'; + 'Jul revised'; await expect( verifyRealIndexFixture("USDGO", USDGO_INDEPENDENT_ASSURANCE_PROFILE, fixture, ambiguous), ).rejects.toThrow("reviewed report URL is missing or duplicated"); diff --git a/worker/src/cron/reserve-adapters/__tests__/river-protocol-info.test.ts b/worker/src/cron/reserve-adapters/__tests__/river-protocol-info.test.ts index 4bfa057c2f..1ab1b50572 100644 --- a/worker/src/cron/reserve-adapters/__tests__/river-protocol-info.test.ts +++ b/worker/src/cron/reserve-adapters/__tests__/river-protocol-info.test.ts @@ -73,6 +73,10 @@ function primeRiverChainMocks(overrides: Record> = { const satUsd = state.debtToken ?? SATUSD_BY_CHAIN[chain]; return Promise.resolve(calls.map(({ label }) => { + const appIndex = label.match(/^app:trove-manager:(\d+)$/); + if (appIndex && Number(appIndex[1]) >= state.troveManagers.length) { + return { label, success: false, returnData: "0x" as const }; + } const returnData = ((): `0x${string}` => { if (label === "app:debt-token") return word(satUsd); if (label === "app:balances") return `${word(ONE)}${word(state.totalDebt).slice(2)}` as `0x${string}`; @@ -224,7 +228,7 @@ describe("fetchRiverProtocolInfoReserves branch redemption telemetry", () => { } as never); }); - it("sums per-chain trove debt and reports the highest branch redemption rate", async () => { + it("ignores reverting unused manager slots while summing debt and bounding branch fees", async () => { const result = await fetchRiverProtocolInfoReserves(makeCoin(), liveConfig, AbortSignal.timeout(5_000)); expect(result.metadata?.redemption).toMatchObject({ diff --git a/worker/src/cron/reserve-adapters/__tests__/usdgo-transparency.test.ts b/worker/src/cron/reserve-adapters/__tests__/usdgo-transparency.test.ts index feb7dd6b97..710838c0a8 100644 --- a/worker/src/cron/reserve-adapters/__tests__/usdgo-transparency.test.ts +++ b/worker/src/cron/reserve-adapters/__tests__/usdgo-transparency.test.ts @@ -4,25 +4,10 @@ import type { LiveReservesConfig } from "@shared/types/live-reserves"; import { fetchUsdgoTransparencyReserves } from "../usdgo-transparency"; import { validateAdapterOutput } from "../validate"; import { getReserveAdapter } from "../index"; -import { fetchEvmRpcBatch } from "../../../lib/evm-rpc"; import { fetchJsonWithRetry } from "../helpers"; import { fetchIndependentAssuranceReserves } from "../independent-assurance"; -const REPORT_BLOCK = 89_166_720; -const REPORT_BLOCK_HASH = "0xf39651e0ea42f8f78d0d375fa39ddd531896083e3f5c1daef7e7efa987ee7939"; -const REPORT_TIMESTAMP = 1_782_863_999; -const BUIDL_RAW = 170_977_843_010_000n; -const CODE_HASH = "0xee8a105971995661291a9f284262a87abf2381b3cdc93b2c8fbeffe4cd636dd9"; - -vi.mock("viem/utils", async () => { - const actual = await vi.importActual("viem"); - return { ...actual, keccak256: vi.fn(() => CODE_HASH) }; -}); - -vi.mock("../../../lib/evm-rpc", async () => { - const actual = await vi.importActual("../../../lib/evm-rpc"); - return { ...actual, fetchEvmRpcBatch: vi.fn() }; -}); +const REPORT_TIMESTAMP = 1_785_542_399; vi.mock("../helpers", async () => { const actual = await vi.importActual("../helpers"); @@ -37,7 +22,7 @@ vi.mock("../independent-assurance", async () => { const coin = { id: "usdgo-osl", symbol: "USDGO" } as StablecoinMeta; const config = { adapter: "usdgo-transparency", - version: 2, + version: 3, semantics: "attestation-mix", inputs: { primary: { @@ -51,24 +36,9 @@ const config = { indexHost: "www.anchorage.com", reportHosts: ["learn.anchorage.com"], issuerCrossCheckUrl: "https://www.usdgo.com/api/lark-bitable", - avalancheRpcUrl: "https://api.avax.network/ext/bc/C/rpc", - avalancheBuidlToken: "0x53fc82f14f009009b440a706e31c9021e1196a2f", - avalancheBuidlWallet: "0xc1d56e817d8f6c53d42ed50ed0d789eeb1495b5e", - avalancheBuidlBlock: REPORT_BLOCK, - avalancheBuidlBlockHash: REPORT_BLOCK_HASH, - expectedBuidlCodeHash: CODE_HASH, }, } as LiveReservesConfig; -function mockBuidl(balanceRaw = BUIDL_RAW, blockHash = REPORT_BLOCK_HASH): void { - vi.mocked(fetchEvmRpcBatch).mockResolvedValue([ - { number: `0x${REPORT_BLOCK.toString(16)}`, hash: blockHash, timestamp: `0x${REPORT_TIMESTAMP.toString(16)}` }, - "0x6000", - `0x${balanceRaw.toString(16).padStart(64, "0")}`, - "0x" + "6".padStart(64, "0"), - ]); -} - function mockIssuer(lastUpdated = "Aug 11, 2026", overrides: Record = {}): void { vi.mocked(fetchJsonWithRetry).mockResolvedValue({ ok: true, @@ -90,19 +60,18 @@ beforeEach(() => { vi.clearAllMocks(); vi.mocked(fetchIndependentAssuranceReserves).mockResolvedValue({ slices: [ - { name: "FDIC-insured bank cash", pct: 1.092853, risk: "very-low", assetClass: "bank-deposit" }, - { name: "BlackRock BUIDL", pct: 19.856381, risk: "low", coinId: "buidl-blackrock", assetClass: "fund-share" }, - { name: "Goldman Sachs STBXX (CUSIP 38151N205)", pct: 11.510182, risk: "low", assetClass: "money-market-fund" }, - { name: "JPMorgan JLTXX (CUSIP 46655R119)", pct: 67.540584, risk: "low", assetClass: "money-market-fund" }, + { name: "FDIC-insured bank cash", pct: 0.990893, risk: "very-low", assetClass: "bank-deposit" }, + { name: "BlackRock BUIDL", pct: 27.987907, risk: "low", coinId: "buidl-blackrock", assetClass: "fund-share" }, + { name: "Goldman Sachs STBXX (CUSIP 38151N205)", pct: 8.904468, risk: "low", assetClass: "money-market-fund" }, + { name: "JPMorgan JLTXX (CUSIP 46655R119)", pct: 62.116731, risk: "low", assetClass: "money-market-fund" }, ], metadata: { sourceTimestamp: REPORT_TIMESTAMP, freshnessMode: "verified", - collateralizationRatio: 861_072_523 / 859_224_943, - details: { assurance: { reportUrl: "https://learn.anchorage.com/06.30.26_USDGO-Stablecoin-Attestation-Report.pdf" } }, + collateralizationRatio: 1_116_301_304 / 1_112_640_495, + details: { assurance: { reportUrl: "https://learn.anchorage.com/07.31.26_USDGO-Stablecoin-Attestation-Report-signed.pdf" } }, }, }); - mockBuidl(); mockIssuer(); }); @@ -111,18 +80,19 @@ describe("usdgo-transparency independent promotion", () => { const result = await fetchUsdgoTransparencyReserves(coin, config, new AbortController().signal); expect(result.slices).toHaveLength(4); + expect(result.metadata).not.toHaveProperty("buidlOnchain"); expect(result.metadata).toMatchObject({ sourceTimestamp: REPORT_TIMESTAMP, freshnessMode: "verified", - totalReserveUsd: 861_072_523, - totalAssetsUsd: 861_072_523, - totalLiabilitiesUsd: 859_224_943, - supplyUsd: 859_224_943, - shareholderEquityUsd: 1_847_580, + totalReserveUsd: 1_116_301_304, + totalAssetsUsd: 1_116_301_304, + totalLiabilitiesUsd: 1_112_640_495, + supplyUsd: 1_112_640_495, + shareholderEquityUsd: 3_660_809, unknownExposurePct: 0, details: { authoritativeBasis: "Deloitte examination report; issuer API is cross-check only", - reportSurplusUsd: 1_847_580, + reportSurplusUsd: 3_660_809, }, }); expect(result.metadata?.issuerCrossCheck).toMatchObject({ sourceTimestamp: expect.any(Number) }); @@ -131,22 +101,15 @@ describe("usdgo-transparency independent promotion", () => { ]); }); - it("fails closed when the pinned BUIDL balance no longer matches the examined report", async () => { - mockBuidl(BUIDL_RAW + 2_000_000n); - await expect(fetchUsdgoTransparencyReserves(coin, config, new AbortController().signal)).rejects.toThrow( - "BUIDL chain balance diverges", - ); - }); - - it("fails closed on a Rootstock-style pinned block identity drift in the BUIDL proof", async () => { - mockBuidl(BUIDL_RAW, "0x" + "1".repeat(64)); + it("fails closed when the current independent report cannot be verified", async () => { + vi.mocked(fetchIndependentAssuranceReserves).mockRejectedValue(new Error("report hash drift")); await expect(fetchUsdgoTransparencyReserves(coin, config, new AbortController().signal)).rejects.toThrow( - "BUIDL block hash drifted", + "report hash drift", ); }); it("treats a same-period issuer disagreement as fatal", async () => { - mockIssuer("Jun 30, 2026", { buidlUsdM: "150", backingAssetsM: "950.46", circulationSupplyMFormatted: "859.224943" }); + mockIssuer("Jul 31, 2026", { buidlUsdM: "150", backingAssetsM: "950.46", circulationSupplyMFormatted: "859.224943" }); await expect(fetchUsdgoTransparencyReserves(coin, config, new AbortController().signal)).rejects.toThrow( "issuer cross-check disagrees", ); diff --git a/worker/src/cron/reserve-adapters/river-protocol-info.ts b/worker/src/cron/reserve-adapters/river-protocol-info.ts index 389a1e6e17..ba1c2cd10a 100644 --- a/worker/src/cron/reserve-adapters/river-protocol-info.ts +++ b/worker/src/cron/reserve-adapters/river-protocol-info.ts @@ -157,6 +157,7 @@ async function executeRiverObservation( contract: entry.contract, data: entry.data, allowFailure: entry.allowFailure, + optional: entry.allowFailure, })), read: (planCalls) => fetchOnchainMulticall3({ calls: planCalls, diff --git a/worker/src/cron/reserve-adapters/usdgo-transparency.ts b/worker/src/cron/reserve-adapters/usdgo-transparency.ts index c9f88e772e..05a3640d19 100644 --- a/worker/src/cron/reserve-adapters/usdgo-transparency.ts +++ b/worker/src/cron/reserve-adapters/usdgo-transparency.ts @@ -3,15 +3,11 @@ import { parseLiveReserveAdapterParams, type LiveReserveAdapterParamsByKey } fro import { getIndependentAssuranceManifest } from "@shared/lib/independent-assurance"; import type { StablecoinMeta } from "@shared/types/core"; import type { LiveReserveWarning, LiveReservesConfig } from "@shared/types/live-reserves"; -import { keccak256, toBytes } from "viem/utils"; -import { fetchEvmRpcBatch } from "../../lib/evm-rpc"; -import { encodeAddressCallData } from "../../lib/evm-selectors"; import type { AdapterContext, AdapterResult } from "./types"; import { fetchJsonWithRetry, reserveInfoWarning, } from "./helpers"; -import { runAdapterIo } from "./concurrency"; import { fetchIndependentAssuranceReserves, type IndependentAssuranceProfile, @@ -19,13 +15,8 @@ import { import { formatValidIsoDate } from "./report-date"; const ADAPTER_KEY = "usdgo-transparency"; -const BUIDL_DECIMALS = 6; -const BUIDL_REPORT_AMOUNT_USD = 170_977_843; -const BUIDL_MAX_ROUNDING_DIVERGENCE_USD = 1; const SAME_PERIOD_CROSS_CHECK_MAX_AGE_SEC = 3 * 24 * 60 * 60; const CROSS_CHECK_TOLERANCE_PCT = 1; -const BUIDL_BALANCE_OF_SELECTOR = "0x70a08231"; -const BUIDL_DECIMALS_SELECTOR = "0x313ce567"; export const USDGO_INDEPENDENT_ASSURANCE_PROFILE: IndependentAssuranceProfile = { adapterName: ADAPTER_KEY, @@ -77,16 +68,6 @@ interface UsdgoIssuerCrossCheckPayload { data?: Record; } -interface BuidlObservation { - blockNumber: number; - blockHash: string; - blockTimestamp: number; - balanceUsd: number; - balanceRaw: bigint; - decimals: number; - codeHash: string; -} - function usdgoReportDate(href: string): string | null { const fileName = decodeURIComponent(new URL(href).pathname.split("/").pop() ?? ""); const match = fileName.match(/^(\d{2})[.](\d{2})[.](\d{2})_USDGO[-_]Stablecoin[-_]Attestation[-_]Report/i); @@ -97,106 +78,6 @@ function usdgoReportDate(href: string): string | null { return formatValidIsoDate(year, month, day, 2000); } -function requireHex(value: unknown, label: string): string { - if (typeof value !== "string" || !/^0x[0-9a-fA-F]+$/.test(value)) { - throw new Error(`${ADAPTER_KEY}: ${label} is not a hex result`); - } - return value; -} - -function requireUint(value: unknown, label: string): bigint { - const raw = requireHex(value, label); - try { - return BigInt(raw); - } catch { - throw new Error(`${ADAPTER_KEY}: ${label} is not an integer`); - } -} - -function requireBlock(value: unknown, expectedNumber: number, expectedHash: string): { - number: number; - hash: string; - timestamp: number; -} { - if (!value || typeof value !== "object") throw new Error(`${ADAPTER_KEY}: BUIDL block header is missing`); - const block = value as { number?: unknown; hash?: unknown; timestamp?: unknown }; - const numberRaw = requireHex(block.number, "BUIDL block number"); - const hash = requireHex(block.hash, "BUIDL block hash"); - const timestampRaw = requireHex(block.timestamp, "BUIDL block timestamp"); - const number = Number(BigInt(numberRaw)); - const timestamp = Number(BigInt(timestampRaw)); - if (!Number.isSafeInteger(number) || number !== expectedNumber) { - throw new Error(`${ADAPTER_KEY}: BUIDL block number drifted`); - } - if (!/^0x[0-9a-fA-F]{64}$/.test(hash) || hash.toLowerCase() !== expectedHash.toLowerCase()) { - throw new Error(`${ADAPTER_KEY}: BUIDL block hash drifted`); - } - if (!Number.isSafeInteger(timestamp) || timestamp <= 0) { - throw new Error(`${ADAPTER_KEY}: BUIDL block timestamp is invalid`); - } - return { number, hash, timestamp }; -} - -async function readBuidlObservation( - params: LiveReserveAdapterParamsByKey["usdgo-transparency"], - signal: AbortSignal, - ctx?: AdapterContext, -): Promise { - const blockTag = `0x${params.avalancheBuidlBlock.toString(16)}`; - const results = await runAdapterIo( - ctx, - `${ADAPTER_KEY}:buidl-rpc`, - () => fetchEvmRpcBatch("avalanche", [ - { method: "eth_getBlockByNumber", params: [blockTag, false] }, - { method: "eth_getCode", params: [params.avalancheBuidlToken, blockTag] }, - { - method: "eth_call", - params: [{ to: params.avalancheBuidlToken, data: encodeAddressCallData(BUIDL_BALANCE_OF_SELECTOR, params.avalancheBuidlWallet) }, blockTag], - }, - { - method: "eth_call", - params: [{ to: params.avalancheBuidlToken, data: BUIDL_DECIMALS_SELECTOR }, blockTag], - }, - ], { - extraRpcUrls: [params.avalancheRpcUrl], - signal, - timeoutMs: 8_000, - maxRetries: 0, - chainRpcs: ctx?.chainRpcs, - }), - { signal }, - ); - if (!results) throw new Error(`${ADAPTER_KEY}: Avalanche BUIDL RPC batch failed`); - - const block = requireBlock(results[0], params.avalancheBuidlBlock, params.avalancheBuidlBlockHash); - const code = requireHex(results[1], "BUIDL token code"); - if (code === "0x" || keccak256(toBytes(code)).toLowerCase() !== params.expectedBuidlCodeHash.toLowerCase()) { - throw new Error(`${ADAPTER_KEY}: BUIDL token code hash drifted`); - } - const balanceRaw = requireUint(results[2], "BUIDL balanceOf"); - const decimals = Number(requireUint(results[3], "BUIDL decimals")); - if (decimals !== BUIDL_DECIMALS) throw new Error(`${ADAPTER_KEY}: BUIDL decimals drifted`); - - const balanceUsd = Number(balanceRaw) / 10 ** decimals; - if (balanceRaw <= 0n || balanceRaw > BigInt(Number.MAX_SAFE_INTEGER) || !Number.isFinite(balanceUsd) || balanceUsd <= 0) { - throw new Error(`${ADAPTER_KEY}: BUIDL balance is invalid`); - } - const divergenceUsd = Math.abs(balanceUsd - BUIDL_REPORT_AMOUNT_USD); - if (divergenceUsd > BUIDL_MAX_ROUNDING_DIVERGENCE_USD) { - throw new Error(`${ADAPTER_KEY}: BUIDL chain balance diverges from Deloitte report by $${divergenceUsd.toFixed(2)}`); - } - - return { - blockNumber: block.number, - blockHash: block.hash, - blockTimestamp: block.timestamp, - balanceUsd, - balanceRaw, - decimals, - codeHash: keccak256(toBytes(code)), - }; -} - function parseMillionUsd(value: unknown, label: string): number { const parsed = typeof value === "number" ? value : typeof value === "string" ? Number(value) : Number.NaN; if (!Number.isFinite(parsed) || parsed < 0) throw new Error(`${ADAPTER_KEY}: invalid issuer ${label}`); @@ -317,11 +198,10 @@ export async function fetchUsdgoTransparencyReserves( params, ctx, ); - const buidlPromise = readBuidlObservation(params, signal, ctx); const crossCheckPromise = readIssuerCrossCheck(params.issuerCrossCheckUrl, signal, ctx).catch((error: unknown) => ({ error: toErrorMessage(error), })); - const [assurance, buidl, crossCheck] = await Promise.all([assurancePromise, buidlPromise, crossCheckPromise]); + const [assurance, crossCheck] = await Promise.all([assurancePromise, crossCheckPromise]); const warnings = [...(assurance.warnings ?? [])]; let issuerCrossCheckDetails: Record | undefined; @@ -356,20 +236,6 @@ export async function fetchUsdgoTransparencyReserves( supplyUsd: reportValues.supplyUsd, shareholderEquityUsd: reportSurplusUsd, collateralizationRatio: reportValues.totalReserveUsd / reportValues.supplyUsd, - buidlOnchain: { - chain: "avalanche", - tokenAddress: params.avalancheBuidlToken, - walletAddress: params.avalancheBuidlWallet, - blockNumber: buidl.blockNumber, - blockHash: buidl.blockHash, - blockTimestamp: buidl.blockTimestamp, - balanceRaw: buidl.balanceRaw.toString(), - decimals: buidl.decimals, - balanceUsd: buidl.balanceUsd, - reportBalanceUsd: BUIDL_REPORT_AMOUNT_USD, - divergenceUsd: buidl.balanceUsd - BUIDL_REPORT_AMOUNT_USD, - codeHash: buidl.codeHash, - }, ...(issuerCrossCheckDetails ? { issuerCrossCheck: issuerCrossCheckDetails } : {}), details: { ...assuranceDetails, diff --git a/worker/src/cron/sync-live-reserves.ts b/worker/src/cron/sync-live-reserves.ts index e346d9ec05..b7a8a5273d 100644 --- a/worker/src/cron/sync-live-reserves.ts +++ b/worker/src/cron/sync-live-reserves.ts @@ -291,6 +291,7 @@ async function runAdapterAttempt( try { const result = await raceWithAbortSignal( adapter.fetch(coin, config, attemptSignal, Object.assign({}, adapterCtx, { + nowSec: Math.floor(startedMs / 1_000), abortSignal: attemptSignal, ioLimiter: instrumentedLimiter, })), @@ -739,7 +740,6 @@ export async function syncLiveReserves( const effectiveAdapterCtx: AdapterContext = { db, ...(adapterCtx ?? {}), - nowSec: runStartedAt, requestCache, }; const cohortTotal = orderedCoins.length; From 358ee1188be5e9a78ff2f5cf77fa5a2a4fd472e9 Mon Sep 17 00:00:00 2001 From: TokenBrice Date: Sat, 5 Sep 2026 20:36:35 +0200 Subject: [PATCH 2/4] fix(yield): retry transient D1 overload during retention A successful yield publication was reported failed when post-publication cleanup hit D1 queue overload. Retry only the idempotent cleanup with the existing bounded abort-aware policy, preserving terminal schema and exhausted-retry errors. Plan: agents/2026-09-05-night-watch/README.md Tasks: Y1 --- docs/yield-intelligence.md | 2 +- .../cron/__tests__/yield-publication.test.ts | 20 ++++++++++++++++++- .../cron/yield-sync/coordinator-persist.ts | 2 ++ worker/src/cron/yield-sync/publication.ts | 11 ++++++++++ 4 files changed, 33 insertions(+), 2 deletions(-) diff --git a/docs/yield-intelligence.md b/docs/yield-intelligence.md index 58718ac50c..026fff3631 100644 --- a/docs/yield-intelligence.md +++ b/docs/yield-intelligence.md @@ -586,7 +586,7 @@ The methodology above is the durable public contract. Runtime topology, storage - `yield_publication_generations` and `yield_source_decisions` record publication state and bounded source-selection evidence. Repeated unchanged anomaly evidence is 30-day audit data; source switches and anomaly-episode boundaries remain durable. - Public rankings expose only a validated published generation. Failed validation, stale-writer, or publication attempts leave the previous public snapshot intact. - The final resolve-stage eligibility pass removes null, non-finite, or thin measured venue-TVL candidates in the three deposit-venue classes before evaluation and arbitration, across tracked, explicit, auto-discovered, supplemental, and linked-variant paths. Only eligible candidates can reach a validated published generation. -- History retention is enforced by the producer. Legacy rows remain explicitly partial rather than receiving invented evidence. +- History retention is enforced by the producer. Idempotent cleanup retries transient D1 overloads with the shared bounded, abort-aware retry policy; exhausted retries and schema errors still fail visibly without replaying publication. Legacy rows remain explicitly partial rather than receiving invented evidence. ### Producers And Consumers diff --git a/worker/src/cron/__tests__/yield-publication.test.ts b/worker/src/cron/__tests__/yield-publication.test.ts index b4afb0e54c..710e46313f 100644 --- a/worker/src/cron/__tests__/yield-publication.test.ts +++ b/worker/src/cron/__tests__/yield-publication.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from "vitest"; +import { describe, expect, it, vi } from "vitest"; import { existsSync, readFileSync } from "node:fs"; import path from "node:path"; @@ -555,6 +555,24 @@ describe("publishYieldCoordinatorResults", () => { }); describe("pruneYieldTables", () => { + it("retries transient retention overload without dropping cleanup", async () => { + const { sqlite, db } = createLatestSchemaSqlite(); + const prepare = db.prepare.bind(db); + let retentionAttempts = 0; + vi.spyOn(db, "prepare").mockImplementation((sql) => { + if (sql.includes("pharos:yield-sync:daily-history-retention-delete") && ++retentionAttempts === 1) { + throw new Error("D1_ERROR: D1 DB is overloaded. Requests queued for too long."); + } + return prepare(sql); + }); + try { + await pruneYieldTables(db, Math.floor(FIXED_NOW.getTime() / 1000)); + expect(retentionAttempts).toBe(2); + } finally { + sqlite.close(); + } + }); + it("surfaces a missing mandatory daily-history table during materialization", async () => { const { sqlite, db } = createLatestSchemaSqlite(); try { diff --git a/worker/src/cron/yield-sync/coordinator-persist.ts b/worker/src/cron/yield-sync/coordinator-persist.ts index 2af0b2a24a..4430f94a2e 100644 --- a/worker/src/cron/yield-sync/coordinator-persist.ts +++ b/worker/src/cron/yield-sync/coordinator-persist.ts @@ -199,6 +199,7 @@ export async function publishYieldCoordinatorResults(params: { }); await pruneYieldTables(params.db, params.startSec, { allowDestructiveCleanup: false, + signal: params.signal, }); return { ok: true, @@ -226,6 +227,7 @@ export async function publishYieldCoordinatorResults(params: { throwIfAborted(params.signal); await pruneYieldTables(params.db, params.startSec, { allowDestructiveCleanup: params.degradationReasons.length === 0, + signal: params.signal, }); return { diff --git a/worker/src/cron/yield-sync/publication.ts b/worker/src/cron/yield-sync/publication.ts index ebfa70b472..f00e91e6c8 100644 --- a/worker/src/cron/yield-sync/publication.ts +++ b/worker/src/cron/yield-sync/publication.ts @@ -3,6 +3,7 @@ import { bucketUnixSecondsToUtcDay } from "@shared/lib/time-buckets"; import { ACTIVE_STABLECOINS, FROZEN_IDS } from "@shared/lib/stablecoins/registry"; import { YIELD_HISTORY_MAX_DAYS, YIELD_HISTORY_RAW_DAYS } from "@shared/lib/yield-history-policy"; import { deleteOrphanYieldRows, deleteStaleYieldRows, purgeYieldHistoryOwnershipHandoffs } from "./history"; +import { runWithOverloadRetry } from "../../lib/d1-overload-retry"; export { derivePreviousYieldRankingsCount, @@ -135,7 +136,17 @@ export async function pruneYieldTables( startSec: number, options?: { allowDestructiveCleanup?: boolean; + signal?: AbortSignal; }, +): Promise { + // Cleanup is idempotent; retry transient overload without replaying publication. + await runWithOverloadRetry(() => pruneYieldTablesOnce(db, startSec, options), 3, options?.signal); +} + +async function pruneYieldTablesOnce( + db: D1Database, + startSec: number, + options?: { allowDestructiveCleanup?: boolean }, ): Promise { const allowDestructiveCleanup = options?.allowDestructiveCleanup ?? true; const managedYieldIds = ACTIVE_STABLECOINS.map((meta) => meta.id); From 561f3262e23819c94d44e830c613206d53d53252 Mon Sep 17 00:00:00 2001 From: TokenBrice Date: Sat, 5 Sep 2026 20:36:35 +0200 Subject: [PATCH 3/4] fix(pricing): preserve hourly observations through publication Hourly fallback observations were routed through ordinary replay policy, discarding approved low-volume quotes and non-replay sources. Keep actual fetched observations in a CAS-fenced handoff, enforce both stage and source ages, and revalidate missing prices without elevating fallback authority. Plan: agents/2026-09-05-night-watch/README.md Tasks: P1 --- docs/pricing-pipeline.md | 4 +- .../sync-stablecoins-post-enrichment.test.ts | 70 +++++++++++++++++++ .../__tests__/price-corroboration.test.ts | 32 ++++++++- .../cron/sync-stablecoins/post-enrichment.ts | 36 ++++++++++ .../price-corroboration-observations.ts | 56 +++++++++++++++ .../sync-stablecoins/price-corroboration.ts | 13 +++- 6 files changed, 207 insertions(+), 4 deletions(-) create mode 100644 worker/src/cron/sync-stablecoins/price-corroboration-observations.ts diff --git a/docs/pricing-pipeline.md b/docs/pricing-pipeline.md index ae4c713a93..159e125df7 100644 --- a/docs/pricing-pipeline.md +++ b/docs/pricing-pipeline.md @@ -13,7 +13,7 @@ Supply fallback behavior is owned by [Supply Snapshot: Supply Pipeline](./supply Pharos separates critical publication from best-effort corroboration: 1. **15-minute publication** runs the full primary consensus: DefiLlama and CoinGecko, the curated CoinGecko ticker and CEX lanes, RedStone, Curve on-chain/oracle, reserve NAV telemetry, promoted DEX observations, and the post-consensus pool challenge. Registered authoritative overrides then run before publication. -2. **Hourly corroboration** runs `enrichMissingPrices()` and the explicitly enabled exact-address provider against only the latest missing or low-depth rows, then stages provenance in `price_cache` for a later publication to revalidate. +2. **Hourly corroboration** runs `enrichMissingPrices()` and the explicitly enabled exact-address provider against only the latest missing or low-depth rows. It stages actual fetched observations in `price:corroboration-observations:v1` for a later publication to revalidate, separately from ordinary `price_cache` replay provenance. The output is the cached `price`, `priceSource`, `priceConfidence`, `priceObservedAt`, `priceObservedAtMode`, `priceSyncedAt`, optional `priceSourceConfidenceProfile`, and compatibility `priceUpdatedAt` fields served through `/api/stablecoins`. @@ -351,6 +351,8 @@ The same registry also supports historical replay for backfills where a provider ## Fallback Enrichment +The hourly observation handoff expires after one hour plus one 15-minute publication interval, allowing the next hourly primary to consume it before replacement corroboration runs. It independently enforces each source's existing observation-age limit, including the reviewed seven-day CoinGecko low-volume window. Only freshly collected provider observations enter it; published references and cached replays never do. A missing-price consumer revalidates candidates with the normal fallback and severe-downside corroboration guards, preserves their source and observation time, and publishes only `fallback` confidence. The existing `cachedFallbackCount` includes this D1 handoff and ordinary replay recovery. Empty successful collections clear the handoff; older writers cannot replace newer snapshots. Ordinary replay lifetime and trust rules are unchanged. + Fallbacks are detached from the critical publication. On the existing quarter-hour trigger's top-of-hour invocation, `runPriceCorroboration()` reads the just-published `stablecoins` cache, selects only rows that are missing a price or have fewer than three consensus sources, and runs `enrichMissingPrices()` against isolated probe copies. A fallback error cannot change the completed `sync-stablecoins` result or block its canonical cache write. Successful probes update `price_cache`; the next 15-minute publication revalidates those entries before using them as continuity. The hourly pass order is: 1. **Pass 1:** DefiLlama `coins.llama.fi` by canonical tracked contract identity, using the upstream row address when present and falling back to curated tracked `contracts` metadata when the upstream row is addressless. Accepted quotes must carry a fresh upstream timestamp, confidence, and matching symbol, then pass shared peg-aware bounds before they can resolve the asset. Schema-invalid OK responses record `dl-coins` breaker failures instead of being treated as healthy empty coverage. diff --git a/worker/src/cron/__tests__/sync-stablecoins-post-enrichment.test.ts b/worker/src/cron/__tests__/sync-stablecoins-post-enrichment.test.ts index 4e377ea218..ad433f2195 100644 --- a/worker/src/cron/__tests__/sync-stablecoins-post-enrichment.test.ts +++ b/worker/src/cron/__tests__/sync-stablecoins-post-enrichment.test.ts @@ -1,5 +1,12 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; import { mockD1 } from "@shared/test-utils/mock-d1"; +import { DatabaseSync } from "node:sqlite"; +import { createSqliteD1 } from "../../test-helpers/sqlite-d1"; +import { normalizeStablecoinsPayload } from "../sync-stablecoins/shared"; +import { + loadPriceCorroborationObservations, + writePriceCorroborationObservations, +} from "../sync-stablecoins/price-corroboration-observations"; vi.mock("@shared/lib/stablecoins/registry", async (importOriginal) => { const actual = await importOriginal(); @@ -65,6 +72,69 @@ describe("runPostEnrichmentPricePipeline", () => { fetchCurrentNativePegImpliedUsdQuotesMock.mockReset().mockResolvedValue(new Map()); }); + it.each([ + ["tryb-bilira", "peggedTRY", 0.022, "coingecko-low-volume", 40 * 3600, 900, true], + ["gbpm-mento", "peggedGBP", 1.32, "coingecko-low-volume", 40 * 3600, 900, true], + ["cadm-mento", "peggedCAD", 0.73, "coingecko-low-volume", 40 * 3600, 900, true], + ["usdt-tether", "peggedUSD", 1, "coinmarketcap", 1200, 900, true], + ["usdt-tether", "peggedUSD", 1, "coingecko-low-volume", 8 * 86400, 900, false], + ["usdt-tether", "peggedUSD", 1, "coinmarketcap", 3601, 900, false], + ["usdt-tether", "peggedUSD", 1, "coingecko-onchain-address", 901, 900, false], + ["gbpm-mento", "peggedGBP", 1.32, "coingecko-low-volume", 41 * 3600, 3601, true], + ["usdt-tether", "peggedUSD", 1, "coingecko-low-volume", 40 * 3600, 4501, false], + ["usdt-tether", "peggedUSD", 1, "cached", 60, 30, false], + ["usdt-tether", "peggedUSD", 0.45, "coinmarketcap", 60, 30, false], + ["usdt-tether", "peggedUSD", 1, "coinmarketcap", -60, 30, false], + ] as const)("revalidates staged %s %s %s %s evidence aged %s with stage age %s", async ( + id, pegType, price, source, observationAge, stageAge, accepted, + ) => { + const now = 1_800_000_000; + const clock = vi.spyOn(Date, "now").mockReturnValue(now * 1000); + const sqlite = new DatabaseSync(":memory:"); + sqlite.exec("CREATE TABLE cache (key TEXT PRIMARY KEY, value TEXT NOT NULL, updated_at INTEGER NOT NULL)"); + const db = createSqliteD1(sqlite); + const asset = makeAsset({ id, pegType, price: null, priceSource: undefined, priceConfidence: null }); + try { + await writePriceCorroborationObservations(db, [{ + id, source, price, observedAt: now - observationAge, observedAtMode: "upstream", + }], now - stageAge); + const result = await runPostEnrichmentPricePipeline({ + assets: [asset], missingBefore: new Set([id]), db, syncStartSec: now, + priceCache: new Map(), validationContexts: { get: makeValidationContext }, + validationReferences: { rates: { [pegType]: pegType === "peggedUSD" ? 1 : price }, type: "fresh", updatedAt: now }, + previousTrustedPrices: new Map(), returnIfAborted: () => null, + abortResult: () => ({ status: "error", metadata: "{}" }), + }, ""); + expect(isAbortResult(result)).toBe(false); + const normalized = normalizeStablecoinsPayload({ peggedAssets: [asset] }).peggedAssets[0]!; + expect(normalized.price).toBe(accepted ? price : null); + if (accepted) { + expect(normalized.priceSource).toBe(source); + expect(normalized.priceConfidence).toBe("fallback"); + expect(normalized.priceObservedAt).toBe(now - observationAge); + } + } finally { + clock.mockRestore(); + sqlite.close(); + } + }); + + it("clears an empty hourly collection and rejects an older stage writer", async () => { + const sqlite = new DatabaseSync(":memory:"); + sqlite.exec("CREATE TABLE cache (key TEXT PRIMARY KEY, value TEXT NOT NULL, updated_at INTEGER NOT NULL)"); + const db = createSqliteD1(sqlite); + const rows = [{ id: "usdt-tether", source: "coinmarketcap", price: 1, observedAt: 1000, observedAtMode: "upstream" as const }]; + try { + await writePriceCorroborationObservations(db, rows, 1000); + expect((await loadPriceCorroborationObservations(db, 1001)).size).toBe(1); + await writePriceCorroborationObservations(db, [], 1002); + await writePriceCorroborationObservations(db, rows, 1001); + expect((await loadPriceCorroborationObservations(db, 1003)).size).toBe(0); + } finally { + sqlite.close(); + } + }); + it("replaces weak non-USD fiat prices with fresh native-implied USD prices", async () => { const asset = makeAsset(); const db = mockD1(); diff --git a/worker/src/cron/sync-stablecoins/__tests__/price-corroboration.test.ts b/worker/src/cron/sync-stablecoins/__tests__/price-corroboration.test.ts index 3193869b7a..c798475772 100644 --- a/worker/src/cron/sync-stablecoins/__tests__/price-corroboration.test.ts +++ b/worker/src/cron/sync-stablecoins/__tests__/price-corroboration.test.ts @@ -1,10 +1,16 @@ -import { describe, expect, it } from "vitest"; +import { describe, expect, it, vi } from "vitest"; +import { DatabaseSync } from "node:sqlite"; +import { createSqliteD1 } from "../../../test-helpers/sqlite-d1"; +import * as enrichment from "../enrich-prices"; +import * as shared from "../shared"; +import { PRICE_CORROBORATION_OBSERVATIONS_KEY } from "../price-corroboration-observations"; import type { AddressPriceQuote } from "../../../lib/address-price-providers"; import type { PeggedAsset } from "../enrich-prices"; import { buildPriceCorroborationCacheEntries, buildPriceCorroborationCohort, isPriceCorroborationSlot, + runPriceCorroboration, } from "../price-corroboration"; import { makePeggedAsset } from "./_fixtures"; @@ -21,6 +27,30 @@ function quote(stablecoinId: string, priceUsd: number): AddressPriceQuote { } describe("hourly price corroboration", () => { + it("stages fetched observations without promoting the previous published reference", async () => { + const sqlite = new DatabaseSync(":memory:"); + sqlite.exec("CREATE TABLE cache (key TEXT PRIMARY KEY, value TEXT NOT NULL, updated_at INTEGER NOT NULL)"); + const db = createSqliteD1(sqlite); + const previous = makePeggedAsset({ id: "usdt-tether", price: 1, priceSource: "coingecko", priceConfidence: "single-source" }); + const previousLoad = vi.spyOn(shared, "loadPreviousStablecoinsById").mockResolvedValue({ + previousAssetsById: new Map([[previous.id, previous]]), cacheState: { state: "ok" }, + }); + const collect = vi.spyOn(enrichment, "enrichMissingPrices").mockImplementation(async (assets) => { + expect(assets[0]?.price).toBeNull(); + // No provider returned a fresh quote. The published reference cannot enter the handoff. + return {} as Awaited>; + }); + try { + await runPriceCorroboration({ db, syncStartSec: 1_800_000_000 }); + const row = sqlite.prepare("SELECT value FROM cache WHERE key = ?").get(PRICE_CORROBORATION_OBSERVATIONS_KEY); + expect(JSON.parse(String(row?.value))).toEqual([]); + } finally { + previousLoad.mockRestore(); + collect.mockRestore(); + sqlite.close(); + } + }); + it("runs only for the top-of-hour quarter-hour invocation", () => { expect(isPriceCorroborationSlot(1_800_000_000)).toBe(true); expect(isPriceCorroborationSlot(1_800_000_900)).toBe(false); diff --git a/worker/src/cron/sync-stablecoins/post-enrichment.ts b/worker/src/cron/sync-stablecoins/post-enrichment.ts index f51b50fc07..3bde79ca08 100644 --- a/worker/src/cron/sync-stablecoins/post-enrichment.ts +++ b/worker/src/cron/sync-stablecoins/post-enrichment.ts @@ -6,6 +6,9 @@ import { logWorkerEventArgs } from "../../lib/structured-log"; * Extracted to eliminate code duplication (Q-002, Q-011, CC-001). */ import { isPricingSourceSoftGuardrailExempt } from "@shared/lib/pricing-source-registry"; +import { DIVERGENCE_THRESHOLD_BPS } from "@shared/lib/pricing-pipeline-constants"; +import { pricesAgreeWithinBps } from "../../lib/price-divergence"; +import { loadPriceCorroborationObservations } from "./price-corroboration-observations"; import { countDepegAuthoritativeSources, getPriceCacheMaxAgeSec, @@ -356,8 +359,41 @@ export async function runPostEnrichmentPricePipeline( if (stillMissing.length > 0) { const priceCacheReadAbort = returnIfAborted(signal, `${abortStagePrefix}read-price-cache`); if (priceCacheReadAbort) return priceCacheReadAbort; + const staged = await loadPriceCorroborationObservations(db, now, signal); const priceCache = input.priceCache ?? await getPriceCache(db); for (const asset of stillMissing) { + const candidates = staged.get(asset.id) ?? []; + const candidatePrices = Object.fromEntries(candidates.map((candidate) => [candidate.source, candidate.price])); + for (const candidate of candidates) { + const agreeSources = [...new Set(candidates + .filter((other) => pricesAgreeWithinBps(candidate.price, other.price, DIVERGENCE_THRESHOLD_BPS)) + .map((other) => other.source))]; + const decision = validateFallbackPriceCandidate({ + price: candidate.price, + source: candidate.source, + confidence: "fallback", + agreeSources, + candidatePrices, + validationContext: validationContexts.get(asset), + validationReferences, + previousTrustedPrice: previousTrustedPrices?.get(asset.id) ?? null, + }); + if (!decision.accepted) continue; + applyAcceptedPriceCandidate({ + asset, + price: candidate.price, + source: candidate.source, + confidence: "fallback", + observedAt: candidate.observedAt, + observedAtMode: candidate.observedAtMode, + consensusSources: Object.keys(candidatePrices), + agreeSources, + syncedAt: input.syncStartSec, + }); + cachedFallbackCount++; + break; + } + if (!hasMissingPrice(asset)) continue; const cached = priceCache.get(asset.id); if (!cached) continue; const maxAgeSec = getPriceCacheMaxAgeSec(cached.source, PRICE_CACHE_TTL); diff --git a/worker/src/cron/sync-stablecoins/price-corroboration-observations.ts b/worker/src/cron/sync-stablecoins/price-corroboration-observations.ts new file mode 100644 index 0000000000..75c56e6aaf --- /dev/null +++ b/worker/src/cron/sync-stablecoins/price-corroboration-observations.ts @@ -0,0 +1,56 @@ +import { z } from "zod"; +import { getPricingSourceRegistryEntry } from "@shared/lib/pricing-source-registry"; +import { getCache, setCacheIfNewer } from "../../lib/db-cache"; +import { rethrowIfAborted } from "../../lib/abort"; +import { logWorkerEventArgs } from "../../lib/structured-log"; + +export const PRICE_CORROBORATION_OBSERVATIONS_KEY = "price:corroboration-observations:v1"; +// The next hourly primary publishes before its replacement corroboration runs. +const STAGING_MAX_AGE_SEC = (60 + 15) * 60; +const ObservationsSchema = z.array(z.object({ + id: z.string().min(1), + source: z.string().min(1), + price: z.number().finite().positive(), + observedAt: z.number().int().positive().nullable(), + observedAtMode: z.enum(["upstream", "local_fetch", "unknown"]).nullable(), +})); +export type PriceCorroborationObservation = z.infer[number]; + +export async function writePriceCorroborationObservations( + db: D1Database, + observations: PriceCorroborationObservation[], + slotStartedAt: number, + signal?: AbortSignal, +): Promise { + await setCacheIfNewer(db, PRICE_CORROBORATION_OBSERVATIONS_KEY, + JSON.stringify(observations), slotStartedAt, signal); +} + +export async function loadPriceCorroborationObservations( + db: D1Database, + nowSec: number, + signal?: AbortSignal, +): Promise> { + const byId = new Map(); + try { + const cached = await getCache(db, PRICE_CORROBORATION_OBSERVATIONS_KEY, signal); + if (!cached || cached.updatedAt > nowSec || nowSec - cached.updatedAt >= STAGING_MAX_AGE_SEC) return byId; + const parsed = ObservationsSchema.safeParse(JSON.parse(cached.value)); + if (!parsed.success) return byId; + for (const observation of parsed.data) { + const source = getPricingSourceRegistryEntry(observation.source); + const maxAge = source?.maxTrustedAgeSec; + if (!source || source.isRetired || source.trustTier === "cached_replay" || + !maxAge || maxAge <= 0 || observation.observedAt == null || + observation.observedAtMode == null || observation.observedAtMode === "unknown" || + observation.observedAt > nowSec || nowSec - observation.observedAt >= maxAge) continue; + const rows = byId.get(observation.id) ?? []; + rows.push(observation); + byId.set(observation.id, rows); + } + } catch (error) { + rethrowIfAborted(error, signal); + logWorkerEventArgs("handler", "warn", "[sync-stablecoins] Hourly price observations unavailable:", error); + } + return byId; +} diff --git a/worker/src/cron/sync-stablecoins/price-corroboration.ts b/worker/src/cron/sync-stablecoins/price-corroboration.ts index 0a795025b9..bae735535f 100644 --- a/worker/src/cron/sync-stablecoins/price-corroboration.ts +++ b/worker/src/cron/sync-stablecoins/price-corroboration.ts @@ -15,6 +15,7 @@ import { import { enrichMissingPrices, type EnrichmentStats } from "./enrich-prices"; import type { PeggedAsset } from "./enrich-prices-shared"; import { clearPriceMetadata, loadPreviousStablecoinsById } from "./shared"; +import { writePriceCorroborationObservations } from "./price-corroboration-observations"; const PRICE_CORROBORATION_SOURCE_DEPTH = 3; @@ -177,7 +178,7 @@ export async function runPriceCorroboration(params: { sourceAllowed, config: params.addressProvider, signal: params.signal, - nowSec: params.syncStartSec, + nowSec: Math.floor(Date.now() / 1_000), }); addressQuotes = result.quotesByStablecoinId; providerDiagnosticCount = result.diagnostics.length; @@ -188,9 +189,17 @@ export async function runPriceCorroboration(params: { } } + const fallbackProbesById = new Map(fallbackProbes.map((asset) => [asset.id, asset])); + // Only this run's fetched observations enter the handoff, never published references. + const observations = cohort.flatMap((asset) => { + const fallback = fallbackObservation(fallbackProbesById.get(asset.id)); + return [...(fallback ? [fallback] : []), ...addressObservations(addressQuotes.get(asset.id))] + .map((observation) => ({ ...observation, id: asset.id, observedAtMode: observation.observedAtMode ?? null })); + }); + await writePriceCorroborationObservations(params.db, observations, params.syncStartSec, params.signal); const entries = buildPriceCorroborationCacheEntries({ publishedAssets: cohort, - fallbackProbes: new Map(fallbackProbes.map((asset) => [asset.id, asset])), + fallbackProbes: fallbackProbesById, addressQuotes, syncedAt: params.syncStartSec, }); From 148e14a48b3d2aaabc2e049cf73a55941879014e Mon Sep 17 00:00:00 2001 From: TokenBrice Date: Sat, 5 Sep 2026 20:44:12 +0200 Subject: [PATCH 4/4] test(pricing): register observed handoff coverage baseline Enroll the new critical observation staging module at its measured 100 percent line coverage so the existing no-regression gate can evaluate it. Preserve all existing baselines. --- .ci/critical-coverage-baseline.json | 1 + 1 file changed, 1 insertion(+) diff --git a/.ci/critical-coverage-baseline.json b/.ci/critical-coverage-baseline.json index 179b8909ea..5ec91c0390 100644 --- a/.ci/critical-coverage-baseline.json +++ b/.ci/critical-coverage-baseline.json @@ -29,6 +29,7 @@ "worker/src/api/health.ts": 100, "worker/src/cron/sync-stablecoins.ts": 97.3, "worker/src/cron/sync-stablecoins/enrich-prices.ts": 100, + "worker/src/cron/sync-stablecoins/price-corroboration-observations.ts": 100, "worker/src/cron/sync-stablecoins/price-corroboration.ts": 72.9, "worker/src/cron/sync-stablecoins/pricing.ts": 88.5, "worker/src/cron/detect-depegs.ts": 100,