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/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/tests/constants.ts b/tests/constants.ts index 36b8c446..f4ce6e0b 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 = ( diff --git a/tests/handleBurn.test.ts b/tests/handleBurn.test.ts new file mode 100644 index 00000000..523d2792 --- /dev/null +++ b/tests/handleBurn.test.ts @@ -0,0 +1,201 @@ +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) + + 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('Transaction', MOCK_EVENT.transaction.hash.toHexString(), [ + ['blockNumber', MOCK_EVENT.block.number.toString()], + ['timestamp', MOCK_EVENT.block.timestamp.toString()], + ['gasUsed', '0'], + ['gasPrice', MOCK_EVENT.transaction.gasPrice.toString()], + ]) + + 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) + + // liquidity should not be updated + assertObjectMatches('Pool', USDC_WETH_03_MAINNET_POOL, [['liquidity', liquidityBeforeBurn.toString()]]) + }) +}) diff --git a/tests/handleMint.test.ts b/tests/handleMint.test.ts new file mode 100644 index 00000000..65902ede --- /dev/null +++ b/tests/handleMint.test.ts @@ -0,0 +1,194 @@ +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) + + 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('Transaction', MOCK_EVENT.transaction.hash.toHexString(), [ + ['blockNumber', MOCK_EVENT.block.number.toString()], + ['timestamp', MOCK_EVENT.block.timestamp.toString()], + ['gasUsed', '0'], + ['gasPrice', MOCK_EVENT.transaction.gasPrice.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) + + // liquidity should not be updated + assertObjectMatches('Pool', USDC_WETH_03_MAINNET_POOL, [['liquidity', liquidityBeforeMint.toString()]]) + }) +})