From ea8b05a5f74966a49c6694f0be430e3e1ccc2efa Mon Sep 17 00:00:00 2001 From: Michael Tan Date: Thu, 16 Jul 2026 11:10:58 -0400 Subject: [PATCH 1/3] Add STRATO (Mercata) yield adapter Surfaces STRATO/Mercata yield products on yields.llama.fi, read purely via eth_call against the public STRATO RPC (chain `strato`, protocolId 7862): - saveUSDST savings vault (apyBase from the per-second savings rate) - USDST lending pool / lendUSDST (supply apyBase + borrow apy) - STRATO staking (apyReward in STRATO) - AMM LP pools (TVL from reserves; TVL-only for now) Co-Authored-By: Claude Opus 4.8 (1M context) --- src/adaptors/strato/index.js | 317 +++++++++++++++++++++++++++++++++++ 1 file changed, 317 insertions(+) create mode 100644 src/adaptors/strato/index.js diff --git a/src/adaptors/strato/index.js b/src/adaptors/strato/index.js new file mode 100644 index 0000000000..3510751473 --- /dev/null +++ b/src/adaptors/strato/index.js @@ -0,0 +1,317 @@ +const sdk = require('@defillama/sdk'); + +const utils = require('../utils'); + +// STRATO (Mercata) is BlockApps' institutional L1. It is served over a public, +// unauthenticated JSON-RPC node that the DefiLlama TVL adapter already uses. +// @defillama/sdk resolves the RPC for chain `strato` from the STRATO_RPC env var. +const RPC_URL = 'https://noderpc.strato.nexus/rpc'; +if (!process.env.STRATO_RPC) process.env.STRATO_RPC = RPC_URL; + +const CHAIN = 'strato'; +const PROJECT = 'strato'; +const URL = 'https://app.strato.nexus'; + +const RAY = 10n ** 27n; +const SECONDS_PER_YEAR = 31_536_000; + +// Core protocol contracts (system precompile-style addresses) + live deployments. +const PRICE_ORACLE = '0x0000000000000000000000000000000000001002'; +const LENDING_REGISTRY = '0x0000000000000000000000000000000000001007'; +const LENDING_POOL = '0x0000000000000000000000000000000000001005'; +const POOL_FACTORY = '0x000000000000000000000000000000000000100a'; +const SAVE_USDST_VAULT = '0x22550671fcad04a213697ac7ae4f4366e96446ed'; +const STAKING = '0xf30a022ce83bed7adeafc286c719388dcc3b3988'; +const USDST = '0x937efa7e3a77e20bbdbd7c0d32b6514f368c1010'; +const STRATO = '0x2ca3e170e6714282da77815f7864b17f612f5f83'; + +const NULL_ADDRESS = '0x0000000000000000000000000000000000000000'; + +// ---- helpers --------------------------------------------------------------- + +const call = async (target, abi, params) => + (await sdk.api.abi.call({ target, abi, params, chain: CHAIN })).output; + +// Compound a per-second RAY (1e27) growth factor to an annual percentage. +// Isolate the small per-second increment before converting to float to keep precision. +const rayPerSecondToApy = (rayFactor) => { + const increment = Number(BigInt(rayFactor) - RAY) / 1e27; + return (Math.pow(1 + increment, SECONDS_PER_YEAR) - 1) * 100; +}; + +// DefiLlama prices most STRATO tokens (USDST, GOLDST, ETH, ...). Tokens it does +// not index (e.g. STRATO) fall back to the on-chain PriceOracle (1e18 USD). +const getPrices = async (tokens) => { + const uniq = [...new Set(tokens.map((t) => t.toLowerCase()))]; + const keyFor = (a) => `${CHAIN}:${a}`; + + let coins = {}; + try { + const path = `/prices/current/${uniq.map(keyFor).join(',')}`; + coins = (await utils.getPriceApiData(path)).coins || {}; + } catch (e) { + coins = {}; + } + + const prices = {}; + for (const a of uniq) { + const c = coins[keyFor(a)]; + if (c && Number.isFinite(c.price) && c.price > 0) { + prices[a] = c.price; + continue; + } + try { + const p = await call( + PRICE_ORACLE, + 'function getAssetPrice(address) view returns (uint256)', + a + ); + const v = Number(BigInt(p)) / 1e18; + if (Number.isFinite(v) && v > 0) prices[a] = v; + } catch (e) { + // no price available — caller decides whether to skip the pool + } + } + return prices; +}; + +// ---- pools ----------------------------------------------------------------- + +// saveUSDST savings vault: USDST in, share token appreciates at the savings rate. +async function saveVaultPool() { + const [totalAssets, savingsRate] = await Promise.all([ + call(SAVE_USDST_VAULT, 'function totalAssets() view returns (uint256)'), + call( + SAVE_USDST_VAULT, + 'function perSecondSavingsRate() view returns (uint256)' + ), + ]); + + const prices = await getPrices([USDST]); + const usdstPrice = prices[USDST.toLowerCase()]; + if (!usdstPrice) return []; + + return [ + { + pool: `${SAVE_USDST_VAULT}-strato`.toLowerCase(), + chain: utils.formatChain(CHAIN), + project: PROJECT, + symbol: 'USDST', + tvlUsd: (Number(BigInt(totalAssets)) / 1e18) * usdstPrice, + apyBase: rayPerSecondToApy(savingsRate), + underlyingTokens: [USDST], + poolMeta: 'saveUSDST savings vault', + url: URL, + }, + ]; +} + +// USDST lending pool. Supply APY = borrowApr * utilization * (1 - reserveFactor). +async function lendingPool() { + const prices = await getPrices([USDST]); + const usdstPrice = prices[USDST.toLowerCase()]; + if (!usdstPrice) return []; + + const [borrowIndex, totalScaledDebt, cfg, liquidityPool] = await Promise.all([ + call(LENDING_POOL, 'function previewBorrowIndex() view returns (uint256)'), + call(LENDING_POOL, 'function totalScaledDebt() view returns (uint256)'), + call( + LENDING_POOL, + 'function getAssetConfig(address) view returns (uint256 ltv, uint256 liquidationThreshold, uint256 liquidationBonus, uint256 interestRate, uint256 reserveFactor, uint256 perSecondFactorRAY)', + USDST + ), + call(LENDING_REGISTRY, 'function liquidityPool() view returns (address)'), + ]); + + const cash = BigInt(await call(USDST, 'erc20:balanceOf', liquidityPool)); + const borrows = (BigInt(totalScaledDebt) * BigInt(borrowIndex)) / RAY; + const supplied = cash + borrows; + if (supplied === 0n) return []; + + const totalSupplied = Number(supplied) / 1e18; + const totalBorrows = Number(borrows) / 1e18; + const utilization = totalBorrows / totalSupplied; + + const reserveFactor = Number(cfg.reserveFactor ?? cfg[4]) / 1e4; + const ltv = Number(cfg.ltv ?? cfg[0]) / 1e4; + const borrowApr = rayPerSecondToApy(cfg.perSecondFactorRAY ?? cfg[5]); // percent + + return [ + { + pool: `${LENDING_POOL}-strato`.toLowerCase(), + chain: utils.formatChain(CHAIN), + project: PROJECT, + symbol: 'USDST', + tvlUsd: totalSupplied * usdstPrice, + apyBase: (borrowApr / 100) * utilization * (1 - reserveFactor) * 100, + apyBaseBorrow: borrowApr, + totalSupplyUsd: totalSupplied * usdstPrice, + totalBorrowUsd: totalBorrows * usdstPrice, + ltv, + borrowable: true, + underlyingTokens: [USDST], + poolMeta: 'lendUSDST lending', + url: URL, + }, + ]; +} + +// STRATO staking. Rewards are paid in STRATO, so apyReward is price-independent. +async function stakingPool() { + const [totalStake, rewardAmount, periodStart, periodFinish] = + await Promise.all([ + call(STAKING, 'function totalRewardableStake() view returns (uint256)'), + call(STAKING, 'function rewardPeriodAmount() view returns (uint256)'), + call(STAKING, 'function periodStart() view returns (uint256)'), + call(STAKING, 'function periodFinish() view returns (uint256)'), + ]); + + const totalStakeBI = BigInt(totalStake); + if (totalStakeBI === 0n) return []; + + const prices = await getPrices([STRATO]); + const stratoPrice = prices[STRATO.toLowerCase()]; + if (!stratoPrice) return []; + + const now = Math.floor(Date.now() / 1000); + const duration = Number(periodFinish) - Number(periodStart); + + let apyReward = 0; + if (Number(periodFinish) > now && duration > 0) { + const annualReward = + (Number(BigInt(rewardAmount)) / 1e18) * (SECONDS_PER_YEAR / duration); + const stakedTokens = Number(totalStakeBI) / 1e18; + apyReward = (annualReward / stakedTokens) * 100; + } + + return [ + { + pool: `${STAKING}-strato`.toLowerCase(), + chain: utils.formatChain(CHAIN), + project: PROJECT, + symbol: 'STRATO', + tvlUsd: (Number(totalStakeBI) / 1e18) * stratoPrice, + apyReward, + rewardTokens: [STRATO], + underlyingTokens: [STRATO], + poolMeta: 'STRATO staking', + url: URL, + }, + ]; +} + +async function enumeratePools() { + const pools = []; + for (let i = 0; i < 500; i++) { + let addr; + try { + addr = await call( + POOL_FACTORY, + 'function allPools(uint256) view returns (address)', + i + ); + } catch (e) { + break; // out of bounds -> reverts + } + if (!addr || addr.toLowerCase() === NULL_ADDRESS) break; + pools.push(addr.toLowerCase()); + } + return pools; +} + +// AMM LP pools. TVL is read from on-chain reserves. Swap fees are not reported: +// the pool fee rate has no public getter over eth_call, and the active pools have +// no swap volume in a trailing window, so base APY would be 0 regardless. Pools are +// reported TVL-only (apyBase 0); fee APY can be added if a fee getter is exposed. +async function ammPools() { + const poolAddrs = await enumeratePools(); + if (!poolAddrs.length) return []; + + const metas = []; + for (const pool of poolAddrs) { + try { + const [tokenA, tokenB] = await Promise.all([ + call(pool, 'function tokenA() view returns (address)'), + call(pool, 'function tokenB() view returns (address)'), + ]); + const [balA, balB] = await Promise.all([ + call(tokenA, 'erc20:balanceOf', pool), + call(tokenB, 'erc20:balanceOf', pool), + ]); + metas.push({ + pool, + tokenA: tokenA.toLowerCase(), + tokenB: tokenB.toLowerCase(), + balA: BigInt(balA), + balB: BigInt(balB), + }); + } catch (e) { + // not a readable 2-token pool -> skip + } + } + + const active = metas.filter((m) => m.balA > 0n || m.balB > 0n); + if (!active.length) return []; + + const tokens = [...new Set(active.flatMap((m) => [m.tokenA, m.tokenB]))]; + const [prices, symbols] = await Promise.all([ + getPrices(tokens), + (async () => { + const s = {}; + for (const t of tokens) { + try { + s[t] = await call(t, 'erc20:symbol'); + } catch (e) { + s[t] = '?'; + } + } + return s; + })(), + ]); + + const pools = []; + for (const m of active) { + const priceA = prices[m.tokenA]; + const priceB = prices[m.tokenB]; + if (!priceA || !priceB) continue; // avoid mispriced TVL + + const tvlUsd = + (Number(m.balA) / 1e18) * priceA + (Number(m.balB) / 1e18) * priceB; + if (!Number.isFinite(tvlUsd) || tvlUsd <= 0) continue; + + pools.push({ + pool: `${m.pool}-strato`.toLowerCase(), + chain: utils.formatChain(CHAIN), + project: PROJECT, + symbol: utils.formatSymbol(`${symbols[m.tokenA]}-${symbols[m.tokenB]}`), + tvlUsd, + apyBase: 0, + underlyingTokens: [m.tokenA, m.tokenB], + poolMeta: 'AMM LP', + url: URL, + }); + } + + return pools; +} + +const apy = async () => { + const groups = await Promise.all([ + saveVaultPool().catch(() => []), + lendingPool().catch(() => []), + stakingPool().catch(() => []), + ammPools().catch(() => []), + ]); + + return groups + .flat() + .filter(Boolean) + .filter((p) => Number.isFinite(p.tvlUsd) && p.tvlUsd >= utils.MIN_TVL_USD); +}; + +module.exports = { + protocolId: '7862', + timetravel: false, + apy, + url: `${URL}/pools`, +}; From 1c47d924a6da3d14ccacf31998605b937b431628 Mon Sep 17 00:00:00 2001 From: Michael Tan Date: Thu, 16 Jul 2026 11:20:44 -0400 Subject: [PATCH 2/3] Fix comment for STRATO institutional L1 description --- src/adaptors/strato/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/adaptors/strato/index.js b/src/adaptors/strato/index.js index 3510751473..458d0cba85 100644 --- a/src/adaptors/strato/index.js +++ b/src/adaptors/strato/index.js @@ -2,7 +2,7 @@ const sdk = require('@defillama/sdk'); const utils = require('../utils'); -// STRATO (Mercata) is BlockApps' institutional L1. It is served over a public, +// STRATO is BlockApps' institutional L1. It is served over a public, // unauthenticated JSON-RPC node that the DefiLlama TVL adapter already uses. // @defillama/sdk resolves the RPC for chain `strato` from the STRATO_RPC env var. const RPC_URL = 'https://noderpc.strato.nexus/rpc'; From d0bd303e975d5cdf0508f708a3db7e7efbbbb4d9 Mon Sep 17 00:00:00 2001 From: Michael Tan Date: Mon, 20 Jul 2026 10:35:57 -0400 Subject: [PATCH 3/3] Address PR review feedback on STRATO adapter - Remove STRATO_RPC env var override; strato is in the latest @defillama/sdk - Lending: compound the supply per-second rate before annualizing (was overstating supply APY by scaling the annual borrow APY) - Lending: report available cash as tvlUsd, keep gross supply/debt in totalSupplyUsd/totalBorrowUsd; gate the pool on totalSupplyUsd to match downstream lending filtering - Staking: require periodStart <= now so future scheduled reward periods don't advertise APY Co-Authored-By: Claude Fable 5 --- src/adaptors/strato/index.js | 39 ++++++++++++++++++++++++------------ 1 file changed, 26 insertions(+), 13 deletions(-) diff --git a/src/adaptors/strato/index.js b/src/adaptors/strato/index.js index 458d0cba85..0342a09bf4 100644 --- a/src/adaptors/strato/index.js +++ b/src/adaptors/strato/index.js @@ -2,12 +2,6 @@ const sdk = require('@defillama/sdk'); const utils = require('../utils'); -// STRATO is BlockApps' institutional L1. It is served over a public, -// unauthenticated JSON-RPC node that the DefiLlama TVL adapter already uses. -// @defillama/sdk resolves the RPC for chain `strato` from the STRATO_RPC env var. -const RPC_URL = 'https://noderpc.strato.nexus/rpc'; -if (!process.env.STRATO_RPC) process.env.STRATO_RPC = RPC_URL; - const CHAIN = 'strato'; const PROJECT = 'strato'; const URL = 'https://app.strato.nexus'; @@ -106,7 +100,8 @@ async function saveVaultPool() { ]; } -// USDST lending pool. Supply APY = borrowApr * utilization * (1 - reserveFactor). +// USDST lending pool. Supply rate = borrow rate * utilization * (1 - reserveFactor), +// compounded per-second before annualizing. TVL is available cash (liquidity). async function lendingPool() { const prices = await getPrices([USDST]); const usdstPrice = prices[USDST.toLowerCase()]; @@ -134,7 +129,16 @@ async function lendingPool() { const reserveFactor = Number(cfg.reserveFactor ?? cfg[4]) / 1e4; const ltv = Number(cfg.ltv ?? cfg[0]) / 1e4; - const borrowApr = rayPerSecondToApy(cfg.perSecondFactorRAY ?? cfg[5]); // percent + // Derive the supply per-second rate before annualizing; compounding the + // borrow APY first would overstate the supply APY. + const borrowRatePerSecond = + Number(BigInt(cfg.perSecondFactorRAY ?? cfg[5]) - RAY) / 1e27; + const borrowApy = + (Math.pow(1 + borrowRatePerSecond, SECONDS_PER_YEAR) - 1) * 100; + const supplyRatePerSecond = + borrowRatePerSecond * utilization * (1 - reserveFactor); + const supplyApy = + (Math.pow(1 + supplyRatePerSecond, SECONDS_PER_YEAR) - 1) * 100; return [ { @@ -142,9 +146,9 @@ async function lendingPool() { chain: utils.formatChain(CHAIN), project: PROJECT, symbol: 'USDST', - tvlUsd: totalSupplied * usdstPrice, - apyBase: (borrowApr / 100) * utilization * (1 - reserveFactor) * 100, - apyBaseBorrow: borrowApr, + tvlUsd: (Number(cash) / 1e18) * usdstPrice, + apyBase: supplyApy, + apyBaseBorrow: borrowApy, totalSupplyUsd: totalSupplied * usdstPrice, totalBorrowUsd: totalBorrows * usdstPrice, ltv, @@ -177,7 +181,11 @@ async function stakingPool() { const duration = Number(periodFinish) - Number(periodStart); let apyReward = 0; - if (Number(periodFinish) > now && duration > 0) { + if ( + Number(periodStart) <= now && + Number(periodFinish) > now && + duration > 0 + ) { const annualReward = (Number(BigInt(rewardAmount)) / 1e18) * (SECONDS_PER_YEAR / duration); const stakedTokens = Number(totalStakeBI) / 1e18; @@ -306,7 +314,12 @@ const apy = async () => { return groups .flat() .filter(Boolean) - .filter((p) => Number.isFinite(p.tvlUsd) && p.tvlUsd >= utils.MIN_TVL_USD); + .filter((p) => { + // Lending pools report available liquidity as tvlUsd but are sized by + // gross supply downstream, so gate them on totalSupplyUsd to match. + const size = p.totalSupplyUsd ?? p.tvlUsd; + return Number.isFinite(size) && size >= utils.MIN_TVL_USD; + }); }; module.exports = {