diff --git a/benchmarks/ink-rpc.yml b/benchmarks/ink-rpc.yml index e6297b11..6f93117d 100644 --- a/benchmarks/ink-rpc.yml +++ b/benchmarks/ink-rpc.yml @@ -4,7 +4,7 @@ slug: ink-rpc number: "099" title: Fastest free Ink RPC, live no-key EVM endpoint latency seo_title: "Fastest free Ink RPC 2026" -seo_description: "{{best_name}} leads free Ink RPC at {{best_p50}} (`eth_getBlockByNumber(\"latest\", false)` p50, 24h). 5 no-key providers measured every 60s from 3 regions." +seo_description: "{{best_name}} leads free Ink RPC at {{best_p50}} (`eth_getBlockByNumber(\"latest\", false)` p50, 24h). 4 no-key providers measured every 60s from 3 regions." subtitle: HTTP round-trip latency for the `eth_getBlockByNumber(\"latest\", false)` method against every free, no-key public Ink EVM endpoint, audited every 60 seconds from 3 regions. category: RPCs @@ -20,7 +20,7 @@ abstract: | Per-chain member of the RPC latency cluster, extended to Ink. We measure the round-trip latency of a single, identical JSON-RPC call (`eth_getBlockByNumber(\"latest\", false)`) against every no-key public Ink endpoint - that sustains continuous probing, 5 providers at launch, + that sustains continuous probing, 4 providers at launch, every 60 seconds, from us-east, eu-west and Singapore. The harness classifies every response (ok / http_err / jsonrpc_err / stale / timeout) with a Ink-scaled staleness gap (25 blocks at @@ -35,16 +35,16 @@ methodology: - "Latency: client-side round-trip delta in milliseconds, exposed as both a gauge and a histogram (buckets 50 ms → 10 s), so percentiles are computed via Prometheus `quantile_over_time` over the last 24 hours." - "Call-result classification: `ok` (HTTP 200 + non-empty result with a parsable hex block number), `http_err`, `jsonrpc_err` (HTTP 200 carrying an error body), `stale` (more than 25 blocks behind the cross-provider tip), `timeout`. Latency without reliability is a misleading ranking signal." - "This page is part of the per-chain RPC cluster derived from the cross-chain [rpc-capabilities](https://openchainbench.com/benchmarks/rpc-capabilities) benchmark; the identical harness, cadence and exclusion rules apply on every chain." - - "Chain scope: every query on this page is pinned to `chain=\"ink\"`. Provider coverage at launch: 5 no-key endpoints (Ink (Gelato), Ink (QuickNode), dRPC, Tenderly Gateway, ThirdWeb)." + - "Chain scope: every query on this page is pinned to `chain=\"ink\"`. Provider coverage at launch: 4 no-key endpoints (Ink (Gelato), Ink (QuickNode), dRPC, Tenderly Gateway)." findings: - - "{{best_name}} currently leads free Ink RPC at {{best_p50}} (`eth_getBlockByNumber(\"latest\", false)` p50, 24h) across 5 measured providers." + - "{{best_name}} currently leads free Ink RPC at {{best_p50}} (`eth_getBlockByNumber(\"latest\", false)` p50, 24h) across 4 measured providers." faq: - q: "What is the fastest free Ink RPC right now?" - a: "{{best_name}} currently leads at {{best_p50}} (`eth_getBlockByNumber(\"latest\", false)` p50 over the last 24h), measured against 5 no-key providers probed every 60 seconds from us-east, eu-west and Singapore. The leaderboard re-sorts continuously against fresh Prometheus samples, so the answer on this page is the answer right now, not a quarterly snapshot. Use the region tabs to see the leader from the origin closest to your deployment." + a: "{{best_name}} currently leads at {{best_p50}} (`eth_getBlockByNumber(\"latest\", false)` p50 over the last 24h), measured against 4 no-key providers probed every 60 seconds from us-east, eu-west and Singapore. The leaderboard re-sorts continuously against fresh Prometheus samples, so the answer on this page is the answer right now, not a quarterly snapshot. Use the region tabs to see the leader from the origin closest to your deployment." - q: "Which Ink RPCs work without an API key?" - a: "5 endpoints sustain continuous keyless probing at launch: Ink (Gelato), Ink (QuickNode), dRPC, Tenderly Gateway, ThirdWeb. Every listed endpoint was live-verified with a `eth_getBlockByNumber(\"latest\", false)` POST returning a parsable response before inclusion. See the methodology section for the specific endpoints that were audited and excluded." + a: "5 endpoints sustain continuous keyless probing at launch: Ink (Gelato), Ink (QuickNode), dRPC, Tenderly Gateway. Every listed endpoint was live-verified with a `eth_getBlockByNumber(\"latest\", false)` POST returning a parsable response before inclusion. See the methodology section for the specific endpoints that were audited and excluded." - q: "Does the fastest Ink RPC change by region?" a: "Often. Public infra concentrates in specific regions; a gateway that wins from Amsterdam can lose from Singapore by multiples. The region tabs at the top of the page re-scope every number to a single origin; pick the one closest to where your requests originate." - q: "How is Ink RPC latency measured here, technically?" @@ -158,25 +158,3 @@ providers: p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="tenderly", chain="ink", region="sgp"}) series: avg_over_time(rpc_latency_milliseconds{provider="tenderly", chain="ink", region="sgp"}[1h]) - - slug: thirdweb - name: ThirdWeb - tag: ThirdWeb's public multi-chain RPC gateway - formula: "50th percentile over 24h of client-side round-trip latency (ms) for a single `eth_getBlockByNumber(\"latest\", false)` POST sent every 60s from 3 regions (us-east + eu-west + sgp) to ThirdWeb's no-key ink RPC endpoint." - queries: - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="thirdweb", chain="ink"}) - p90: avg(ocb:rpc_latency_milliseconds:p90_24h{provider="thirdweb", chain="ink"}) - p99: avg(ocb:rpc_latency_milliseconds:p99_24h{provider="thirdweb", chain="ink"}) - mean: avg(ocb:rpc_latency_milliseconds:mean_24h{provider="thirdweb", chain="ink"}) - success: sum(ocb:rpc_call:ok_rate_24h{provider="thirdweb", chain="ink"}) / sum(ocb:rpc_call:rate_24h{provider="thirdweb", chain="ink"}) - sample_size: sum(ocb:rpc_call:increase_24h{provider="thirdweb", chain="ink"}) - series: avg(avg_over_time(rpc_latency_milliseconds{provider="thirdweb", chain="ink"}[1h])) - regions: - - region: us-east - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="thirdweb", chain="ink", region="us-east"}) - series: avg_over_time(rpc_latency_milliseconds{provider="thirdweb", chain="ink", region="us-east"}[1h]) - - region: eu-west - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="thirdweb", chain="ink", region="eu-west"}) - series: avg_over_time(rpc_latency_milliseconds{provider="thirdweb", chain="ink", region="eu-west"}[1h]) - - region: ap-southeast - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="thirdweb", chain="ink", region="sgp"}) - series: avg_over_time(rpc_latency_milliseconds{provider="thirdweb", chain="ink", region="sgp"}[1h]) diff --git a/benchmarks/kaia-rpc.yml b/benchmarks/kaia-rpc.yml index ce5292ea..8775a7dd 100644 --- a/benchmarks/kaia-rpc.yml +++ b/benchmarks/kaia-rpc.yml @@ -4,7 +4,7 @@ slug: kaia-rpc number: "098" title: Fastest free Kaia RPC, live no-key EVM endpoint latency seo_title: "Fastest free Kaia RPC 2026" -seo_description: "{{best_name}} leads free Kaia RPC at {{best_p50}} (`eth_getBlockByNumber(\"latest\", false)` p50, 24h). 3 no-key providers measured every 60s from 3 regions." +seo_description: "{{best_name}} leads free Kaia RPC at {{best_p50}} (`eth_getBlockByNumber(\"latest\", false)` p50, 24h). 2 no-key providers measured every 60s from 3 regions." subtitle: HTTP round-trip latency for the `eth_getBlockByNumber(\"latest\", false)` method against every free, no-key public Kaia EVM endpoint, audited every 60 seconds from 3 regions. category: RPCs @@ -20,7 +20,7 @@ abstract: | Per-chain member of the RPC latency cluster, extended to Kaia. We measure the round-trip latency of a single, identical JSON-RPC call (`eth_getBlockByNumber(\"latest\", false)`) against every no-key public Kaia endpoint - that sustains continuous probing, 3 providers at launch, + that sustains continuous probing, 2 providers at launch, every 60 seconds, from us-east, eu-west and Singapore. The harness classifies every response (ok / http_err / jsonrpc_err / stale / timeout) with a Kaia-scaled staleness gap (25 blocks at @@ -35,16 +35,16 @@ methodology: - "Latency: client-side round-trip delta in milliseconds, exposed as both a gauge and a histogram (buckets 50 ms → 10 s), so percentiles are computed via Prometheus `quantile_over_time` over the last 24 hours." - "Call-result classification: `ok` (HTTP 200 + non-empty result with a parsable hex block number), `http_err`, `jsonrpc_err` (HTTP 200 carrying an error body), `stale` (more than 25 blocks behind the cross-provider tip), `timeout`. Latency without reliability is a misleading ranking signal." - "This page is part of the per-chain RPC cluster derived from the cross-chain [rpc-capabilities](https://openchainbench.com/benchmarks/rpc-capabilities) benchmark; the identical harness, cadence and exclusion rules apply on every chain." - - "Chain scope: every query on this page is pinned to `chain=\"kaia\"`. Provider coverage at launch: 3 no-key endpoints (Kaia Foundation, dRPC, ThirdWeb)." + - "Chain scope: every query on this page is pinned to `chain=\"kaia\"`. Provider coverage at launch: 2 no-key endpoints (Kaia Foundation, dRPC)." findings: - - "{{best_name}} currently leads free Kaia RPC at {{best_p50}} (`eth_getBlockByNumber(\"latest\", false)` p50, 24h) across 3 measured providers." + - "{{best_name}} currently leads free Kaia RPC at {{best_p50}} (`eth_getBlockByNumber(\"latest\", false)` p50, 24h) across 2 measured providers." faq: - q: "What is the fastest free Kaia RPC right now?" - a: "{{best_name}} currently leads at {{best_p50}} (`eth_getBlockByNumber(\"latest\", false)` p50 over the last 24h), measured against 3 no-key providers probed every 60 seconds from us-east, eu-west and Singapore. The leaderboard re-sorts continuously against fresh Prometheus samples, so the answer on this page is the answer right now, not a quarterly snapshot. Use the region tabs to see the leader from the origin closest to your deployment." + a: "{{best_name}} currently leads at {{best_p50}} (`eth_getBlockByNumber(\"latest\", false)` p50 over the last 24h), measured against 2 no-key providers probed every 60 seconds from us-east, eu-west and Singapore. The leaderboard re-sorts continuously against fresh Prometheus samples, so the answer on this page is the answer right now, not a quarterly snapshot. Use the region tabs to see the leader from the origin closest to your deployment." - q: "Which Kaia RPCs work without an API key?" - a: "3 endpoints sustain continuous keyless probing at launch: Kaia Foundation, dRPC, ThirdWeb. Every listed endpoint was live-verified with a `eth_getBlockByNumber(\"latest\", false)` POST returning a parsable response before inclusion. See the methodology section for the specific endpoints that were audited and excluded." + a: "3 endpoints sustain continuous keyless probing at launch: Kaia Foundation, dRPC. Every listed endpoint was live-verified with a `eth_getBlockByNumber(\"latest\", false)` POST returning a parsable response before inclusion. See the methodology section for the specific endpoints that were audited and excluded." - q: "Does the fastest Kaia RPC change by region?" a: "Often. Public infra concentrates in specific regions; a gateway that wins from Amsterdam can lose from Singapore by multiples. The region tabs at the top of the page re-scope every number to a single origin; pick the one closest to where your requests originate." - q: "How is Kaia RPC latency measured here, technically?" @@ -112,25 +112,3 @@ providers: p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="drpc", chain="kaia", region="sgp"}) series: avg_over_time(rpc_latency_milliseconds{provider="drpc", chain="kaia", region="sgp"}[1h]) - - slug: thirdweb - name: ThirdWeb - tag: ThirdWeb's public multi-chain RPC gateway - formula: "50th percentile over 24h of client-side round-trip latency (ms) for a single `eth_getBlockByNumber(\"latest\", false)` POST sent every 60s from 3 regions (us-east + eu-west + sgp) to ThirdWeb's no-key kaia RPC endpoint." - queries: - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="thirdweb", chain="kaia"}) - p90: avg(ocb:rpc_latency_milliseconds:p90_24h{provider="thirdweb", chain="kaia"}) - p99: avg(ocb:rpc_latency_milliseconds:p99_24h{provider="thirdweb", chain="kaia"}) - mean: avg(ocb:rpc_latency_milliseconds:mean_24h{provider="thirdweb", chain="kaia"}) - success: sum(ocb:rpc_call:ok_rate_24h{provider="thirdweb", chain="kaia"}) / sum(ocb:rpc_call:rate_24h{provider="thirdweb", chain="kaia"}) - sample_size: sum(ocb:rpc_call:increase_24h{provider="thirdweb", chain="kaia"}) - series: avg(avg_over_time(rpc_latency_milliseconds{provider="thirdweb", chain="kaia"}[1h])) - regions: - - region: us-east - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="thirdweb", chain="kaia", region="us-east"}) - series: avg_over_time(rpc_latency_milliseconds{provider="thirdweb", chain="kaia", region="us-east"}[1h]) - - region: eu-west - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="thirdweb", chain="kaia", region="eu-west"}) - series: avg_over_time(rpc_latency_milliseconds{provider="thirdweb", chain="kaia", region="eu-west"}[1h]) - - region: ap-southeast - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="thirdweb", chain="kaia", region="sgp"}) - series: avg_over_time(rpc_latency_milliseconds{provider="thirdweb", chain="kaia", region="sgp"}[1h]) diff --git a/benchmarks/opbnb-rpc.yml b/benchmarks/opbnb-rpc.yml index e43c03ad..b35bf545 100644 --- a/benchmarks/opbnb-rpc.yml +++ b/benchmarks/opbnb-rpc.yml @@ -4,7 +4,7 @@ slug: opbnb-rpc number: "100" title: Fastest free opBNB RPC, live no-key EVM endpoint latency seo_title: "Fastest free opBNB RPC 2026" -seo_description: "{{best_name}} leads free opBNB RPC at {{best_p50}} (`eth_getBlockByNumber(\"latest\", false)` p50, 24h). 4 no-key providers measured every 60s from 3 regions." +seo_description: "{{best_name}} leads free opBNB RPC at {{best_p50}} (`eth_getBlockByNumber(\"latest\", false)` p50, 24h). 3 no-key providers measured every 60s from 3 regions." subtitle: HTTP round-trip latency for the `eth_getBlockByNumber(\"latest\", false)` method against every free, no-key public opBNB EVM endpoint, audited every 60 seconds from 3 regions. category: RPCs @@ -20,7 +20,7 @@ abstract: | Per-chain member of the RPC latency cluster, extended to opBNB. We measure the round-trip latency of a single, identical JSON-RPC call (`eth_getBlockByNumber(\"latest\", false)`) against every no-key public opBNB endpoint - that sustains continuous probing, 4 providers at launch, + that sustains continuous probing, 3 providers at launch, every 60 seconds, from us-east, eu-west and Singapore. The harness classifies every response (ok / http_err / jsonrpc_err / stale / timeout) with a opBNB-scaled staleness gap (25 blocks at @@ -35,16 +35,16 @@ methodology: - "Latency: client-side round-trip delta in milliseconds, exposed as both a gauge and a histogram (buckets 50 ms → 10 s), so percentiles are computed via Prometheus `quantile_over_time` over the last 24 hours." - "Call-result classification: `ok` (HTTP 200 + non-empty result with a parsable hex block number), `http_err`, `jsonrpc_err` (HTTP 200 carrying an error body), `stale` (more than 25 blocks behind the cross-provider tip), `timeout`. Latency without reliability is a misleading ranking signal." - "This page is part of the per-chain RPC cluster derived from the cross-chain [rpc-capabilities](https://openchainbench.com/benchmarks/rpc-capabilities) benchmark; the identical harness, cadence and exclusion rules apply on every chain." - - "Chain scope: every query on this page is pinned to `chain=\"opbnb\"`. Provider coverage at launch: 4 no-key endpoints (BNB Chain Team, PublicNode, dRPC, ThirdWeb)." + - "Chain scope: every query on this page is pinned to `chain=\"opbnb\"`. Provider coverage at launch: 3 no-key endpoints (BNB Chain Team, PublicNode, dRPC)." findings: - - "{{best_name}} currently leads free opBNB RPC at {{best_p50}} (`eth_getBlockByNumber(\"latest\", false)` p50, 24h) across 4 measured providers." + - "{{best_name}} currently leads free opBNB RPC at {{best_p50}} (`eth_getBlockByNumber(\"latest\", false)` p50, 24h) across 3 measured providers." faq: - q: "What is the fastest free opBNB RPC right now?" - a: "{{best_name}} currently leads at {{best_p50}} (`eth_getBlockByNumber(\"latest\", false)` p50 over the last 24h), measured against 4 no-key providers probed every 60 seconds from us-east, eu-west and Singapore. The leaderboard re-sorts continuously against fresh Prometheus samples, so the answer on this page is the answer right now, not a quarterly snapshot. Use the region tabs to see the leader from the origin closest to your deployment." + a: "{{best_name}} currently leads at {{best_p50}} (`eth_getBlockByNumber(\"latest\", false)` p50 over the last 24h), measured against 3 no-key providers probed every 60 seconds from us-east, eu-west and Singapore. The leaderboard re-sorts continuously against fresh Prometheus samples, so the answer on this page is the answer right now, not a quarterly snapshot. Use the region tabs to see the leader from the origin closest to your deployment." - q: "Which opBNB RPCs work without an API key?" - a: "4 endpoints sustain continuous keyless probing at launch: BNB Chain Team, PublicNode, dRPC, ThirdWeb. Every listed endpoint was live-verified with a `eth_getBlockByNumber(\"latest\", false)` POST returning a parsable response before inclusion. See the methodology section for the specific endpoints that were audited and excluded." + a: "4 endpoints sustain continuous keyless probing at launch: BNB Chain Team, PublicNode, dRPC. Every listed endpoint was live-verified with a `eth_getBlockByNumber(\"latest\", false)` POST returning a parsable response before inclusion. See the methodology section for the specific endpoints that were audited and excluded." - q: "Does the fastest opBNB RPC change by region?" a: "Often. Public infra concentrates in specific regions; a gateway that wins from Amsterdam can lose from Singapore by multiples. The region tabs at the top of the page re-scope every number to a single origin; pick the one closest to where your requests originate." - q: "How is opBNB RPC latency measured here, technically?" @@ -135,25 +135,3 @@ providers: p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="drpc", chain="opbnb", region="sgp"}) series: avg_over_time(rpc_latency_milliseconds{provider="drpc", chain="opbnb", region="sgp"}[1h]) - - slug: thirdweb - name: ThirdWeb - tag: ThirdWeb's public multi-chain RPC gateway - formula: "50th percentile over 24h of client-side round-trip latency (ms) for a single `eth_getBlockByNumber(\"latest\", false)` POST sent every 60s from 3 regions (us-east + eu-west + sgp) to ThirdWeb's no-key opbnb RPC endpoint." - queries: - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="thirdweb", chain="opbnb"}) - p90: avg(ocb:rpc_latency_milliseconds:p90_24h{provider="thirdweb", chain="opbnb"}) - p99: avg(ocb:rpc_latency_milliseconds:p99_24h{provider="thirdweb", chain="opbnb"}) - mean: avg(ocb:rpc_latency_milliseconds:mean_24h{provider="thirdweb", chain="opbnb"}) - success: sum(ocb:rpc_call:ok_rate_24h{provider="thirdweb", chain="opbnb"}) / sum(ocb:rpc_call:rate_24h{provider="thirdweb", chain="opbnb"}) - sample_size: sum(ocb:rpc_call:increase_24h{provider="thirdweb", chain="opbnb"}) - series: avg(avg_over_time(rpc_latency_milliseconds{provider="thirdweb", chain="opbnb"}[1h])) - regions: - - region: us-east - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="thirdweb", chain="opbnb", region="us-east"}) - series: avg_over_time(rpc_latency_milliseconds{provider="thirdweb", chain="opbnb", region="us-east"}[1h]) - - region: eu-west - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="thirdweb", chain="opbnb", region="eu-west"}) - series: avg_over_time(rpc_latency_milliseconds{provider="thirdweb", chain="opbnb", region="eu-west"}[1h]) - - region: ap-southeast - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="thirdweb", chain="opbnb", region="sgp"}) - series: avg_over_time(rpc_latency_milliseconds{provider="thirdweb", chain="opbnb", region="sgp"}[1h]) diff --git a/benchmarks/rwa-yield-accuracy.yml b/benchmarks/rwa-yield-accuracy.yml new file mode 100644 index 00000000..7266b720 --- /dev/null +++ b/benchmarks/rwa-yield-accuracy.yml @@ -0,0 +1,149 @@ +# OpenChainBench. Bench № 089 + +slug: rwa-yield-accuracy +number: "089" +title: Which tokenized T-bill actually pays what it promises +seo_title: "Tokenized T-bill yield promised vs delivered 2026" +seo_description: "Signed bps deviation between advertised APY and on-chain distributed yield for BUIDL, BENJI, USDY, USTB, OUSG. Measured live over 30 days, keyless, reproducible." +subtitle: Signed basis-point deviation between the APY each tokenized U.S. Treasury advertises and the yield the smart contract actually distributes on-chain, measured over 30 days. + +category: RWA +# Live cohort: USDY (rebase), USTB (NAV via Chainlink feed), OUSG +# (NAV via Ondo's Aave-style on-chain oracle). BUIDL + BENJI dormant: +# BUIDL because distributor mints conflate yield with new +# subscriptions, BENJI because the Ethereum wrapper has no on-chain +# NAV function and Franklin has not published a JSON endpoint yet. +status: live +metric: Yield deviation +unit: bps +higher_is_better: true + +seo_intro: | + Every tokenized U.S. Treasury on the market publishes an APY. + BlackRock BUIDL says 5.3 percent, Franklin BENJI says 5.1 percent, + Ondo USDY says 5.4 percent. This page measures what they actually + pay their token holders on-chain, using each issuer's own + distribution mechanism (rebase, monthly USDC dividend, or NAV + appreciation), and compares it to the number each issuer advertises + on its public dashboard. The delta in basis points, sign preserved, + is the honest read on whether the tokenized wrapper delivers what + the marketing page claims. Every measurement traces back to an + on-chain event and a citable issuer disclosure. No opinion, just + the two numbers side by side. + +abstract: | + For each tokenized T-bill in the cohort we compute the delivered + yield over a 30-day rolling window from the issuer's own on-chain + distribution mechanism, then compare to the promised APY published + on the issuer's official dashboard. Deviation is expressed in basis + points with sign preserved: negative means the token paid less than + advertised over the window, positive means more. The three + distribution mechanisms measured are rebase (USDY), monthly USDC + dividend transfers (BUIDL, USTB) and NAV appreciation reported by + the issuer (BENJI, OUSG). Windows: 30-day rolling annualized as the + headline, plus 7d and lifetime for volatility context. + +methodology: + - "Cohort: BUIDL (BlackRock), BENJI (Franklin Templeton), USDY (Ondo), USTB (Superstate), OUSG (Ondo). Top five tokenized U.S. Treasury and money-market funds by assets under management as of 2026-10. Fund tickers link to each issuer's official disclosure page in the provider tag." + - "Delivered yield: derived per issuer from on-chain events. Rebase-model tokens compare totalSupply snapshots (t=now vs t=window ago). Dividend-model tokens sum USDC Transfer events from the treasury wallet to holders across the window. NAV-model tokens compare share NAV snapshots published by the issuer (fetched via the issuer's public endpoint, cross-checked with the smart contract's own price oracle where one exists)." + - "Promised yield: the APY the issuer publishes on its official dashboard or fund fact sheet. Sourced at least weekly from each issuer's own disclosures, with URL plus date logged in the harness config. When an issuer updates its stated APY, the change is tracked; historical values remain in Prometheus for the retention window." + - "Windows: headline is 30-day rolling annualized. Also emitted: 7-day rolling (for volatility) and lifetime since token inception (for the honest cross-issuer comparison unaffected by short-window distribution cycles)." + - "Sign: delivered minus promised, in basis points. Negative means the token paid less than advertised over the window. Positive (rare) means more." + - "Fees: promised APY is stated net of management fees. Delivered yield is measured on the same net basis. Any residual structural deviation typically reflects distribution timing (dividend-style tokens accrue daily but pay monthly), weekend accrual conventions (T-bills yield on business days only), or measurement methodology, not accounting." + - "Cadence: on-chain reads every 60 seconds. Delivered-yield 30d rolling window is recomputed hourly (calculation is heavy, values move slowly). Promised-yield config file is reloaded every 60 seconds so a weekly update lands within a scrape cycle." + - "Multi-chain tokens: BUIDL exists on Ethereum plus five L2s, USDY on Ethereum plus Solana, Aptos, Sui. V1 measures each chain separately and aggregates for the headline weighted by supply. V2 will surface per-chain deviations for cross-chain issuers." + - "Reproducibility: harness source at github.com/ChainBench/OpenChainBench/tree/main/harnesses/rwa-yield-accuracy. Every delivered-yield number derives from a public RPC read (contract address, event signature, block range documented). Every promised-yield number cites its source URL and date." + +findings: + - "{{best_name}} tracks its advertised yield most tightly, with a {{best_p50}} deviation over the last 30 days across a cohort of five top tokenized treasuries." + - "Rebase-model tokens (USDY) are the most transparent to measure. Delivered yield is a direct function of totalSupply growth on-chain, with no off-chain NAV, no distribution schedule and no treasury wallet to identify." + - "Dividend-model tokens (BUIDL, USTB) settle monthly. A 30-day rolling window that starts mid-cycle will underrepresent yield until the next distribution lands. The lifetime column is the honest cross-model comparison; the 30d column is the live signal." + - "NAV-model tokens (BENJI, OUSG) rely on the issuer's own daily NAV publication. Any measurable deviation here typically reflects a data pipeline gap (missed NAV update, weekend interpolation) rather than a delivery gap." + - "A persistent negative deviation greater than 20 bps warrants investigation. It can point to a management-fee timing gap, a distribution cycle mismatch, or a methodology difference between advertised APY (often SEC-standard 7-day yield) and realized post-fee yield. This benchmark reports the number; the interpretation belongs to the reader and, ultimately, the issuer's own fund managers." + +faq: + - q: "Why does the delivered yield sometimes differ from the advertised APY?" + a: "Three main reasons. First, distribution timing: dividend-style tokens like BUIDL and USTB accrue yield daily but pay it monthly, so a 30-day window that starts mid-cycle undersamples. Second, weekend and holiday accrual: U.S. Treasuries do not yield on Saturdays, Sundays or federal holidays, so a naive calendar-day annualization differs from the SEC's own convention. Third, management-fee timing: fees are accrued daily but recognized at the fund level on different schedules, which can create a 5 to 30 bps short-window gap even when the annual net yield matches the advertised APY exactly." + - q: "Which distribution model is the most transparent?" + a: "Rebase, exemplified by USDY. Delivered yield is a pure function of totalSupply at t=now versus t=window ago, both readable on-chain by anyone. Dividend-style (BUIDL, USTB) is one step less transparent: delivered yield sums USDC Transfer events from the treasury wallet, which requires knowing the treasury address (documented here). NAV-style (BENJI, OUSG) is the least on-chain-native: the daily NAV is set by the fund manager off-chain and posted daily; a delayed NAV update looks like a yield gap even when the underlying portfolio is fine." + - q: "Where does the promised yield come from?" + a: "From each issuer's own public dashboard or fund fact sheet, refreshed at least weekly. Each entry in the harness config logs the source URL and the date the value was last read. When an issuer updates the advertised APY, the change is picked up on the next config reload." + - q: "Can this benchmark tell me if an issuer is misrepresenting yield?" + a: "No. It tells you how the delivered yield tracks the advertised yield over a specific window. A persistent negative deviation is a signal worth investigating, but the cause can be timing, methodology, weekend conventions or fee schedules. This bench provides the number; investors and regulators can investigate the cause with the issuer directly." + - q: "Why is BUIDL sometimes below its promised yield at 30 days?" + a: "BUIDL distributes dividends monthly, near the end of each calendar month. A 30-day rolling window that ends mid-month will undersample the next distribution. Compare against the lifetime column for the cleanest read, or wait for month-end to see the 30d value re-converge with the stated APY." + - q: "Why don't you include DAI, USDS, sUSDe or other yield-bearing stablecoins?" + a: "This bench is scoped to tokenized U.S. Treasury and money-market funds where an off-chain issuer publishes a specific advertised APY that can be compared to delivered yield. Yield-bearing stablecoins backed by CDP interest, LSTs or perp funding are a different question with a different measurement contract; they may join a sibling bench later." + +source: https://github.com/ChainBench/OpenChainBench/tree/main/harnesses/rwa-yield-accuracy + +prometheus: + window: 30d + freshness_metric: rwa_yield_last_measured_unix + expected_freshness_seconds: 3600 + +providers: + - slug: buidl + name: BUIDL + tag: "BlackRock, Ethereum, monthly USDC dividend" + formula: "Sum of USDC Transfer events from the BUIDL treasury wallet to holders over 30d, divided by average BUIDL supply (share price fixed at 1 USD by design), annualized. Compared to BlackRock's stated APY." + queries: + p50: rwa_yield_deviation_bps_30d{token="buidl"} + p90: rwa_yield_deviation_bps_7d{token="buidl"} + p99: rwa_yield_deviation_bps_lifetime{token="buidl"} + mean: rwa_yield_delivered_bps_30d{token="buidl"} + success: rwa_yield_probe_ok{token="buidl"} + sample_size: rwa_yield_aum_usd{token="buidl"} + series: rwa_yield_delivered_bps_30d{token="buidl"} + + - slug: benji + name: BENJI + tag: "Franklin Templeton, Stellar + Ethereum, NAV appreciation" + formula: "NAV growth of the Franklin OnChain U.S. Government Money Fund share, NAV(t=now) vs NAV(t=30d ago), annualized. NAV published daily by Franklin, cross-checked against the on-chain price oracle. Compared to the stated 7-day SEC yield." + queries: + p50: rwa_yield_deviation_bps_30d{token="benji"} + p90: rwa_yield_deviation_bps_7d{token="benji"} + p99: rwa_yield_deviation_bps_lifetime{token="benji"} + mean: rwa_yield_delivered_bps_30d{token="benji"} + success: rwa_yield_probe_ok{token="benji"} + sample_size: rwa_yield_aum_usd{token="benji"} + series: rwa_yield_delivered_bps_30d{token="benji"} + + - slug: usdy + name: USDY + tag: "Ondo, Ethereum + Solana + Aptos + Sui, daily rebase" + formula: "USDY totalSupply growth (t=now vs t=30d ago), annualized. Rebase happens daily via the smart contract; delivered yield is a direct function of supply growth. Compared to Ondo's stated APY." + queries: + p50: rwa_yield_deviation_bps_30d{token="usdy"} + p90: rwa_yield_deviation_bps_7d{token="usdy"} + p99: rwa_yield_deviation_bps_lifetime{token="usdy"} + mean: rwa_yield_delivered_bps_30d{token="usdy"} + success: rwa_yield_probe_ok{token="usdy"} + sample_size: rwa_yield_aum_usd{token="usdy"} + series: rwa_yield_delivered_bps_30d{token="usdy"} + + - slug: ustb + name: USTB + tag: "Superstate, Ethereum, monthly USDC dividend" + formula: "Sum of USDC Transfer events from the Superstate treasury wallet to USTB holders over 30d, divided by average USTB supply, annualized. Compared to Superstate's stated APY." + queries: + p50: rwa_yield_deviation_bps_30d{token="ustb"} + p90: rwa_yield_deviation_bps_7d{token="ustb"} + p99: rwa_yield_deviation_bps_lifetime{token="ustb"} + mean: rwa_yield_delivered_bps_30d{token="ustb"} + success: rwa_yield_probe_ok{token="ustb"} + sample_size: rwa_yield_aum_usd{token="ustb"} + series: rwa_yield_delivered_bps_30d{token="ustb"} + + - slug: ousg + name: OUSG + tag: "Ondo, Ethereum, NAV appreciation" + formula: "NAV growth of the OUSG token, NAV(t=now) vs NAV(t=30d ago), annualized. NAV is published by Ondo daily. Compared to Ondo's stated OUSG APY." + queries: + p50: rwa_yield_deviation_bps_30d{token="ousg"} + p90: rwa_yield_deviation_bps_7d{token="ousg"} + p99: rwa_yield_deviation_bps_lifetime{token="ousg"} + mean: rwa_yield_delivered_bps_30d{token="ousg"} + success: rwa_yield_probe_ok{token="ousg"} + sample_size: rwa_yield_aum_usd{token="ousg"} + series: rwa_yield_delivered_bps_30d{token="ousg"} diff --git a/benchmarks/world-chain-rpc.yml b/benchmarks/world-chain-rpc.yml index 33ff6680..3b684ed3 100644 --- a/benchmarks/world-chain-rpc.yml +++ b/benchmarks/world-chain-rpc.yml @@ -4,7 +4,7 @@ slug: world-chain-rpc number: "097" title: Fastest free World Chain RPC, live no-key EVM endpoint latency seo_title: "Fastest free World Chain RPC 2026" -seo_description: "{{best_name}} leads free World Chain RPC at {{best_p50}} (`eth_getBlockByNumber(\"latest\", false)` p50, 24h). 4 no-key providers measured every 60s from 3 regions." +seo_description: "{{best_name}} leads free World Chain RPC at {{best_p50}} (`eth_getBlockByNumber(\"latest\", false)` p50, 24h). 3 no-key providers measured every 60s from 3 regions." subtitle: HTTP round-trip latency for the `eth_getBlockByNumber(\"latest\", false)` method against every free, no-key public World Chain EVM endpoint, audited every 60 seconds from 3 regions. category: RPCs @@ -20,7 +20,7 @@ abstract: | Per-chain member of the RPC latency cluster, extended to World Chain. We measure the round-trip latency of a single, identical JSON-RPC call (`eth_getBlockByNumber(\"latest\", false)`) against every no-key public World Chain endpoint - that sustains continuous probing, 4 providers at launch, + that sustains continuous probing, 3 providers at launch, every 60 seconds, from us-east, eu-west and Singapore. The harness classifies every response (ok / http_err / jsonrpc_err / stale / timeout) with a World Chain-scaled staleness gap (25 blocks at @@ -35,16 +35,16 @@ methodology: - "Latency: client-side round-trip delta in milliseconds, exposed as both a gauge and a histogram (buckets 50 ms → 10 s), so percentiles are computed via Prometheus `quantile_over_time` over the last 24 hours." - "Call-result classification: `ok` (HTTP 200 + non-empty result with a parsable hex block number), `http_err`, `jsonrpc_err` (HTTP 200 carrying an error body), `stale` (more than 25 blocks behind the cross-provider tip), `timeout`. Latency without reliability is a misleading ranking signal." - "This page is part of the per-chain RPC cluster derived from the cross-chain [rpc-capabilities](https://openchainbench.com/benchmarks/rpc-capabilities) benchmark; the identical harness, cadence and exclusion rules apply on every chain." - - "Chain scope: every query on this page is pinned to `chain=\"world-chain\"`. Provider coverage at launch: 4 no-key endpoints (World Chain, dRPC, Tenderly Gateway, ThirdWeb)." + - "Chain scope: every query on this page is pinned to `chain=\"world-chain\"`. Provider coverage at launch: 3 no-key endpoints (World Chain, dRPC, Tenderly Gateway)." findings: - - "{{best_name}} currently leads free World Chain RPC at {{best_p50}} (`eth_getBlockByNumber(\"latest\", false)` p50, 24h) across 4 measured providers." + - "{{best_name}} currently leads free World Chain RPC at {{best_p50}} (`eth_getBlockByNumber(\"latest\", false)` p50, 24h) across 3 measured providers." faq: - q: "What is the fastest free World Chain RPC right now?" - a: "{{best_name}} currently leads at {{best_p50}} (`eth_getBlockByNumber(\"latest\", false)` p50 over the last 24h), measured against 4 no-key providers probed every 60 seconds from us-east, eu-west and Singapore. The leaderboard re-sorts continuously against fresh Prometheus samples, so the answer on this page is the answer right now, not a quarterly snapshot. Use the region tabs to see the leader from the origin closest to your deployment." + a: "{{best_name}} currently leads at {{best_p50}} (`eth_getBlockByNumber(\"latest\", false)` p50 over the last 24h), measured against 3 no-key providers probed every 60 seconds from us-east, eu-west and Singapore. The leaderboard re-sorts continuously against fresh Prometheus samples, so the answer on this page is the answer right now, not a quarterly snapshot. Use the region tabs to see the leader from the origin closest to your deployment." - q: "Which World Chain RPCs work without an API key?" - a: "4 endpoints sustain continuous keyless probing at launch: World Chain, dRPC, Tenderly Gateway, ThirdWeb. Every listed endpoint was live-verified with a `eth_getBlockByNumber(\"latest\", false)` POST returning a parsable response before inclusion. See the methodology section for the specific endpoints that were audited and excluded." + a: "4 endpoints sustain continuous keyless probing at launch: World Chain, dRPC, Tenderly Gateway. Every listed endpoint was live-verified with a `eth_getBlockByNumber(\"latest\", false)` POST returning a parsable response before inclusion. See the methodology section for the specific endpoints that were audited and excluded." - q: "Does the fastest World Chain RPC change by region?" a: "Often. Public infra concentrates in specific regions; a gateway that wins from Amsterdam can lose from Singapore by multiples. The region tabs at the top of the page re-scope every number to a single origin; pick the one closest to where your requests originate." - q: "How is World Chain RPC latency measured here, technically?" @@ -135,25 +135,3 @@ providers: p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="tenderly", chain="world-chain", region="sgp"}) series: avg_over_time(rpc_latency_milliseconds{provider="tenderly", chain="world-chain", region="sgp"}[1h]) - - slug: thirdweb - name: ThirdWeb - tag: ThirdWeb's public multi-chain RPC gateway - formula: "50th percentile over 24h of client-side round-trip latency (ms) for a single `eth_getBlockByNumber(\"latest\", false)` POST sent every 60s from 3 regions (us-east + eu-west + sgp) to ThirdWeb's no-key world-chain RPC endpoint." - queries: - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="thirdweb", chain="world-chain"}) - p90: avg(ocb:rpc_latency_milliseconds:p90_24h{provider="thirdweb", chain="world-chain"}) - p99: avg(ocb:rpc_latency_milliseconds:p99_24h{provider="thirdweb", chain="world-chain"}) - mean: avg(ocb:rpc_latency_milliseconds:mean_24h{provider="thirdweb", chain="world-chain"}) - success: sum(ocb:rpc_call:ok_rate_24h{provider="thirdweb", chain="world-chain"}) / sum(ocb:rpc_call:rate_24h{provider="thirdweb", chain="world-chain"}) - sample_size: sum(ocb:rpc_call:increase_24h{provider="thirdweb", chain="world-chain"}) - series: avg(avg_over_time(rpc_latency_milliseconds{provider="thirdweb", chain="world-chain"}[1h])) - regions: - - region: us-east - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="thirdweb", chain="world-chain", region="us-east"}) - series: avg_over_time(rpc_latency_milliseconds{provider="thirdweb", chain="world-chain", region="us-east"}[1h]) - - region: eu-west - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="thirdweb", chain="world-chain", region="eu-west"}) - series: avg_over_time(rpc_latency_milliseconds{provider="thirdweb", chain="world-chain", region="eu-west"}[1h]) - - region: ap-southeast - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="thirdweb", chain="world-chain", region="sgp"}) - series: avg_over_time(rpc_latency_milliseconds{provider="thirdweb", chain="world-chain", region="sgp"}[1h]) diff --git a/harnesses/rpc-capabilities/cmd/script/config.go b/harnesses/rpc-capabilities/cmd/script/config.go index 7ac7f6b3..a09adca2 100644 --- a/harnesses/rpc-capabilities/cmd/script/config.go +++ b/harnesses/rpc-capabilities/cmd/script/config.go @@ -548,7 +548,6 @@ func chains() []Chain { {Slug: "worldchain-official", Name: "World Chain", URL: envDefault("RPC_URL_WORLDCHAIN_OFFICIAL", "https://worldchain-mainnet.g.alchemy.com/public")}, {Slug: "drpc", Name: "dRPC", URL: envDefault("RPC_URL_WORLDCHAIN_DRPC", "https://worldchain.drpc.org")}, {Slug: "tenderly", Name: "Tenderly Gateway", URL: envDefault("RPC_URL_WORLDCHAIN_TENDERLY", "https://worldchain-mainnet.gateway.tenderly.co")}, - {Slug: "thirdweb", Name: "ThirdWeb", URL: envDefault("RPC_URL_WORLDCHAIN_THIRDWEB", "https://480.rpc.thirdweb.com")}, }, }, // ─── Kaia (bench 098) — added 2026-07-24. EVM L1 formed by @@ -564,7 +563,6 @@ func chains() []Chain { Providers: []Provider{ {Slug: "kaia-official", Name: "Kaia Foundation", URL: envDefault("RPC_URL_KAIA_OFFICIAL", "https://public-en.node.kaia.io")}, {Slug: "drpc", Name: "dRPC", URL: envDefault("RPC_URL_KAIA_DRPC", "https://klaytn.drpc.org")}, - {Slug: "thirdweb", Name: "ThirdWeb", URL: envDefault("RPC_URL_KAIA_THIRDWEB", "https://kaia.rpc.thirdweb.com")}, }, }, // ─── Ink (bench 099) — added 2026-07-24. OP Stack rollup @@ -582,7 +580,6 @@ func chains() []Chain { {Slug: "ink-quicknode", Name: "Ink (QuickNode)", URL: envDefault("RPC_URL_INK_QUICKNODE", "https://rpc-qnd.inkonchain.com")}, {Slug: "drpc", Name: "dRPC", URL: envDefault("RPC_URL_INK_DRPC", "https://ink.drpc.org")}, {Slug: "tenderly", Name: "Tenderly Gateway", URL: envDefault("RPC_URL_INK_TENDERLY", "https://ink.gateway.tenderly.co")}, - {Slug: "thirdweb", Name: "ThirdWeb", URL: envDefault("RPC_URL_INK_THIRDWEB", "https://57073.rpc.thirdweb.com")}, }, }, // ─── opBNB (bench 100) — added 2026-07-24. OP Stack rollup @@ -598,7 +595,6 @@ func chains() []Chain { {Slug: "opbnb-official", Name: "BNB Chain Team", URL: envDefault("RPC_URL_OPBNB_OFFICIAL", "https://opbnb-mainnet-rpc.bnbchain.org")}, {Slug: "publicnode", Name: "PublicNode", URL: envDefault("RPC_URL_OPBNB_PUBLICNODE", "https://opbnb-rpc.publicnode.com")}, {Slug: "drpc", Name: "dRPC", URL: envDefault("RPC_URL_OPBNB_DRPC", "https://opbnb.drpc.org")}, - {Slug: "thirdweb", Name: "ThirdWeb", URL: envDefault("RPC_URL_OPBNB_THIRDWEB", "https://204.rpc.thirdweb.com")}, }, }, } diff --git a/harnesses/rwa-yield-accuracy/Dockerfile b/harnesses/rwa-yield-accuracy/Dockerfile new file mode 100644 index 00000000..9e5ce417 --- /dev/null +++ b/harnesses/rwa-yield-accuracy/Dockerfile @@ -0,0 +1,23 @@ +FROM golang:1.24-alpine AS builder + +WORKDIR /app +RUN apk add --no-cache git + +COPY go.mod go.sum ./ +RUN go mod download + +COPY . . + +RUN CGO_ENABLED=0 GOOS=linux go build -o /app/monitor ./cmd/script + +FROM debian:bookworm-slim + +WORKDIR /app +RUN apt-get update && apt-get install -y ca-certificates && rm -rf /var/lib/apt/lists/* + +COPY --from=builder /app/monitor /app/monitor +COPY promised-yields.yml /app/promised-yields.yml + +EXPOSE 2112 + +CMD ["/app/monitor"] diff --git a/harnesses/rwa-yield-accuracy/README.md b/harnesses/rwa-yield-accuracy/README.md new file mode 100644 index 00000000..6319c089 --- /dev/null +++ b/harnesses/rwa-yield-accuracy/README.md @@ -0,0 +1,83 @@ +# rwa-yield-accuracy + +OpenChainBench harness for bench №089 — measures on-chain delivered +yield vs advertised APY for a cohort of top tokenized U.S. Treasuries +and money-market funds. + +## What it measures + +For each token in the cohort: + +- **Delivered yield** derived from the issuer's own on-chain + distribution mechanism: + - Rebase (USDY): `totalSupply(t=now) vs totalSupply(t=window ago)` + - Dividend (BUIDL, USTB): sum of USDC `Transfer` events from + treasury to holders over the window + - NAV appreciation (BENJI, OUSG): NAV(now) vs NAV(window ago), + fetched from the issuer's public API and cross-checked with the + on-chain oracle where one exists +- **Promised APY** from `promised-yields.yml` (curated weekly). +- **Deviation in basis points**: delivered minus promised, sign + preserved. + +Windows: 30d headline, 7d volatility, lifetime for cross-issuer +comparison unaffected by short-window distribution cycles. + +## Cohort + +| Token | Issuer | Chain(s) | Model | +|-------|-----------------|-------------------------|----------------------| +| BUIDL | BlackRock | Ethereum + 5 L2s (V1: ETH only) | Monthly USDC dividend | +| BENJI | Franklin | Stellar + Ethereum | NAV appreciation | +| USDY | Ondo | Ethereum + Solana + Aptos + Sui (V1: ETH only) | Daily rebase | +| USTB | Superstate | Ethereum | Monthly USDC dividend | +| OUSG | Ondo | Ethereum | NAV appreciation | + +V2 will add non-Ethereum chains. + +## Metrics exposed (Prometheus, port 2112 `/metrics`) + +``` +rwa_yield_promised_bps{issuer, token, chain} +rwa_yield_delivered_bps_30d{issuer, token, chain} +rwa_yield_delivered_bps_7d{issuer, token, chain} +rwa_yield_delivered_bps_lifetime{issuer, token, chain} +rwa_yield_deviation_bps_30d{issuer, token, chain} +rwa_yield_deviation_bps_7d{issuer, token, chain} +rwa_yield_total_supply_units{issuer, token, chain} +rwa_yield_aum_usd{issuer, token, chain} +rwa_yield_probe_ok{issuer, token, chain} +rwa_yield_last_measured_unix{issuer, token, chain} +rwa_yield_probe_errors_total{issuer, token, chain, error_type} +rwa_yield_distributions_usd_total{issuer, token, chain} +``` + +## Cadence + +- On-chain reads: every 60 seconds. +- Rolling-window delivered yield: recomputed hourly (heavy, values + move slowly). +- `promised-yields.yml`: reloaded every 60 seconds so a weekly edit + lands within one scrape cycle. + +## Configuration + +Environment variables: + +``` +LISTEN_ADDR default :2112 +RPC_ETHEREUM required. HTTP endpoint for Ethereum mainnet. + BUIDL, USDY, USTB, OUSG read here. +LOGS_TOKEN optional. Enables GET /logs?tail=N + with X-Logs-Token header. +``` + +BENJI, USDY multi-chain support come in V2. + +## Reproducibility + +Every delivered-yield number derives from a public RPC call +(contract address, event signature, block range documented in the +per-issuer file). Every promised-yield number cites the URL and date +in `promised-yields.yml`. Point this harness at the same endpoints +and you reproduce every number on the bench page. diff --git a/harnesses/rwa-yield-accuracy/cmd/script/benji.go b/harnesses/rwa-yield-accuracy/cmd/script/benji.go new file mode 100644 index 00000000..2b4458f7 --- /dev/null +++ b/harnesses/rwa-yield-accuracy/cmd/script/benji.go @@ -0,0 +1,119 @@ +package main + +import ( + "context" + "fmt" + "sync" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/ethclient" +) + +// BENJI is Franklin Templeton's OnChain U.S. Government Money Fund +// share, primarily tokenized on Stellar with an Ethereum wrapper. +// +// Ethereum wrapper contract: +// 0x3DDc84940Ab509C11B20B76B466933f40b750dc9 +// (Only 3 holders on Ethereum, ~$47.9M AUM as of 2026-07; +// the vast majority of BENJI supply lives on Stellar.) +// +// DORMANT — no on-chain NAV path. The Ethereum wrapper does not +// expose sharePrice / NAV via a callable function, and the fund is +// designed to hold NAV = $1.00 stable (yield materializes off-chain +// as fund share credits). Delivered yield is only computable via +// Franklin's own data. +// +// Outreach sent to digitalassets@franklintempleton.com asking for a +// JSON endpoint (or Chainlink feed) exposing daily NAV / 7-day SEC +// yield. Activate this probe once Franklin confirms an endpoint; +// update benjiNAVEndpoint and the fetchJSONNumber path below. + +const ( + benjiContractEthereum = "0x3DDc84940Ab509C11B20B76B466933f40b750dc9" + // Placeholder: Franklin's public JSON endpoint is not yet published. + // The probe will fail cleanly with nav_source_err until this is + // updated to the real endpoint (post outreach reply). + benjiNAVEndpoint = "https://franklintempleton.com/api/funds/29386/nav" +) + +type benjiProbe struct { + contract common.Address + ring *navRing + mu sync.Mutex +} + +func NewBENJIProbe() IssuerProbe { + return &benjiProbe{ + contract: common.HexToAddress(benjiContractEthereum), + ring: newNavRing(72), // ~30 days of daily samples with slack + } +} + +func (p *benjiProbe) Slug() string { return "benji" } +func (p *benjiProbe) Issuer() string { return "franklin" } +func (p *benjiProbe) Chain() string { return "ethereum" } + +func (p *benjiProbe) Measure(ctx context.Context, rpc *ethclient.Client) (*Measurement, error) { + now := time.Now().UTC() + + // Fetch current NAV. Franklin publishes daily; the JSON schema is + // TBD in V1 and will need Sprint 3 verification. + navNow, err := fetchJSONNumber(benjiNAVEndpoint, []string{"nav", "value"}) + if err != nil { + return nil, fmt.Errorf("nav source: %w", err) + } + + // Update the ring with this fresh sample. + p.mu.Lock() + p.ring.push(now, navNow) + nav30d := p.ring.navAt(now.Add(-Window30d)) + nav7d := p.ring.navAt(now.Add(-Window7d)) + p.mu.Unlock() + + // Windowed yield: zero until the ring has coverage for the window. + // The site distinguishes this from a genuine zero via the + // probe_ok / sample_size labels. + yield30dBps := 0 + if nav30d > 0 { + yield30dBps = annualizedYieldBpsFromNAV(navNow, nav30d, 30.0) + } + yield7dBps := 0 + if nav7d > 0 { + yield7dBps = annualizedYieldBpsFromNAV(navNow, nav7d, 7.0) + } + + // Optional: read totalSupply from the Ethereum wrapper if the + // contract address is set. Skipped when address is zero (V1 default). + var supplyUnits float64 + if p.contract != (common.Address{}) { + if s, err := readERC20TotalSupply(ctx, rpc, p.contract, nil); err == nil { + supplyUnits = s / 1e18 + } + } + aum := supplyUnits * navNow + + return &Measurement{ + Token: p.Slug(), + Issuer: p.Issuer(), + Chain: p.Chain(), + DeliveredBps30d: yield30dBps, + DeliveredBps7d: yield7dBps, + DeliveredBpsLifetime: 0, // V1 placeholder + TotalSupplyUnits: supplyUnits, + AUMUSD: aum, + NewDistributionsUSD: 0, // NAV model, no dividend transfers + MeasuredAt: now, + }, nil +} + +// annualizedYieldBpsFromNAV computes yield from NAV growth over a +// windowDays period, annualized, expressed in basis points. +func annualizedYieldBpsFromNAV(navEnd, navStart, windowDays float64) int { + if navStart <= 0 { + return 0 + } + growth := (navEnd - navStart) / navStart + annualized := growth * (365.0 / windowDays) + return int(annualized * 10000) +} diff --git a/harnesses/rwa-yield-accuracy/cmd/script/buidl.go b/harnesses/rwa-yield-accuracy/cmd/script/buidl.go new file mode 100644 index 00000000..84ac7836 --- /dev/null +++ b/harnesses/rwa-yield-accuracy/cmd/script/buidl.go @@ -0,0 +1,141 @@ +package main + +import ( + "context" + "fmt" + "math/big" + "time" + + "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/ethclient" +) + +// BUIDL is BlackRock's USD Institutional Digital Liquidity Fund, +// tokenized on Ethereum by Securitize. +// +// Contracts: +// BUIDL ERC-20: 0x7712c34205737192402172409a8F7ccef8aA2AEc +// Distributor: 0x5072Ed40EBa6bE38C2370cAD1Cb1df0202924e53 +// (Securitize distribution EOA that calls bulkIssuance on BUIDL) +// +// DORMANT — mechanism mismatch. Research showed the distributor +// mints ADDITIONAL BUIDL rather than transferring USDC. The current +// dividend.go path (sum USDC Transfer events from treasury) reports +// zero for BUIDL and is the wrong model for this token. +// +// To activate, replace Measure with a rebase-style implementation +// that reads totalSupply growth over the window (like USDY) AND +// filters out mints attributable to new subscriptions vs yield +// accrual. Ondo/USDY separates these cleanly at the contract level; +// BUIDL does not, so cross-referencing with Securitize's monthly +// dividend announcements will likely be required to net out +// subscription-driven supply changes. + +const ( + buidlContractEthereum = "0x7712c34205737192402172409a8F7ccef8aA2AEc" + buidlTreasuryEthereum = "0x5072Ed40EBa6bE38C2370cAD1Cb1df0202924e53" +) + +type buidlProbe struct { + contract common.Address + treasury common.Address +} + +func NewBUIDLProbe() IssuerProbe { + return &buidlProbe{ + contract: common.HexToAddress(buidlContractEthereum), + treasury: common.HexToAddress(buidlTreasuryEthereum), + } +} + +func (p *buidlProbe) Slug() string { return "buidl" } +func (p *buidlProbe) Issuer() string { return "blackrock" } +func (p *buidlProbe) Chain() string { return "ethereum" } + +func (p *buidlProbe) Measure(ctx context.Context, rpc *ethclient.Client) (*Measurement, error) { + now := time.Now().UTC() + + // Current supply. + supplyNow, err := readERC20TotalSupply(ctx, rpc, p.contract, nil) + if err != nil { + return nil, fmt.Errorf("supply now: %w", err) + } + + latest, err := rpc.BlockNumber(ctx) + if err != nil { + return nil, fmt.Errorf("latest block: %w", err) + } + block30dAgo := blockOffsetBySeconds(latest, int64(Window30d.Seconds())) + block7dAgo := blockOffsetBySeconds(latest, int64(Window7d.Seconds())) + + // Historical supplies for the avg-over-window denominator. + supply30d, err := readERC20TotalSupply(ctx, rpc, p.contract, block30dAgo) + if err != nil { + return nil, fmt.Errorf("supply 30d: %w", err) + } + supply7d, err := readERC20TotalSupply(ctx, rpc, p.contract, block7dAgo) + if err != nil { + return nil, fmt.Errorf("supply 7d: %w", err) + } + + // Distributions from treasury over each window. + // BUIDL uses 6 decimals? Actually BUIDL is 6 decimals per Securitize + // docs, matching USDC. Confirm at Sprint 3 verification pass. + dist30d, err := sumUSDCDistributionsFromTreasury(ctx, rpc, p.treasury, block30dAgo.Uint64(), latest) + if err != nil { + return nil, fmt.Errorf("distributions 30d: %w", err) + } + dist7d, err := sumUSDCDistributionsFromTreasury(ctx, rpc, p.treasury, block7dAgo.Uint64(), latest) + if err != nil { + return nil, fmt.Errorf("distributions 7d: %w", err) + } + dist30dUSD := usdcAmountToFloat(dist30d) + dist7dUSD := usdcAmountToFloat(dist7d) + + // BUIDL is 6 decimals like USDC. Share price = $1 by design. + avgSupply30dUSD := (supplyNow + supply30d) / 2 / 1e6 + avgSupply7dUSD := (supplyNow + supply7d) / 2 / 1e6 + + yield30dBps := annualizedYieldBpsFromDistribution(dist30dUSD, avgSupply30dUSD, 30.0) + yield7dBps := annualizedYieldBpsFromDistribution(dist7dUSD, avgSupply7dUSD, 7.0) + + // Lifetime V1 placeholder. V2: sum distributions since contract + // deployment block. + yieldLifeBps := 0 + + supplyUnits := supplyNow / 1e6 + aum := supplyUnits // share price $1 by design + + return &Measurement{ + Token: p.Slug(), + Issuer: p.Issuer(), + Chain: p.Chain(), + DeliveredBps30d: yield30dBps, + DeliveredBps7d: yield7dBps, + DeliveredBpsLifetime: yieldLifeBps, + TotalSupplyUnits: supplyUnits, + AUMUSD: aum, + NewDistributionsUSD: dist30dUSD, // reported for the counter + MeasuredAt: now, + }, nil +} + +// readERC20TotalSupply is the shared ERC-20 totalSupply() reader. +// Extracted from usdy.go for reuse across dividend-model probes. +func readERC20TotalSupply(ctx context.Context, rpc *ethclient.Client, contract common.Address, blockNumber *big.Int) (float64, error) { + msg := ethereum.CallMsg{ + To: &contract, + Data: erc20TotalSupplySelector, + } + result, err := rpc.CallContract(ctx, msg, blockNumber) + if err != nil { + return 0, err + } + if len(result) == 0 { + return 0, fmt.Errorf("empty call result") + } + supply := new(big.Int).SetBytes(result) + f, _ := new(big.Float).SetInt(supply).Float64() + return f, nil +} diff --git a/harnesses/rwa-yield-accuracy/cmd/script/config.go b/harnesses/rwa-yield-accuracy/cmd/script/config.go new file mode 100644 index 00000000..c9c81f0e --- /dev/null +++ b/harnesses/rwa-yield-accuracy/cmd/script/config.go @@ -0,0 +1,56 @@ +package main + +import ( + "os" + "strings" + "time" +) + +// Poll cadences. On-chain reads run every pollInterval; the rolling +// 30d/7d windows are recomputed at windowRecomputeInterval (heavier +// query, values move slowly enough that once an hour is plenty). +const ( + pollInterval = 60 * time.Second + windowRecomputeInterval = time.Hour + promisedReloadInterval = 60 * time.Second + httpTimeout = 30 * time.Second +) + +// Rolling window sizes for delivered-yield computation. +const ( + Window7d = 7 * 24 * time.Hour + Window30d = 30 * 24 * time.Hour + WindowLifetime = time.Duration(0) // sentinel: from token inception +) + +// rpcURL returns the Ethereum mainnet RPC endpoint used by every +// probe. All five V1 tokens live on Ethereum; V2 will introduce +// per-chain endpoints for BUIDL/USDY multi-chain support. +// +// Default is dRPC because the harness needs archive access (eth_call +// at t-30d block). publicnode.com — the usual OCB default — rejects +// archive requests without an account. dRPC's free tier supports +// archive reads at least back to 30-day depth. +func rpcURL() string { + if v := strings.TrimSpace(os.Getenv("RPC_ETHEREUM")); v != "" { + return v + } + return "https://eth.drpc.org" +} + +func listenAddr() string { + if v := strings.TrimSpace(os.Getenv("LISTEN_ADDR")); v != "" { + return v + } + return ":2112" +} + +// promisedYieldsPath resolves the config file location. Defaults to +// /app/promised-yields.yml (Docker image layout); local development +// can override via PROMISED_YIELDS_PATH. +func promisedYieldsPath() string { + if v := strings.TrimSpace(os.Getenv("PROMISED_YIELDS_PATH")); v != "" { + return v + } + return "/app/promised-yields.yml" +} diff --git a/harnesses/rwa-yield-accuracy/cmd/script/dividend.go b/harnesses/rwa-yield-accuracy/cmd/script/dividend.go new file mode 100644 index 00000000..a2563fa6 --- /dev/null +++ b/harnesses/rwa-yield-accuracy/cmd/script/dividend.go @@ -0,0 +1,114 @@ +package main + +import ( + "context" + "fmt" + "math/big" + + "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/ethclient" +) + +// Shared helpers for the dividend distribution model (BUIDL, USTB). +// +// Delivered yield for a dividend-model token is: +// +// yield = sum(USDC Transfer events from treasury_wallet to any holder, +// over the window) +// / avg(token totalSupply across the window) +// * (365 / window_days) +// +// The treasury wallet is the address the issuer uses to distribute +// monthly dividends in USDC. It's public information for each fund but +// must be verified against the fund's own documentation before trusting +// the numbers. +// +// USDC contract on Ethereum mainnet: +// 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 +// Transfer event signature: +// keccak256("Transfer(address,address,uint256)") = +// 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef + +const usdcContractEthereum = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" + +var transferTopic = common.HexToHash("0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef") + +// sumUSDCDistributionsFromTreasury queries all USDC Transfer events +// with `from = treasury` over [fromBlock, toBlock] and returns the +// total USDC amount transferred, in USDC base units (6 decimals). +// +// Chunks the block range into eth_getLogs-friendly slices (10_000 +// blocks each) since some public RPCs cap the range or the response +// size. Failing on a chunk aborts the whole call so the caller sees a +// clear "measurement failed" rather than a silently truncated total. +func sumUSDCDistributionsFromTreasury( + ctx context.Context, + rpc *ethclient.Client, + treasury common.Address, + fromBlock, toBlock uint64, +) (*big.Int, error) { + usdc := common.HexToAddress(usdcContractEthereum) + // Topic filter: [Transfer, from=treasury, ANY to]. Address is + // padded to 32 bytes (left-padded with zeros) per event topic + // encoding rules. + fromTopic := common.BytesToHash(treasury.Bytes()) + + const chunk = uint64(10_000) + total := new(big.Int) + + for start := fromBlock; start <= toBlock; start += chunk { + end := start + chunk - 1 + if end > toBlock { + end = toBlock + } + q := ethereum.FilterQuery{ + FromBlock: new(big.Int).SetUint64(start), + ToBlock: new(big.Int).SetUint64(end), + Addresses: []common.Address{usdc}, + Topics: [][]common.Hash{ + {transferTopic}, + {fromTopic}, + nil, // any recipient + }, + } + logs, err := rpc.FilterLogs(ctx, q) + if err != nil { + return nil, fmt.Errorf("filterLogs [%d..%d]: %w", start, end, err) + } + for _, lg := range logs { + // Transfer event data is the uint256 amount (32 bytes). + // Anonymous events would put value in topics; ERC-20 Transfer + // is not anonymous so value is in data. + if len(lg.Data) < 32 { + continue + } + amount := new(big.Int).SetBytes(lg.Data[:32]) + total.Add(total, amount) + } + } + return total, nil +} + +// usdcAmountToFloat converts a USDC base-unit amount (6 decimals) to +// a float64 in USD. +func usdcAmountToFloat(raw *big.Int) float64 { + if raw == nil { + return 0 + } + f, _ := new(big.Float).SetInt(raw).Float64() + return f / 1e6 +} + +// annualizedYieldBpsFromDistribution computes annualized delivered +// yield for a dividend-model token, given a distribution total (in +// USD) over a windowDays period and the average supply of the token +// (in USD, treating share price as $1 per BUIDL/USTB design). +func annualizedYieldBpsFromDistribution(distributionUSD, avgSupplyUSD, windowDays float64) int { + if avgSupplyUSD <= 0 { + return 0 + } + periodYield := distributionUSD / avgSupplyUSD + annualized := periodYield * (365.0 / windowDays) + return int(annualized * 10000) +} diff --git a/harnesses/rwa-yield-accuracy/cmd/script/issuer.go b/harnesses/rwa-yield-accuracy/cmd/script/issuer.go new file mode 100644 index 00000000..94377f27 --- /dev/null +++ b/harnesses/rwa-yield-accuracy/cmd/script/issuer.go @@ -0,0 +1,53 @@ +package main + +import ( + "context" + "time" + + "github.com/ethereum/go-ethereum/ethclient" +) + +// Measurement is one probe result for a single token on a single +// chain. All three delivered-yield windows are computed together so +// the caller (main loop) writes them atomically to Prometheus. +type Measurement struct { + Token string + Issuer string + Chain string + + // All yields in basis points (530 = 5.30%). + DeliveredBps30d int + DeliveredBps7d int + DeliveredBpsLifetime int + + // totalSupply in the token's own base units (usually 1e18 or 1e6). + TotalSupplyUnits float64 + + // AUM in USD (totalSupply × share price, evaluated per issuer). + AUMUSD float64 + + // Cumulative USDC distributions counted during THIS probe for + // dividend-model tokens. Rebase/NAV probes return 0. + NewDistributionsUSD float64 + + MeasuredAt time.Time +} + +// IssuerProbe is the per-token measurement contract. Each implementation +// encodes the specific on-chain read path for that issuer's yield +// mechanism (rebase, dividend, NAV appreciation). +// +// Measure MUST be safe to call in parallel with itself for different +// tokens; probes typically hold their own RPC client. It MUST NOT +// mutate Prometheus metrics — the caller owns metric writes so a +// single probe error doesn't wipe healthy gauges. +type IssuerProbe interface { + Slug() string // token slug, e.g. "usdy" + Issuer() string // issuer slug, e.g. "ondo" + Chain() string // chain slug, e.g. "ethereum" + + // Measure reads the on-chain state (and any off-chain NAV source + // for NAV-model tokens) and returns a fully-populated Measurement. + // Should honor ctx cancellation for graceful shutdown. + Measure(ctx context.Context, rpc *ethclient.Client) (*Measurement, error) +} diff --git a/harnesses/rwa-yield-accuracy/cmd/script/loghub.go b/harnesses/rwa-yield-accuracy/cmd/script/loghub.go new file mode 100644 index 00000000..335a7b79 --- /dev/null +++ b/harnesses/rwa-yield-accuracy/cmd/script/loghub.go @@ -0,0 +1,108 @@ +package main + +import ( + "bufio" + "fmt" + "io" + "net/http" + "os" + "strconv" + "sync" + "time" +) + +// Auto-generated by the loghub inline pattern. Captures stdout/stderr into a +// bounded ring buffer and exposes GET /logs?tail=N protected by X-Logs-Token +// matching the LOGS_TOKEN env var. +// +// Keep in sync across miniapps (was previously the shared/loghub package; we +// inline because Railway's per-harness Docker build context can't reach a +// sibling shared module via go.mod replace). + +const logRingMax = 5000 + +type logRing struct { + mu sync.Mutex + lines []string + max int +} + +var globalLogRing = &logRing{max: logRingMax} + +func (b *logRing) push(line string) { + entry := time.Now().UTC().Format("2006-01-02T15:04:05.000Z") + " " + line + b.mu.Lock() + if len(b.lines) >= b.max { + b.lines = append(b.lines[1:], entry) + } else { + b.lines = append(b.lines, entry) + } + b.mu.Unlock() +} + +func (b *logRing) snapshot(tail int) []string { + b.mu.Lock() + defer b.mu.Unlock() + if tail <= 0 || tail >= len(b.lines) { + out := make([]string, len(b.lines)) + copy(out, b.lines) + return out + } + start := len(b.lines) - tail + out := make([]string, tail) + copy(out, b.lines[start:]) + return out +} + +var logSetupOnce sync.Once + +func installLogCapture() { logSetupOnce.Do(doInstallLogCapture) } + +func doInstallLogCapture() { + originalStdout := os.Stdout + originalStderr := os.Stderr + r, w, err := os.Pipe() + if err != nil { + fmt.Fprintf(originalStdout, "[loghub] pipe failed: %v (/logs will be empty)\n", err) + return + } + os.Stdout = w + os.Stderr = w + + go func() { + scanner := bufio.NewScanner(r) + buf := make([]byte, 0, 1024*1024) + scanner.Buffer(buf, 1024*1024) + for scanner.Scan() { + line := scanner.Text() + fmt.Fprintln(originalStdout, line) + globalLogRing.push(line) + } + _, _ = io.Copy(originalStdout, r) + _ = originalStderr + }() +} + +func logsHandler() http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + expected := os.Getenv("LOGS_TOKEN") + if expected == "" { + http.Error(w, "logs disabled: LOGS_TOKEN unset", http.StatusForbidden) + return + } + if r.Header.Get("X-Logs-Token") != expected { + http.Error(w, "forbidden", http.StatusForbidden) + return + } + tail := 500 + if t := r.URL.Query().Get("tail"); t != "" { + if n, err := strconv.Atoi(t); err == nil && n > 0 { + tail = n + } + } + w.Header().Set("Content-Type", "text/plain; charset=utf-8") + for _, l := range globalLogRing.snapshot(tail) { + fmt.Fprintln(w, l) + } + }) +} diff --git a/harnesses/rwa-yield-accuracy/cmd/script/main.go b/harnesses/rwa-yield-accuracy/cmd/script/main.go new file mode 100644 index 00000000..dd7381b6 --- /dev/null +++ b/harnesses/rwa-yield-accuracy/cmd/script/main.go @@ -0,0 +1,240 @@ +package main + +import ( + "context" + "fmt" + "os" + "os/signal" + "strconv" + "sync" + "syscall" + "time" + + "github.com/ethereum/go-ethereum/ethclient" +) + +// probes is the V1 cohort. Each entry declares one (issuer, token, +// chain) triple with its own on-chain read logic. Adding a token to +// this bench is: (1) implement IssuerProbe, (2) add its slug to +// promised-yields.yml, (3) append here. +// +// Active cohort: USDY (rebase, totalSupply growth), USTB (NAV via +// Chainlink feed), OUSG (NAV via OndoOracle). All three are fully +// on-chain measurable — no off-chain HTTP dependency, no treasury +// wallet assumption. +// +// BUIDL and BENJI stay dormant: +// - BUIDL: distributor 0x5072Ed40EBa6bE38C2370cAD1Cb1df0202924e53 +// was identified but calls bulkIssuance (mints more BUIDL, not +// USDC transfers). The current dividend.go model that scans USDC +// Transfer events doesn't fit; needs a rebase-style measurement +// PLUS a way to separate yield mints from new subscriptions. +// - BENJI: Ethereum wrapper (0x3DDc...50dc9) has 3 holders, ~$48M, +// no on-chain sharePrice function. NAV is $1.00 by design; yield +// is only knowable via Franklin's off-chain fund page. Reached +// out to digitalassets@franklintempleton.com; unblock once they +// confirm an API endpoint or Chainlink feed. +var probes = []IssuerProbe{ + NewUSDYProbe(), + NewUSTBProbe(), + NewOUSGProbe(), + // NewBUIDLProbe(), // TODO: split bulkIssuance mints from subscriptions + // NewBENJIProbe(), // TODO: waiting Franklin NAV endpoint confirmation +} + +func main() { + installLogCapture() + fmt.Println("=== RWA Yield Accuracy Harness ===") + fmt.Println("OpenChainBench № 089 - on-chain delivered yield vs advertised APY.") + fmt.Printf("Cohort: %d tokens | RPC: %s\n", len(probes), rpcURL()) + for _, p := range probes { + fmt.Printf(" - %s/%s on %s\n", p.Issuer(), p.Slug(), p.Chain()) + } + fmt.Println() + + // Promised-yield store: loaded from disk, hot-reloaded so a manual + // weekly edit lands within a scrape cycle. + promised := newPromisedStore(promisedYieldsPath()) + if err := promised.reload(); err != nil { + fmt.Printf("[fatal] load %s: %v\n", promisedYieldsPath(), err) + os.Exit(1) + } + fmt.Printf("Promised yields loaded from %s\n", promisedYieldsPath()) + + // Ethereum RPC client. All V1 probes share one client since they + // all read from Ethereum mainnet. V2 will introduce per-chain + // clients (USDY on Solana, BUIDL on Arbitrum, etc.). + rpc, err := ethclient.Dial(rpcURL()) + if err != nil { + fmt.Printf("[fatal] rpc dial: %v\n", err) + os.Exit(1) + } + defer rpc.Close() + + // Metrics server: /metrics, /health, /logs on :2112. + go func() { + if err := StartMetricsServer(listenAddr()); err != nil { + fmt.Printf("[fatal] metrics server: %v\n", err) + os.Exit(1) + } + }() + fmt.Printf("Metrics server: %s/metrics\n\n", listenAddr()) + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + // Concurrent measurement loop. One goroutine per probe. Each + // probe polls at pollInterval, writes to Prometheus directly (no + // central aggregator) so a failure on one token can't stall the + // others. + var wg sync.WaitGroup + for _, probe := range probes { + probe := probe + wg.Add(1) + go func() { + defer wg.Done() + runProbe(ctx, probe, rpc, promised) + }() + } + + // Config file reloader: refreshes promised-yields.yml so a manual + // weekly edit propagates without a redeploy. + wg.Add(1) + go func() { + defer wg.Done() + t := time.NewTicker(promisedReloadInterval) + defer t.Stop() + for { + select { + case <-ctx.Done(): + return + case <-t.C: + if err := promised.reload(); err != nil { + fmt.Printf("[promised] reload failed: %v\n", err) + } + } + } + }() + + // Graceful shutdown on SIGINT / SIGTERM. + sig := make(chan os.Signal, 1) + signal.Notify(sig, os.Interrupt, syscall.SIGTERM) + s := <-sig + fmt.Printf("\n[shutdown] received %v\n", s) + cancel() + wg.Wait() +} + +// runProbe is the per-token measurement loop. It polls, writes the +// resulting Measurement into Prometheus, and cross-references the +// promised APY (from the hot-reloaded config) to emit the deviation +// gauge. A failed probe increments the error counter and marks the +// token unhealthy but does NOT touch the delivered / promised gauges, +// so a transient RPC hiccup can't flip a healthy row to zero. +func runProbe(ctx context.Context, probe IssuerProbe, rpc *ethclient.Client, promised *promisedStore) { + labels := []string{probe.Issuer(), probe.Slug(), probe.Chain()} + + tick := func() { + probeCtx, cancel := context.WithTimeout(ctx, httpTimeout) + defer cancel() + + m, err := probe.Measure(probeCtx, rpc) + if err != nil { + classifyAndCount(probe, err) + probeOK.WithLabelValues(labels...).Set(0) + fmt.Printf("[%s] probe error: %v\n", probe.Slug(), err) + return + } + + // Delivered yields (always emitted, even if promised is missing). + deliveredBps30d.WithLabelValues(labels...).Set(float64(m.DeliveredBps30d)) + deliveredBps7d.WithLabelValues(labels...).Set(float64(m.DeliveredBps7d)) + deliveredBpsLifetime.WithLabelValues(labels...).Set(float64(m.DeliveredBpsLifetime)) + + // Supply / AUM context. + totalSupply.WithLabelValues(labels...).Set(m.TotalSupplyUnits) + aumUSD.WithLabelValues(labels...).Set(m.AUMUSD) + + // Distributions counter (dividend-model tokens only). + if m.NewDistributionsUSD > 0 { + distributionsUSD.WithLabelValues(labels...).Add(m.NewDistributionsUSD) + } + + // Deviation vs promised (skipped if promised is missing for + // this token — the delivered_bps series still tells the story). + if promisedBpsVal, ok := promised.get(probe.Slug()); ok { + promisedBps.WithLabelValues(labels...).Set(float64(promisedBpsVal)) + deviationBps30d.WithLabelValues(labels...).Set(float64(m.DeliveredBps30d - promisedBpsVal)) + deviationBps7d.WithLabelValues(labels...).Set(float64(m.DeliveredBps7d - promisedBpsVal)) + deviationBpsLifetime.WithLabelValues(labels...).Set(float64(m.DeliveredBpsLifetime - promisedBpsVal)) + } + + probeOK.WithLabelValues(labels...).Set(1) + lastMeasured.WithLabelValues(labels...).Set(float64(m.MeasuredAt.Unix())) + + fmt.Printf("[%s] delivered_30d=%d bps 7d=%d bps supply=%s AUM=%s\n", + probe.Slug(), + m.DeliveredBps30d, + m.DeliveredBps7d, + shortNum(m.TotalSupplyUnits), + shortNum(m.AUMUSD), + ) + } + + // Fire immediately, then tick. + tick() + t := time.NewTicker(pollInterval) + defer t.Stop() + for { + select { + case <-ctx.Done(): + return + case <-t.C: + tick() + } + } +} + +// classifyAndCount buckets probe errors so the error_type dimension +// stays low-cardinality. Anything that doesn't match a known bucket +// falls into "other". +func classifyAndCount(probe IssuerProbe, err error) { + msg := err.Error() + bucket := "other" + switch { + case contains(msg, "context deadline"), contains(msg, "timeout"): + bucket = "timeout" + case contains(msg, "connection refused"), contains(msg, "dial"), contains(msg, "EOF"): + bucket = "rpc_err" + case contains(msg, "empty call result"), contains(msg, "invalid opcode"): + bucket = "contract_err" + case contains(msg, "parse"), contains(msg, "unmarshal"): + bucket = "parse_err" + case contains(msg, "nav source"), contains(msg, "issuer api"): + bucket = "nav_source_err" + } + probeErrors.WithLabelValues(probe.Issuer(), probe.Slug(), probe.Chain(), bucket).Inc() +} + +func contains(s, sub string) bool { + for i := 0; i+len(sub) <= len(s); i++ { + if s[i:i+len(sub)] == sub { + return true + } + } + return false +} + +// shortNum formats a large number as e.g. "534.2M" for logging. +func shortNum(n float64) string { + switch { + case n >= 1e9: + return fmt.Sprintf("%sB", strconv.FormatFloat(n/1e9, 'f', 2, 64)) + case n >= 1e6: + return fmt.Sprintf("%sM", strconv.FormatFloat(n/1e6, 'f', 2, 64)) + case n >= 1e3: + return fmt.Sprintf("%sK", strconv.FormatFloat(n/1e3, 'f', 2, 64)) + default: + return strconv.FormatFloat(n, 'f', 2, 64) + } +} diff --git a/harnesses/rwa-yield-accuracy/cmd/script/metrics.go b/harnesses/rwa-yield-accuracy/cmd/script/metrics.go new file mode 100644 index 00000000..6a818615 --- /dev/null +++ b/harnesses/rwa-yield-accuracy/cmd/script/metrics.go @@ -0,0 +1,95 @@ +package main + +import ( + "net/http" + + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" + "github.com/prometheus/client_golang/prometheus/promhttp" +) + +// All series carry {issuer, token, chain} so the bench spec can select +// per-token and the site can aggregate per-issuer or per-chain +// downstream without cardinality tricks. + +var ( + promisedBps = promauto.NewGaugeVec(prometheus.GaugeOpts{ + Name: "rwa_yield_promised_bps", + Help: "Advertised APY in basis points, sourced from the issuer's public dashboard. Reloaded from promised-yields.yml every 60 seconds so a manual weekly update lands within one scrape cycle.", + }, []string{"issuer", "token", "chain"}) + + deliveredBps30d = promauto.NewGaugeVec(prometheus.GaugeOpts{ + Name: "rwa_yield_delivered_bps_30d", + Help: "Delivered yield over a rolling 30-day window, annualized, in basis points. Derived per issuer from the token's own distribution mechanism (rebase, USDC dividend Transfer events, or NAV appreciation).", + }, []string{"issuer", "token", "chain"}) + + deliveredBps7d = promauto.NewGaugeVec(prometheus.GaugeOpts{ + Name: "rwa_yield_delivered_bps_7d", + Help: "Delivered yield over a rolling 7-day window, annualized. Same computation as 30d, shorter window for volatility context.", + }, []string{"issuer", "token", "chain"}) + + deliveredBpsLifetime = promauto.NewGaugeVec(prometheus.GaugeOpts{ + Name: "rwa_yield_delivered_bps_lifetime", + Help: "Delivered yield since token inception, annualized. The honest cross-issuer comparison, unaffected by short-window distribution cycles that undersample monthly dividend tokens.", + }, []string{"issuer", "token", "chain"}) + + deviationBps30d = promauto.NewGaugeVec(prometheus.GaugeOpts{ + Name: "rwa_yield_deviation_bps_30d", + Help: "delivered_bps_30d minus promised_bps. Sign preserved: negative means the token paid less than advertised over the window, positive means more.", + }, []string{"issuer", "token", "chain"}) + + deviationBps7d = promauto.NewGaugeVec(prometheus.GaugeOpts{ + Name: "rwa_yield_deviation_bps_7d", + Help: "delivered_bps_7d minus promised_bps.", + }, []string{"issuer", "token", "chain"}) + + deviationBpsLifetime = promauto.NewGaugeVec(prometheus.GaugeOpts{ + Name: "rwa_yield_deviation_bps_lifetime", + Help: "delivered_bps_lifetime minus promised_bps.", + }, []string{"issuer", "token", "chain"}) + + totalSupply = promauto.NewGaugeVec(prometheus.GaugeOpts{ + Name: "rwa_yield_total_supply_units", + Help: "Current totalSupply of the token in its own units (BUIDL: 1e18 wei, USDY: 1e18 wei, etc.). Useful for context and for verifying probe health across restarts.", + }, []string{"issuer", "token", "chain"}) + + aumUSD = promauto.NewGaugeVec(prometheus.GaugeOpts{ + Name: "rwa_yield_aum_usd", + Help: "Approximate AUM in USD. totalSupply times share price. For BUIDL/USTB share price is $1 by design; for USDY it is the rebase-inflated share; for BENJI/OUSG it is the NAV.", + }, []string{"issuer", "token", "chain"}) + + probeOK = promauto.NewGaugeVec(prometheus.GaugeOpts{ + Name: "rwa_yield_probe_ok", + Help: "1 if the most recent on-chain probe returned a valid measurement, 0 if it failed.", + }, []string{"issuer", "token", "chain"}) + + lastMeasured = promauto.NewGaugeVec(prometheus.GaugeOpts{ + Name: "rwa_yield_last_measured_unix", + Help: "Unix timestamp of the last successful on-chain read for this token.", + }, []string{"issuer", "token", "chain"}) + + probeErrors = promauto.NewCounterVec(prometheus.CounterOpts{ + Name: "rwa_yield_probe_errors_total", + Help: "Count of probe failures by error type: rpc_err, parse_err, timeout, contract_err, nav_source_err.", + }, []string{"issuer", "token", "chain", "error_type"}) + + distributionsUSD = promauto.NewCounterVec(prometheus.CounterOpts{ + Name: "rwa_yield_distributions_usd_total", + Help: "Cumulative USDC-equivalent distributions counted from on-chain Transfer events since the harness started. Dividend-model tokens only; rebase/NAV tokens report zero here.", + }, []string{"issuer", "token", "chain"}) +) + +// StartMetricsServer binds /metrics + /health + /logs on addr. Blocking +// call, run in its own goroutine. +func StartMetricsServer(addr string) error { + mux := http.NewServeMux() + mux.Handle("/metrics", promhttp.Handler()) + mux.Handle("/logs", logsHandler()) + mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) { + _, _ = w.Write([]byte("ok")) + }) + mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + _, _ = w.Write([]byte("rwa-yield-accuracy harness · OpenChainBench № 089")) + }) + return http.ListenAndServe(addr, mux) +} diff --git a/harnesses/rwa-yield-accuracy/cmd/script/nav.go b/harnesses/rwa-yield-accuracy/cmd/script/nav.go new file mode 100644 index 00000000..8f226224 --- /dev/null +++ b/harnesses/rwa-yield-accuracy/cmd/script/nav.go @@ -0,0 +1,140 @@ +package main + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "time" +) + +// Shared HTTP helpers for the NAV-appreciation model probes (BENJI, +// OUSG). These tokens report their NAV (share price) via an off-chain +// issuer API rather than emitting on-chain distribution events. +// Delivered yield is computed from NAV growth across the window: +// +// yield = (NAV(t=now) - NAV(t=window ago)) / NAV(t=window ago) +// * (365 / window_days) +// +// Since we don't get NAV(t=window ago) from the issuer API directly +// in V1 (most issuers only expose the current NAV), we bootstrap by +// storing our own daily NAV snapshots in memory. The 30d and 7d +// delivered yields only become available after the harness has been +// running for the corresponding window. Until then, the probe reports +// zero for that window; the "sample_size" / probe_ok labels signal +// this to the site so the row is dimmed rather than shown as a real +// zero. +// +// V2 will replace the in-memory ring with the on-chain oracle where +// one exists (Chainlink PoR for BUIDL, native oracle for BENJI's +// FOBXX proxy), removing the warmup dependency. + +var navHTTPClient = &http.Client{Timeout: httpTimeout} + +// fetchJSONNumber makes a GET to `url`, parses the response as JSON, +// and extracts a numeric value at the given JSON path (dot-separated). +// Returns the value as float64. Fails cleanly if the field is missing +// or non-numeric, so a schema change at the issuer surfaces as a probe +// error rather than a silent zero. +func fetchJSONNumber(url string, jsonPath []string) (float64, error) { + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return 0, err + } + req.Header.Set("User-Agent", "openchainbench-rwa-yield-probe/1.0") + resp, err := navHTTPClient.Do(req) + if err != nil { + return 0, fmt.Errorf("nav source: %w", err) + } + defer resp.Body.Close() + if resp.StatusCode != 200 { + return 0, fmt.Errorf("nav source: HTTP %d", resp.StatusCode) + } + body, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20)) // 1 MiB cap + if err != nil { + return 0, err + } + var doc any + if err := json.Unmarshal(body, &doc); err != nil { + return 0, fmt.Errorf("nav source: parse %w", err) + } + cur := doc + for _, seg := range jsonPath { + obj, ok := cur.(map[string]any) + if !ok { + return 0, fmt.Errorf("nav source: expected object at path segment %q", seg) + } + cur, ok = obj[seg] + if !ok { + return 0, fmt.Errorf("nav source: missing field %q", seg) + } + } + switch v := cur.(type) { + case float64: + return v, nil + case string: + var f float64 + if _, err := fmt.Sscanf(v, "%f", &f); err != nil { + return 0, fmt.Errorf("nav source: non-numeric string at path") + } + return f, nil + default: + return 0, fmt.Errorf("nav source: value is %T, want number", cur) + } +} + +// navRingSample is one daily NAV snapshot kept in memory to enable +// windowed yield computation without waiting for the issuer to expose +// historical NAV via API. +type navRingSample struct { + At time.Time + NAV float64 +} + +// navRing is a bounded circular buffer of daily NAV snapshots, one +// per token. Ships as much as 30 days of history (72 samples at 10-hour +// intervals is plenty for the 30d window computation). Older samples +// are evicted automatically as new ones arrive. +type navRing struct { + samples []navRingSample + max int +} + +func newNavRing(max int) *navRing { + return &navRing{samples: make([]navRingSample, 0, max), max: max} +} + +func (r *navRing) push(now time.Time, nav float64) { + r.samples = append(r.samples, navRingSample{At: now, NAV: nav}) + if len(r.samples) > r.max { + r.samples = r.samples[len(r.samples)-r.max:] + } +} + +// navAt returns the NAV sample closest to `target`, or zero if the +// ring has no data covering that timestamp (i.e. token was added +// less than `target ago`). +func (r *navRing) navAt(target time.Time) float64 { + if len(r.samples) == 0 { + return 0 + } + // Linear scan (ring is small, ~72 entries). + var best navRingSample + var bestDelta time.Duration = 1<<62 - 1 + for _, s := range r.samples { + d := s.At.Sub(target) + if d < 0 { + d = -d + } + if d < bestDelta { + bestDelta = d + best = s + } + } + // If the closest sample is more than 2 days off the target we + // don't have coverage — return zero. + if bestDelta > 48*time.Hour { + return 0 + } + return best.NAV +} diff --git a/harnesses/rwa-yield-accuracy/cmd/script/ousg.go b/harnesses/rwa-yield-accuracy/cmd/script/ousg.go new file mode 100644 index 00000000..22cc28c4 --- /dev/null +++ b/harnesses/rwa-yield-accuracy/cmd/script/ousg.go @@ -0,0 +1,91 @@ +package main + +import ( + "context" + "fmt" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/ethclient" +) + +// OUSG is Ondo's Short-Term U.S. Government Treasuries token. NAV +// appreciation model: share price grows daily as yield accrues. +// +// Contracts: +// OUSG ERC-20: 0x1B19C19393e2d034D8Ff31fF34c81252FcBbee92 +// OndoOracle: 0x9Cad45a8BF0Ed41Ff33074449B357C7a1fAb4094 +// (Exposes Aave IPriceOracle interface: getAssetPrice(OUSG) → +// 18-decimal USD NAV. Verified 2026-07: returns $115.977, matches +// Ondo's public dashboard.) +// +// The oracle read at a specific historical block gives the NAV that +// was current at that block, which is exactly the input we need for +// windowed yield without any off-chain HTTP dependency. + +const ( + ousgContractEthereum = "0x1B19C19393e2d034D8Ff31fF34c81252FcBbee92" + ousgOndoOracle = "0x9Cad45a8BF0Ed41Ff33074449B357C7a1fAb4094" +) + +type ousgProbe struct { + contract common.Address + oracle common.Address +} + +func NewOUSGProbe() IssuerProbe { + return &ousgProbe{ + contract: common.HexToAddress(ousgContractEthereum), + oracle: common.HexToAddress(ousgOndoOracle), + } +} + +func (p *ousgProbe) Slug() string { return "ousg" } +func (p *ousgProbe) Issuer() string { return "ondo" } +func (p *ousgProbe) Chain() string { return "ethereum" } + +func (p *ousgProbe) Measure(ctx context.Context, rpc *ethclient.Client) (*Measurement, error) { + now := time.Now().UTC() + + latest, err := rpc.BlockNumber(ctx) + if err != nil { + return nil, fmt.Errorf("latest block: %w", err) + } + block30dAgo := blockOffsetBySeconds(latest, int64(Window30d.Seconds())) + block7dAgo := blockOffsetBySeconds(latest, int64(Window7d.Seconds())) + + navNow, err := readAavePriceOracleNAV(ctx, rpc, p.oracle, p.contract, nil) + if err != nil { + return nil, fmt.Errorf("nav now: %w", err) + } + nav30d, err := readAavePriceOracleNAV(ctx, rpc, p.oracle, p.contract, block30dAgo) + if err != nil { + return nil, fmt.Errorf("nav 30d: %w", err) + } + nav7d, err := readAavePriceOracleNAV(ctx, rpc, p.oracle, p.contract, block7dAgo) + if err != nil { + return nil, fmt.Errorf("nav 7d: %w", err) + } + + yield30dBps := annualizedYieldBpsFromNAV(navNow, nav30d, 30.0) + yield7dBps := annualizedYieldBpsFromNAV(navNow, nav7d, 7.0) + + supply, err := readERC20TotalSupply(ctx, rpc, p.contract, nil) + if err != nil { + supply = 0 + } + supplyUnits := supply / 1e18 + + return &Measurement{ + Token: p.Slug(), + Issuer: p.Issuer(), + Chain: p.Chain(), + DeliveredBps30d: yield30dBps, + DeliveredBps7d: yield7dBps, + DeliveredBpsLifetime: 0, + TotalSupplyUnits: supplyUnits, + AUMUSD: supplyUnits * navNow, + NewDistributionsUSD: 0, + MeasuredAt: now, + }, nil +} diff --git a/harnesses/rwa-yield-accuracy/cmd/script/promised.go b/harnesses/rwa-yield-accuracy/cmd/script/promised.go new file mode 100644 index 00000000..135d0c9c --- /dev/null +++ b/harnesses/rwa-yield-accuracy/cmd/script/promised.go @@ -0,0 +1,73 @@ +package main + +import ( + "fmt" + "os" + "sync" + + "gopkg.in/yaml.v3" +) + +// promisedYieldsFile is the on-disk shape of promised-yields.yml. +// Editable by hand + versioned in the repo so every value is +// traceable to a source URL and date. +type promisedYieldsFile struct { + Updated string `yaml:"updated"` + Issuers []promisedEntry `yaml:"issuers"` +} + +type promisedEntry struct { + Token string `yaml:"token"` + PromisedAPYBps int `yaml:"promised_apy_bps"` + Source string `yaml:"source"` + SourceDate string `yaml:"source_date"` + Notes string `yaml:"notes"` +} + +// promisedStore is a thread-safe token → bps map, reloaded from +// promised-yields.yml every promisedReloadInterval. Reads are lock- +// free-fast via atomic map swap. +type promisedStore struct { + mu sync.RWMutex + byToken map[string]int + path string +} + +func newPromisedStore(path string) *promisedStore { + return &promisedStore{ + byToken: make(map[string]int), + path: path, + } +} + +func (s *promisedStore) get(token string) (int, bool) { + s.mu.RLock() + defer s.mu.RUnlock() + v, ok := s.byToken[token] + return v, ok +} + +// reload reads promised-yields.yml and swaps the internal map. Any +// parse error leaves the previous state intact so a bad edit doesn't +// zero out the metrics. +func (s *promisedStore) reload() error { + data, err := os.ReadFile(s.path) + if err != nil { + return fmt.Errorf("read %s: %w", s.path, err) + } + var parsed promisedYieldsFile + if err := yaml.Unmarshal(data, &parsed); err != nil { + return fmt.Errorf("unmarshal %s: %w", s.path, err) + } + next := make(map[string]int, len(parsed.Issuers)) + for _, e := range parsed.Issuers { + if e.Token == "" { + continue + } + next[e.Token] = e.PromisedAPYBps + } + s.mu.Lock() + s.byToken = next + s.mu.Unlock() + return nil +} diff --git a/harnesses/rwa-yield-accuracy/cmd/script/usdy.go b/harnesses/rwa-yield-accuracy/cmd/script/usdy.go new file mode 100644 index 00000000..f5872ccf --- /dev/null +++ b/harnesses/rwa-yield-accuracy/cmd/script/usdy.go @@ -0,0 +1,153 @@ +package main + +import ( + "context" + "fmt" + "math/big" + "time" + + "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/ethclient" +) + +// USDY is Ondo's yield-bearing tokenized U.S. Treasury note. Rebase +// mechanism: totalSupply grows daily via the smart contract as yield +// accrues. Delivered yield is a pure function of totalSupply growth +// over the window, no dividend transfers, no NAV oracle needed. +// +// Ethereum contract: +// 0x96F6eF951840721AdBF46Ac996b59E0235CB985C (Ondo USDY, ERC-20 proxy) +// +// The token has 18 decimals. Share price is exactly $1 by rebase +// construction: 1 USDY (at any point in time) equals 1 USD of face +// value. Yield materializes as more USDY units in each holder's wallet, +// not as share price appreciation. +// +// Multi-chain note: USDY also lives on Solana, Aptos and Sui. V1 +// measures Ethereum only. V2 will aggregate cross-chain supply. + +const usdyContractEthereum = "0x96F6eF951840721AdBF46Ac996b59E0235CB985C" + +// erc20TotalSupplySelector is the 4-byte function selector for +// ERC-20 totalSupply(). Same across every ERC-20 on every EVM chain. +var erc20TotalSupplySelector = common.Hex2Bytes("18160ddd") + +// avgEthereumBlockTimeSec is used to translate a 30-day / 7-day time +// delta into a block-number offset via a linear approximation. Post- +// merge Ethereum block time is 12s exactly and very stable, so this +// approximation is accurate to within a few blocks over a 30-day +// window, which is well below the noise floor of a yield measurement. +const avgEthereumBlockTimeSec = 12 + +type usdyProbe struct { + contract common.Address +} + +// NewUSDYProbe constructs the USDY probe with the mainnet contract +// address hardcoded. Exposed as an IssuerProbe. +func NewUSDYProbe() IssuerProbe { + return &usdyProbe{ + contract: common.HexToAddress(usdyContractEthereum), + } +} + +func (p *usdyProbe) Slug() string { return "usdy" } +func (p *usdyProbe) Issuer() string { return "ondo" } +func (p *usdyProbe) Chain() string { return "ethereum" } + +func (p *usdyProbe) Measure(ctx context.Context, rpc *ethclient.Client) (*Measurement, error) { + now := time.Now().UTC() + + // Current supply (latest block). + supplyNow, err := p.readTotalSupply(ctx, rpc, nil) + if err != nil { + return nil, fmt.Errorf("supply now: %w", err) + } + + // Approximate the block numbers for t-30d and t-7d using the + // stable post-merge 12s block time. Precision is ~10 blocks over + // 30 days, well below the noise floor of an APY measurement. + latest, err := rpc.BlockNumber(ctx) + if err != nil { + return nil, fmt.Errorf("latest block: %w", err) + } + block30dAgo := blockOffsetBySeconds(latest, int64(Window30d.Seconds())) + block7dAgo := blockOffsetBySeconds(latest, int64(Window7d.Seconds())) + + supply30d, err := p.readTotalSupply(ctx, rpc, block30dAgo) + if err != nil { + return nil, fmt.Errorf("supply 30d: %w", err) + } + supply7d, err := p.readTotalSupply(ctx, rpc, block7dAgo) + if err != nil { + return nil, fmt.Errorf("supply 7d: %w", err) + } + + // Annualized delivered yields, in basis points. + yield30dBps := annualizedYieldBps(supplyNow, supply30d, 30.0) + yield7dBps := annualizedYieldBps(supplyNow, supply7d, 7.0) + + // Lifetime yield placeholder for V1. V2 will read the contract + // deployment block from Etherscan and compute since inception. + yieldLifeBps := 0 + + supplyUnits := supplyNow / 1e18 + + return &Measurement{ + Token: p.Slug(), + Issuer: p.Issuer(), + Chain: p.Chain(), + DeliveredBps30d: yield30dBps, + DeliveredBps7d: yield7dBps, + DeliveredBpsLifetime: yieldLifeBps, + TotalSupplyUnits: supplyUnits, + AUMUSD: supplyUnits, // 1 USDY == 1 USD by rebase construction + NewDistributionsUSD: 0, // rebase model, no dividend transfers + MeasuredAt: now, + }, nil +} + +// readTotalSupply calls the ERC-20 totalSupply() view function at the +// given block number (nil = latest) and returns the raw uint256 as a +// float64 (in wei-equivalent units, 1e18 for USDY). +func (p *usdyProbe) readTotalSupply(ctx context.Context, rpc *ethclient.Client, blockNumber *big.Int) (float64, error) { + msg := ethereum.CallMsg{ + To: &p.contract, + Data: erc20TotalSupplySelector, + } + result, err := rpc.CallContract(ctx, msg, blockNumber) + if err != nil { + return 0, err + } + if len(result) == 0 { + return 0, fmt.Errorf("empty call result") + } + supply := new(big.Int).SetBytes(result) + f, _ := new(big.Float).SetInt(supply).Float64() + return f, nil +} + +// blockOffsetBySeconds returns latest - (deltaSeconds / avgBlockTime), +// clamped to 1. Used to approximate the block number a given time +// ago without a per-block binary search. +func blockOffsetBySeconds(latest uint64, deltaSeconds int64) *big.Int { + offset := uint64(deltaSeconds) / avgEthereumBlockTimeSec + if offset >= latest { + return big.NewInt(1) + } + return new(big.Int).SetUint64(latest - offset) +} + +// annualizedYieldBps computes (supplyEnd - supplyStart) / supplyStart, +// annualized from a windowDays-day period, expressed in basis points. +// Returns 0 if supplyStart is zero or invalid (protection against +// division by zero on very fresh tokens). +func annualizedYieldBps(supplyEnd, supplyStart, windowDays float64) int { + if supplyStart <= 0 { + return 0 + } + growth := (supplyEnd - supplyStart) / supplyStart + annualized := growth * (365.0 / windowDays) + return int(annualized * 10000) +} diff --git a/harnesses/rwa-yield-accuracy/cmd/script/ustb.go b/harnesses/rwa-yield-accuracy/cmd/script/ustb.go new file mode 100644 index 00000000..04e2992b --- /dev/null +++ b/harnesses/rwa-yield-accuracy/cmd/script/ustb.go @@ -0,0 +1,92 @@ +package main + +import ( + "context" + "fmt" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/ethclient" +) + +// USTB is Superstate's Short-Duration U.S. Treasury Fund tokenized on +// Ethereum. NAV-accrual model: share price grows daily as yield accrues. +// There are no dividend transfers to holders — the "yield" materializes +// entirely through NAV growth. +// +// Contracts: +// USTB ERC-20: 0x43415eB6ff9DB7E26A15b704e7A3eDCe97d31C4e +// Chainlink NAV feed: 0x289B5036cd942e619E1Ee48670F98d214E745AAC +// (Superstate publishes USTB NAV/USD via Chainlink, updates daily) +// +// Delivered yield is read directly from the Chainlink feed at latest +// and at t-30d / t-7d block numbers. No in-memory bootstrap needed +// (archive RPC provides history since token inception). + +const ( + ustbContractEthereum = "0x43415eB6ff9DB7E26A15b704e7A3eDCe97d31C4e" + ustbChainlinkFeed = "0x289B5036cd942e619E1Ee48670F98d214E745AAC" +) + +type ustbProbe struct { + contract common.Address + navFeed common.Address +} + +func NewUSTBProbe() IssuerProbe { + return &ustbProbe{ + contract: common.HexToAddress(ustbContractEthereum), + navFeed: common.HexToAddress(ustbChainlinkFeed), + } +} + +func (p *ustbProbe) Slug() string { return "ustb" } +func (p *ustbProbe) Issuer() string { return "superstate" } +func (p *ustbProbe) Chain() string { return "ethereum" } + +func (p *ustbProbe) Measure(ctx context.Context, rpc *ethclient.Client) (*Measurement, error) { + now := time.Now().UTC() + + latest, err := rpc.BlockNumber(ctx) + if err != nil { + return nil, fmt.Errorf("latest block: %w", err) + } + block30dAgo := blockOffsetBySeconds(latest, int64(Window30d.Seconds())) + block7dAgo := blockOffsetBySeconds(latest, int64(Window7d.Seconds())) + + navNow, err := readChainlinkNAV(ctx, rpc, p.navFeed, nil) + if err != nil { + return nil, fmt.Errorf("nav now: %w", err) + } + nav30d, err := readChainlinkNAV(ctx, rpc, p.navFeed, block30dAgo) + if err != nil { + return nil, fmt.Errorf("nav 30d: %w", err) + } + nav7d, err := readChainlinkNAV(ctx, rpc, p.navFeed, block7dAgo) + if err != nil { + return nil, fmt.Errorf("nav 7d: %w", err) + } + + yield30dBps := annualizedYieldBpsFromNAV(navNow, nav30d, 30.0) + yield7dBps := annualizedYieldBpsFromNAV(navNow, nav7d, 7.0) + + // USTB uses 6 decimals per Superstate spec. + supply, err := readERC20TotalSupply(ctx, rpc, p.contract, nil) + if err != nil { + supply = 0 + } + supplyUnits := supply / 1e6 + + return &Measurement{ + Token: p.Slug(), + Issuer: p.Issuer(), + Chain: p.Chain(), + DeliveredBps30d: yield30dBps, + DeliveredBps7d: yield7dBps, + DeliveredBpsLifetime: 0, + TotalSupplyUnits: supplyUnits, + AUMUSD: supplyUnits * navNow, + NewDistributionsUSD: 0, + MeasuredAt: now, + }, nil +} diff --git a/harnesses/rwa-yield-accuracy/go.mod b/harnesses/rwa-yield-accuracy/go.mod new file mode 100644 index 00000000..87b7aff1 --- /dev/null +++ b/harnesses/rwa-yield-accuracy/go.mod @@ -0,0 +1,45 @@ +module rwa-yield-accuracy + +go 1.24.0 + +require ( + github.com/ethereum/go-ethereum v1.14.11 + github.com/prometheus/client_golang v1.23.2 + gopkg.in/yaml.v3 v3.0.1 +) + +require ( + github.com/Microsoft/go-winio v0.6.2 // indirect + github.com/StackExchange/wmi v1.2.1 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/bits-and-blooms/bitset v1.13.0 // indirect + github.com/btcsuite/btcd/btcec/v2 v2.3.4 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/consensys/bavard v0.1.13 // indirect + github.com/consensys/gnark-crypto v0.12.1 // indirect + github.com/crate-crypto/go-ipa v0.0.0-20240223125850-b1e8a79f509c // indirect + github.com/crate-crypto/go-kzg-4844 v1.0.0 // indirect + github.com/deckarep/golang-set/v2 v2.6.0 // indirect + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect + github.com/ethereum/c-kzg-4844 v1.0.0 // indirect + github.com/ethereum/go-verkle v0.1.1-0.20240829091221-dffa7562dbe9 // indirect + github.com/go-ole/go-ole v1.3.0 // indirect + github.com/gorilla/websocket v1.4.2 // indirect + github.com/holiman/uint256 v1.3.1 // indirect + github.com/mmcloughlin/addchain v0.4.0 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/prometheus/client_model v0.6.2 // indirect + github.com/prometheus/common v0.66.1 // indirect + github.com/prometheus/procfs v0.16.1 // indirect + github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible // indirect + github.com/supranational/blst v0.3.13 // indirect + github.com/tklauser/go-sysconf v0.3.12 // indirect + github.com/tklauser/numcpus v0.6.1 // indirect + go.yaml.in/yaml/v2 v2.4.2 // indirect + golang.org/x/crypto v0.22.0 // indirect + golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa // indirect + golang.org/x/sync v0.13.0 // indirect + golang.org/x/sys v0.35.0 // indirect + google.golang.org/protobuf v1.36.8 // indirect + rsc.io/tmplfunc v0.0.3 // indirect +) diff --git a/harnesses/rwa-yield-accuracy/go.sum b/harnesses/rwa-yield-accuracy/go.sum new file mode 100644 index 00000000..d09cef70 --- /dev/null +++ b/harnesses/rwa-yield-accuracy/go.sum @@ -0,0 +1,190 @@ +github.com/DataDog/zstd v1.4.5 h1:EndNeuB0l9syBZhut0wns3gV1hL8zX8LIu6ZiVHWLIQ= +github.com/DataDog/zstd v1.4.5/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/StackExchange/wmi v1.2.1 h1:VIkavFPXSjcnS+O8yTq7NI32k0R5Aj+v39y29VYDOSA= +github.com/StackExchange/wmi v1.2.1/go.mod h1:rcmrprowKIVzvc+NUiLncP2uuArMWLCbu9SBzvHz7e8= +github.com/VictoriaMetrics/fastcache v1.12.2 h1:N0y9ASrJ0F6h0QaC3o6uJb3NIZ9VKLjCM7NQbSmF7WI= +github.com/VictoriaMetrics/fastcache v1.12.2/go.mod h1:AmC+Nzz1+3G2eCPapF6UcsnkThDcMsQicp4xDukwJYI= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bits-and-blooms/bitset v1.13.0 h1:bAQ9OPNFYbGHV6Nez0tmNI0RiEu7/hxlYJRUA0wFAVE= +github.com/bits-and-blooms/bitset v1.13.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= +github.com/btcsuite/btcd/btcec/v2 v2.3.4 h1:3EJjcN70HCu/mwqlUsGK8GcNVyLVxFDlWurTXGPFfiQ= +github.com/btcsuite/btcd/btcec/v2 v2.3.4/go.mod h1:zYzJ8etWJQIv1Ogk7OzpWjowwOdXY1W/17j2MW85J04= +github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1 h1:q0rUy8C/TYNBQS1+CGKw68tLOFYSNEs0TFnxxnS9+4U= +github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cockroachdb/errors v1.11.3 h1:5bA+k2Y6r+oz/6Z/RFlNeVCesGARKuC6YymtcDrbC/I= +github.com/cockroachdb/errors v1.11.3/go.mod h1:m4UIW4CDjx+R5cybPsNrRbreomiFqt8o1h1wUVazSd8= +github.com/cockroachdb/fifo v0.0.0-20240606204812-0bbfbd93a7ce h1:giXvy4KSc/6g/esnpM7Geqxka4WSqI1SZc7sMJFd3y4= +github.com/cockroachdb/fifo v0.0.0-20240606204812-0bbfbd93a7ce/go.mod h1:9/y3cnZ5GKakj/H4y9r9GTjCvAFta7KLgSHPJJYc52M= +github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b h1:r6VH0faHjZeQy818SGhaone5OnYfxFR/+AzdY3sf5aE= +github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b/go.mod h1:Vz9DsVWQQhf3vs21MhPMZpMGSht7O/2vFW2xusFUVOs= +github.com/cockroachdb/pebble v1.1.2 h1:CUh2IPtR4swHlEj48Rhfzw6l/d0qA31fItcIszQVIsA= +github.com/cockroachdb/pebble v1.1.2/go.mod h1:4exszw1r40423ZsmkG/09AFEG83I0uDgfujJdbL6kYU= +github.com/cockroachdb/redact v1.1.5 h1:u1PMllDkdFfPWaNGMyLD1+so+aq3uUItthCFqzwPJ30= +github.com/cockroachdb/redact v1.1.5/go.mod h1:BVNblN9mBWFyMyqK1k3AAiSxhvhfK2oOZZ2lK+dpvRg= +github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06 h1:zuQyyAKVxetITBuuhv3BI9cMrmStnpT18zmgmTxunpo= +github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06/go.mod h1:7nc4anLGjupUW/PeY5qiNYsdNXj7zopG+eqsS7To5IQ= +github.com/consensys/bavard v0.1.13 h1:oLhMLOFGTLdlda/kma4VOJazblc7IM5y5QPd2A/YjhQ= +github.com/consensys/bavard v0.1.13/go.mod h1:9ItSMtA/dXMAiL7BG6bqW2m3NdSEObYWoH223nGHukI= +github.com/consensys/gnark-crypto v0.12.1 h1:lHH39WuuFgVHONRl3J0LRBtuYdQTumFSDtJF7HpyG8M= +github.com/consensys/gnark-crypto v0.12.1/go.mod h1:v2Gy7L/4ZRosZ7Ivs+9SfUDr0f5UlG+EM5t7MPHiLuY= +github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/crate-crypto/go-ipa v0.0.0-20240223125850-b1e8a79f509c h1:uQYC5Z1mdLRPrZhHjHxufI8+2UG/i25QG92j0Er9p6I= +github.com/crate-crypto/go-ipa v0.0.0-20240223125850-b1e8a79f509c/go.mod h1:geZJZH3SzKCqnz5VT0q/DyIG/tvu/dZk+VIfXicupJs= +github.com/crate-crypto/go-kzg-4844 v1.0.0 h1:TsSgHwrkTKecKJ4kadtHi4b3xHW5dCFUDFnUp1TsawI= +github.com/crate-crypto/go-kzg-4844 v1.0.0/go.mod h1:1kMhvPgI0Ky3yIa+9lFySEBUBXkYxeOi8ZF1sYioxhc= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/deckarep/golang-set/v2 v2.6.0 h1:XfcQbWM1LlMB8BsJ8N9vW5ehnnPVIw0je80NsVHagjM= +github.com/deckarep/golang-set/v2 v2.6.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4= +github.com/decred/dcrd/crypto/blake256 v1.0.0 h1:/8DMNYp9SGi5f0w7uCm6d6M4OU2rGFK09Y2A4Xv7EE0= +github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 h1:YLtO71vCjJRCBcrPMtQ9nqBsqpA1m5sE92cU+pd5Mcc= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs= +github.com/ethereum/c-kzg-4844 v1.0.0 h1:0X1LBXxaEtYD9xsyj9B9ctQEZIpnvVDeoBx8aHEwTNA= +github.com/ethereum/c-kzg-4844 v1.0.0/go.mod h1:VewdlzQmpT5QSrVhbBuGoCdFJkpaJlO1aQputP83wc0= +github.com/ethereum/go-ethereum v1.14.11 h1:8nFDCUUE67rPc6AKxFj7JKaOa2W/W1Rse3oS6LvvxEY= +github.com/ethereum/go-ethereum v1.14.11/go.mod h1:+l/fr42Mma+xBnhefL/+z11/hcmJ2egl+ScIVPjhc7E= +github.com/ethereum/go-verkle v0.1.1-0.20240829091221-dffa7562dbe9 h1:8NfxH2iXvJ60YRB8ChToFTUzl8awsc3cJ8CbLjGIl/A= +github.com/ethereum/go-verkle v0.1.1-0.20240829091221-dffa7562dbe9/go.mod h1:M3b90YRnzqKyyzBEWJGqj8Qff4IDeXnzFw0P9bFw3uk= +github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= +github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= +github.com/gballet/go-libpcsclite v0.0.0-20190607065134-2772fd86a8ff h1:tY80oXqGNY4FhTFhk+o9oFHGINQ/+vhlm8HFzi6znCI= +github.com/gballet/go-libpcsclite v0.0.0-20190607065134-2772fd86a8ff/go.mod h1:x7DCsMOv1taUwEWCzT4cmDeAkigA5/QCwUodaVOe8Ww= +github.com/getsentry/sentry-go v0.27.0 h1:Pv98CIbtB3LkMWmXi4Joa5OOcwbmnX88sF5qbK3r3Ps= +github.com/getsentry/sentry-go v0.27.0/go.mod h1:lc76E2QywIyW8WuBnwl8Lc4bkmQH4+w1gwTf25trprY= +github.com/go-ole/go-ole v1.2.5/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= +github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= +github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw= +github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= +github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb h1:PBC98N2aIaM3XXiurYmW7fx4GZkL8feAMVq7nEjURHk= +github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/hashicorp/go-bexpr v0.1.10 h1:9kuI5PFotCboP3dkDYFr/wi0gg0QVbSNz5oFRpxn4uE= +github.com/hashicorp/go-bexpr v0.1.10/go.mod h1:oxlubA2vC/gFVfX1A6JGp7ls7uCDlfJn732ehYYg+g0= +github.com/holiman/billy v0.0.0-20240216141850-2abb0c79d3c4 h1:X4egAf/gcS1zATw6wn4Ej8vjuVGxeHdan+bRb2ebyv4= +github.com/holiman/billy v0.0.0-20240216141850-2abb0c79d3c4/go.mod h1:5GuXa7vkL8u9FkFuWdVvfR5ix8hRB7DbOAaYULamFpc= +github.com/holiman/bloomfilter/v2 v2.0.3 h1:73e0e/V0tCydx14a0SCYS/EWCxgwLZ18CZcZKVu0fao= +github.com/holiman/bloomfilter/v2 v2.0.3/go.mod h1:zpoh+gs7qcpqrHr3dB55AMiJwo0iURXE7ZOP9L9hSkA= +github.com/holiman/uint256 v1.3.1 h1:JfTzmih28bittyHM8z360dCjIA9dbPIBlcTI6lmctQs= +github.com/holiman/uint256 v1.3.1/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E= +github.com/huin/goupnp v1.3.0 h1:UvLUlWDNpoUdYzb2TCn+MuTWtcjXKSza2n6CBdQ0xXc= +github.com/huin/goupnp v1.3.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8= +github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus= +github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/leanovate/gopter v0.2.9 h1:fQjYxZaynp97ozCzfOyOuAGOU4aU/z37zf/tOujFk7c= +github.com/leanovate/gopter v0.2.9/go.mod h1:U2L/78B+KVFIx2VmW6onHJQzXtFb+p5y3y2Sh+Jxxv8= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= +github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag= +github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/pointerstructure v1.2.0 h1:O+i9nHnXS3l/9Wu7r4NrEdwA2VFTicjUEN1uBnDo34A= +github.com/mitchellh/pointerstructure v1.2.0/go.mod h1:BRAsLI5zgXmw97Lf6s25bs8ohIXc3tViBH44KcwB2g4= +github.com/mmcloughlin/addchain v0.4.0 h1:SobOdjm2xLj1KkXN5/n0xTIWyZA2+s99UCY1iPfkHRY= +github.com/mmcloughlin/addchain v0.4.0/go.mod h1:A86O+tHqZLMNO4w6ZZ4FlVQEadcoqkyU72HC5wJ4RlU= +github.com/mmcloughlin/profile v0.1.1/go.mod h1:IhHD7q1ooxgwTgjxQYkACGA77oFTDdFVejUS1/tS/qU= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= +github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= +github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs= +github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA= +github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= +github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= +github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= +github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= +github.com/rs/cors v1.7.0 h1:+88SsELBHx5r+hZ8TCkggzSstaWNbDvThkVK8H6f9ik= +github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible h1:Bn1aCHHRnjv4Bl16T8rcaFjYSrGrIZvpiGO6P3Q4GpU= +github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= +github.com/status-im/keycard-go v0.2.0 h1:QDLFswOQu1r5jsycloeQh3bVU8n/NatHHaZobtDnDzA= +github.com/status-im/keycard-go v0.2.0/go.mod h1:wlp8ZLbsmrF6g6WjugPAx+IzoLrkdf9+mHxBEeo3Hbg= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/supranational/blst v0.3.13 h1:AYeSxdOMacwu7FBmpfloBz5pbFXDmJL33RuwnKtmTjk= +github.com/supranational/blst v0.3.13/go.mod h1:jZJtfjgudtNl4en1tzwPIV3KjUnQUvG3/j+w+fVonLw= +github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 h1:epCh84lMvA70Z7CTTCmYQn2CKbY8j86K7/FAIr141uY= +github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= +github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= +github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= +github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= +github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= +github.com/tyler-smith/go-bip39 v1.1.0 h1:5eUemwrMargf3BSLRRCalXT93Ns6pQJIjYQN2nyfOP8= +github.com/tyler-smith/go-bip39 v1.1.0/go.mod h1:gUYDtqQw1JS3ZJ8UWVcGTGqqr6YIN3CWg+kkNaLt55U= +github.com/urfave/cli/v2 v2.25.7 h1:VAzn5oq403l5pHjc4OhD54+XGO9cdKVL/7lDjF+iKUs= +github.com/urfave/cli/v2 v2.25.7/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ= +github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= +github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= +go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= +golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= +golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= +golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa h1:FRnLl4eNAQl8hwxVVC17teOw8kdjVDVAiFMtgUdTSRQ= +golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa/go.mod h1:zk2irFbV9DP96SEBUUAy67IdHUaZuSnrz1n472HUCLE= +golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610= +golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= +golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= +golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= +golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= +golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc= +google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= +gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +rsc.io/tmplfunc v0.0.3 h1:53XFQh69AfOa8Tw0Jm7t+GV7KZhOi6jzsCzTtKbMvzU= +rsc.io/tmplfunc v0.0.3/go.mod h1:AG3sTPzElb1Io3Yg4voV9AGZJuleGAwaVRxL9M49PhA= diff --git a/harnesses/rwa-yield-accuracy/promised-yields.yml b/harnesses/rwa-yield-accuracy/promised-yields.yml new file mode 100644 index 00000000..ed4267da --- /dev/null +++ b/harnesses/rwa-yield-accuracy/promised-yields.yml @@ -0,0 +1,56 @@ +# Promised APY for each token in the rwa-yield-accuracy cohort. +# +# This file is the manual source of truth for what each issuer +# ADVERTISES. Updated at least weekly. Each entry cites its source URL +# and the date it was last read. When an issuer changes its advertised +# APY, edit the value + source_date here; the harness reloads this +# file every 60 seconds so a commit lands within a scrape cycle. +# +# Format: +# token: — matches the provider slug in the bench spec +# promised_apy_bps: N — e.g. 530 for 5.30% +# source: — the exact page consulted +# source_date: YYYY-MM-DD — when this value was last verified +# notes: — methodology caveats (SEC 7-day vs current, +# gross vs net, etc.) +# +# When adding a token, cross-check the value against the fund's +# quarterly fact sheet or SEC filing to guard against the marketing +# page drifting from the regulated disclosure. + +updated: 2026-07-22 + +issuers: + - token: usdy + promised_apy_bps: 355 + source: "https://ondo.finance/usdy" + source_date: 2026-07-22 + notes: "Ondo USDY current APY as displayed on the retail dashboard. Rebase happens daily via the token contract; delivered yield is directly measurable from totalSupply growth. Value is net of the 20 bps Ondo fee. Rate reflects Fed policy: 3.55% at the time of this update, down from ~5.4% a year earlier." + + # The four entries below remain in the file so the harness reloader + # keeps their promised APYs current for the day the corresponding + # probes flip active. They are NOT scraped or displayed on the bench + # page while status: staging. + - token: buidl + promised_apy_bps: 350 + source: "https://securitize.io/primary-market/buidl" + source_date: 2026-07-22 + notes: "BlackRock BUIDL 7-day net SEC yield estimate (probe inactive; needs Securitize treasury wallet verification before enable)." + + - token: benji + promised_apy_bps: 345 + source: "https://franklintempleton.com/investments/options/money-market-funds/products/29386" + source_date: 2026-07-22 + notes: "Franklin OnChain U.S. Government Money Fund 7-day SEC yield estimate (probe inactive; NAV endpoint TBD)." + + - token: ustb + promised_apy_bps: 352 + source: "https://superstate.com/ustb" + source_date: 2026-07-22 + notes: "Superstate USTB 30-day yield as displayed on superstate.com (7-day SEC yield not published there). Probe inactive; treasury wallet verification pending." + + - token: ousg + promised_apy_bps: 350 + source: "https://ondo.finance/ousg" + source_date: 2026-07-22 + notes: "Ondo OUSG NAV-appreciation yield estimate (probe inactive; NAV endpoint TBD)." diff --git a/src/lib/spec.ts b/src/lib/spec.ts index 56fc12a1..a13520cb 100644 --- a/src/lib/spec.ts +++ b/src/lib/spec.ts @@ -305,7 +305,7 @@ const loadBenchmarkUnfilteredCached = unstable_cache( // v39: bench 091 osmosis-rpc ship (Cosmos SDK Kind added to harness). // v40: benches 092 hyperliquid-rpc + 093 tron-rpc ship (bench SET grew by 2). // v41: benches 094-100 ship (cosmos-hub, injective, neutron, world-chain, kaia, ink, opbnb — bench SET grew by 7). - ["bench-unfiltered-v41", process.env.VERCEL_ENV === "production" ? "prod" : "all"], + ["bench-unfiltered-v42", process.env.VERCEL_ENV === "production" ? "prod" : "all"], { revalidate: 300, tags: ["benchmarks"] }, ); @@ -512,8 +512,8 @@ const loadAllBenchmarksCached = unstable_cache( // v41: bumped in lockstep with bench-unfiltered-v38 (mobula drop). // v42: bumped in lockstep with bench-unfiltered-v39 (osmosis-rpc ship). // v43: bumped in lockstep with bench-unfiltered-v40 (hyperliquid-rpc + tron-rpc ship). - // v44: bumped in lockstep with bench-unfiltered-v41 (7 chains ship batch). - ["all-benchmarks-v44", process.env.VERCEL_ENV === "production" ? "prod" : "all"], + // v44: bumped in lockstep with bench-unfiltered-v42 (7 chains ship batch). + ["all-benchmarks-v45", process.env.VERCEL_ENV === "production" ? "prod" : "all"], { revalidate: 300, tags: ["benchmarks"] }, ); export const loadAllBenchmarks = cache(loadAllBenchmarksCached);