diff --git a/fees/strata-markets/index.ts b/fees/strata-markets/index.ts index df3e3befab..73ec90a060 100644 --- a/fees/strata-markets/index.ts +++ b/fees/strata-markets/index.ts @@ -9,6 +9,9 @@ type CDOConfig = { jrt: string; srt: string; start: string; + // If set, the adapter reads getAprPairProjected() from this provider address + // instead of CDOLens when CDOLens returns zero APRs (zero-projection mode). + provider?: string; }; const CDOS: CDOConfig[] = [ @@ -74,23 +77,26 @@ const CDOS: CDOConfig[] = [ jrt: "0x1b2b8cFEF0b7B1Fad216b55fefeEb0c3349Da141", srt: "0x8a646Edc4633ADBA5Ec87DedaF3Af958e268FE96", start: "2026-07-09", + provider: "0x1FE39BE01BA0AF9f8D61A8a581eb7Df29c0BCe97", }, ]; -// events -const ERC4626_DEPOSIT = "event Deposit(address indexed sender, address indexed owner, uint256 assets, uint256 shares)"; -const ERC4626_WITHDRAW = "event Withdraw(address indexed sender, address indexed receiver, address indexed owner, uint256 assets, uint256 shares)"; -const FEE_ACCRUED = "event FeeAccrued(bool isJrt, uint256 amountToReserve, uint256 amountToTranche)"; -const RESERVE_REDUCED = "event ReserveReduced(address token, uint256 amount)"; +const CDO_LENS = "0xeA62e3a2D5FE8D5b66dc8E1bd2405AD23C851f4e"; -// ABIs +// ABIs +const GET_APRS_ABI = + "function getAPRs(address cdo) external view returns (int64 base, int64 target, int64 jrt, int64 srt)"; +const GET_APR_PAIR_PROJECTED_ABI = + "function getAprPairProjected() external view returns (int64 aprTarget, int64 aprBase, uint64 updatedAt)"; const STRATEGY_TOTAL_ASSETS_ABI = "function totalAssets() view returns (uint256)"; const RESERVE_BPS_ABI = "function reserveBps() view returns (uint256)"; const ASSET_ABI = "function asset() view returns (address)"; -const CONVERT_TO_ASSETS_ABI = "function convertToAssets(address token, uint256 amount, uint8 rounding) view returns (uint256)"; -const sumLogField = (logs: any[], field: string): bigint => - logs.reduce((acc, l) => acc + BigInt(l[field]), 0n); +// CDOLens APR values: raw / 1e10 = percentage (e.g. 39162436840 → 3.92%). +// To convert to a fraction we divide by 1e12 (= 1e10 × 100). +const APR_PRECISION = BigInt(1e12); +const SECONDS_PER_YEAR = 365 * 24 * 60 * 60; // 31_536_000 +const ONE_WAD = 10n ** 18n; async function processCDO( options: FetchOptions, @@ -98,89 +104,54 @@ async function processCDO( dailyFees: any, dailyRevenue: any, dailyProtocolRevenue: any, - dailySupplySideRevenue: any + dailySupplySideRevenue: any, ) { - const { fromApi, toApi, getLogs } = options; + const { toApi } = options; - const [baseAsset, navStartRaw, navEndRaw, reserveBpsRaw] = await Promise.all([ - toApi.call({ target: cfg.jrt, abi: ASSET_ABI }) as Promise, - fromApi.call({ target: cfg.strategy, abi: STRATEGY_TOTAL_ASSETS_ABI }), + // 1. Read APRs, strategy TVL, reserve fee rate, and base asset in parallel + const [aprs, tvlRaw, reserveBpsRaw, baseAsset] = await Promise.all([ + toApi.call({ target: CDO_LENS, abi: GET_APRS_ABI, params: [cfg.cdo] }), toApi.call({ target: cfg.strategy, abi: STRATEGY_TOTAL_ASSETS_ABI }), toApi.call({ target: cfg.accounting, abi: RESERVE_BPS_ABI }), + toApi.call({ target: cfg.jrt, abi: ASSET_ABI }) as Promise, ]); - const navStart = BigInt(navStartRaw); - const navEnd = BigInt(navEndRaw); - const reserveBps = BigInt(reserveBpsRaw); - - const [ - jrtDeposits, - jrtWithdraws, - srtDeposits, - srtWithdraws, - feeAccrued, - reserveReduced, - ] = await Promise.all([ - getLogs({ target: cfg.jrt, eventAbi: ERC4626_DEPOSIT }), - getLogs({ target: cfg.jrt, eventAbi: ERC4626_WITHDRAW }), - getLogs({ target: cfg.srt, eventAbi: ERC4626_DEPOSIT }), - getLogs({ target: cfg.srt, eventAbi: ERC4626_WITHDRAW }), - getLogs({ target: cfg.accounting, eventAbi: FEE_ACCRUED }), - getLogs({ target: cfg.cdo, eventAbi: RESERVE_REDUCED }), - ]); - - const inflows = - sumLogField(jrtDeposits, "assets") + sumLogField(srtDeposits, "assets"); - const outflowsToUsers = - sumLogField(jrtWithdraws, "assets") + sumLogField(srtWithdraws, "assets"); - - let reserveOut = 0n; - for (const log of reserveReduced) { - const token = (log.token as string).toLowerCase(); - if (token === baseAsset.toLowerCase()) { - reserveOut += BigInt(log.amount); - } else { - const inBaseAssets: string = await toApi.call({ - target: cfg.strategy, - abi: CONVERT_TO_ASSETS_ABI, - params: [log.token, log.amount, 0], + // 2. Determine the base APR (gross yield rate of the underlying strategy) + // Priority: base → target → provider (for zero-projection markets) + let baseApr = Number(aprs.base); + if (baseApr <= 0) { + const target = Number(aprs.target); + if (target > 0) { + baseApr = target; + } else if (cfg.provider) { + const projected = await toApi.call({ + target: cfg.provider, + abi: GET_APR_PAIR_PROJECTED_ABI, }); - reserveOut += BigInt(inBaseAssets); + baseApr = Number(projected.aprBase); } } - const exitFeeToReserve = sumLogField(feeAccrued, "amountToReserve"); - const exitFeeToTranche = sumLogField(feeAccrued, "amountToTranche"); - const exitFeesTotal = exitFeeToReserve + exitFeeToTranche; - - // we calculate this yield from the delta of strategy assets. - // this can be negative when the strategy marks down, which happens on the - // RWA-backed CDOs whose NAV follows a discrete oracle (sUSDat/STRC) rather - // than a monotonic exchange rate. those losses are absorbed by the tranches, - // so they belong in supply side revenue as a negative, not clamped away. - // clamping each window at zero only ever books the up moves and ratchets - // cumulative fees upwards, which is worse under pullHourly because a day is - // cut into 24 chances to discard downside instead of 1. - const yieldAmount = navEnd - navStart - inflows + outflowsToUsers + reserveOut; - - // the reserve takes a performance fee out of yield, but does not refund it on - // a loss, so on a negative window the whole markdown lands on the tranches. - const ONE = 10n ** 18n; - const protocolFromYield = - yieldAmount > 0n ? (yieldAmount * reserveBps) / ONE : 0n; - const supplyFromYield = yieldAmount - protocolFromYield; - - dailyFees.add(baseAsset, yieldAmount.toString()); - dailyFees.add(baseAsset, exitFeesTotal.toString()); - - dailyRevenue.add(baseAsset, protocolFromYield.toString()); - dailyRevenue.add(baseAsset, exitFeeToReserve.toString()); - - dailyProtocolRevenue.add(baseAsset, protocolFromYield.toString()); - dailyProtocolRevenue.add(baseAsset, exitFeeToReserve.toString()); - - dailySupplySideRevenue.add(baseAsset, supplyFromYield.toString()); - dailySupplySideRevenue.add(baseAsset, exitFeeToTranche.toString()); + if (baseApr <= 0) return; + + // 3. Compute yield for the actual time window + const windowSeconds = options.endTimestamp - options.startTimestamp; + const tvl = BigInt(tvlRaw); + const reserveBps = BigInt(reserveBpsRaw); + const windowYield = + (tvl * BigInt(baseApr) * BigInt(windowSeconds)) / + (APR_PRECISION * BigInt(SECONDS_PER_YEAR)); + + // 4. Split into protocol revenue (performance fee) and supply-side + const protocolRevenue = + reserveBps > 0n ? (windowYield * reserveBps) / ONE_WAD : 0n; + const supplySideRevenue = windowYield - protocolRevenue; + + // 5. Report + dailyFees.add(baseAsset, windowYield.toString()); + dailyRevenue.add(baseAsset, protocolRevenue.toString()); + dailyProtocolRevenue.add(baseAsset, protocolRevenue.toString()); + dailySupplySideRevenue.add(baseAsset, supplySideRevenue.toString()); } const fetch = async (options: FetchOptions) => { @@ -191,20 +162,20 @@ const fetch = async (options: FetchOptions) => { const active = CDOS.filter( (c) => - new Date(c.start + "T00:00:00Z").getTime() / 1000 <= options.startTimestamp + new Date(c.start + "T00:00:00Z").getTime() / 1000 <= options.startTimestamp, ); await Promise.all( - active.map(async (cfg) => { - await processCDO( + active.map((cfg) => + processCDO( options, cfg, dailyFees, dailyRevenue, dailyProtocolRevenue, - dailySupplySideRevenue - ); - }) + dailySupplySideRevenue, + ), + ), ); return { @@ -216,25 +187,23 @@ const fetch = async (options: FetchOptions) => { }; const methodology = { - Fees: "Includes yield generated on deposited assets and redemption fees charged by Strata.", - Revenue: "Protocol revenue consists of performance fees (5-10%) charged by Strata on the yield generated and redemption fees paid by the users.", - ProtocolRevenue: "Protocol revenue consists of performance and redemption fees collected by Strata, including the portion of fees shared with reserve.", - SupplySideRevenue: "Net yield distributed to tranches (after performance fees) plus the portion of redemption fees that remain in the tranche. Goes negative on days a strategy marks down, since those losses are absorbed by the tranches.", + Fees: "Yield generated across all Strata tranches, computed as on-chain base APR × strategy TVL, pro-rated to the time window. APRs are read from CDOLens (falling back to target APR or provider for zero-projection markets like nOPAL).", + Revenue: "Performance fees collected by the protocol, computed as gross yield × reserveBps.", + ProtocolRevenue: "Same as Revenue — performance fees accrued to the Strata reserve.", + SupplySideRevenue: "Net yield distributed to senior and junior tranche depositors after performance fees.", }; const earliestStart = CDOS.reduce( (min, c) => (c.start < min ? c.start : min), - CDOS[0].start + CDOS[0].start, ); const adapter: SimpleAdapter = { version: 2, - pullHourly: true, fetch, chains: [CHAIN.ETHEREUM], start: earliestStart, methodology, - allowNegativeValue: true, // strategy NAV can mark down, the loss is absorbed by the tranches }; export default adapter;