diff --git a/package.json b/package.json index 774d2fc..7791512 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,8 @@ }, "dependencies": { "husky": "8.0.3", - "lint-staged": "^15.2.7" + "lint-staged": "^15.2.7", + "@sorellalabs/angstrom-assembly-helper": "^1.6.5" }, "lint-staged": { "*.{js,ts}": [ diff --git a/src/mappings/swap.ts b/src/mappings/swap.ts index 3c79665..2cf356a 100644 --- a/src/mappings/swap.ts +++ b/src/mappings/swap.ts @@ -1,7 +1,8 @@ -import { BigDecimal, BigInt } from '@graphprotocol/graph-ts' +import { BigDecimal, BigInt, log } from '@graphprotocol/graph-ts' import { Swap as SwapEvent } from '../types/PoolManager/PoolManager' import { Bundle, Pool, PoolManager, Swap, Token } from '../types/schema' +import { processAngstromBundle } from '../utils/angstrom' import { getSubgraphConfig, SubgraphConfig } from '../utils/chains' import { ONE_BI, ZERO_BD } from '../utils/constants' import { convertTokenToDecimal, loadTransaction, safeDiv } from '../utils/index' @@ -41,11 +42,46 @@ export function handleSwapHelper(event: SwapEvent, subgraphConfig: SubgraphConfi const token0 = Token.load(pool.token0) const token1 = Token.load(pool.token1) + // Detect if this is an Angstrom bundle swap + let isAngstromBundleSwap = false + if (subgraphConfig.angstromAddress != '') { + isAngstromBundleSwap = + pool.hooks.toString() == subgraphConfig.angstromAddress.toString() && + BigInt.fromI32(event.params.fee).equals(BigInt.fromI32(0)) + } + if (token0 && token1) { - // amounts - 0/1 are token deltas: can be positive or negative - // Unlike V3, a negative amount represents that amount is being sent to the pool and vice versa, so invert the sign - const amount0 = convertTokenToDecimal(event.params.amount0, token0.decimals).times(BigDecimal.fromString('-1')) - const amount1 = convertTokenToDecimal(event.params.amount1, token1.decimals).times(BigDecimal.fromString('-1')) + let amount0: BigDecimal + let amount1: BigDecimal + + let angstromFeesUSD = ZERO_BD + if (isAngstromBundleSwap) { + log.debug('handleSwapHelper: detected Angstrom bundle swap for pool {}', [pool.id]) + const transactionInput = event.transaction.input.toHexString() + const angstromResult = processAngstromBundle(transactionInput, pool, token0, token1, event.block.timestamp) + + if (angstromResult.found) { + amount0 = angstromResult.amount0.times(BigDecimal.fromString('-1')) + amount1 = angstromResult.amount1.times(BigDecimal.fromString('-1')) + angstromFeesUSD = angstromResult.feesUSD + log.debug('handleSwapHelper: using Angstrom amounts {} {} with fees {}', [ + amount0.toString(), + amount1.toString(), + angstromFeesUSD.toString(), + ]) + } else { + // Fallback to regular amounts if Angstrom decoding fails + log.warning('handleSwapHelper: Angstrom decoding failed, using regular amounts for pool {}', [pool.id]) + amount0 = convertTokenToDecimal(event.params.amount0, token0.decimals).times(BigDecimal.fromString('-1')) + amount1 = convertTokenToDecimal(event.params.amount1, token1.decimals).times(BigDecimal.fromString('-1')) + } + } else { + // Regular swap amounts + // amounts - 0/1 are token deltas: can be positive or negative + // Unlike V3, a negative amount represents that amount is being sent to the pool and vice versa, so invert the sign + amount0 = convertTokenToDecimal(event.params.amount0, token0.decimals).times(BigDecimal.fromString('-1')) + amount1 = convertTokenToDecimal(event.params.amount1, token1.decimals).times(BigDecimal.fromString('-1')) + } // Update the pool feeTier with the fee from the swap event // This is important for dynamic fee pools where we want to keep store the actual last fee rather storing the dynamic flag (8388608) @@ -73,8 +109,16 @@ export function handleSwapHelper(event: SwapEvent, subgraphConfig: SubgraphConfi const amountTotalETHTracked = safeDiv(amountTotalUSDTracked, bundle.ethPriceUSD) const amountTotalUSDUntracked = amount0USD.plus(amount1USD).div(BigDecimal.fromString('2')) - const feesETH = amountTotalETHTracked.times(pool.feeTier.toBigDecimal()).div(BigDecimal.fromString('1000000')) - const feesUSD = amountTotalUSDTracked.times(pool.feeTier.toBigDecimal()).div(BigDecimal.fromString('1000000')) + // Calculate fees - use Angstrom fees for Angstrom swaps, otherwise calculate vanilla fees + let feesETH = ZERO_BD + let feesUSD = ZERO_BD + if (isAngstromBundleSwap) { + feesUSD = angstromFeesUSD + feesETH = safeDiv(feesUSD, bundle.ethPriceUSD) + } else { + feesETH = amountTotalETHTracked.times(pool.feeTier.toBigDecimal()).div(BigDecimal.fromString('1000000')) + feesUSD = amountTotalUSDTracked.times(pool.feeTier.toBigDecimal()).div(BigDecimal.fromString('1000000')) + } // global updates poolManager.txCount = poolManager.txCount.plus(ONE_BI) diff --git a/src/utils/angstrom.ts b/src/utils/angstrom.ts new file mode 100644 index 0000000..d9fa95d --- /dev/null +++ b/src/utils/angstrom.ts @@ -0,0 +1,224 @@ +import { BigDecimal, BigInt, log } from '@graphprotocol/graph-ts' +import { decode_bundle, PoolReward, Transaction } from '@sorellalabs/angstrom-assembly-helper' + +import { Bundle, Pool, Token } from '../types/schema' +import { convertTokenToDecimal, hexToBigInt } from '.' +import { ZERO_BD } from './constants' + +export class AngstromBundleResult { + amount0: BigDecimal + amount1: BigDecimal + feesUSD: BigDecimal + found: boolean + + constructor(amount0: BigDecimal, amount1: BigDecimal, feesUSD: BigDecimal, found: boolean) { + this.amount0 = amount0 + this.amount1 = amount1 + this.feesUSD = feesUSD + this.found = found + } +} + +export function processAngstromBundle( + transactionInput: string, + pool: Pool, + token0: Token, + token1: Token, + blockTimestamp: BigInt, +): AngstromBundleResult { + log.debug('processAngstromBundle: decoding bundle for pool {}', [pool.id]) + const bundle = decode_bundle(transactionInput) + log.debug('processAngstromBundle: decoded bundle, txn count: {}, reward count: {}', [ + bundle.transactions.length.toString(), + bundle.rewards.length.toString(), + ]) + + // Process transactions to find matching swap amounts + let swapResult: AngstromBundleResult | null = null + const transactions = bundle.transactions + for (let i = 0; i < transactions.length; i++) { + const result = processTransaction(transactions[i], pool, token0, token1) + if (result.found) { + swapResult = result + break + } + } + + // Process rewards for fee tracking and calculate total fees + let totalFeesUSD = ZERO_BD + const rewards = bundle.rewards + for (let i = 0; i < rewards.length; i++) { + const rewardFeeUSD = processReward(rewards[i], blockTimestamp, pool) + totalFeesUSD = totalFeesUSD.plus(rewardFeeUSD) + } + + if (swapResult !== null) { + return new AngstromBundleResult(swapResult.amount0, swapResult.amount1, totalFeesUSD, true) + } else { + log.debug('processAngstromBundle: no matching transaction found for pool {}', [pool.id]) + return new AngstromBundleResult(ZERO_BD, ZERO_BD, totalFeesUSD, false) + } +} + +function processTransaction(transaction: Transaction, _pool: Pool, token0: Token, token1: Token): AngstromBundleResult { + // Check if this transaction matches our pool's token pair + const transactionToken0 = transaction.token0.toLowerCase() + const transactionToken1 = transaction.token1.toLowerCase() + const poolToken0 = token0.id.toLowerCase() + const poolToken1 = token1.id.toLowerCase() + + if (transactionToken0 == poolToken0 && transactionToken1 == poolToken1) { + log.debug('processTransaction: found matching transaction for tokens {} and {}', [poolToken0, poolToken1]) + + const dummyBundleFee = BigInt.fromI32(0) // for now using 0 as bundle fee + const parsedTransaction = parseTransaction(transaction, dummyBundleFee) + + // Convert amounts to decimals and apply sign correction + const amount0 = convertTokenToDecimal(parsedTransaction.token0Amount, token0.decimals).times( + BigDecimal.fromString('-1'), + ) + const amount1 = convertTokenToDecimal(parsedTransaction.token1Amount, token1.decimals).times( + BigDecimal.fromString('-1'), + ) + + return new AngstromBundleResult(amount0, amount1, ZERO_BD, true) + } + + return new AngstromBundleResult(ZERO_BD, ZERO_BD, ZERO_BD, false) +} + +function processReward(reward: PoolReward, _blockTimestamp: BigInt, contextPool: Pool): BigDecimal { + // Find tokens from reward + const token0 = Token.load(reward.token0.toLowerCase()) + const token1 = Token.load(reward.token1.toLowerCase()) + + if (token0 === null || token1 === null) { + log.debug('processReward: token not found - token0: {} token1: {}', [reward.token0, reward.token1]) + return ZERO_BD + } + + // Check if this reward matches the current pool's tokens + const contextToken0 = Token.load(contextPool.token0) + const contextToken1 = Token.load(contextPool.token1) + + if (contextToken0 === null || contextToken1 === null) { + log.debug('processReward: context pool tokens not found', []) + return ZERO_BD + } + + const rewardToken0 = reward.token0.toLowerCase() + const rewardToken1 = reward.token1.toLowerCase() + const poolToken0 = contextToken0.id.toLowerCase() + const poolToken1 = contextToken1.id.toLowerCase() + + // Only process rewards that match the current pool's token pair + const isMatchingPool = rewardToken0 == poolToken0 && rewardToken1 == poolToken1 + + if (!isMatchingPool) { + log.debug('processReward: reward tokens {} {} do not match pool tokens {} {}', [ + rewardToken0, + rewardToken1, + poolToken0, + poolToken1, + ]) + return ZERO_BD + } + + // Calculate total reward amount + let aggregatedRewardAmount = BigInt.zero() + for (let i = 0; i < reward.rewards.length; i++) { + aggregatedRewardAmount = aggregatedRewardAmount.plus(hexToBigInt(reward.rewards[i])) + } + + if (aggregatedRewardAmount.equals(BigInt.zero())) { + return ZERO_BD + } + + // Convert to token0 decimals (assuming rewards are in token0 for now) + const rewardAmount = convertTokenToDecimal(aggregatedRewardAmount, token0.decimals) + const bundle = Bundle.load('1')! + const feeUSD = rewardAmount.times(token0.derivedETH).times(bundle.ethPriceUSD) + + log.debug('processReward: processed fee of {} USD for tokens {} and {}', [feeUSD.toString(), token0.id, token1.id]) + return feeUSD +} + +class ParsedTransaction { + token0: string + token0Amount: BigInt + token1: string + token1Amount: BigInt + origin: string + sqrtPriceX96: BigInt + tick: BigInt + + constructor( + token0: string, + token0Amount: BigInt, + token1: string, + token1Amount: BigInt, + origin: string, + sqrtPriceX96: BigInt, + tick: BigInt, + ) { + this.token0 = token0 + this.token0Amount = token0Amount + this.token1 = token1 + this.token1Amount = token1Amount + this.origin = origin + this.sqrtPriceX96 = sqrtPriceX96 + this.tick = tick + } +} + +function parseTransaction(transaction: Transaction, bundleFee: BigInt): ParsedTransaction { + const zeroForOne = transaction.zeroForOne + const exactIn = transaction.exactIn + const gasUsedAsset0 = hexToBigInt(transaction.gasUsedAsset0) + const bundleFeeBigInt = BigInt.fromI32(1000000).minus(bundleFee) + const price_1over0 = hexToBigInt(transaction.price_1over0) + const token0 = transaction.token0 + let token0Amount = hexToBigInt(transaction.token0Amount) + const token1 = transaction.token1 + let token1Amount = hexToBigInt(transaction.token1Amount) + const origin = transaction.origin + const sqrtPriceX96 = price_1over0 + .times(BigInt.fromI32(2).pow(96)) + .div(BigInt.fromI32(10).pow(27)) + .sqrt() + const tick = BigInt.fromI32(0) + + if (token0Amount.equals(BigInt.zero())) { + const token0WithoutGas = token1Amount + .times(bundleFeeBigInt) + .div(BigInt.fromI32(1000000)) + .times(BigInt.fromI32(10).pow(27)) + .div(price_1over0) + if (exactIn) { + token0Amount = token0WithoutGas.minus(gasUsedAsset0) + } else { + token0Amount = token0WithoutGas.plus(gasUsedAsset0) + } + } else if (token1Amount.equals(BigInt.zero())) { + const price = price_1over0.times(bundleFeeBigInt).div(BigInt.fromI32(1000000)) + let token0AfterGas: BigInt + if (exactIn) { + token0AfterGas = token0Amount.minus(gasUsedAsset0) + } else { + token0AfterGas = token0Amount.plus(gasUsedAsset0) + } + token1Amount = token0AfterGas.times(price).div(BigInt.fromI32(10).pow(27)) + } else { + token0Amount = token0Amount.minus(gasUsedAsset0) + } + + return new ParsedTransaction( + token0, + zeroForOne ? token0Amount : token0Amount.neg(), + token1, + zeroForOne ? token1Amount.neg() : token1Amount, + origin, + sqrtPriceX96, + tick, + ) +} diff --git a/src/utils/chains.ts b/src/utils/chains.ts index 1d28173..3fe7ead 100644 --- a/src/utils/chains.ts +++ b/src/utils/chains.ts @@ -66,6 +66,9 @@ export class SubgraphConfig { // native token details for the chain. nativeTokenDetails: NativeTokenDetails + + // angstrom address for the chain + angstromAddress: string } export function getSubgraphConfig(): SubgraphConfig { @@ -97,6 +100,7 @@ export function getSubgraphConfig(): SubgraphConfig { name: 'Ethereum', decimals: BigInt.fromI32(18), }, + angstromAddress: '', } } else if (selectedNetwork == UNICHAIN_SEPOLIA_NETWORK_NAME) { return { @@ -121,6 +125,7 @@ export function getSubgraphConfig(): SubgraphConfig { name: 'Ethereum', decimals: BigInt.fromI32(18), }, + angstromAddress: '', } } else if (selectedNetwork == ARBITRUM_SEPOLIA_NETWORK_NAME) { return { @@ -145,6 +150,7 @@ export function getSubgraphConfig(): SubgraphConfig { name: 'Ethereum', decimals: BigInt.fromI32(18), }, + angstromAddress: '', } } else if (selectedNetwork == BASE_SEPOLIA_NETWORK_NAME) { return { @@ -170,6 +176,7 @@ export function getSubgraphConfig(): SubgraphConfig { name: 'Ethereum', decimals: BigInt.fromI32(18), }, + angstromAddress: '', } } else if (selectedNetwork == ARBITRUM_ONE_NETWORK_NAME) { return { @@ -213,6 +220,7 @@ export function getSubgraphConfig(): SubgraphConfig { name: 'Ethereum', decimals: BigInt.fromI32(18), }, + angstromAddress: '', } } else if (selectedNetwork == BASE_NETWORK_NAME) { return { @@ -238,6 +246,7 @@ export function getSubgraphConfig(): SubgraphConfig { name: 'Ethereum', decimals: BigInt.fromI32(18), }, + angstromAddress: '', } } else if (selectedNetwork == MATIC_NETWORK_NAME) { return { @@ -267,6 +276,7 @@ export function getSubgraphConfig(): SubgraphConfig { name: 'Polygon', decimals: BigInt.fromI32(18), }, + angstromAddress: '', } } else if (selectedNetwork == BSC_NETWORK_NAME) { return { @@ -293,6 +303,7 @@ export function getSubgraphConfig(): SubgraphConfig { name: 'Binance Coin', decimals: BigInt.fromI32(18), }, + angstromAddress: '', } } else if (selectedNetwork == OPTIMISM_NETWORK_NAME) { return { @@ -334,6 +345,7 @@ export function getSubgraphConfig(): SubgraphConfig { name: 'Ethereum', decimals: BigInt.fromI32(18), }, + angstromAddress: '', } } else if (selectedNetwork == AVALANCHE_NETWORK_NAME) { return { @@ -369,6 +381,7 @@ export function getSubgraphConfig(): SubgraphConfig { name: 'Avalanche', decimals: BigInt.fromI32(18), }, + angstromAddress: '', } } else if (selectedNetwork == WORLDCHAIN_MAINNET_NETWORK_NAME) { return { @@ -396,6 +409,7 @@ export function getSubgraphConfig(): SubgraphConfig { name: 'Ethereum', decimals: BigInt.fromI32(18), }, + angstromAddress: '', } } else if (selectedNetwork == ZORA_MAINNET_NETWORK_NAME) { return { @@ -420,6 +434,7 @@ export function getSubgraphConfig(): SubgraphConfig { name: 'Ethereum', decimals: BigInt.fromI32(18), }, + angstromAddress: '', } } else if (selectedNetwork == MAINNET_NETWORK_NAME) { return { @@ -504,6 +519,7 @@ export function getSubgraphConfig(): SubgraphConfig { name: 'Ethereum', decimals: BigInt.fromI32(18), }, + angstromAddress: '0x0000000aa232009084bd71a5797d089aa4edfad4', } } else if (selectedNetwork == BLAST_MAINNET_NETWORK_NAME) { return { @@ -528,6 +544,7 @@ export function getSubgraphConfig(): SubgraphConfig { name: 'Ethereum', decimals: BigInt.fromI32(18), }, + angstromAddress: '', } } else if (selectedNetwork == UNICHAIN_MAINNET_NETWORK_NAME) { return { @@ -557,6 +574,7 @@ export function getSubgraphConfig(): SubgraphConfig { name: 'Ethereum', decimals: BigInt.fromI32(18), }, + angstromAddress: '', } } else if (selectedNetwork == SONEIUM_MAINNET_NETWORK_NAME) { return { @@ -581,6 +599,7 @@ export function getSubgraphConfig(): SubgraphConfig { name: 'Ethereum', decimals: BigInt.fromI32(18), }, + angstromAddress: '', } } else { throw new Error('Unsupported Network') diff --git a/subgraph.yaml b/subgraph.yaml index 4a50f5a..7fedf28 100644 --- a/subgraph.yaml +++ b/subgraph.yaml @@ -87,3 +87,6 @@ dataSources: handler: handleHookDeployed - event: PoolUninstalled(indexed address,indexed address,indexed address,address) handler: handleHookUninstalled +graft: + base: QmUnr6Jcra3rACvzLPZLmCxKcCUjF27YNNhcK7qyB5zxwY + block: 22971000 diff --git a/yarn.lock b/yarn.lock index b12d742..a64def1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -553,6 +553,11 @@ resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.10.1.tgz#7ca168b6937818e9a74b47ac4e2112b2e1a024cf" integrity sha512-S3Kq8e7LqxkA9s7HKLqXGTGck1uwis5vAXan3FnU5yw1Ec5hsSGnq4s/UCaSqABPOnOTg7zASLyst7+ohgWexg== +"@sorellalabs/angstrom-assembly-helper@^1.6.5": + version "1.6.5" + resolved "https://registry.yarnpkg.com/@sorellalabs/angstrom-assembly-helper/-/angstrom-assembly-helper-1.6.5.tgz#2f92c5d2e30d6636fcfa83d40bce3a8bcb4efdd8" + integrity sha512-S4lpEkLGNNcfaadLrj8KKU2QwUr3Ngpqx2DyzbIQhZfFX4vKCBwsUfydL0G4y2OkRslRvNHYn/cSu1knZniP+g== + "@tsconfig/node10@^1.0.7": version "1.0.11" resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.11.tgz#6ee46400685f130e278128c7b38b7e031ff5b2f2"