11import dayjs from "dayjs" ;
22import { queryAllium } from "../../helpers/allium" ;
3+ import { getSingleLlamaPrice } from "../../utils/prices" ;
4+
35type WormholeBridgeEvent = {
46 block_timestamp : string ;
57 transaction_hash : string ;
@@ -9,7 +11,145 @@ type WormholeBridgeEvent = {
911 token_usd_amount : string ;
1012 token_amount : string ;
1113 source_chain : string ;
14+ source_token_address : string | null ;
15+ source_token_amount : string | null ;
1216 destination_chain : string ;
17+ application_protocol_ids : string [ ] | string | null ;
18+ } ;
19+
20+ const WORMHOLE_REPRICE_USD_THRESHOLD = Number ( process . env . WORMHOLE_REPRICE_USD_THRESHOLD ?? 1_000_000 ) ;
21+
22+ const priceChainMapping : Record < string , string > = {
23+ avalanche : "avax" ,
24+ avax : "avax" ,
25+ bnb_smart_chain : "bsc" ,
26+ BNB_Smart_Chain : "bsc" ,
27+ bsc : "bsc" ,
28+ ethereum : "ethereum" ,
29+ polygon : "polygon" ,
30+ arbitrum : "arbitrum" ,
31+ optimism : "optimism" ,
32+ base : "base" ,
33+ solana : "solana" ,
34+ fantom : "fantom" ,
35+ celo : "celo" ,
36+ mantle : "mantle" ,
37+ scroll : "scroll" ,
38+ } ;
39+
40+ const normalizeProtocolIds = ( protocolIds : string [ ] | string | null | undefined ) : string [ ] => {
41+ if ( ! protocolIds ) return [ ] ;
42+ if ( Array . isArray ( protocolIds ) ) return protocolIds . map ( String ) ;
43+ return String ( protocolIds )
44+ . replace ( / [ { } \\ [ \\ ] \" ] / g, "" )
45+ . split ( "," )
46+ . map ( ( protocolId ) => protocolId . trim ( ) )
47+ . filter ( Boolean ) ;
48+ } ;
49+
50+ const shouldRepriceWormholeEvent = ( event : WormholeBridgeEvent ) : boolean => {
51+ const tokenUsdAmount = Number ( event . token_usd_amount || 0 ) ;
52+ if ( ! Number . isFinite ( tokenUsdAmount ) || tokenUsdAmount < WORMHOLE_REPRICE_USD_THRESHOLD ) return false ;
53+ if ( ! event . source_token_address || ! event . source_token_amount ) return false ;
54+
55+ const protocolIds = normalizeProtocolIds ( event . application_protocol_ids ) ;
56+ return protocolIds . includes ( "MAYAN" ) && protocolIds . some ( ( protocolId ) => protocolId . startsWith ( "SWIFT" ) ) ;
57+ } ;
58+
59+ const getSourcePriceChainAndToken = ( event : WormholeBridgeEvent ) => {
60+ const sourceChain = event . source_chain ;
61+ const priceChain = priceChainMapping [ sourceChain ] || priceChainMapping [ sourceChain ?. toLowerCase ?.( ) ] ;
62+ if ( ! priceChain || ! event . source_token_address ) return null ;
63+
64+ const token = priceChain === "solana" ? event . source_token_address : event . source_token_address . toLowerCase ( ) ;
65+
66+ return { priceChain, token } ;
67+ } ;
68+
69+ const getRepricedUsdAmount = async (
70+ event : WormholeBridgeEvent ,
71+ priceCache : Map < string , Promise < any > >
72+ ) : Promise < number | null > => {
73+ const sourceTokenAmount = Number ( event . source_token_amount || 0 ) ;
74+ if ( ! Number . isFinite ( sourceTokenAmount ) || sourceTokenAmount <= 0 ) return null ;
75+
76+ const priceKey = getSourcePriceChainAndToken ( event ) ;
77+ if ( ! priceKey ) return null ;
78+
79+ const hourTimestamp = Math . floor ( Number ( event . block_timestamp ) / 3600 ) * 3600 ;
80+ const cacheKey = `${ priceKey . priceChain } :${ priceKey . token } :${ hourTimestamp } ` ;
81+ if ( ! priceCache . has ( cacheKey ) ) {
82+ priceCache . set ( cacheKey , getSingleLlamaPrice ( priceKey . priceChain , priceKey . token , hourTimestamp ) ) ;
83+ }
84+
85+ const priceData = await priceCache . get ( cacheKey ) ;
86+ const price = Number ( priceData ?. price ) ;
87+ if ( ! Number . isFinite ( price ) || price <= 0 ) return null ;
88+
89+ const alliumUsdAmount = Number ( event . token_usd_amount || 0 ) ;
90+ const directUsdAmount = sourceTokenAmount * price ;
91+ const decimals = Number ( priceData ?. decimals ) ;
92+
93+ if ( Number . isFinite ( decimals ) && decimals >= 0 && directUsdAmount > alliumUsdAmount * 100 ) {
94+ const decimalAdjustedUsdAmount = ( sourceTokenAmount / 10 ** decimals ) * price ;
95+ if ( Number . isFinite ( decimalAdjustedUsdAmount ) && decimalAdjustedUsdAmount <= alliumUsdAmount * 10 ) {
96+ return decimalAdjustedUsdAmount ;
97+ }
98+ }
99+
100+ return directUsdAmount ;
101+ } ;
102+
103+ const repriceSuspiciousWormholeEvents = async ( events : WormholeBridgeEvent [ ] ) : Promise < WormholeBridgeEvent [ ] > => {
104+ const priceCache = new Map < string , Promise < any > > ( ) ;
105+ const safeEvents = [ ] as WormholeBridgeEvent [ ] ;
106+ let corrected = 0 ;
107+ let skipped = 0 ;
108+
109+ for ( const event of events ) {
110+ if ( ! shouldRepriceWormholeEvent ( event ) ) {
111+ safeEvents . push ( event ) ;
112+ continue ;
113+ }
114+
115+ const originalUsdAmount = Number ( event . token_usd_amount || 0 ) ;
116+ let repricedUsdAmount : number | null = null ;
117+ try {
118+ repricedUsdAmount = await getRepricedUsdAmount ( event , priceCache ) ;
119+ } catch ( e : any ) {
120+ console . warn (
121+ `[Wormhole] Skipping suspicious event ${ event . transaction_hash } ; price lookup failed for ` +
122+ `${ event . source_chain } :${ event . source_token_address } : ${ e ?. message } `
123+ ) ;
124+ }
125+
126+ if ( repricedUsdAmount === null || ! Number . isFinite ( repricedUsdAmount ) ) {
127+ skipped ++ ;
128+ console . warn (
129+ `[Wormhole] Skipping suspicious event ${ event . transaction_hash } ; no DefiLlama price for ` +
130+ `${ event . source_chain } :${ event . source_token_address } `
131+ ) ;
132+ continue ;
133+ }
134+
135+ corrected ++ ;
136+ safeEvents . push ( {
137+ ...event ,
138+ token_usd_amount : String ( repricedUsdAmount ) ,
139+ } ) ;
140+
141+ if ( originalUsdAmount / Math . max ( repricedUsdAmount , 1 ) > 100 ) {
142+ console . warn (
143+ `[Wormhole] Repriced suspicious event ${ event . transaction_hash } from ${ originalUsdAmount } to ${ repricedUsdAmount } `
144+ ) ;
145+ }
146+ }
147+
148+ if ( corrected || skipped ) {
149+ console . log ( `[Wormhole] Repriced ${ corrected } suspicious events; skipped ${ skipped } without prices.` ) ;
150+ }
151+
152+ return safeEvents ;
13153} ;
14154
15155const chains = [
@@ -110,7 +250,10 @@ export const fetchWormholeEvents = async (
110250 TOKEN_USD_AMOUNT,
111251 TOKEN_AMOUNT,
112252 SOURCE_CHAIN,
253+ SOURCE_TOKEN_ADDRESS,
254+ SOURCE_TOKEN_AMOUNT,
113255 DESTINATION_CHAIN,
256+ APPLICATION_PROTOCOL_IDS,
114257 UNIQUE_ID AS transaction_hash
115258 from org_db__defillama.default.wormhole_token_transfers
116259 where
@@ -127,8 +270,9 @@ export const fetchWormholeEvents = async (
127270 token_usd_amount : String ( parseFloat ( row . token_usd_amount || "0" ) || 0 ) ,
128271 block_timestamp : dayjs ( row . block_timestamp ) . unix ( ) ,
129272 } ) ) ;
273+ const repricedBatch = await repriceSuspiciousWormholeEvents ( normalizedBatch ) ;
130274
131- allResults = [ ...allResults , ...normalizedBatch ] ;
275+ allResults = [ ...allResults , ...repricedBatch ] ;
132276 console . log ( `Fetched ${ allResults . length } Wormhole events.` ) ;
133277
134278 currentTimestamp = normalizedBatch [ normalizedBatch . length - 1 ] . block_timestamp + 1 ;
0 commit comments