From 4ed7e9ab4eabe1b35d4bda587d27521a91d0f6dc Mon Sep 17 00:00:00 2001 From: Flotapponnier <160007691+Flotapponnier@users.noreply.github.com> Date: Fri, 24 Jul 2026 16:12:27 +0200 Subject: [PATCH] Merge pull request #1366 from ChainBench/feat/rwa-yield-add-syrupusdc rwa-yield-accuracy: add SyrupUSDC (Maple, ERC-4626) to cohort --- benchmarks/rwa-yield-accuracy.yml | 14 ++ .../rwa-yield-accuracy/cmd/script/main.go | 23 +-- .../cmd/script/syrupusdc.go | 138 ++++++++++++++++++ .../rwa-yield-accuracy/promised-yields.yml | 6 + 4 files changed, 166 insertions(+), 15 deletions(-) create mode 100644 harnesses/rwa-yield-accuracy/cmd/script/syrupusdc.go diff --git a/benchmarks/rwa-yield-accuracy.yml b/benchmarks/rwa-yield-accuracy.yml index 7266b720..5e94e365 100644 --- a/benchmarks/rwa-yield-accuracy.yml +++ b/benchmarks/rwa-yield-accuracy.yml @@ -147,3 +147,17 @@ providers: success: rwa_yield_probe_ok{token="ousg"} sample_size: rwa_yield_aum_usd{token="ousg"} series: rwa_yield_delivered_bps_30d{token="ousg"} + + - slug: syrup-usdc + name: SyrupUSDC + tag: "Maple, Ethereum, ERC-4626 NAV" + formula: "ERC-4626 convertToAssets(1 share) delta over 30d, annualized. SyrupUSDC is Maple's yield-bearing USDC vault backed by institutional loans, not T-bills. Compared to the pool's base APY on DefiLlama (excludes SYRUP token incentives)." + queries: + p50: rwa_yield_deviation_bps_30d{token="syrup-usdc"} + p90: rwa_yield_deviation_bps_7d{token="syrup-usdc"} + p99: rwa_yield_deviation_bps_lifetime{token="syrup-usdc"} + mean: rwa_yield_delivered_bps_30d{token="syrup-usdc"} + success: rwa_yield_probe_ok{token="syrup-usdc"} + sample_size: rwa_yield_aum_usd{token="syrup-usdc"} + series: rwa_yield_delivered_bps_30d{token="syrup-usdc"} + diff --git a/harnesses/rwa-yield-accuracy/cmd/script/main.go b/harnesses/rwa-yield-accuracy/cmd/script/main.go index dd7381b6..c2ef8450 100644 --- a/harnesses/rwa-yield-accuracy/cmd/script/main.go +++ b/harnesses/rwa-yield-accuracy/cmd/script/main.go @@ -18,26 +18,19 @@ import ( // 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. +// Active cohort: +// - USDY (Ondo, rebase) +// - USTB (Superstate, Chainlink NAV feed) +// - OUSG (Ondo, OndoOracle IPriceOracle) +// - SyrupUSDC (Maple, ERC-4626 convertToAssets) // -// 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. +// BUIDL and BENJI dormant: distributor mints conflate yield with +// subscriptions (BUIDL), Ethereum wrapper lacks on-chain NAV (BENJI). var probes = []IssuerProbe{ NewUSDYProbe(), NewUSTBProbe(), NewOUSGProbe(), + NewSyrupUSDCProbe(), // NewBUIDLProbe(), // TODO: split bulkIssuance mints from subscriptions // NewBENJIProbe(), // TODO: waiting Franklin NAV endpoint confirmation } diff --git a/harnesses/rwa-yield-accuracy/cmd/script/syrupusdc.go b/harnesses/rwa-yield-accuracy/cmd/script/syrupusdc.go new file mode 100644 index 00000000..8d272316 --- /dev/null +++ b/harnesses/rwa-yield-accuracy/cmd/script/syrupusdc.go @@ -0,0 +1,138 @@ +package main + +import ( + "context" + "fmt" + "math/big" + "time" + + "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/ethclient" +) + +// SyrupUSDC is Maple Finance's yield-bearing USDC token — an ERC-4626 +// vault whose underlying position is a book of over-collateralized +// (and, more recently, undercollateralized) institutional USDC loans. +// Share price grows daily as pool interest accrues; no rebase, no +// dividend transfers. +// +// Contract (Ethereum mainnet): +// 0x80ac24aA929eaF5013f6436cdA2a7ba190f5Cc0b (6 decimals, asset=USDC) +// +// Yield model: NAV appreciation via ERC-4626 convertToAssets(). +// Passing 1e6 (one share in 6 decimals) returns the current USDC value +// of one share directly. Called at a historical block, we get the NAV +// that was current at that block — same clean methodology as OUSG and +// USTB, no off-chain data needed. +// +// Scope note: SyrupUSDC is NOT a T-bill wrapper — it's collateralized +// pool lending, so deviation from advertised APY reflects both loan- +// book performance AND yield disclosure honesty. Included in bench 089 +// under the broader framing "on-chain yield-bearing stables promising +// an APY," not the narrow "tokenized T-bill" reading. +// +// Advertised APY tracked here is the pool's BASE APY (yield generated +// by the loan book), not the boosted APY including SYRUP token +// incentives. This matches what we can measure on-chain: convertToAssets +// only reflects underlying pool yield, not off-chain reward +// distributions. + +const ( + syrupUSDCContractEthereum = "0x80ac24aA929eaF5013f6436cdA2a7ba190f5Cc0b" + // One share in the token's 6-decimal base units. Passing this to + // convertToAssets returns USDC-per-share directly. + syrupUSDCOneShareRaw = uint64(1_000_000) +) + +var erc4626ConvertToAssetsSelector []byte + +func init() { + erc4626ConvertToAssetsSelector = crypto.Keccak256([]byte("convertToAssets(uint256)"))[:4] +} + +type syrupUSDCProbe struct { + contract common.Address +} + +func NewSyrupUSDCProbe() IssuerProbe { + return &syrupUSDCProbe{ + contract: common.HexToAddress(syrupUSDCContractEthereum), + } +} + +func (p *syrupUSDCProbe) Slug() string { return "syrup-usdc" } +func (p *syrupUSDCProbe) Issuer() string { return "maple" } +func (p *syrupUSDCProbe) Chain() string { return "ethereum" } + +func (p *syrupUSDCProbe) 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())) + + // USDC-per-share at each snapshot. 6-decimal in, 6-decimal out; the + // returned float64 is already scaled to USD. + navNow, err := p.readSharePriceUSD(ctx, rpc, nil) + if err != nil { + return nil, fmt.Errorf("nav now: %w", err) + } + nav30d, err := p.readSharePriceUSD(ctx, rpc, block30dAgo) + if err != nil { + return nil, fmt.Errorf("nav 30d: %w", err) + } + nav7d, err := p.readSharePriceUSD(ctx, rpc, 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 / 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 +} + +// readSharePriceUSD returns USDC-per-share as a float. Calls +// convertToAssets(1 share) which for a 6-decimal vault yields a +// 6-decimal USDC amount that we scale to USD (divide by 1e6). +func (p *syrupUSDCProbe) readSharePriceUSD(ctx context.Context, rpc *ethclient.Client, blockNumber *big.Int) (float64, error) { + // ABI-encoded call: selector + uint256(1_000_000) left-padded. + amount := new(big.Int).SetUint64(syrupUSDCOneShareRaw) + data := make([]byte, 0, 4+32) + data = append(data, erc4626ConvertToAssetsSelector...) + data = append(data, common.LeftPadBytes(amount.Bytes(), 32)...) + + msg := ethereum.CallMsg{To: &p.contract, Data: data} + result, err := rpc.CallContract(ctx, msg, blockNumber) + if err != nil { + return 0, err + } + if len(result) < 32 { + return 0, fmt.Errorf("convertToAssets: short response") + } + assets := new(big.Int).SetBytes(result[0:32]) + f, _ := new(big.Float).SetInt(assets).Float64() + return f / 1e6, nil +} diff --git a/harnesses/rwa-yield-accuracy/promised-yields.yml b/harnesses/rwa-yield-accuracy/promised-yields.yml index ed4267da..1502f927 100644 --- a/harnesses/rwa-yield-accuracy/promised-yields.yml +++ b/harnesses/rwa-yield-accuracy/promised-yields.yml @@ -54,3 +54,9 @@ issuers: source: "https://ondo.finance/ousg" source_date: 2026-07-22 notes: "Ondo OUSG NAV-appreciation yield estimate (probe inactive; NAV endpoint TBD)." + + - token: syrup-usdc + promised_apy_bps: 482 + source: "https://yields.llama.fi/pools" + source_date: 2026-07-24 + notes: "Maple SyrupUSDC base APY (loan-book yield only, excludes SYRUP token incentives). Sourced from DefiLlama filter {project:maple, symbol:USDC, chain:Ethereum}. Delivered yield measured via ERC-4626 convertToAssets rate delta - matches the base disclosure, not the boosted APY."