11const ADDRESSES = require ( '../helper/coreAssets.json' )
22const sdk = require ( "@defillama/sdk" ) ;
3- const { getCache, setCache } = require ( "../helper/cache" ) ;
3+ const { getCache, setCache, getConfig } = require ( "../helper/cache" ) ;
44const ethers = require ( "ethers" ) ;
55const 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+
719const 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+
107216const 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
205338module . 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
210343Object . keys ( config ) . forEach ( ( chain ) => {
211344 module . exports [ chain ] = { tvl } ;
212- } ) ;
345+ } ) ;
0 commit comments