Skip to content

Commit 7e32d06

Browse files
authored
fix spectra doublecount (#18537)
2 parents 0efe5ca + cfcd23c commit 7e32d06

File tree

1 file changed

+139
-6
lines changed

1 file changed

+139
-6
lines changed

projects/spectra-metavaults/index.js

Lines changed: 139 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,21 @@
11
const ADDRESSES = require('../helper/coreAssets.json')
22
const sdk = require("@defillama/sdk");
3-
const { getCache, setCache } = require("../helper/cache");
3+
const { getCache, setCache, getConfig } = require("../helper/cache");
44
const ethers = require("ethers");
55
const config = require("./config.json");
66

7+
// Chain name (as used in config.json) to Spectra API network slug
8+
const CHAIN_TO_API_NETWORK = {
9+
ethereum: "ethereum",
10+
arbitrum: "arbitrum",
11+
base: "base",
12+
avax: "avalanche",
13+
katana: "katana",
14+
flare: "flare",
15+
};
16+
17+
const SPECTRA_API_BASE = "https://api.spectra.finance/v1";
18+
719
const LOG_CACHE_FOLDER = "logs";
820

921
/**
@@ -104,6 +116,103 @@ const isMetavaultCountValid = (count) => {
104116
}
105117
};
106118

119+
async function fetchMetavaultOwnerMap(chain) {
120+
const ownerMap = {};
121+
const network = CHAIN_TO_API_NETWORK[chain];
122+
if (!network) return ownerMap;
123+
124+
try {
125+
const metavaults = await getConfig(
126+
`spectra-metavaults-api/${network}`,
127+
`${SPECTRA_API_BASE}/${network}/metavaults`
128+
);
129+
if (!Array.isArray(metavaults)) return ownerMap;
130+
for (const mv of metavaults) {
131+
if (mv.address) {
132+
ownerMap[mv.address.toLowerCase()] = mv;
133+
}
134+
if (mv.remote) {
135+
for (const remoteChainId of Object.keys(mv.remote)) {
136+
const remote = mv.remote[remoteChainId];
137+
if (remote?.address) {
138+
ownerMap[remote.address.toLowerCase()] = mv;
139+
}
140+
}
141+
}
142+
}
143+
} catch (e) {
144+
sdk.log(`spectra-metavaults: failed to fetch API for ${network}:`, e.message);
145+
}
146+
147+
return ownerMap;
148+
}
149+
150+
/**
151+
* Compute the Spectra-allocated amount for a metavault across all chains,
152+
* denominated in raw underlying token units (matching totalAssets units).
153+
*
154+
* "Spectra" = PT + YT + LP positions + wrapper IBT balances.
155+
*/
156+
function computeSpectraAllocation(metavault) {
157+
if (!metavault?.positions?.length) return 0;
158+
159+
const underlyingDecimals = metavault.underlying?.decimals ?? metavault.decimals ?? 18;
160+
let totalSpectraUnderlying = 0;
161+
162+
for (const pos of metavault.positions) {
163+
164+
const ptDecimals = pos.decimals ?? 18;
165+
const pool = pos.pools?.[0];
166+
167+
// --- PT balance ---
168+
const ptBalanceRaw = BigInt(pos.balance || 0);
169+
if (ptBalanceRaw > 0n) {
170+
let ptPriceUnderlying;
171+
if (pool?.ptPrice?.underlying != null) {
172+
ptPriceUnderlying = pool.ptPrice.underlying;
173+
} else if (pos.maturityValue?.underlying != null) {
174+
ptPriceUnderlying = pos.maturityValue.underlying;
175+
}
176+
if (ptPriceUnderlying != null) {
177+
totalSpectraUnderlying +=
178+
Number(ptBalanceRaw) / 10 ** ptDecimals * ptPriceUnderlying;
179+
}
180+
}
181+
182+
// --- YT balance ---
183+
const ytBalanceRaw = BigInt(pos.yt?.balance || 0);
184+
if (ytBalanceRaw > 0n && pool?.ytPrice?.underlying != null) {
185+
totalSpectraUnderlying +=
186+
Number(ytBalanceRaw) / 10 ** ptDecimals * pool.ytPrice.underlying;
187+
}
188+
189+
// --- LP balances (across all pools) ---
190+
if (pos.pools) {
191+
for (const p of pos.pools) {
192+
const lpBalanceRaw = BigInt(p.lpt?.balance || 0);
193+
if (lpBalanceRaw > 0n && p.lpt?.price?.underlying != null) {
194+
const lpDecimals = p.lpt.decimals ?? 18;
195+
totalSpectraUnderlying +=
196+
Number(lpBalanceRaw) / 10 ** lpDecimals * p.lpt.price.underlying;
197+
}
198+
}
199+
}
200+
201+
// --- Wrapper IBT balance (IBT that wraps another token via Spectra) ---
202+
if (pos.ibt?.baseIbt?.balance) {
203+
const ibtBalanceRaw = BigInt(pos.ibt.baseIbt.balance);
204+
if (ibtBalanceRaw > 0n && pos.ibt?.price?.underlying != null) {
205+
const ibtDecimals = pos.ibt.decimals ?? 18;
206+
totalSpectraUnderlying +=
207+
Number(ibtBalanceRaw) / 10 ** ibtDecimals * pos.ibt.price.underlying;
208+
}
209+
}
210+
}
211+
212+
// Convert from underlying floating-point back to raw token units
213+
return Math.floor(totalSpectraUnderlying * 10 ** underlyingDecimals);
214+
}
215+
107216
const getMetavaultTVL = async (api, metavaultSources) => {
108217
if (!metavaultSources.length) return;
109218

@@ -119,6 +228,12 @@ const getMetavaultTVL = async (api, metavaultSources) => {
119228
throw e;
120229
}
121230
}
231+
// Use a small buffer to avoid race conditions where the RPC node
232+
// hasn't synced the very latest block yet (observed on katana)
233+
toBlock = toBlock - 10;
234+
235+
// Fetch API metavault data (for Spectra allocation deduction)
236+
const apiOwnerMap = await fetchMetavaultOwnerMap(api.chain);
122237

123238
const logs = await getCachedEventLogs({
124239
chain: api.chain,
@@ -162,13 +277,18 @@ const getMetavaultTVL = async (api, metavaultSources) => {
162277
permitFailure: true,
163278
});
164279

280+
// Track infraVault to owner address for API lookup
165281
const uniqueInfraVaults = {};
166-
validWrappers.forEach(({ infraVaultFromEvent }, i) => {
282+
const infraVaultToOwner = {};
283+
validWrappers.forEach(({ owner, wrapper, infraVaultFromEvent }, i) => {
167284
let infraVault = wrapperInfraVaults[i];
168285
if (!infraVault || infraVault.toLowerCase() === ZERO_ADDRESS)
169286
infraVault = infraVaultFromEvent;
170287
if (!infraVault || infraVault.toLowerCase() === ZERO_ADDRESS) return;
171-
uniqueInfraVaults[infraVault.toLowerCase()] = infraVault;
288+
const infraKey = infraVault.toLowerCase();
289+
uniqueInfraVaults[infraKey] = infraVault;
290+
// owner from the event is the metavault identity (matches API mv.address)
291+
infraVaultToOwner[infraKey] = (typeof owner === 'string' ? owner : '').toLowerCase();
172292
});
173293

174294
const infraVaults = Object.values(uniqueInfraVaults);
@@ -190,7 +310,20 @@ const getMetavaultTVL = async (api, metavaultSources) => {
190310
assets.forEach((asset, i) => {
191311
const balance = totalAssets[i];
192312
if (!asset || asset.toLowerCase() === ZERO_ADDRESS || !balance) return;
193-
api.add(asset, balance);
313+
314+
// Deduct the portion already deposited into Spectra (PT/YT/LP/wrapper IBT)
315+
// Skip entirely if we can't find this metavault in the API response
316+
const infraKey = infraVaults[i].toLowerCase();
317+
const ownerAddr = infraVaultToOwner[infraKey];
318+
const apiMetavault = ownerAddr ? apiOwnerMap[ownerAddr] : undefined;
319+
if (!apiMetavault) return;
320+
321+
const spectraAmount = BigInt(computeSpectraAllocation(apiMetavault));
322+
const adjustedBalance = BigInt(balance) - spectraAmount;
323+
324+
if (adjustedBalance > 0n) {
325+
api.add(asset, adjustedBalance.toString());
326+
}
194327
});
195328
};
196329

@@ -203,10 +336,10 @@ const tvl = async (api) => {
203336
};
204337

205338
module.exports = {
206-
methodology: `TVL is the total value of assets deposited in Spectra MetaVaults.`,
339+
methodology: `TVL is the total value of assets deposited in Spectra MetaVaults, excluding the portion allocated to Spectra V2 (PT, YT, LP, wrapper IBT).`,
207340
hallmarks: [["2026-02-12", "MetaVaults Launch"]],
208341
};
209342

210343
Object.keys(config).forEach((chain) => {
211344
module.exports[chain] = { tvl };
212-
});
345+
});

0 commit comments

Comments
 (0)