From 395f9f14a8be916851c9d9ba7d3fd64c5f2e49c3 Mon Sep 17 00:00:00 2001 From: Michael Wang <44713145+mzywang@users.noreply.github.com> Date: Sat, 25 May 2024 15:59:08 -0400 Subject: [PATCH] add unit tests for swap --- src/mappings/pool/initialize.ts | 3 +- src/mappings/pool/swap.ts | 41 ++++- src/utils/pricing.ts | 3 +- tests/handleSwap.test.ts | 260 ++++++++++++++++++++++++++++++++ 4 files changed, 298 insertions(+), 9 deletions(-) create mode 100644 tests/handleSwap.test.ts diff --git a/src/mappings/pool/initialize.ts b/src/mappings/pool/initialize.ts index 29617ab2..3e7c56a0 100644 --- a/src/mappings/pool/initialize.ts +++ b/src/mappings/pool/initialize.ts @@ -8,6 +8,7 @@ import { getEthPriceInUSD, MINIMUM_ETH_LOCKED, STABLE_COINS, + STABLECOIN_IS_TOKEN0, USDC_WETH_03_POOL, WETH_ADDRESS, } from '../../utils/pricing' @@ -19,7 +20,7 @@ export function handleInitialize(event: Initialize): void { export function handleInitializeHelper( event: Initialize, stablecoinWrappedNativePoolAddress: string = USDC_WETH_03_POOL, - stablecoinIsToken0: boolean = true, + stablecoinIsToken0: boolean = STABLECOIN_IS_TOKEN0, wrappedNativeAddress: string = WETH_ADDRESS, stablecoinAddresses: string[] = STABLE_COINS, minimumEthLocked: BigDecimal = MINIMUM_ETH_LOCKED, diff --git a/src/mappings/pool/swap.ts b/src/mappings/pool/swap.ts index f20d680a..45458c2d 100644 --- a/src/mappings/pool/swap.ts +++ b/src/mappings/pool/swap.ts @@ -11,9 +11,32 @@ import { updateTokenHourData, updateUniswapDayData, } from '../../utils/intervalUpdates' -import { findEthPerToken, getEthPriceInUSD, getTrackedAmountUSD, sqrtPriceX96ToTokenPrices } from '../../utils/pricing' +import { + findEthPerToken, + getEthPriceInUSD, + getTrackedAmountUSD, + MINIMUM_ETH_LOCKED, + sqrtPriceX96ToTokenPrices, + STABLE_COINS, + STABLECOIN_IS_TOKEN0, + USDC_WETH_03_POOL, + WETH_ADDRESS, + WHITELIST_TOKENS, +} from '../../utils/pricing' export function handleSwap(event: SwapEvent): void { + handleSwapHelper(event) +} + +export function handleSwapHelper( + event: SwapEvent, + stablecoinWrappedNativePoolAddress: string = USDC_WETH_03_POOL, + stablecoinIsToken0: boolean = STABLECOIN_IS_TOKEN0, + wrappedNativeAddress: string = WETH_ADDRESS, + stablecoinAddresses: string[] = STABLE_COINS, + minimumEthLocked: BigDecimal = MINIMUM_ETH_LOCKED, + whitelistTokens: string[] = WHITELIST_TOKENS, +): void { const bundle = Bundle.load('1')! const factory = Factory.load(FACTORY_ADDRESS)! const pool = Pool.load(event.address.toHexString())! @@ -47,9 +70,13 @@ export function handleSwap(event: SwapEvent): void { const amount1USD = amount1ETH.times(bundle.ethPriceUSD) // get amount that should be tracked only - div 2 because cant count both input and output as volume - const amountTotalUSDTracked = getTrackedAmountUSD(amount0Abs, token0 as Token, amount1Abs, token1 as Token).div( - BigDecimal.fromString('2'), - ) + const amountTotalUSDTracked = getTrackedAmountUSD( + amount0Abs, + token0 as Token, + amount1Abs, + token1 as Token, + whitelistTokens, + ).div(BigDecimal.fromString('2')) const amountTotalETHTracked = safeDiv(amountTotalUSDTracked, bundle.ethPriceUSD) const amountTotalUSDUntracked = amount0USD.plus(amount1USD).div(BigDecimal.fromString('2')) @@ -106,10 +133,10 @@ export function handleSwap(event: SwapEvent): void { pool.save() // update USD pricing - bundle.ethPriceUSD = getEthPriceInUSD() + bundle.ethPriceUSD = getEthPriceInUSD(stablecoinWrappedNativePoolAddress, stablecoinIsToken0) bundle.save() - token0.derivedETH = findEthPerToken(token0 as Token) - token1.derivedETH = findEthPerToken(token1 as Token) + token0.derivedETH = findEthPerToken(token0 as Token, wrappedNativeAddress, stablecoinAddresses, minimumEthLocked) + token1.derivedETH = findEthPerToken(token1 as Token, wrappedNativeAddress, stablecoinAddresses, minimumEthLocked) /** * Things afffected by new USD rates diff --git a/src/utils/pricing.ts b/src/utils/pricing.ts index b9ff7d4d..6b834ff2 100644 --- a/src/utils/pricing.ts +++ b/src/utils/pricing.ts @@ -6,6 +6,7 @@ import { ONE_BD, ZERO_BD, ZERO_BI } from './constants' export const WETH_ADDRESS = '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2' export const USDC_WETH_03_POOL = '0x8ad599c3a0ff1de082011efddc58f1908eb6e6d8' +export const STABLECOIN_IS_TOKEN0 = true // token where amounts should contribute to tracked volume and liquidity // usually tokens that many tokens are paired with s @@ -56,7 +57,7 @@ export function sqrtPriceX96ToTokenPrices(sqrtPriceX96: BigInt, token0: Token, t export function getEthPriceInUSD( stablecoinWrappedNativePoolAddress: string = USDC_WETH_03_POOL, - stablecoinIsToken0: boolean = true, // true is stablecoin is token0, false if stablecoin is token1 + stablecoinIsToken0: boolean = STABLECOIN_IS_TOKEN0, // true is stablecoin is token0, false if stablecoin is token1 ): BigDecimal { const stablecoinWrappedNativePool = Pool.load(stablecoinWrappedNativePoolAddress) if (stablecoinWrappedNativePool !== null) { diff --git a/tests/handleSwap.test.ts b/tests/handleSwap.test.ts new file mode 100644 index 00000000..4f6c47de --- /dev/null +++ b/tests/handleSwap.test.ts @@ -0,0 +1,260 @@ +import { Address, BigDecimal, BigInt, ethereum } from '@graphprotocol/graph-ts' +import { beforeAll, describe, test } from 'matchstick-as' + +import { handleSwapHelper } from '../src/mappings/pool/swap' +import { Bundle, Token } from '../src/types/schema' +import { Swap } from '../src/types/templates/Pool/Pool' +import { convertTokenToDecimal, safeDiv } from '../src/utils' +import { FACTORY_ADDRESS, ZERO_BD } from '../src/utils/constants' +import { findEthPerToken, getEthPriceInUSD, getTrackedAmountUSD, sqrtPriceX96ToTokenPrices } from '../src/utils/pricing' +import { + assertObjectMatches, + invokePoolCreatedWithMockedEthCalls, + MOCK_EVENT, + POOL_FEE_TIER_03, + POOL_TICK_SPACING_03, + TEST_ETH_PRICE_USD, + TEST_USDC_DERIVED_ETH, + TEST_WETH_DERIVED_ETH, + USDC_MAINNET_FIXTURE, + USDC_WETH_03_MAINNET_POOL, + WETH_MAINNET_FIXTURE, +} from './constants' + +class SwapFixture { + sender: Address + recipient: Address + amount0: BigInt + amount1: BigInt + sqrtPriceX96: BigInt + liquidity: BigInt + tick: i32 +} + +// https://etherscan.io/tx/0xd6005a794596212a1bdc19178e04e18eb8e9e0963d7073303bcb47d6186e757e#eventlog +const SWAP_FIXTURE: SwapFixture = { + sender: Address.fromString('0x6F1cDbBb4d53d226CF4B917bF768B94acbAB6168'), + recipient: Address.fromString('0x6F1cDbBb4d53d226CF4B917bF768B94acbAB6168'), + amount0: BigInt.fromString('-77505140556'), + amount1: BigInt.fromString('20824112148200096620'), + sqrtPriceX96: BigInt.fromString('1296814378469562426931209291431936'), + liquidity: BigInt.fromString('8433670604946078834'), + tick: 194071, +} + +const SWAP_EVENT = new Swap( + Address.fromString(USDC_WETH_03_MAINNET_POOL), + MOCK_EVENT.logIndex, + MOCK_EVENT.transactionLogIndex, + MOCK_EVENT.logType, + MOCK_EVENT.block, + MOCK_EVENT.transaction, + [ + new ethereum.EventParam('sender', ethereum.Value.fromAddress(SWAP_FIXTURE.sender)), + new ethereum.EventParam('recipient', ethereum.Value.fromAddress(SWAP_FIXTURE.recipient)), + new ethereum.EventParam('amount0', ethereum.Value.fromUnsignedBigInt(SWAP_FIXTURE.amount0)), + new ethereum.EventParam('amount1', ethereum.Value.fromUnsignedBigInt(SWAP_FIXTURE.amount1)), + new ethereum.EventParam('sqrtPriceX96', ethereum.Value.fromUnsignedBigInt(SWAP_FIXTURE.sqrtPriceX96)), + new ethereum.EventParam('liquidity', ethereum.Value.fromUnsignedBigInt(SWAP_FIXTURE.liquidity)), + new ethereum.EventParam('tick', ethereum.Value.fromI32(SWAP_FIXTURE.tick)), + ], + MOCK_EVENT.receipt, +) + +describe('handleSwap', () => { + beforeAll(() => { + invokePoolCreatedWithMockedEthCalls( + MOCK_EVENT, + FACTORY_ADDRESS, + USDC_MAINNET_FIXTURE, + WETH_MAINNET_FIXTURE, + USDC_WETH_03_MAINNET_POOL, + POOL_FEE_TIER_03, + POOL_TICK_SPACING_03, + ) + + const bundle = new Bundle('1') + bundle.ethPriceUSD = TEST_ETH_PRICE_USD + bundle.save() + + const usdcEntity = Token.load(USDC_MAINNET_FIXTURE.address)! + usdcEntity.derivedETH = TEST_USDC_DERIVED_ETH + usdcEntity.save() + + const wethEntity = Token.load(WETH_MAINNET_FIXTURE.address)! + wethEntity.derivedETH = TEST_WETH_DERIVED_ETH + wethEntity.save() + }) + + test('success', () => { + const stablecoinWrappedNativePoolAddress = USDC_WETH_03_MAINNET_POOL + const stablecoinIsToken0 = true + const wrappedNativeAddress = WETH_MAINNET_FIXTURE.address + const stablecoinAddresses = [USDC_MAINNET_FIXTURE.address] + const minimumEthLocked = ZERO_BD + const whitelistTokens = [USDC_MAINNET_FIXTURE.address, WETH_MAINNET_FIXTURE.address] + + const token0 = Token.load(USDC_MAINNET_FIXTURE.address)! + const token1 = Token.load(WETH_MAINNET_FIXTURE.address)! + + const amount0 = convertTokenToDecimal(SWAP_FIXTURE.amount0, BigInt.fromString(USDC_MAINNET_FIXTURE.decimals)) + const amount1 = convertTokenToDecimal(SWAP_FIXTURE.amount1, BigInt.fromString(WETH_MAINNET_FIXTURE.decimals)) + + const amount0Abs = amount0.lt(ZERO_BD) ? amount0.times(BigDecimal.fromString('-1')) : amount0 + const amount1Abs = amount1.lt(ZERO_BD) ? amount1.times(BigDecimal.fromString('-1')) : amount1 + + // calculate this before calling handleSwapHelper because it updates the derivedETH of the tokens which will affect calculations + const amountTotalUSDTracked = getTrackedAmountUSD(amount0Abs, token0, amount1Abs, token1, whitelistTokens).div( + BigDecimal.fromString('2'), + ) + + const amount0ETH = amount0Abs.times(TEST_USDC_DERIVED_ETH) + const amount1ETH = amount1Abs.times(TEST_WETH_DERIVED_ETH) + + const amount0USD = amount0ETH.times(TEST_ETH_PRICE_USD) + const amount1USD = amount1ETH.times(TEST_ETH_PRICE_USD) + + const amountTotalETHTRacked = safeDiv(amountTotalUSDTracked, TEST_ETH_PRICE_USD) + const amountTotalUSDUntracked = amount0USD.plus(amount1USD).div(BigDecimal.fromString('2')) + + const feeTierBD = BigDecimal.fromString(POOL_FEE_TIER_03.toString()) + const feesETH = amountTotalETHTRacked.times(feeTierBD).div(BigDecimal.fromString('1000000')) + const feesUSD = amountTotalUSDTracked.times(feeTierBD).div(BigDecimal.fromString('1000000')) + + handleSwapHelper( + SWAP_EVENT, + stablecoinWrappedNativePoolAddress, + stablecoinIsToken0, + wrappedNativeAddress, + stablecoinAddresses, + minimumEthLocked, + whitelistTokens, + ) + + const newEthPrice = getEthPriceInUSD(USDC_WETH_03_MAINNET_POOL, true) + const newPoolPrices = sqrtPriceX96ToTokenPrices(SWAP_FIXTURE.sqrtPriceX96, token0, token1) + const newToken0DerivedETH = findEthPerToken(token0, wrappedNativeAddress, stablecoinAddresses, minimumEthLocked) + const newToken1DerivedETH = findEthPerToken(token1, wrappedNativeAddress, stablecoinAddresses, minimumEthLocked) + + const totalValueLockedETH = amount0.times(newToken0DerivedETH).plus(amount1.times(newToken1DerivedETH)) + + assertObjectMatches('Factory', FACTORY_ADDRESS, [ + ['txCount', '1'], + ['totalVolumeETH', amountTotalETHTRacked.toString()], + ['totalVolumeUSD', amountTotalUSDTracked.toString()], + ['untrackedVolumeUSD', amountTotalUSDUntracked.toString()], + ['totalFeesETH', feesETH.toString()], + ['totalFeesUSD', feesUSD.toString()], + ['totalValueLockedETH', totalValueLockedETH.toString()], + ['totalValueLockedUSD', totalValueLockedETH.times(newEthPrice).toString()], + ]) + + assertObjectMatches('Pool', USDC_WETH_03_MAINNET_POOL, [ + ['volumeToken0', amount0Abs.toString()], + ['volumeToken1', amount1Abs.toString()], + ['volumeUSD', amountTotalUSDTracked.toString()], + ['untrackedVolumeUSD', amountTotalUSDUntracked.toString()], + ['feesUSD', feesUSD.toString()], + ['txCount', '1'], + ['liquidity', SWAP_FIXTURE.liquidity.toString()], + ['tick', SWAP_FIXTURE.tick.toString()], + ['sqrtPrice', SWAP_FIXTURE.sqrtPriceX96.toString()], + ['totalValueLockedToken0', amount0.toString()], + ['totalValueLockedToken1', amount1.toString()], + ['token0Price', newPoolPrices[0].toString()], + ['token1Price', newPoolPrices[1].toString()], + ['totalValueLockedETH', totalValueLockedETH.toString()], + ['totalValueLockedUSD', totalValueLockedETH.times(newEthPrice).toString()], + ]) + + assertObjectMatches('Token', USDC_MAINNET_FIXTURE.address, [ + ['volume', amount0Abs.toString()], + ['totalValueLocked', amount0.toString()], + ['volumeUSD', amountTotalUSDTracked.toString()], + ['untrackedVolumeUSD', amountTotalUSDUntracked.toString()], + ['feesUSD', feesUSD.toString()], + ['txCount', '1'], + ['derivedETH', newToken0DerivedETH.toString()], + ['totalValueLockedUSD', amount0.times(newToken0DerivedETH).times(newEthPrice).toString()], + ]) + + assertObjectMatches('Token', WETH_MAINNET_FIXTURE.address, [ + ['volume', amount1Abs.toString()], + ['totalValueLocked', amount1.toString()], + ['volumeUSD', amountTotalUSDTracked.toString()], + ['untrackedVolumeUSD', amountTotalUSDUntracked.toString()], + ['feesUSD', feesUSD.toString()], + ['txCount', '1'], + ['derivedETH', newToken1DerivedETH.toString()], + ['totalValueLockedUSD', amount1.times(newToken1DerivedETH).times(newEthPrice).toString()], + ]) + + assertObjectMatches('Swap', MOCK_EVENT.transaction.hash.toHexString() + '-' + MOCK_EVENT.logIndex.toString(), [ + ['transaction', MOCK_EVENT.transaction.hash.toHexString()], + ['timestamp', MOCK_EVENT.block.timestamp.toString()], + ['pool', USDC_WETH_03_MAINNET_POOL], + ['token0', USDC_MAINNET_FIXTURE.address], + ['token1', WETH_MAINNET_FIXTURE.address], + ['sender', SWAP_FIXTURE.sender.toHexString()], + ['origin', MOCK_EVENT.transaction.from.toHexString()], + ['recipient', SWAP_FIXTURE.recipient.toHexString()], + ['amount0', amount0.toString()], + ['amount1', amount1.toString()], + ['amountUSD', amountTotalUSDTracked.toString()], + ['tick', SWAP_FIXTURE.tick.toString()], + ['sqrtPriceX96', SWAP_FIXTURE.sqrtPriceX96.toString()], + ['logIndex', MOCK_EVENT.logIndex.toString()], + ]) + + const dayId = MOCK_EVENT.block.timestamp.toI32() / 86400 + const hourId = MOCK_EVENT.block.timestamp.toI32() / 3600 + + assertObjectMatches('UniswapDayData', dayId.toString(), [ + ['volumeETH', amountTotalETHTRacked.toString()], + ['volumeUSD', amountTotalUSDTracked.toString()], + ['feesUSD', feesUSD.toString()], + ]) + + assertObjectMatches('PoolDayData', USDC_WETH_03_MAINNET_POOL + '-' + dayId.toString(), [ + ['volumeUSD', amountTotalUSDTracked.toString()], + ['volumeToken0', amount0Abs.toString()], + ['volumeToken1', amount1Abs.toString()], + ['feesUSD', feesUSD.toString()], + ]) + + assertObjectMatches('PoolHourData', USDC_WETH_03_MAINNET_POOL + '-' + hourId.toString(), [ + ['volumeUSD', amountTotalUSDTracked.toString()], + ['volumeToken0', amount0Abs.toString()], + ['volumeToken1', amount1Abs.toString()], + ['feesUSD', feesUSD.toString()], + ]) + + assertObjectMatches('TokenDayData', USDC_MAINNET_FIXTURE.address + '-' + dayId.toString(), [ + ['volume', amount0Abs.toString()], + ['volumeUSD', amountTotalUSDTracked.toString()], + ['untrackedVolumeUSD', amountTotalUSDTracked.toString()], + ['feesUSD', feesUSD.toString()], + ]) + + assertObjectMatches('TokenDayData', WETH_MAINNET_FIXTURE.address + '-' + dayId.toString(), [ + ['volume', amount1Abs.toString()], + ['volumeUSD', amountTotalUSDTracked.toString()], + ['untrackedVolumeUSD', amountTotalUSDTracked.toString()], + ['feesUSD', feesUSD.toString()], + ]) + + assertObjectMatches('TokenHourData', USDC_MAINNET_FIXTURE.address + '-' + hourId.toString(), [ + ['volume', amount0Abs.toString()], + ['volumeUSD', amountTotalUSDTracked.toString()], + ['untrackedVolumeUSD', amountTotalUSDTracked.toString()], + ['feesUSD', feesUSD.toString()], + ]) + + assertObjectMatches('TokenHourData', WETH_MAINNET_FIXTURE.address + '-' + hourId.toString(), [ + ['volume', amount1Abs.toString()], + ['volumeUSD', amountTotalUSDTracked.toString()], + ['untrackedVolumeUSD', amountTotalUSDTracked.toString()], + ['feesUSD', feesUSD.toString()], + ]) + }) +})