Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions benchmarks/rwa-yield-accuracy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@ seo_description: "Signed bps deviation between advertised APY and on-chain distr
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
# Draft until the 4 non-USDY probes (BUIDL/USTB dividend, BENJI/OUSG
# NAV) are wired to verified treasury addresses and NAV endpoints.
# V1 ships with USDY only (rebase, fully on-chain verifiable). Others
# will flip to live once each contract address and issuer endpoint has
# been cross-checked with the fund's own documentation.
status: draft
# 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
Expand Down
38 changes: 18 additions & 20 deletions harnesses/rwa-yield-accuracy/cmd/script/benji.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,31 +11,29 @@ import (
)

// BENJI is Franklin Templeton's OnChain U.S. Government Money Fund
// share tokenized primarily on Stellar, with an Ethereum wrapper. NAV
// appreciation model: share price grows daily as yield accrues; there
// are no on-chain dividend transfers to holders.
// share, primarily tokenized on Stellar with an Ethereum wrapper.
//
// Ethereum wrapper contract (BENJI):
// 0x... (TODO Sprint 3: confirm Ethereum wrapper address; primary
// chain is Stellar)
// 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.)
//
// NAV source: Franklin Templeton's fund page publishes 7-day SEC yield
// and current NAV. V1 uses the current NAV endpoint and stores daily
// snapshots in memory for windowed yield computation (see nav.go).
// 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.
//
// V1 caveats:
// - Ethereum wrapper may not be widely traded; the "true" BENJI
// supply lives on Stellar. Ethereum totalSupply is a lower bound.
// - Franklin's official endpoint URL is TBD (V1 uses a placeholder;
// Sprint 3 verifies via Franklin's own dashboard).
// - The 30d yield only becomes reliable after 30 days of harness
// uptime, since we bootstrap NAV history from scratch.
// 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 = "0x0000000000000000000000000000000000000000" // TODO Sprint 3
// Franklin publishes the fund's current 7-day SEC yield on its
// public fund page. Actual JSON endpoint TBD Sprint 3; placeholder
// URL fails cleanly and the probe reports nav_source_err.
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"
)

Expand Down
40 changes: 17 additions & 23 deletions harnesses/rwa-yield-accuracy/cmd/script/buidl.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,35 +12,29 @@ import (
)

// BUIDL is BlackRock's USD Institutional Digital Liquidity Fund,
// tokenized on Ethereum by Securitize. Share price is fixed at
// exactly $1 by design; yield materializes as monthly USDC dividends
// distributed from the Securitize treasury wallet to BUIDL holders.
// tokenized on Ethereum by Securitize.
//
// Ethereum contract (BUIDL ERC-20):
// 0x7712c34205737192402172409a8F7ccef8aA2AEc
// Contracts:
// BUIDL ERC-20: 0x7712c34205737192402172409a8F7ccef8aA2AEc
// Distributor: 0x5072Ed40EBa6bE38C2370cAD1Cb1df0202924e53
// (Securitize distribution EOA that calls bulkIssuance on BUIDL)
//
// Securitize distribution treasury (source of USDC dividends):
// 0x... (verified against Securitize's own docs on Etherscan)
// 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.
//
// The 30d delivered yield is the sum of USDC Transfer events from
// treasury to BUIDL holders over the 30-day window, divided by the
// average BUIDL supply across the window (share price = $1), then
// annualized.
//
// V1 caveats:
// - Treasury address is TBD in V1: needs to be verified against
// Etherscan for the specific distribution wallet used by the
// current BUIDL contract. Placeholder address will make the probe
// return zero distributions until confirmed.
// - BUIDL distributes monthly, near the end of each calendar month.
// A 30-day rolling window that ends mid-month will underrepresent
// the yield. Cross-check against lifetime column at publication.
// 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"
// TODO(Sprint 3): verify this treasury address on Etherscan.
// Placeholder until confirmed against Securitize's own docs.
buidlTreasuryEthereum = "0x0000000000000000000000000000000000000000"
buidlTreasuryEthereum = "0x5072Ed40EBa6bE38C2370cAD1Cb1df0202924e53"
)

type buidlProbe struct {
Expand Down
7 changes: 6 additions & 1 deletion harnesses/rwa-yield-accuracy/cmd/script/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,16 @@ const (
// 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://ethereum-rpc.publicnode.com"
return "https://eth.drpc.org"
}

func listenAddr() string {
Expand Down
39 changes: 19 additions & 20 deletions harnesses/rwa-yield-accuracy/cmd/script/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,29 +18,28 @@ import (
// this bench is: (1) implement IssuerProbe, (2) add its slug to
// promised-yields.yml, (3) append here.
//
// V1 cohort restricted to USDY only. USDY's rebase model is fully
// verifiable on-chain (totalSupply growth on a single known ERC-20
// contract, no treasury wallet or off-chain NAV endpoint required)
// which lets the bench ship with confidence that every number is
// grounded.
// 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.
//
// The other 4 probes (BUIDL, USTB dividend model; BENJI, OUSG NAV
// model) exist in this repo (buidl.go, ustb.go, benji.go, ousg.go)
// but stay dormant until each one's treasury address or issuer NAV
// endpoint has been cross-checked with the fund's own documentation.
// Enabling them without that check would ship placeholder-zero yield
// values that look catastrophic (deviation of -530 bps vs promised).
//
// To activate a probe once verified: uncomment its constructor below
// and confirm the corresponding address / endpoint constant in the
// probe file. The bench spec's provider block for that token becomes
// meaningful automatically.
// 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(),
// NewBUIDLProbe(), // TODO(V2): verify Securitize treasury wallet
// NewUSTBProbe(), // TODO(V2): verify Superstate treasury wallet
// NewBENJIProbe(), // TODO(V2): verify Franklin NAV JSON endpoint
// NewOUSGProbe(), // TODO(V2): verify Ondo NAV JSON endpoint
NewUSTBProbe(),
NewOUSGProbe(),
// NewBUIDLProbe(), // TODO: split bulkIssuance mints from subscriptions
// NewBENJIProbe(), // TODO: waiting Franklin NAV endpoint confirmation
}

func main() {
Expand Down
66 changes: 32 additions & 34 deletions harnesses/rwa-yield-accuracy/cmd/script/ousg.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,43 +3,40 @@ package main
import (
"context"
"fmt"
"sync"
"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; no dividend transfers.
// appreciation model: share price grows daily as yield accrues.
//
// Ethereum contract (OUSG):
// 0x1B19C19393e2d034D8Ff31fF34c81252FcBbee92
// 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.)
//
// NAV source: Ondo publishes daily NAV on the OUSG dashboard. V1 uses
// the public endpoint (TBD schema, verified Sprint 3) plus in-memory
// NAV history for windowed yield.
//
// Ondo also runs a Chainlink Proof-of-Reserves feed for OUSG that
// could serve as an on-chain NAV source. V2 will switch to that feed
// where available to remove the offchain HTTP dependency.
// 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"
// Ondo publishes NAV on its API. Actual endpoint TBD Sprint 3.
ousgNAVEndpoint = "https://api.ondo.finance/v1/products/ousg/nav"
ousgOndoOracle = "0x9Cad45a8BF0Ed41Ff33074449B357C7a1fAb4094"
)

type ousgProbe struct {
contract common.Address
ring *navRing
mu sync.Mutex
oracle common.Address
}

func NewOUSGProbe() IssuerProbe {
return &ousgProbe{
contract: common.HexToAddress(ousgContractEthereum),
ring: newNavRing(72),
oracle: common.HexToAddress(ousgOndoOracle),
}
}

Expand All @@ -50,43 +47,44 @@ func (p *ousgProbe) Chain() string { return "ethereum" }
func (p *ousgProbe) Measure(ctx context.Context, rpc *ethclient.Client) (*Measurement, error) {
now := time.Now().UTC()

navNow, err := fetchJSONNumber(ousgNAVEndpoint, []string{"nav"})
latest, err := rpc.BlockNumber(ctx)
if err != nil {
return nil, fmt.Errorf("nav source: %w", err)
return nil, fmt.Errorf("latest block: %w", err)
}
block30dAgo := blockOffsetBySeconds(latest, int64(Window30d.Seconds()))
block7dAgo := blockOffsetBySeconds(latest, int64(Window7d.Seconds()))

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()

yield30dBps := 0
if nav30d > 0 {
yield30dBps = annualizedYieldBpsFromNAV(navNow, nav30d, 30.0)
navNow, err := readAavePriceOracleNAV(ctx, rpc, p.oracle, p.contract, nil)
if err != nil {
return nil, fmt.Errorf("nav now: %w", err)
}
yield7dBps := 0
if nav7d > 0 {
yield7dBps = annualizedYieldBpsFromNAV(navNow, nav7d, 7.0)
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 {
// Non-fatal: NAV yield can still be reported without supply.
supply = 0
}
supplyUnits := supply / 1e18
aum := supplyUnits * navNow

return &Measurement{
Token: p.Slug(),
Issuer: p.Issuer(),
Chain: p.Chain(),
DeliveredBps30d: yield30dBps,
DeliveredBps7d: yield7dBps,
DeliveredBpsLifetime: 0, // V1 placeholder
DeliveredBpsLifetime: 0,
TotalSupplyUnits: supplyUnits,
AUMUSD: aum,
AUMUSD: supplyUnits * navNow,
NewDistributionsUSD: 0,
MeasuredAt: now,
}, nil
Expand Down
Loading
Loading