Skip to content
Draft

wip #44

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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}": [
Expand Down
58 changes: 51 additions & 7 deletions src/mappings/swap.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
224 changes: 224 additions & 0 deletions src/utils/angstrom.ts
Original file line number Diff line number Diff line change
@@ -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

Check failure on line 185 in src/utils/angstrom.ts

View workflow job for this annotation

GitHub Actions / lint

Replace `⏎····.times(BigInt.fromI32(2).pow(96))⏎····.div(BigInt.fromI32(10).pow(27))⏎····` with `.times(BigInt.fromI32(2).pow(96)).div(BigInt.fromI32(10).pow(27))`
.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,
)
}
Loading
Loading