1- import { ContractBase , event , viewFun } from '@subsquid/evm-abi'
1+ import { event } from '@subsquid/evm-abi'
22import * as p from '@subsquid/evm-codec'
33
44import * as erc20Abi from '../../../abi/erc20'
55import { addresses , arms } from '../../../utils/addresses'
66import { sonicAddresses } from '../../../utils/addresses/addresses-sonic'
77import { getAddressesPyName } from '../../../utils/addresses/names'
88import { formatAmount } from '../../../utils/formatAmount'
9+ import { discordIconOrName } from '../../const'
910import { explorerUrl } from '../../format'
1011import { registerEventRenderer } from '../event'
1112import { defaultEventRenderer } from './default'
@@ -17,39 +18,12 @@ const TraderateChanged = event(
1718 { traderate0 : p . uint256 , traderate1 : p . uint256 } ,
1819)
1920
20- const armFunctions = {
21- traderate0 : viewFun ( '0x45059a6b' , 'traderate0()' , { } , p . uint256 ) ,
22- traderate1 : viewFun ( '0xcf1de5d8' , 'traderate1()' , { } , p . uint256 ) ,
23- }
24-
25- const erc4626Functions = {
26- convertToAssets : viewFun ( '0x07a2d13a' , 'convertToAssets(uint256)' , { shares : p . uint256 } , p . uint256 ) ,
27- }
28-
29- class ArmContract extends ContractBase {
30- traderate0 ( ) {
31- return this . eth_call ( armFunctions . traderate0 , { } )
32- }
33- traderate1 ( ) {
34- return this . eth_call ( armFunctions . traderate1 , { } )
35- }
36- }
37-
38- class ERC4626Contract extends ContractBase {
39- convertToAssets ( shares : bigint ) {
40- return this . eth_call ( erc4626Functions . convertToAssets , { shares } )
41- }
42- }
43-
4421interface ArmConfig {
4522 address : string
4623 symbol0 : string
4724 symbol1 : string
4825 token0 : string
4926 token1 : string
50- // ERC4626 vault token (e.g. sUSDe). When set, traderates are raw spread rates
51- // and the vault's convertToAssets rate must be applied for output estimation.
52- vaultToken ?: string
5327}
5428
5529// Build lookup from ARM contract address to config
@@ -81,7 +55,6 @@ registerArm({
8155 symbol1 : 'sUSDe' ,
8256 token0 : addresses . tokens . USDe ,
8357 token1 : addresses . tokens . sUSDe ,
84- vaultToken : addresses . tokens . sUSDe ,
8558} )
8659
8760registerArm ( {
@@ -92,60 +65,13 @@ registerArm({
9265 token1 : sonicAddresses . tokens . OS ,
9366} )
9467
95- // Cache traderates per ARM address
96- const traderateCache = new Map < string , { traderate0 : bigint ; traderate1 : bigint } > ( )
97-
98- // Cache vault exchange rate (e.g. sUSDe → USDe via convertToAssets)
99- const vaultRateCache = new Map < string , bigint > ( )
100- const ONE = 10n ** 18n
101-
102- type RendererCtx = Parameters < Parameters < typeof registerEventRenderer > [ 1 ] > [ 0 ] [ 'ctx' ]
103- type RendererBlock = Parameters < Parameters < typeof registerEventRenderer > [ 1 ] > [ 0 ] [ 'log' ] [ 'block' ]
104-
105- async function getVaultRate ( ctx : RendererCtx , block : RendererBlock , vaultToken : string ) : Promise < bigint > {
106- const cached = vaultRateCache . get ( vaultToken )
107- if ( cached ) return cached
108- try {
109- const contract = new ERC4626Contract ( ctx , block , vaultToken )
110- const rate = await contract . convertToAssets ( ONE )
111- vaultRateCache . set ( vaultToken , rate )
112- return rate
113- } catch {
114- return ONE // Fallback: assume 1:1
115- }
116- }
117-
118- async function getTraderates (
119- ctx : RendererCtx ,
120- block : RendererBlock ,
121- armAddress : string ,
122- ) : Promise < { traderate0 : bigint ; traderate1 : bigint } | undefined > {
123- const cached = traderateCache . get ( armAddress )
124- if ( cached ) return cached
125- try {
126- const contract = new ArmContract ( ctx , block , armAddress )
127- const [ traderate0 , traderate1 ] = await Promise . all ( [ contract . traderate0 ( ) , contract . traderate1 ( ) ] )
128- const rates = { traderate0, traderate1 }
129- traderateCache . set ( armAddress , rates )
130- return rates
131- } catch {
132- return undefined
133- }
134- }
135-
13668// TraderateChanged renderer — shows raw ARM spread rates
13769registerEventRenderer ( TraderateChanged . topic , async ( params ) => {
13870 const arm = armConfigs . get ( params . log . address . toLowerCase ( ) )
13971 if ( ! arm ) return defaultEventRenderer ( params )
14072
14173 const data = TraderateChanged . decode ( params . log )
14274
143- // Cache for swap calculations
144- traderateCache . set ( params . log . address . toLowerCase ( ) , {
145- traderate0 : data . traderate0 ,
146- traderate1 : data . traderate1 ,
147- } )
148-
14975 const sellRate = 10n ** 72n / data . traderate0 // 1e36 precision
15076 const buyRate = data . traderate1 // 1e36 precision
15177 const rateLabel = `${ arm . symbol1 } /${ arm . symbol0 } `
@@ -164,7 +90,9 @@ registerEventRenderer(TraderateChanged.topic, async (params) => {
16490 } )
16591} )
16692
167- // Transfer renderer — renders ARM swaps, falls back to default for non-ARM transfers
93+ // Transfer renderer — renders ARM swaps using actual transaction logs.
94+ // Requires transactionLogs: true on the alert rule to access sibling logs.
95+ // Falls back to default for non-ARM transfers or deposits/withdrawals.
16896registerEventRenderer ( erc20Abi . events . Transfer . topic , async ( params ) => {
16997 // Check if topic2 (the `to` address) is a known ARM
17098 const topic2 = params . log . topics [ 2 ]
@@ -174,83 +102,68 @@ registerEventRenderer(erc20Abi.events.Transfer.topic, async (params) => {
174102 const arm = armConfigs . get ( toAddress )
175103 if ( ! arm ) return defaultEventRenderer ( params )
176104
177- const data = erc20Abi . events . Transfer . decode ( params . log )
178- const tokenAddress = params . log . address . toLowerCase ( )
179- const isToken0In = tokenAddress === arm . token0 . toLowerCase ( )
180- const isToken1In = tokenAddress === arm . token1 . toLowerCase ( )
181- if ( ! isToken0In && ! isToken1In ) return defaultEventRenderer ( params )
105+ // Need transaction logs to find the counterpart transfer
106+ const txLogs = params . log . transaction ?. logs
107+ if ( ! txLogs ) return defaultEventRenderer ( params )
182108
183- const inSymbol = isToken0In ? arm . symbol0 : arm . symbol1
184- const outSymbol = isToken0In ? arm . symbol1 : arm . symbol0
109+ // Find the transfer IN (to ARM) and transfer OUT (from ARM) in this transaction
110+ const transferInLog = txLogs . find (
111+ ( l ) => l . topics [ 0 ] === erc20Abi . events . Transfer . topic && l . topics [ 2 ] && ( '0x' + l . topics [ 2 ] . slice ( 26 ) ) . toLowerCase ( ) === arm . address . toLowerCase ( ) ,
112+ )
113+ const transferOutLog = txLogs . find (
114+ ( l ) => l . topics [ 0 ] === erc20Abi . events . Transfer . topic && l . topics [ 1 ] && ( '0x' + l . topics [ 1 ] . slice ( 26 ) ) . toLowerCase ( ) === arm . address . toLowerCase ( ) ,
115+ )
185116
186- const rates = await getTraderates ( params . ctx , params . log . block , toAddress )
187- const explorer = explorerUrl ( params . ctx . chain )
117+ // If we don't have both in and out, it's a deposit/withdrawal — not a swap
118+ if ( ! transferInLog || ! transferOutLog ) return
188119
189- // Source address from topic1 (the `from` field)
190- const fromAddress = params . log . topics [ 1 ] ? ( '0x' + params . log . topics [ 1 ] . slice ( 26 ) ) . toLowerCase ( ) : undefined
191- const sourceName = fromAddress
192- ? getAddressesPyName ( fromAddress ) ?? `${ fromAddress . slice ( 0 , 6 ) } ...${ fromAddress . slice ( - 4 ) } `
193- : undefined
120+ // Only render once per swap (on the inbound transfer)
121+ if ( params . log . logIndex !== transferInLog . logIndex ) return
194122
195- const fields : { name : string ; value : string ; inline ?: boolean } [ ] = [
196- {
197- name : formatAmount ( data . value , 18 , { maximumFractionDigits : 6 } ) ,
198- value : `${ inSymbol } in` ,
199- inline : true ,
200- } ,
201- ]
123+ const transferInData = erc20Abi . events . Transfer . decode ( transferInLog )
124+ const transferOutData = erc20Abi . events . Transfer . decode ( transferOutLog )
202125
203- if ( rates ) {
204- // Traderates are raw spread rates (~1.0). For vault tokens (e.g. sUSDe),
205- // the ARM contract applies the vault conversion internally during swaps,
206- // so we must apply it here too for output estimation.
207- //
208- // traderate0: for 1 token0 from trader, how many token1 the pool sends (raw)
209- // traderate1: for 1 token1 from trader, how many token0 the pool sends (raw)
210- let outAmount : bigint
126+ const explorer = explorerUrl ( params . ctx . chain )
211127
212- if ( isToken0In ) {
213- // token0 → token1: traderate0 is a price (token0/token1), so divide
214- // For vault: further divide by vaultRate (fewer vault tokens per unit)
215- outAmount = ( data . value * 10n ** 36n ) / rates . traderate0
216- if ( arm . vaultToken ) {
217- const vaultRate = await getVaultRate ( params . ctx , params . log . block , arm . vaultToken )
218- outAmount = ( outAmount * ONE ) / vaultRate
219- }
220- } else {
221- // token1 → token0: outAmount = inAmount * traderate1 / 1e36
222- // For vault: actual token0 out = inAmount * vaultRate * traderate1 / 1e36 / 1e18
223- outAmount = ( data . value * rates . traderate1 ) / 10n ** 36n
224- if ( arm . vaultToken ) {
225- const vaultRate = await getVaultRate ( params . ctx , params . log . block , arm . vaultToken )
226- outAmount = ( outAmount * vaultRate ) / ONE
227- }
228- }
128+ // Source address from the inbound transfer's `from` field
129+ const fromAddress = transferInData . from . toLowerCase ( )
130+ const sourceName =
131+ getAddressesPyName ( fromAddress ) ?? discordIconOrName ( fromAddress ) ?? `${ fromAddress . slice ( 0 , 6 ) } ...${ fromAddress . slice ( - 4 ) } `
229132
230- // Rate = token0/token1, derived from actual in/out amounts
231- const rate = isToken0In
232- ? Number ( data . value ) / Number ( outAmount )
233- : Number ( outAmount ) / Number ( data . value )
133+ const inTokenAddress = transferInLog . address . toLowerCase ( )
134+ const outTokenAddress = transferOutLog . address . toLowerCase ( )
135+ const inSymbol = discordIconOrName ( inTokenAddress ) ?? ( inTokenAddress === arm . token0 . toLowerCase ( ) ? arm . symbol0 : arm . symbol1 )
136+ const outSymbol = discordIconOrName ( outTokenAddress ) ?? ( outTokenAddress === arm . token0 . toLowerCase ( ) ? arm . symbol0 : arm . symbol1 )
234137
235- fields . push ( {
236- name : formatAmount ( outAmount , 18 , { maximumFractionDigits : 6 } ) ,
237- value : `${ outSymbol } out` ,
238- inline : true ,
239- } )
240- fields . push ( {
241- name : rate . toLocaleString ( 'en-US' , { maximumFractionDigits : 6 } ) ,
242- value : 'Rate' ,
243- inline : true ,
244- } )
245- }
138+ // Rate = token0/token1, matching original renderer behavior
139+ const isToken0In = inTokenAddress === arm . token0 . toLowerCase ( )
140+ const rate = isToken0In
141+ ? Number ( transferInData . value ) / Number ( transferOutData . value )
142+ : Number ( transferOutData . value ) / Number ( transferInData . value )
246143
247144 renderDiscordEmbed ( {
248145 sortId : `${ params . log . block . height } :${ params . log . transactionIndex } :${ params . log . logIndex } ` ,
249146 topic : params . topic ,
250147 severity : params . severity ,
251148 title : `${ params . name } - Swap` ,
252149 titleUrl : `${ explorer } /tx/${ params . log . transactionHash } ` ,
253- description : sourceName ? `Source: [${ sourceName } ](${ explorer } /address/${ fromAddress } )` : undefined ,
254- fields,
150+ description : `Source: [${ sourceName } ](${ explorer } /address/${ fromAddress } )` ,
151+ fields : [
152+ {
153+ name : formatAmount ( transferInData . value , 18 , { maximumFractionDigits : 6 } ) ,
154+ value : `${ inSymbol } in` ,
155+ inline : true ,
156+ } ,
157+ {
158+ name : formatAmount ( transferOutData . value , 18 , { maximumFractionDigits : 6 } ) ,
159+ value : `${ outSymbol } out` ,
160+ inline : true ,
161+ } ,
162+ {
163+ name : rate . toLocaleString ( 'en-US' , { maximumFractionDigits : 6 } ) ,
164+ value : 'Rate' ,
165+ inline : true ,
166+ } ,
167+ ] ,
255168 } )
256169} )
0 commit comments