diff --git a/src/mappings/pool/burn.ts b/src/mappings/pool/burn.ts index 0f2b2354..3f924c6c 100644 --- a/src/mappings/pool/burn.ts +++ b/src/mappings/pool/burn.ts @@ -12,12 +12,16 @@ import { updateUniswapDayData, } from '../../utils/intervalUpdates' -// Note: this handler need not adjust TVL because that is accounted for in the handleCollect handler export function handleBurn(event: BurnEvent): void { + handleBurnHelper(event) +} + +// Note: this handler need not adjust TVL because that is accounted for in the handleCollect handler +export function handleBurnHelper(event: BurnEvent, factoryAddress: string = FACTORY_ADDRESS): void { const bundle = Bundle.load('1')! const poolAddress = event.address.toHexString() const pool = Pool.load(poolAddress)! - const factory = Factory.load(FACTORY_ADDRESS)! + const factory = Factory.load(factoryAddress)! const token0 = Token.load(pool.token0) const token1 = Token.load(pool.token1) diff --git a/src/mappings/pool/collect.ts b/src/mappings/pool/collect.ts index af71d561..6cbf87a4 100644 --- a/src/mappings/pool/collect.ts +++ b/src/mappings/pool/collect.ts @@ -11,16 +11,24 @@ import { updateTokenHourData, updateUniswapDayData, } from '../../utils/intervalUpdates' -import { getTrackedAmountUSD } from '../../utils/pricing' +import { getTrackedAmountUSD, WHITELIST_TOKENS } from '../../utils/pricing' export function handleCollect(event: CollectEvent): void { + handleCollectHelper(event) +} + +export function handleCollectHelper( + event: CollectEvent, + factoryAddress: string = FACTORY_ADDRESS, + whitelistTokens: string[] = WHITELIST_TOKENS, +): void { const bundle = Bundle.load('1')! const pool = Pool.load(event.address.toHexString()) if (pool == null) { return } const transaction = loadTransaction(event) - const factory = Factory.load(FACTORY_ADDRESS)! + const factory = Factory.load(factoryAddress)! const token0 = Token.load(pool.token0) const token1 = Token.load(pool.token1) @@ -36,6 +44,7 @@ export function handleCollect(event: CollectEvent): void { token0 as Token, collectedAmountToken1, token1 as Token, + whitelistTokens, ) // Reset tvl aggregates until new amounts calculated diff --git a/src/mappings/pool/mint.ts b/src/mappings/pool/mint.ts index 8fc50749..a71dcf24 100644 --- a/src/mappings/pool/mint.ts +++ b/src/mappings/pool/mint.ts @@ -14,10 +14,14 @@ import { import { createTick } from '../../utils/tick' export function handleMint(event: MintEvent): void { + handleMintHelper(event) +} + +export function handleMintHelper(event: MintEvent, factoryAddress: string = FACTORY_ADDRESS): void { const bundle = Bundle.load('1')! const poolAddress = event.address.toHexString() const pool = Pool.load(poolAddress)! - const factory = Factory.load(FACTORY_ADDRESS)! + const factory = Factory.load(factoryAddress)! const token0 = Token.load(pool.token0) const token1 = Token.load(pool.token1) diff --git a/src/utils/pricing.ts b/src/utils/pricing.ts index 1f53c9d7..83febdcf 100644 --- a/src/utils/pricing.ts +++ b/src/utils/pricing.ts @@ -133,23 +133,24 @@ export function getTrackedAmountUSD( token0: Token, tokenAmount1: BigDecimal, token1: Token, + whitelistTokens: string[] = WHITELIST_TOKENS, ): BigDecimal { const bundle = Bundle.load('1')! const price0USD = token0.derivedETH.times(bundle.ethPriceUSD) const price1USD = token1.derivedETH.times(bundle.ethPriceUSD) // both are whitelist tokens, return sum of both amounts - if (WHITELIST_TOKENS.includes(token0.id) && WHITELIST_TOKENS.includes(token1.id)) { + if (whitelistTokens.includes(token0.id) && whitelistTokens.includes(token1.id)) { return tokenAmount0.times(price0USD).plus(tokenAmount1.times(price1USD)) } // take double value of the whitelisted token amount - if (WHITELIST_TOKENS.includes(token0.id) && !WHITELIST_TOKENS.includes(token1.id)) { + if (whitelistTokens.includes(token0.id) && !whitelistTokens.includes(token1.id)) { return tokenAmount0.times(price0USD).times(BigDecimal.fromString('2')) } // take double value of the whitelisted token amount - if (!WHITELIST_TOKENS.includes(token0.id) && WHITELIST_TOKENS.includes(token1.id)) { + if (!whitelistTokens.includes(token0.id) && whitelistTokens.includes(token1.id)) { return tokenAmount1.times(price1USD).times(BigDecimal.fromString('2')) } diff --git a/src/utils/staticTokenDefinition.ts b/src/utils/staticTokenDefinition.ts index 35bd3aca..2c0b8720 100644 --- a/src/utils/staticTokenDefinition.ts +++ b/src/utils/staticTokenDefinition.ts @@ -10,7 +10,7 @@ export class StaticTokenDefinition { export const getStaticDefinition = ( tokenAddress: Address, - staticDefinitions: Array + staticDefinitions: Array, ): StaticTokenDefinition | null => { const tokenAddressHex = tokenAddress.toHexString() diff --git a/src/utils/token.ts b/src/utils/token.ts index 15857b9f..1aca8117 100644 --- a/src/utils/token.ts +++ b/src/utils/token.ts @@ -8,7 +8,7 @@ import { getStaticDefinition, STATIC_TOKEN_DEFINITIONS, StaticTokenDefinition } export function fetchTokenSymbol( tokenAddress: Address, - staticTokenDefinitions: StaticTokenDefinition[] = STATIC_TOKEN_DEFINITIONS + staticTokenDefinitions: StaticTokenDefinition[] = STATIC_TOKEN_DEFINITIONS, ): string { const contract = ERC20.bind(tokenAddress) const contractSymbolBytes = ERC20SymbolBytes.bind(tokenAddress) @@ -39,7 +39,7 @@ export function fetchTokenSymbol( export function fetchTokenName( tokenAddress: Address, - staticTokenDefinitions: StaticTokenDefinition[] = STATIC_TOKEN_DEFINITIONS + staticTokenDefinitions: StaticTokenDefinition[] = STATIC_TOKEN_DEFINITIONS, ): string { const contract = ERC20.bind(tokenAddress) const contractNameBytes = ERC20NameBytes.bind(tokenAddress) @@ -80,7 +80,7 @@ export function fetchTokenTotalSupply(tokenAddress: Address): BigInt { export function fetchTokenDecimals( tokenAddress: Address, - staticTokenDefinitions: StaticTokenDefinition[] = STATIC_TOKEN_DEFINITIONS + staticTokenDefinitions: StaticTokenDefinition[] = STATIC_TOKEN_DEFINITIONS, ): BigInt | null { const contract = ERC20.bind(tokenAddress) // try types uint8 for decimals diff --git a/tests/constants.ts b/tests/constants.ts index 36b8c446..39e45c6b 100644 --- a/tests/constants.ts +++ b/tests/constants.ts @@ -1,4 +1,4 @@ -import { Address, BigInt, ethereum } from '@graphprotocol/graph-ts' +import { Address, BigDecimal, BigInt, ethereum } from '@graphprotocol/graph-ts' import { assert, createMockedFunction, newMockEvent } from 'matchstick-as' import { handlePoolCreatedHelper } from '../src/mappings/factory' @@ -34,6 +34,10 @@ export const WETH_MAINNET_FIXTURE: TokenFixture = { decimals: '18', } +export const TEST_ETH_PRICE_USD = BigDecimal.fromString('2000') +export const TEST_USDC_DERIVED_ETH = BigDecimal.fromString('1').div(BigDecimal.fromString('2000')) +export const TEST_WETH_DERIVED_ETH = BigDecimal.fromString('1') + export const MOCK_EVENT = newMockEvent() export const createTestPool = ( @@ -43,7 +47,7 @@ export const createTestPool = ( token1: TokenFixture, poolAddressHexString: string, feeTier: number, - tickSpacing: number + tickSpacing: number, ): void => { const mockEvent = newMockEvent() const token0Address = Address.fromString(token0.address) @@ -64,7 +68,7 @@ export const createTestPool = ( mockEvent.block, mockEvent.transaction, parameters, - mockEvent.receipt + mockEvent.receipt, ) // create mock contract calls for token0 createMockedFunction(token0Address, 'symbol', 'symbol():(string)').returns([ethereum.Value.fromString(token0.symbol)]) diff --git a/tests/handleBurn.test.ts b/tests/handleBurn.test.ts new file mode 100644 index 00000000..89ddb76d --- /dev/null +++ b/tests/handleBurn.test.ts @@ -0,0 +1,194 @@ +import { Address, BigDecimal, BigInt, ethereum } from '@graphprotocol/graph-ts' +import { beforeAll, describe, test } from 'matchstick-as' + +import { handleBurnHelper } from '../src/mappings/pool/burn' +import { Bundle, Pool, Tick, Token } from '../src/types/schema' +import { Burn } from '../src/types/templates/Pool/Pool' +import { convertTokenToDecimal, fastExponentiation, safeDiv } from '../src/utils' +import { FACTORY_ADDRESS, ONE_BD, ZERO_BI } from '../src/utils/constants' +import { + assertObjectMatches, + createTestPool, + 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 BurnFixture { + owner: Address + tickLower: i32 + tickUpper: i32 + amount: BigInt + amount0: BigInt + amount1: BigInt +} + +// https://etherscan.io/tx/0x26b168e005a168b28d518675435c9f51816697c086deef7377e0018e4eb65dc9 +const BURN_FIXTURE: BurnFixture = { + owner: Address.fromString('0x8692f704a20d11be3b32de68656651b5291ed26c'), + tickLower: 194280, + tickUpper: 194520, + amount: BigInt.fromString('107031367278175302'), + amount0: BigInt.fromString('77186598043'), + amount1: BigInt.fromString('0'), +} + +const BURN_EVENT = new Burn( + 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('owner', ethereum.Value.fromAddress(BURN_FIXTURE.owner)), + new ethereum.EventParam('tickLower', ethereum.Value.fromI32(BURN_FIXTURE.tickLower)), + new ethereum.EventParam('tickUpper', ethereum.Value.fromI32(BURN_FIXTURE.tickUpper)), + new ethereum.EventParam('amount', ethereum.Value.fromUnsignedBigInt(BURN_FIXTURE.amount)), + new ethereum.EventParam('amount0', ethereum.Value.fromUnsignedBigInt(BURN_FIXTURE.amount0)), + new ethereum.EventParam('amount1', ethereum.Value.fromUnsignedBigInt(BURN_FIXTURE.amount1)), + ], + MOCK_EVENT.receipt, +) + +describe('handleBurn', () => { + beforeAll(() => { + createTestPool( + 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() + + const tickLower = new Tick(USDC_WETH_03_MAINNET_POOL + '#' + BURN_FIXTURE.tickLower.toString()) + tickLower.tickIdx = BigInt.fromI32(BURN_FIXTURE.tickLower) + tickLower.pool = USDC_WETH_03_MAINNET_POOL + tickLower.poolAddress = USDC_WETH_03_MAINNET_POOL + tickLower.createdAtTimestamp = MOCK_EVENT.block.timestamp + tickLower.createdAtBlockNumber = MOCK_EVENT.block.number + tickLower.liquidityGross = ZERO_BI + tickLower.liquidityNet = ZERO_BI + tickLower.price0 = fastExponentiation(BigDecimal.fromString('1.0001'), BURN_FIXTURE.tickLower) + tickLower.price1 = safeDiv(ONE_BD, tickLower.price0) + tickLower.save() + + const tickUpper = new Tick(USDC_WETH_03_MAINNET_POOL + '#' + BURN_FIXTURE.tickUpper.toString()) + tickUpper.tickIdx = BigInt.fromI32(BURN_FIXTURE.tickUpper) + tickUpper.pool = USDC_WETH_03_MAINNET_POOL + tickUpper.poolAddress = USDC_WETH_03_MAINNET_POOL + tickUpper.createdAtTimestamp = MOCK_EVENT.block.timestamp + tickUpper.createdAtBlockNumber = MOCK_EVENT.block.number + tickUpper.liquidityGross = ZERO_BI + tickUpper.liquidityNet = ZERO_BI + tickUpper.price0 = fastExponentiation(BigDecimal.fromString('1.0001'), BURN_FIXTURE.tickUpper) + tickUpper.price1 = safeDiv(ONE_BD, tickUpper.price0) + tickUpper.save() + }) + + // note: all tvl should be zero in this test because burns don't remove TVL, only collects do + test('success - burn event, pool tick is between tickUpper and tickLower', () => { + // put the pools tick in range + const pool = Pool.load(USDC_WETH_03_MAINNET_POOL)! + pool.tick = BigInt.fromI32(BURN_FIXTURE.tickLower + BURN_FIXTURE.tickUpper).div(BigInt.fromI32(2)) + pool.save() + + handleBurnHelper(BURN_EVENT, FACTORY_ADDRESS) + + const amountToken0 = convertTokenToDecimal(BURN_FIXTURE.amount0, BigInt.fromString(USDC_MAINNET_FIXTURE.decimals)) + const amountToken1 = convertTokenToDecimal(BURN_FIXTURE.amount1, BigInt.fromString(WETH_MAINNET_FIXTURE.decimals)) + const poolTotalValueLockedETH = amountToken0 + .times(TEST_USDC_DERIVED_ETH) + .plus(amountToken1.times(TEST_WETH_DERIVED_ETH)) + const poolTotalValueLockedUSD = poolTotalValueLockedETH.times(TEST_ETH_PRICE_USD) + + assertObjectMatches('Factory', FACTORY_ADDRESS, [ + ['txCount', '1'], + ['totalValueLockedETH', '0'], + ['totalValueLockedUSD', '0'], + ]) + + assertObjectMatches('Pool', USDC_WETH_03_MAINNET_POOL, [ + ['txCount', '1'], + ['liquidity', BURN_FIXTURE.amount.neg().toString()], + ['totalValueLockedToken0', '0'], + ['totalValueLockedToken1', '0'], + ['totalValueLockedETH', '0'], + ['totalValueLockedUSD', '0'], + ['totalValueLockedETH', '0'], + ['totalValueLockedUSD', '0'], + ]) + + assertObjectMatches('Token', USDC_MAINNET_FIXTURE.address, [ + ['txCount', '1'], + ['totalValueLocked', '0'], + ['totalValueLockedUSD', '0'], + ]) + + assertObjectMatches('Token', WETH_MAINNET_FIXTURE.address, [ + ['txCount', '1'], + ['totalValueLocked', '0'], + ['totalValueLockedUSD', '0'], + ]) + + assertObjectMatches('Burn', 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], + ['owner', BURN_FIXTURE.owner.toHexString()], + ['origin', MOCK_EVENT.transaction.from.toHexString()], + ['amount', BURN_FIXTURE.amount.toString()], + ['amount0', amountToken0.toString()], + ['amount1', amountToken1.toString()], + ['amountUSD', poolTotalValueLockedUSD.toString()], + ['tickUpper', BURN_FIXTURE.tickUpper.toString()], + ['tickLower', BURN_FIXTURE.tickLower.toString()], + ['logIndex', MOCK_EVENT.logIndex.toString()], + ]) + + assertObjectMatches('Tick', USDC_WETH_03_MAINNET_POOL + '#' + BURN_FIXTURE.tickLower.toString(), [ + ['liquidityGross', BURN_FIXTURE.amount.neg().toString()], + ['liquidityNet', BURN_FIXTURE.amount.neg().toString()], + ]) + + assertObjectMatches('Tick', USDC_WETH_03_MAINNET_POOL + '#' + BURN_FIXTURE.tickUpper.toString(), [ + ['liquidityGross', BURN_FIXTURE.amount.neg().toString()], + ['liquidityNet', BURN_FIXTURE.amount.toString()], + ]) + }) + + test('success - burn event, pool tick is not between tickUpper and tickLower', () => { + // put the pools tick out of range + const pool = Pool.load(USDC_WETH_03_MAINNET_POOL)! + pool.tick = BigInt.fromI32(BURN_FIXTURE.tickLower - 1) + const liquidityBeforeBurn = pool.liquidity + pool.save() + + handleBurnHelper(BURN_EVENT, FACTORY_ADDRESS) + + // liquidity should not be updated + assertObjectMatches('Pool', USDC_WETH_03_MAINNET_POOL, [['liquidity', liquidityBeforeBurn.toString()]]) + }) +}) diff --git a/tests/handleCollect.test.ts b/tests/handleCollect.test.ts new file mode 100644 index 00000000..92f31db5 --- /dev/null +++ b/tests/handleCollect.test.ts @@ -0,0 +1,151 @@ +import { Address, BigInt, ethereum } from '@graphprotocol/graph-ts' +import { beforeAll, describe, test } from 'matchstick-as' + +import { handleCollectHelper } from '../src/mappings/pool/collect' +import { Bundle, Token } from '../src/types/schema' +import { Collect } from '../src/types/templates/Pool/Pool' +import { convertTokenToDecimal } from '../src/utils' +import { FACTORY_ADDRESS, ZERO_BD } from '../src/utils/constants' +import { + assertObjectMatches, + createTestPool, + 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 CollectFixture { + owner: Address + recipient: Address + tickLower: i32 + tickUpper: i32 + amount0: BigInt + amount1: BigInt +} + +// https://etherscan.io/tx/0x328c84a513e6146dd3cf28861e8f2445e38d251c4b8a922057c755e12281c7ea +const COLLECT_FIXTURE: CollectFixture = { + owner: Address.fromString('0xc36442b4a4522e871399cd717abdd847ab11fe88'), + recipient: Address.fromString('0xc36442b4a4522e871399cd717abdd847ab11fe88'), + tickLower: 81600, + tickUpper: 84120, + amount0: BigInt.fromString('0'), + amount1: BigInt.fromString('19275229182128904'), +} + +const COLLECT_EVENT = new Collect( + 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('owner', ethereum.Value.fromAddress(COLLECT_FIXTURE.owner)), + new ethereum.EventParam('recipient', ethereum.Value.fromAddress(COLLECT_FIXTURE.recipient)), + new ethereum.EventParam('tickLower', ethereum.Value.fromI32(COLLECT_FIXTURE.tickLower)), + new ethereum.EventParam('tickUpper', ethereum.Value.fromI32(COLLECT_FIXTURE.tickUpper)), + new ethereum.EventParam('amount0', ethereum.Value.fromUnsignedBigInt(COLLECT_FIXTURE.amount0)), + new ethereum.EventParam('amount1', ethereum.Value.fromUnsignedBigInt(COLLECT_FIXTURE.amount1)), + ], + MOCK_EVENT.receipt, +) + +describe('handleMint', () => { + beforeAll(() => { + createTestPool( + 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 - collect event', () => { + // pass in empty whitelist to simplify this test. Doing so ignores the + // effect of getTrackedAmountUSD which we test separately. + const trackedCollectedAmountUSD = ZERO_BD + handleCollectHelper(COLLECT_EVENT, FACTORY_ADDRESS, []) + + const collectedAmountToken0 = convertTokenToDecimal( + COLLECT_FIXTURE.amount0, + BigInt.fromString(USDC_MAINNET_FIXTURE.decimals), + ) + const collectedAmountToken1 = convertTokenToDecimal( + COLLECT_FIXTURE.amount1, + BigInt.fromString(WETH_MAINNET_FIXTURE.decimals), + ) + const collectedAmountETH = collectedAmountToken0 + .times(TEST_USDC_DERIVED_ETH) + .plus(collectedAmountToken1.times(TEST_WETH_DERIVED_ETH)) + const collectedAmountUSD = collectedAmountETH.times(TEST_ETH_PRICE_USD) + + assertObjectMatches('Factory', FACTORY_ADDRESS, [ + ['txCount', '1'], + ['totalValueLockedETH', collectedAmountETH.neg().toString()], + ['totalValueLockedUSD', collectedAmountUSD.neg().toString()], + ]) + + assertObjectMatches('Pool', USDC_WETH_03_MAINNET_POOL, [ + ['txCount', '1'], + ['totalValueLockedToken0', collectedAmountToken0.neg().toString()], + ['totalValueLockedToken1', collectedAmountToken1.neg().toString()], + ['totalValueLockedETH', collectedAmountETH.neg().toString()], + ['totalValueLockedUSD', collectedAmountUSD.neg().toString()], + ['collectedFeesToken0', collectedAmountToken0.toString()], + ['collectedFeesToken1', collectedAmountToken1.toString()], + ['collectedFeesUSD', trackedCollectedAmountUSD.toString()], + ]) + + assertObjectMatches('Token', USDC_MAINNET_FIXTURE.address, [ + ['txCount', '1'], + ['totalValueLocked', collectedAmountToken0.neg().toString()], + [ + 'totalValueLockedUSD', + collectedAmountToken0.times(TEST_USDC_DERIVED_ETH.times(TEST_ETH_PRICE_USD)).neg().toString(), + ], + ]) + + assertObjectMatches('Token', WETH_MAINNET_FIXTURE.address, [ + ['txCount', '1'], + ['totalValueLocked', collectedAmountToken1.neg().toString()], + [ + 'totalValueLockedUSD', + collectedAmountToken1.times(TEST_WETH_DERIVED_ETH.times(TEST_ETH_PRICE_USD)).neg().toString(), + ], + ]) + + assertObjectMatches('Collect', MOCK_EVENT.transaction.hash.toHexString() + '-' + MOCK_EVENT.logIndex.toString(), [ + ['transaction', MOCK_EVENT.transaction.hash.toHexString()], + ['pool', USDC_WETH_03_MAINNET_POOL], + ['owner', COLLECT_FIXTURE.owner.toHexString()], + ['amount0', collectedAmountToken0.toString()], + ['amount1', collectedAmountToken1.toString()], + ['amountUSD', trackedCollectedAmountUSD.toString()], + ['tickLower', COLLECT_FIXTURE.tickLower.toString()], + ['tickUpper', COLLECT_FIXTURE.tickUpper.toString()], + ['logIndex', MOCK_EVENT.logIndex.toString()], + ]) + }) +}) diff --git a/tests/handleMint.test.ts b/tests/handleMint.test.ts new file mode 100644 index 00000000..195f0316 --- /dev/null +++ b/tests/handleMint.test.ts @@ -0,0 +1,187 @@ +import { Address, BigDecimal, BigInt, ethereum } from '@graphprotocol/graph-ts' +import { beforeAll, describe, test } from 'matchstick-as' + +import { handleMintHelper } from '../src/mappings/pool/mint' +import { Bundle, Pool, Token } from '../src/types/schema' +import { Mint } from '../src/types/templates/Pool/Pool' +import { convertTokenToDecimal, fastExponentiation, safeDiv } from '../src/utils' +import { FACTORY_ADDRESS, ONE_BD } from '../src/utils/constants' +import { + assertObjectMatches, + createTestPool, + 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 MintFixture { + sender: Address + owner: Address + tickLower: i32 + tickUpper: i32 + amount: BigInt + amount0: BigInt + amount1: BigInt +} + +// https://etherscan.io/tx/0x0338617bb36e23bbd4074b068ea79edd07f7ef0db13fc0cd06ab8e57b9012764 +const MINT_FIXTURE: MintFixture = { + sender: Address.fromString('0xc36442b4a4522e871399cd717abdd847ab11fe88'), + owner: Address.fromString('0xc36442b4a4522e871399cd717abdd847ab11fe88'), + tickLower: 195600, + tickUpper: 196740, + amount: BigInt.fromString('386405747494368'), + amount0: BigInt.fromString('1000000000'), + amount1: BigInt.fromString('66726312884609397'), +} + +const MINT_EVENT = new Mint( + 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(MINT_FIXTURE.sender)), + new ethereum.EventParam('owner', ethereum.Value.fromAddress(MINT_FIXTURE.owner)), + new ethereum.EventParam('tickLower', ethereum.Value.fromI32(MINT_FIXTURE.tickLower)), + new ethereum.EventParam('tickUpper', ethereum.Value.fromI32(MINT_FIXTURE.tickUpper)), + new ethereum.EventParam('amount', ethereum.Value.fromUnsignedBigInt(MINT_FIXTURE.amount)), + new ethereum.EventParam('amount0', ethereum.Value.fromUnsignedBigInt(MINT_FIXTURE.amount0)), + new ethereum.EventParam('amount1', ethereum.Value.fromUnsignedBigInt(MINT_FIXTURE.amount1)), + ], + MOCK_EVENT.receipt, +) + +describe('handleMint', () => { + beforeAll(() => { + createTestPool( + 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 - mint event, pool tick is between tickUpper and tickLower', () => { + // put the pools tick in range + const pool = Pool.load(USDC_WETH_03_MAINNET_POOL)! + pool.tick = BigInt.fromI32(MINT_FIXTURE.tickLower + MINT_FIXTURE.tickUpper).div(BigInt.fromI32(2)) + pool.save() + + handleMintHelper(MINT_EVENT, FACTORY_ADDRESS) + + const amountToken0 = convertTokenToDecimal(MINT_FIXTURE.amount0, BigInt.fromString(USDC_MAINNET_FIXTURE.decimals)) + const amountToken1 = convertTokenToDecimal(MINT_FIXTURE.amount1, BigInt.fromString(WETH_MAINNET_FIXTURE.decimals)) + const poolTotalValueLockedETH = amountToken0 + .times(TEST_USDC_DERIVED_ETH) + .plus(amountToken1.times(TEST_WETH_DERIVED_ETH)) + const poolTotalValueLockedUSD = poolTotalValueLockedETH.times(TEST_ETH_PRICE_USD) + + assertObjectMatches('Factory', FACTORY_ADDRESS, [ + ['txCount', '1'], + ['totalValueLockedETH', poolTotalValueLockedETH.toString()], + ['totalValueLockedUSD', poolTotalValueLockedUSD.toString()], + ]) + + assertObjectMatches('Pool', USDC_WETH_03_MAINNET_POOL, [ + ['txCount', '1'], + ['liquidity', MINT_FIXTURE.amount.toString()], + ['totalValueLockedToken0', amountToken0.toString()], + ['totalValueLockedToken1', amountToken1.toString()], + ['totalValueLockedETH', poolTotalValueLockedETH.toString()], + ['totalValueLockedUSD', poolTotalValueLockedUSD.toString()], + ]) + + assertObjectMatches('Token', USDC_MAINNET_FIXTURE.address, [ + ['txCount', '1'], + ['totalValueLocked', amountToken0.toString()], + ['totalValueLockedUSD', amountToken0.times(TEST_USDC_DERIVED_ETH.times(TEST_ETH_PRICE_USD)).toString()], + ]) + + assertObjectMatches('Token', WETH_MAINNET_FIXTURE.address, [ + ['txCount', '1'], + ['totalValueLocked', amountToken1.toString()], + ['totalValueLockedUSD', amountToken1.times(TEST_WETH_DERIVED_ETH.times(TEST_ETH_PRICE_USD)).toString()], + ]) + + assertObjectMatches('Mint', 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], + ['owner', MINT_FIXTURE.owner.toHexString()], + ['sender', MINT_FIXTURE.sender.toHexString()], + ['origin', MOCK_EVENT.transaction.from.toHexString()], + ['amount', MINT_FIXTURE.amount.toString()], + ['amount0', amountToken0.toString()], + ['amount1', amountToken1.toString()], + ['amountUSD', poolTotalValueLockedUSD.toString()], + ['tickUpper', MINT_FIXTURE.tickUpper.toString()], + ['tickLower', MINT_FIXTURE.tickLower.toString()], + ['logIndex', MOCK_EVENT.logIndex.toString()], + ]) + + const lowerTickPrice = fastExponentiation(BigDecimal.fromString('1.0001'), MINT_FIXTURE.tickLower) + assertObjectMatches('Tick', USDC_WETH_03_MAINNET_POOL + '#' + MINT_FIXTURE.tickLower.toString(), [ + ['tickIdx', MINT_FIXTURE.tickLower.toString()], + ['pool', USDC_WETH_03_MAINNET_POOL], + ['poolAddress', USDC_WETH_03_MAINNET_POOL], + ['createdAtTimestamp', MOCK_EVENT.block.timestamp.toString()], + ['createdAtBlockNumber', MOCK_EVENT.block.number.toString()], + ['liquidityGross', MINT_FIXTURE.amount.toString()], + ['liquidityNet', MINT_FIXTURE.amount.toString()], + ['price0', lowerTickPrice.toString()], + ['price1', safeDiv(ONE_BD, lowerTickPrice).toString()], + ]) + + const upperTickPrice = fastExponentiation(BigDecimal.fromString('1.0001'), MINT_FIXTURE.tickUpper) + assertObjectMatches('Tick', USDC_WETH_03_MAINNET_POOL + '#' + MINT_FIXTURE.tickUpper.toString(), [ + ['tickIdx', MINT_FIXTURE.tickUpper.toString()], + ['pool', USDC_WETH_03_MAINNET_POOL], + ['poolAddress', USDC_WETH_03_MAINNET_POOL], + ['createdAtTimestamp', MOCK_EVENT.block.timestamp.toString()], + ['createdAtBlockNumber', MOCK_EVENT.block.number.toString()], + ['liquidityGross', MINT_FIXTURE.amount.toString()], + ['liquidityNet', MINT_FIXTURE.amount.neg().toString()], + ['price0', upperTickPrice.toString()], + ['price1', safeDiv(ONE_BD, upperTickPrice).toString()], + ]) + }) + + test('success - mint event, pool tick is not between tickUpper and tickLower', () => { + // put the pools tick out of range + const pool = Pool.load(USDC_WETH_03_MAINNET_POOL)! + pool.tick = BigInt.fromI32(MINT_FIXTURE.tickLower - 1) + const liquidityBeforeMint = pool.liquidity + pool.save() + + handleMintHelper(MINT_EVENT, FACTORY_ADDRESS) + + // liquidity should not be updated + assertObjectMatches('Pool', USDC_WETH_03_MAINNET_POOL, [['liquidity', liquidityBeforeMint.toString()]]) + }) +}) diff --git a/tests/handlePoolCreated.test.ts b/tests/handlePoolCreated.test.ts index 8c88f406..801a67f6 100644 --- a/tests/handlePoolCreated.test.ts +++ b/tests/handlePoolCreated.test.ts @@ -31,7 +31,7 @@ describe('handlePoolCreated', () => { WETH_MAINNET_FIXTURE, USDC_WETH_03_MAINNET_POOL, POOL_FEE_TIER_03, - POOL_TICK_SPACING_03 + POOL_TICK_SPACING_03, ) assertObjectMatches('Factory', FACTORY_ADDRESS, [