Skip to content

Commit

Permalink
add unit tests for burns, mints, collects
Browse files Browse the repository at this point in the history
  • Loading branch information
mzywang committed May 21, 2024
1 parent 87cfa3d commit 01f3ae7
Show file tree
Hide file tree
Showing 11 changed files with 570 additions and 16 deletions.
8 changes: 6 additions & 2 deletions src/mappings/pool/burn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
13 changes: 11 additions & 2 deletions src/mappings/pool/collect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
Expand Down
6 changes: 5 additions & 1 deletion src/mappings/pool/mint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
7 changes: 4 additions & 3 deletions src/utils/pricing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'))
}

Expand Down
2 changes: 1 addition & 1 deletion src/utils/staticTokenDefinition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export class StaticTokenDefinition {

export const getStaticDefinition = (
tokenAddress: Address,
staticDefinitions: Array<StaticTokenDefinition>
staticDefinitions: Array<StaticTokenDefinition>,
): StaticTokenDefinition | null => {
const tokenAddressHex = tokenAddress.toHexString()

Expand Down
6 changes: 3 additions & 3 deletions src/utils/token.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
10 changes: 7 additions & 3 deletions tests/constants.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -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 = (
Expand All @@ -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)
Expand All @@ -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)])
Expand Down
194 changes: 194 additions & 0 deletions tests/handleBurn.test.ts
Original file line number Diff line number Diff line change
@@ -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()]])
})
})
Loading

0 comments on commit 01f3ae7

Please sign in to comment.