From 52b469d444669b64fb2260f5fdfef0a112c2b3b6 Mon Sep 17 00:00:00 2001 From: Robert Seitz Date: Mon, 11 Aug 2025 22:21:37 +0200 Subject: [PATCH 1/3] backfill zora pools which have non-null whitelisted pools --- src/mappings/swap.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/mappings/swap.ts b/src/mappings/swap.ts index ccb9ea3..92bb108 100644 --- a/src/mappings/swap.ts +++ b/src/mappings/swap.ts @@ -65,6 +65,21 @@ export function handleSwapHelper(event: SwapEvent, subgraphConfig: SubgraphConfi } } + // backfill zora pools which have non-null whitelisted pools + // Creator: 0xd61a675f8a0c67a73dc3b54fb7318b4d91409040 + // Content: 0x9ea932730a7787000042e34390b8e435dd839040 + if ( + pool.hooks === '0xd61a675f8a0c67a73dc3b54fb7318b4d91409040' || + pool.hooks === '0x9ea932730a7787000042e34390b8e435dd839040' + ) { + if (!token0.whitelistPools.includes(pool.id)) { + token0.whitelistPools.push(pool.id) + } + if (!token1.whitelistPools.includes(pool.id)) { + token1.whitelistPools.push(pool.id) + } + } + // 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')) From 1ac68275a27ddeb1c62433be6b9c309b94e1ba0e Mon Sep 17 00:00:00 2001 From: Robert Seitz Date: Tue, 12 Aug 2025 18:48:48 +0200 Subject: [PATCH 2/3] fix zora backfill, add content coin logic in initialize pools --- src/mappings/poolManager.ts | 15 ++++++++++++++ src/mappings/swap.ts | 39 +++++++++++++++++++++---------------- 2 files changed, 37 insertions(+), 17 deletions(-) diff --git a/src/mappings/poolManager.ts b/src/mappings/poolManager.ts index 4462219..834322f 100644 --- a/src/mappings/poolManager.ts +++ b/src/mappings/poolManager.ts @@ -129,6 +129,21 @@ export function handleInitializeHelper( token0.whitelistPools = newPools } + // if pool is a zora content token, add it to the whitelist pools of the content token + // assumes content token pool is always the first pool created for a content token + if (pool.hooks === '0x9ea932730a7787000042e34390b8e435dd839040') { + let contentToken: Token | null = null + if (token0.whitelistPools.length === 0 && token1.whitelistPools.length > 0) { + contentToken = token0 + } + if (token1.whitelistPools.length === 0 && token0.whitelistPools.length > 0) { + contentToken = token1 + } + if (contentToken) { + contentToken.whitelistPools = [pool.id] + } + } + pool.token0 = token0.id pool.token1 = token1.id pool.feeTier = BigInt.fromI32(event.params.fee) diff --git a/src/mappings/swap.ts b/src/mappings/swap.ts index 92bb108..1263c8c 100644 --- a/src/mappings/swap.ts +++ b/src/mappings/swap.ts @@ -47,36 +47,41 @@ export function handleSwapHelper(event: SwapEvent, subgraphConfig: SubgraphConfi const token1 = Token.load(pool.token1) if (token0 && token1) { - // Backfill emppty tokens' whitelist pools to account for grafting to a point later than the block where the pool was initialized - if (token0.whitelistPools === null) { - if (whitelistTokens.includes(token1.id)) { + // backfill zora pools which have non-null whitelisted pools + // Creator: 0xd61a675f8a0c67a73dc3b54fb7318b4d91409040 + // Content: 0x9ea932730a7787000042e34390b8e435dd839040 + + // find the content token + // add the pool to the content token's whitelist pool + // but not add it to the creator token's whitelist pool + // because the creator token would get a huge amount of whitelisted pools and slow things down + if (pool.hooks === '0xd61a675f8a0c67a73dc3b54fb7318b4d91409040') { + // Backfill emppty tokens' whitelist pools to account for grafting to a point later than the block where the pool was initialized + if (whitelistTokens.includes(token1.id) && !token0.whitelistPools.includes(pool.id)) { const newPools = token0.whitelistPools newPools.push(pool.id) token0.whitelistPools = newPools } - } - if (token1.whitelistPools === null) { // update white listed pools - if (whitelistTokens.includes(token0.id)) { + if (whitelistTokens.includes(token0.id) && !token1.whitelistPools.includes(pool.id)) { const newPools = token1.whitelistPools newPools.push(pool.id) token1.whitelistPools = newPools } } - // backfill zora pools which have non-null whitelisted pools - // Creator: 0xd61a675f8a0c67a73dc3b54fb7318b4d91409040 - // Content: 0x9ea932730a7787000042e34390b8e435dd839040 - if ( - pool.hooks === '0xd61a675f8a0c67a73dc3b54fb7318b4d91409040' || - pool.hooks === '0x9ea932730a7787000042e34390b8e435dd839040' - ) { - if (!token0.whitelistPools.includes(pool.id)) { - token0.whitelistPools.push(pool.id) + // backfill whitelisted pools for content tokens + if (pool.hooks === '0x9ea932730a7787000042e34390b8e435dd839040') { + let contentToken: Token | null = null + if (token0.whitelistPools.length === 0 && token1.whitelistPools.length > 0) { + contentToken = token0 + } + if (token1.whitelistPools.length === 0 && token0.whitelistPools.length > 0) { + contentToken = token1 } - if (!token1.whitelistPools.includes(pool.id)) { - token1.whitelistPools.push(pool.id) + if (contentToken) { + contentToken.whitelistPools = [pool.id] } } From a9af324548387f70a12a16668f746139c606e3b3 Mon Sep 17 00:00:00 2001 From: matteen Date: Tue, 12 Aug 2025 14:37:15 -0400 Subject: [PATCH 3/3] tests and fixes --- src/mappings/poolManager.ts | 10 +- src/mappings/swap.ts | 8 +- tests/handlers/handleInitialize.test.ts | 227 +++++++- .../handlers/handleZoraContentTokens.test.ts | 514 ++++++++++++++++++ 4 files changed, 751 insertions(+), 8 deletions(-) create mode 100644 tests/handlers/handleZoraContentTokens.test.ts diff --git a/src/mappings/poolManager.ts b/src/mappings/poolManager.ts index 834322f..7153a3a 100644 --- a/src/mappings/poolManager.ts +++ b/src/mappings/poolManager.ts @@ -131,16 +131,18 @@ export function handleInitializeHelper( // if pool is a zora content token, add it to the whitelist pools of the content token // assumes content token pool is always the first pool created for a content token - if (pool.hooks === '0x9ea932730a7787000042e34390b8e435dd839040') { + if (event.params.hooks.toHexString() == '0x9ea932730a7787000042e34390b8e435dd839040') { let contentToken: Token | null = null - if (token0.whitelistPools.length === 0 && token1.whitelistPools.length > 0) { + if (token0.whitelistPools.length == 0 && token1.whitelistPools.length > 0) { contentToken = token0 } - if (token1.whitelistPools.length === 0 && token0.whitelistPools.length > 0) { + if (token1.whitelistPools.length == 0 && token0.whitelistPools.length > 0) { contentToken = token1 } if (contentToken) { - contentToken.whitelistPools = [pool.id] + const newPools = contentToken.whitelistPools + newPools.push(pool.id) + contentToken.whitelistPools = newPools } } diff --git a/src/mappings/swap.ts b/src/mappings/swap.ts index 1263c8c..7f1c68e 100644 --- a/src/mappings/swap.ts +++ b/src/mappings/swap.ts @@ -55,7 +55,7 @@ export function handleSwapHelper(event: SwapEvent, subgraphConfig: SubgraphConfi // add the pool to the content token's whitelist pool // but not add it to the creator token's whitelist pool // because the creator token would get a huge amount of whitelisted pools and slow things down - if (pool.hooks === '0xd61a675f8a0c67a73dc3b54fb7318b4d91409040') { + if (pool.hooks == '0xd61a675f8a0c67a73dc3b54fb7318b4d91409040') { // Backfill emppty tokens' whitelist pools to account for grafting to a point later than the block where the pool was initialized if (whitelistTokens.includes(token1.id) && !token0.whitelistPools.includes(pool.id)) { const newPools = token0.whitelistPools @@ -72,7 +72,7 @@ export function handleSwapHelper(event: SwapEvent, subgraphConfig: SubgraphConfi } // backfill whitelisted pools for content tokens - if (pool.hooks === '0x9ea932730a7787000042e34390b8e435dd839040') { + if (pool.hooks == '0x9ea932730a7787000042e34390b8e435dd839040') { let contentToken: Token | null = null if (token0.whitelistPools.length === 0 && token1.whitelistPools.length > 0) { contentToken = token0 @@ -81,7 +81,9 @@ export function handleSwapHelper(event: SwapEvent, subgraphConfig: SubgraphConfi contentToken = token1 } if (contentToken) { - contentToken.whitelistPools = [pool.id] + const newPools = contentToken.whitelistPools + newPools.push(pool.id) + contentToken.whitelistPools = newPools } } diff --git a/tests/handlers/handleInitialize.test.ts b/tests/handlers/handleInitialize.test.ts index 2e39f2c..9760a8e 100644 --- a/tests/handlers/handleInitialize.test.ts +++ b/tests/handlers/handleInitialize.test.ts @@ -3,7 +3,7 @@ import { assert, beforeEach, clearStore, describe, test } from 'matchstick-as' import { handleInitializeHelper } from '../../src/mappings/poolManager' import { Initialize } from '../../src/types/PoolManager/PoolManager' -import { Bundle, Pool } from '../../src/types/schema' +import { Bundle, Pool, Token } from '../../src/types/schema' import { ADDRESS_ZERO } from '../../src/utils/constants' import { safeDiv } from '../../src/utils/index' import { findNativePerToken, getNativePriceInUSD, sqrtPriceX96ToTokenPrices } from '../../src/utils/pricing' @@ -258,3 +258,228 @@ describe('findNativePerToken', () => { assert.assertTrue(ethPerToken == BigDecimal.fromString('0')) }) }) + +describe('handleInitialize - Zora Content Token Logic', () => { + // Zora content token test constants + const ZORA_CREATOR_TOKEN_ADDRESS = '0x1234567890123456789012345678901234567890' + const ZORA_CONTENT_TOKEN_ADDRESS = '0x0987654321098765432109876543210987654321' + const ZORA_CONTENT_HOOK = '0x9ea932730a7787000042e34390b8e435dd839040' + const ZORA_POOL_ID = '0x5555555555555555555555555555555555555555555555555555555555555555' + + beforeEach(() => { + clearStore() + + const bundle = new Bundle('1') + bundle.ethPriceUSD = TEST_ETH_PRICE_USD + bundle.save() + }) + + test('content token gets whitelisted when Zora pool is initialized', () => { + // Create creator token (has existing whitelist pools) + const creatorToken = createAndStoreTestToken({ + address: ZORA_CREATOR_TOKEN_ADDRESS, + symbol: 'CREATOR', + name: 'Creator Token', + totalSupply: '1000000', + decimals: '18', + balanceOf: '1000', + }) + creatorToken.whitelistPools = [USDC_WETH_POOL_ID] + creatorToken.save() + + // Create content token (no whitelist pools initially) + const contentToken = createAndStoreTestToken({ + address: ZORA_CONTENT_TOKEN_ADDRESS, + symbol: 'CONTENT', + name: 'Content Token', + totalSupply: '1000000', + decimals: '18', + balanceOf: '1000', + }) + contentToken.whitelistPools = [] + contentToken.save() + + // Create initialize event for Zora content token pool + const zoraInitializeEvent = new Initialize( + MOCK_EVENT.address, + MOCK_EVENT.logIndex, + MOCK_EVENT.transactionLogIndex, + MOCK_EVENT.logType, + MOCK_EVENT.block, + MOCK_EVENT.transaction, + [ + new ethereum.EventParam('id', ethereum.Value.fromFixedBytes(Bytes.fromHexString(ZORA_POOL_ID))), + new ethereum.EventParam( + 'currency0', + ethereum.Value.fromAddress(Address.fromString(ZORA_CONTENT_TOKEN_ADDRESS)), + ), + new ethereum.EventParam( + 'currency1', + ethereum.Value.fromAddress(Address.fromString(ZORA_CREATOR_TOKEN_ADDRESS)), + ), + new ethereum.EventParam('fee', ethereum.Value.fromI32(500)), + new ethereum.EventParam('tickSpacing', ethereum.Value.fromI32(10)), + new ethereum.EventParam('hooks', ethereum.Value.fromAddress(Address.fromString(ZORA_CONTENT_HOOK))), + new ethereum.EventParam( + 'sqrtPriceX96', + ethereum.Value.fromUnsignedBigInt(BigInt.fromString('79228162514264337593543950336')), + ), + new ethereum.EventParam('tick', ethereum.Value.fromI32(0)), + ], + MOCK_EVENT.receipt, + ) + + // Before initialization: content token should have no whitelist pools + const contentTokenBefore = Token.load(ZORA_CONTENT_TOKEN_ADDRESS)! + assert.assertTrue(contentTokenBefore.whitelistPools.length == 0) + + // Execute initialization + handleInitializeHelper(zoraInitializeEvent, TEST_CONFIG) + + // After initialization: content token should be whitelisted + const contentTokenAfter = Token.load(ZORA_CONTENT_TOKEN_ADDRESS)! + assert.assertTrue(contentTokenAfter.whitelistPools.length == 1) + // Use array includes helper to work around string comparison issues + assert.assertTrue(arrayIncludesString(contentTokenAfter.whitelistPools, ZORA_POOL_ID)) + + // Creator token whitelist should remain unchanged + const creatorTokenAfter = Token.load(ZORA_CREATOR_TOKEN_ADDRESS)! + assert.assertTrue(creatorTokenAfter.whitelistPools.length == 1) + assert.assertTrue(creatorTokenAfter.whitelistPools[0] == USDC_WETH_POOL_ID) + }) + + test('content token as token1 gets whitelisted when Zora pool is initialized', () => { + // Test reverse scenario where content token is token1, creator is token0 + const CREATOR_TOKEN_1_ADDRESS = '0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb' + const CONTENT_TOKEN_1_ADDRESS = '0xcccccccccccccccccccccccccccccccccccccccc' + const ZORA_POOL_ID_1 = '0x6666666666666666666666666666666666666666666666666666666666666666' + + // Create creator token + const creatorToken1 = createAndStoreTestToken({ + address: CREATOR_TOKEN_1_ADDRESS, + symbol: 'CREATOR1', + name: 'Creator Token 1', + totalSupply: '1000000', + decimals: '18', + balanceOf: '1000', + }) + creatorToken1.whitelistPools = [USDC_WETH_POOL_ID] + creatorToken1.save() + + // Create content token + const contentToken1 = createAndStoreTestToken({ + address: CONTENT_TOKEN_1_ADDRESS, + symbol: 'CONTENT1', + name: 'Content Token 1', + totalSupply: '1000000', + decimals: '18', + balanceOf: '1000', + }) + contentToken1.whitelistPools = [] + contentToken1.save() + + // Create initialize event with creator as token0, content as token1 + const zoraInitializeEvent1 = new Initialize( + MOCK_EVENT.address, + MOCK_EVENT.logIndex, + MOCK_EVENT.transactionLogIndex, + MOCK_EVENT.logType, + MOCK_EVENT.block, + MOCK_EVENT.transaction, + [ + new ethereum.EventParam('id', ethereum.Value.fromFixedBytes(Bytes.fromHexString(ZORA_POOL_ID_1))), + new ethereum.EventParam('currency0', ethereum.Value.fromAddress(Address.fromString(CREATOR_TOKEN_1_ADDRESS))), + new ethereum.EventParam('currency1', ethereum.Value.fromAddress(Address.fromString(CONTENT_TOKEN_1_ADDRESS))), + new ethereum.EventParam('fee', ethereum.Value.fromI32(500)), + new ethereum.EventParam('tickSpacing', ethereum.Value.fromI32(10)), + new ethereum.EventParam('hooks', ethereum.Value.fromAddress(Address.fromString(ZORA_CONTENT_HOOK))), + new ethereum.EventParam( + 'sqrtPriceX96', + ethereum.Value.fromUnsignedBigInt(BigInt.fromString('79228162514264337593543950336')), + ), + new ethereum.EventParam('tick', ethereum.Value.fromI32(0)), + ], + MOCK_EVENT.receipt, + ) + + // Execute initialization + handleInitializeHelper(zoraInitializeEvent1, TEST_CONFIG) + + // Content token (token1) should be whitelisted + const contentToken1After = Token.load(CONTENT_TOKEN_1_ADDRESS)! + assert.assertTrue(contentToken1After.whitelistPools.length == 1) + assert.assertTrue(arrayIncludesString(contentToken1After.whitelistPools, ZORA_POOL_ID_1)) + }) + + test('non-Zora pools do not affect content token whitelist during initialization', () => { + // Create tokens + const token0 = createAndStoreTestToken({ + address: ZORA_CONTENT_TOKEN_ADDRESS, + symbol: 'CONTENT', + name: 'Content Token', + totalSupply: '1000000', + decimals: '18', + balanceOf: '1000', + }) + token0.whitelistPools = [] + token0.save() + + const token1 = createAndStoreTestToken({ + address: ZORA_CREATOR_TOKEN_ADDRESS, + symbol: 'CREATOR', + name: 'Creator Token', + totalSupply: '1000000', + decimals: '18', + balanceOf: '1000', + }) + token1.whitelistPools = [USDC_WETH_POOL_ID] + token1.save() + + // Create regular pool (no hook) with same tokens + const REGULAR_POOL_ID = '0x7777777777777777777777777777777777777777777777777777777777777777' + const regularInitializeEvent = new Initialize( + MOCK_EVENT.address, + MOCK_EVENT.logIndex, + MOCK_EVENT.transactionLogIndex, + MOCK_EVENT.logType, + MOCK_EVENT.block, + MOCK_EVENT.transaction, + [ + new ethereum.EventParam('id', ethereum.Value.fromFixedBytes(Bytes.fromHexString(REGULAR_POOL_ID))), + new ethereum.EventParam( + 'currency0', + ethereum.Value.fromAddress(Address.fromString(ZORA_CONTENT_TOKEN_ADDRESS)), + ), + new ethereum.EventParam( + 'currency1', + ethereum.Value.fromAddress(Address.fromString(ZORA_CREATOR_TOKEN_ADDRESS)), + ), + new ethereum.EventParam('fee', ethereum.Value.fromI32(500)), + new ethereum.EventParam('tickSpacing', ethereum.Value.fromI32(10)), + new ethereum.EventParam('hooks', ethereum.Value.fromAddress(Address.fromString(ADDRESS_ZERO))), // No hook + new ethereum.EventParam( + 'sqrtPriceX96', + ethereum.Value.fromUnsignedBigInt(BigInt.fromString('79228162514264337593543950336')), + ), + new ethereum.EventParam('tick', ethereum.Value.fromI32(0)), + ], + MOCK_EVENT.receipt, + ) + + // Execute initialization + handleInitializeHelper(regularInitializeEvent, TEST_CONFIG) + + // Content token whitelist should remain empty (regular pool shouldn't be added) + const contentTokenAfter = Token.load(ZORA_CONTENT_TOKEN_ADDRESS)! + assert.assertTrue(contentTokenAfter.whitelistPools.length == 0) + }) +}) + +function arrayIncludesString(array: string[], item: string): boolean { + for (let i = 0; i < array.length; i++) { + if (array[i] == item) { + return true + } + } + return false +} diff --git a/tests/handlers/handleZoraContentTokens.test.ts b/tests/handlers/handleZoraContentTokens.test.ts new file mode 100644 index 0000000..a5a8abf --- /dev/null +++ b/tests/handlers/handleZoraContentTokens.test.ts @@ -0,0 +1,514 @@ +// tests/handlers/handleZoraContentTokens.test.ts + +import { Address, BigDecimal, BigInt, Bytes, ethereum } from '@graphprotocol/graph-ts' +import { assert, beforeAll, beforeEach, describe, log, test } from 'matchstick-as' + +import { handleSwapHelper } from '../../src/mappings/swap' +import { Swap } from '../../src/types/PoolManager/PoolManager' +import { Bundle, Pool, Token } from '../../src/types/schema' +import { ZERO_BD, ZERO_BI } from '../../src/utils/constants' +import { findNativePerToken } from '../../src/utils/pricing' +import { + invokePoolCreatedWithMockedEthCalls, + MOCK_EVENT, + TEST_CONFIG, + TEST_ETH_PRICE_USD, + USDC_WETH_POOL_ID, +} from './constants' // Zora content token test constants +const CREATOR_TOKEN_ADDRESS = '0x1234567890123456789012345678901234567890' +const CONTENT_TOKEN_ADDRESS = '0x0987654321098765432109876543210987654321' +const ZORA_CONTENT_HOOK = '0x9ea932730a7787000042e34390b8e435dd839040' +const ZORA_POOL_ID = '0x5555555555555555555555555555555555555555555555555555555555555555' + +class ZoraSwapFixture { + id: string + sender: Address + amount0: BigInt + amount1: BigInt + sqrtPriceX96: BigInt + liquidity: BigInt + tick: number + fee: number +} + +const ZORA_SWAP_FIXTURE: ZoraSwapFixture = { + id: ZORA_POOL_ID, + sender: Address.fromString('0x841B5A0b3DBc473c8A057E2391014aa4C4751351'), + amount0: BigInt.fromString('-100'), // 100 content tokens out + amount1: BigInt.fromString('1'), // 1 creator token in (scaled for decimals) + sqrtPriceX96: BigInt.fromString('79228162514264337593543950336'), + liquidity: BigInt.fromString('1000000000000000000'), + tick: 0, + fee: 500, +} + +describe('Zora Content Token Pricing', () => { + beforeAll(() => { + invokePoolCreatedWithMockedEthCalls(MOCK_EVENT, TEST_CONFIG) + // Set up bundle + const bundle = new Bundle('1') + bundle.ethPriceUSD = TEST_ETH_PRICE_USD + bundle.save() + + // Create creator token (whitelisted with established pricing) + const creatorToken = new Token(CREATOR_TOKEN_ADDRESS) + creatorToken.symbol = 'CREATOR' + creatorToken.name = 'Creator Token' + creatorToken.decimals = BigInt.fromI32(18) + creatorToken.totalSupply = BigInt.fromString('1000000000000000000000000') + creatorToken.derivedETH = BigDecimal.fromString('0.001') // Has established pricing + creatorToken.volume = ZERO_BD + creatorToken.volumeUSD = ZERO_BD + creatorToken.untrackedVolumeUSD = ZERO_BD + creatorToken.feesUSD = ZERO_BD + creatorToken.totalValueLocked = ZERO_BD + creatorToken.totalValueLockedUSD = ZERO_BD + creatorToken.totalValueLockedUSDUntracked = ZERO_BD + creatorToken.txCount = ZERO_BI + creatorToken.poolCount = ZERO_BI + creatorToken.whitelistPools = [USDC_WETH_POOL_ID] // Has whitelist pools + creatorToken.save() + + // Create content token (not whitelisted initially) + const contentToken = new Token(CONTENT_TOKEN_ADDRESS) + contentToken.symbol = 'CONTENT' + contentToken.name = 'Content Token' + contentToken.decimals = BigInt.fromI32(18) + contentToken.totalSupply = BigInt.fromString('1000000000000000000000000') + contentToken.derivedETH = ZERO_BD // No pricing initially + contentToken.volume = ZERO_BD + contentToken.volumeUSD = ZERO_BD + contentToken.untrackedVolumeUSD = ZERO_BD + contentToken.feesUSD = ZERO_BD + contentToken.totalValueLocked = ZERO_BD + contentToken.totalValueLockedUSD = ZERO_BD + contentToken.totalValueLockedUSDUntracked = ZERO_BD + contentToken.txCount = ZERO_BI + contentToken.poolCount = ZERO_BI + contentToken.whitelistPools = [] // No whitelist pools initially + contentToken.save() + + // Create Zora content token pool + const zoraPool = new Pool(ZORA_POOL_ID) + zoraPool.token0 = CONTENT_TOKEN_ADDRESS + zoraPool.token1 = CREATOR_TOKEN_ADDRESS + zoraPool.feeTier = BigInt.fromI32(500) + zoraPool.hooks = ZORA_CONTENT_HOOK // This is the key - Zora content hook + zoraPool.tickSpacing = BigInt.fromI32(10) + zoraPool.createdAtTimestamp = BigInt.fromI32(1234567890) + zoraPool.createdAtBlockNumber = BigInt.fromI32(12345) + zoraPool.liquidityProviderCount = ZERO_BI + zoraPool.txCount = ZERO_BI + zoraPool.liquidity = BigInt.fromString('1000000000000000000') + zoraPool.sqrtPrice = BigInt.fromString('79228162514264337593543950336') + zoraPool.token0Price = ZERO_BD + zoraPool.token1Price = ZERO_BD + zoraPool.observationIndex = ZERO_BI + zoraPool.totalValueLockedToken0 = BigDecimal.fromString('1000') // 1000 content tokens + zoraPool.totalValueLockedToken1 = BigDecimal.fromString('1') // 1 creator token + zoraPool.totalValueLockedUSD = ZERO_BD + zoraPool.totalValueLockedETH = ZERO_BD + zoraPool.totalValueLockedUSDUntracked = ZERO_BD + zoraPool.volumeToken0 = ZERO_BD + zoraPool.volumeToken1 = ZERO_BD + zoraPool.volumeUSD = ZERO_BD + zoraPool.untrackedVolumeUSD = ZERO_BD + zoraPool.feesUSD = ZERO_BD + zoraPool.collectedFeesToken0 = ZERO_BD + zoraPool.collectedFeesToken1 = ZERO_BD + zoraPool.collectedFeesUSD = ZERO_BD + zoraPool.tick = BigInt.fromI32(0) + zoraPool.save() + }) + + beforeEach(() => { + // Reset content tokens to initial state + const contentToken = Token.load(CONTENT_TOKEN_ADDRESS)! + contentToken.whitelistPools = [] + contentToken.derivedETH = ZERO_BD + contentToken.save() + }) + + test('content token gets whitelisted and priced on first swap', () => { + // Create swap event for the Zora pool + const zoraSwapEvent = new Swap( + MOCK_EVENT.address, + MOCK_EVENT.logIndex, + MOCK_EVENT.transactionLogIndex, + MOCK_EVENT.logType, + MOCK_EVENT.block, + MOCK_EVENT.transaction, + [ + new ethereum.EventParam('id', ethereum.Value.fromFixedBytes(Bytes.fromHexString(ZORA_SWAP_FIXTURE.id))), + new ethereum.EventParam('sender', ethereum.Value.fromAddress(ZORA_SWAP_FIXTURE.sender)), + new ethereum.EventParam('amount0', ethereum.Value.fromSignedBigInt(ZORA_SWAP_FIXTURE.amount0)), + new ethereum.EventParam('amount1', ethereum.Value.fromSignedBigInt(ZORA_SWAP_FIXTURE.amount1)), + new ethereum.EventParam('sqrtPriceX96', ethereum.Value.fromSignedBigInt(ZORA_SWAP_FIXTURE.sqrtPriceX96)), + new ethereum.EventParam('liquidity', ethereum.Value.fromSignedBigInt(ZORA_SWAP_FIXTURE.liquidity)), + new ethereum.EventParam('tick', ethereum.Value.fromI32(ZORA_SWAP_FIXTURE.tick as i32)), + new ethereum.EventParam('fee', ethereum.Value.fromI32(ZORA_SWAP_FIXTURE.fee as i32)), + ], + MOCK_EVENT.receipt, + ) + + // Before swap: content token should have no whitelist pools and zero derivedETH + const contentTokenBefore = Token.load(CONTENT_TOKEN_ADDRESS)! + assert.assertTrue(contentTokenBefore.whitelistPools.length == 0) + assert.assertTrue(contentTokenBefore.derivedETH.equals(ZERO_BD)) + + // Execute the swap + handleSwapHelper(zoraSwapEvent, TEST_CONFIG) + + // Fix creator token pricing - in real world it would have established pricing + // but in test environment the USDC/WETH pool isn't properly set up + const creatorTokenFixed = Token.load(CREATOR_TOKEN_ADDRESS)! + creatorTokenFixed.derivedETH = BigDecimal.fromString('0.001') + creatorTokenFixed.save() + + // Re-run pricing for content token now that creator token has proper derivedETH + const contentTokenToUpdate = Token.load(CONTENT_TOKEN_ADDRESS)! + contentTokenToUpdate.derivedETH = findNativePerToken( + contentTokenToUpdate, + TEST_CONFIG.wrappedNativeAddress, + TEST_CONFIG.stablecoinAddresses, + TEST_CONFIG.minimumNativeLocked, + ) + contentTokenToUpdate.save() + + // After swap: content token should be whitelisted and have derived pricing + const contentTokenAfter = Token.load(CONTENT_TOKEN_ADDRESS)! + const creatorTokenAfter = Token.load(CREATOR_TOKEN_ADDRESS)! + + log.info('Creator token derivedETH after swap: {}', [creatorTokenAfter.derivedETH.toString()]) + log.info('Content token derivedETH after swap: {}', [contentTokenAfter.derivedETH.toString()]) + log.info('Test: contentTokenAfter.whitelistPools.length: {}, checking for: {}', [ + contentTokenAfter.whitelistPools.length.toString(), + ZORA_POOL_ID, + ]) + if (contentTokenAfter.whitelistPools.length > 0) { + log.info('Test: first item in whitelist: {}', [contentTokenAfter.whitelistPools[0]]) + } + + // Content token should now have the pool in its whitelist + assert.assertTrue(contentTokenAfter.whitelistPools.length == 1) + + // Most importantly: Content token should now have derived ETH pricing through standard mechanism + // This proves the Zora whitelist logic is working + assert.assertTrue(contentTokenAfter.derivedETH.gt(ZERO_BD)) + + // Creator token whitelist should remain unchanged (it already had whitelist pools) + assert.assertTrue(creatorTokenAfter.whitelistPools.length == 1) + assert.assertTrue(arrayIncludes(creatorTokenAfter.whitelistPools, USDC_WETH_POOL_ID)) + }) + + test('content token pricing continues to update on subsequent swaps', () => { + // Create a second swap with different amounts to test pricing updates + const secondSwapFixture: ZoraSwapFixture = { + id: ZORA_POOL_ID, + sender: Address.fromString('0x841B5A0b3DBc473c8A057E2391014aa4C4751351'), + amount0: BigInt.fromString('-200'), // Different amount + amount1: BigInt.fromString('2'), // Different amount + sqrtPriceX96: BigInt.fromString('79228162514264337593543950336'), + liquidity: BigInt.fromString('1000000000000000000'), + tick: 0, + fee: 500, + } + + const secondSwapEvent = new Swap( + MOCK_EVENT.address, + BigInt.fromI32(1), // Different log index + MOCK_EVENT.transactionLogIndex, + MOCK_EVENT.logType, + MOCK_EVENT.block, + MOCK_EVENT.transaction, + [ + new ethereum.EventParam('id', ethereum.Value.fromFixedBytes(Bytes.fromHexString(secondSwapFixture.id))), + new ethereum.EventParam('sender', ethereum.Value.fromAddress(secondSwapFixture.sender)), + new ethereum.EventParam('amount0', ethereum.Value.fromSignedBigInt(secondSwapFixture.amount0)), + new ethereum.EventParam('amount1', ethereum.Value.fromSignedBigInt(secondSwapFixture.amount1)), + new ethereum.EventParam('sqrtPriceX96', ethereum.Value.fromSignedBigInt(secondSwapFixture.sqrtPriceX96)), + new ethereum.EventParam('liquidity', ethereum.Value.fromSignedBigInt(secondSwapFixture.liquidity)), + new ethereum.EventParam('tick', ethereum.Value.fromI32(secondSwapFixture.tick as i32)), + new ethereum.EventParam('fee', ethereum.Value.fromI32(secondSwapFixture.fee as i32)), + ], + MOCK_EVENT.receipt, + ) + + // Get pricing before second swap + const contentTokenBefore = Token.load(CONTENT_TOKEN_ADDRESS)! + const _derivedETHBefore = contentTokenBefore.derivedETH + + // Execute second swap + handleSwapHelper(secondSwapEvent, TEST_CONFIG) + + // After second swap: content token should still be whitelisted and pricing should be updated + const contentTokenAfter = Token.load(CONTENT_TOKEN_ADDRESS)! + log.info('Test: contentTokenAfter.whitelistPools.length: {}, checking for: {}', [ + contentTokenAfter.whitelistPools.length.toString(), + ZORA_POOL_ID, + ]) + if (contentTokenAfter.whitelistPools.length > 0) { + log.info('Test: first item in whitelist: {}', [contentTokenAfter.whitelistPools[0]]) + } + + // Should still be whitelisted + assert.assertTrue(contentTokenAfter.whitelistPools.length == 1) + + // Should still have pricing (could be same or different depending on pool state changes) + // This confirms the Zora logic continues to work on subsequent swaps + assert.assertTrue(contentTokenAfter.derivedETH.gt(ZERO_BD)) + }) + + test('non-zora pools do not affect content token whitelist', () => { + // Create a regular pool (non-Zora hook) with the same tokens + const REGULAR_POOL_ID = '0x6666666666666666666666666666666666666666666666666666666666666666' + + const regularPool = new Pool(REGULAR_POOL_ID) + regularPool.token0 = CONTENT_TOKEN_ADDRESS + regularPool.token1 = CREATOR_TOKEN_ADDRESS + regularPool.feeTier = BigInt.fromI32(500) + regularPool.hooks = '0x0000000000000000000000000000000000000000' // No hook + regularPool.tickSpacing = BigInt.fromI32(10) + regularPool.createdAtTimestamp = BigInt.fromI32(1234567890) + regularPool.createdAtBlockNumber = BigInt.fromI32(12345) + regularPool.liquidityProviderCount = ZERO_BI + regularPool.txCount = ZERO_BI + regularPool.liquidity = BigInt.fromString('1000000000000000000') + regularPool.sqrtPrice = BigInt.fromString('79228162514264337593543950336') + regularPool.token0Price = ZERO_BD + regularPool.token1Price = ZERO_BD + regularPool.observationIndex = ZERO_BI + regularPool.totalValueLockedToken0 = BigDecimal.fromString('1000') + regularPool.totalValueLockedToken1 = BigDecimal.fromString('1') + regularPool.totalValueLockedUSD = ZERO_BD + regularPool.totalValueLockedETH = ZERO_BD + regularPool.totalValueLockedUSDUntracked = ZERO_BD + regularPool.volumeToken0 = ZERO_BD + regularPool.volumeToken1 = ZERO_BD + regularPool.volumeUSD = ZERO_BD + regularPool.untrackedVolumeUSD = ZERO_BD + regularPool.feesUSD = ZERO_BD + regularPool.collectedFeesToken0 = ZERO_BD + regularPool.collectedFeesToken1 = ZERO_BD + regularPool.collectedFeesUSD = ZERO_BD + regularPool.tick = BigInt.fromI32(0) + regularPool.save() + + const regularSwapFixture: ZoraSwapFixture = { + id: REGULAR_POOL_ID, + sender: Address.fromString('0x841B5A0b3DBc473c8A057E2391014aa4C4751351'), + amount0: BigInt.fromString('-50'), + amount1: BigInt.fromString('1'), + sqrtPriceX96: BigInt.fromString('79228162514264337593543950336'), + liquidity: BigInt.fromString('1000000000000000000'), + tick: 0, + fee: 500, + } + + const regularSwapEvent = new Swap( + MOCK_EVENT.address, + BigInt.fromI32(2), // Different log index + MOCK_EVENT.transactionLogIndex, + MOCK_EVENT.logType, + MOCK_EVENT.block, + MOCK_EVENT.transaction, + [ + new ethereum.EventParam('id', ethereum.Value.fromFixedBytes(Bytes.fromHexString(regularSwapFixture.id))), + new ethereum.EventParam('sender', ethereum.Value.fromAddress(regularSwapFixture.sender)), + new ethereum.EventParam('amount0', ethereum.Value.fromSignedBigInt(regularSwapFixture.amount0)), + new ethereum.EventParam('amount1', ethereum.Value.fromSignedBigInt(regularSwapFixture.amount1)), + new ethereum.EventParam('sqrtPriceX96', ethereum.Value.fromSignedBigInt(regularSwapFixture.sqrtPriceX96)), + new ethereum.EventParam('liquidity', ethereum.Value.fromSignedBigInt(regularSwapFixture.liquidity)), + new ethereum.EventParam('tick', ethereum.Value.fromI32(regularSwapFixture.tick as i32)), + new ethereum.EventParam('fee', ethereum.Value.fromI32(regularSwapFixture.fee as i32)), + ], + MOCK_EVENT.receipt, + ) + + // Get content token state before regular swap + const contentTokenBefore = Token.load(CONTENT_TOKEN_ADDRESS)! + const whitelistLengthBefore = contentTokenBefore.whitelistPools.length + + // Execute swap on regular (non-Zora) pool + handleSwapHelper(regularSwapEvent, TEST_CONFIG) + + // After regular swap: content token whitelist should be unchanged + const contentTokenAfter = Token.load(CONTENT_TOKEN_ADDRESS)! + log.info('Test: contentTokenAfter.whitelistPools.length: {}, checking for: {}', [ + contentTokenAfter.whitelistPools.length.toString(), + ZORA_POOL_ID, + ]) + if (contentTokenAfter.whitelistPools.length > 0) { + log.info('Test: first item in whitelist: {}', [contentTokenAfter.whitelistPools[0]]) + } + + // Whitelist length should be the same (regular pool should not be added) + assert.assertTrue(contentTokenAfter.whitelistPools.length == whitelistLengthBefore) + + // Should not include the regular pool ID (check if any item matches) + let foundRegularPool = false + for (let i = 0; i < contentTokenAfter.whitelistPools.length; i++) { + if (contentTokenAfter.whitelistPools[i] == REGULAR_POOL_ID) { + foundRegularPool = true + break + } + } + assert.assertTrue(!foundRegularPool) + }) + + test('content token positioned as token1 also gets whitelisted', () => { + // Test the reverse scenario where content token is token1 instead of token0 + const CONTENT_TOKEN_1_ADDRESS = '0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' + const CREATOR_TOKEN_1_ADDRESS = '0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb' + const ZORA_POOL_ID_1 = '0x7777777777777777777777777777777777777777777777777777777777777777' + + // Create creator token (whitelisted) + const creatorToken1 = new Token(CREATOR_TOKEN_1_ADDRESS) + creatorToken1.symbol = 'CREATOR1' + creatorToken1.name = 'Creator Token 1' + creatorToken1.decimals = BigInt.fromI32(18) + creatorToken1.totalSupply = BigInt.fromString('1000000000000000000000000') + creatorToken1.derivedETH = BigDecimal.fromString('0.002') + creatorToken1.volume = ZERO_BD + creatorToken1.volumeUSD = ZERO_BD + creatorToken1.untrackedVolumeUSD = ZERO_BD + creatorToken1.feesUSD = ZERO_BD + creatorToken1.totalValueLocked = ZERO_BD + creatorToken1.totalValueLockedUSD = ZERO_BD + creatorToken1.totalValueLockedUSDUntracked = ZERO_BD + creatorToken1.txCount = ZERO_BI + creatorToken1.poolCount = ZERO_BI + creatorToken1.whitelistPools = [USDC_WETH_POOL_ID] + creatorToken1.save() + + // Create content token (not whitelisted) + const contentToken1 = new Token(CONTENT_TOKEN_1_ADDRESS) + contentToken1.symbol = 'CONTENT1' + contentToken1.name = 'Content Token 1' + contentToken1.decimals = BigInt.fromI32(18) + contentToken1.totalSupply = BigInt.fromString('1000000000000000000000000') + contentToken1.derivedETH = ZERO_BD + contentToken1.volume = ZERO_BD + contentToken1.volumeUSD = ZERO_BD + contentToken1.untrackedVolumeUSD = ZERO_BD + contentToken1.feesUSD = ZERO_BD + contentToken1.totalValueLocked = ZERO_BD + contentToken1.totalValueLockedUSD = ZERO_BD + contentToken1.totalValueLockedUSDUntracked = ZERO_BD + contentToken1.txCount = ZERO_BI + contentToken1.poolCount = ZERO_BI + contentToken1.whitelistPools = [] + contentToken1.save() + + // Create pool with content token as token1, creator token as token0 + const zoraPool1 = new Pool(ZORA_POOL_ID_1) + zoraPool1.token0 = CREATOR_TOKEN_1_ADDRESS // Creator is token0 + zoraPool1.token1 = CONTENT_TOKEN_1_ADDRESS // Content is token1 + zoraPool1.feeTier = BigInt.fromI32(500) + zoraPool1.hooks = ZORA_CONTENT_HOOK + zoraPool1.tickSpacing = BigInt.fromI32(10) + zoraPool1.createdAtTimestamp = BigInt.fromI32(1234567890) + zoraPool1.createdAtBlockNumber = BigInt.fromI32(12345) + zoraPool1.liquidityProviderCount = ZERO_BI + zoraPool1.txCount = ZERO_BI + zoraPool1.liquidity = BigInt.fromString('1000000000000000000') + zoraPool1.sqrtPrice = BigInt.fromString('79228162514264337593543950336') + zoraPool1.token0Price = ZERO_BD + zoraPool1.token1Price = ZERO_BD + zoraPool1.observationIndex = ZERO_BI + zoraPool1.totalValueLockedToken0 = BigDecimal.fromString('1') // 1 creator token + zoraPool1.totalValueLockedToken1 = BigDecimal.fromString('500') // 500 content tokens + zoraPool1.totalValueLockedUSD = ZERO_BD + zoraPool1.totalValueLockedETH = ZERO_BD + zoraPool1.totalValueLockedUSDUntracked = ZERO_BD + zoraPool1.volumeToken0 = ZERO_BD + zoraPool1.volumeToken1 = ZERO_BD + zoraPool1.volumeUSD = ZERO_BD + zoraPool1.untrackedVolumeUSD = ZERO_BD + zoraPool1.feesUSD = ZERO_BD + zoraPool1.collectedFeesToken0 = ZERO_BD + zoraPool1.collectedFeesToken1 = ZERO_BD + zoraPool1.collectedFeesUSD = ZERO_BD + zoraPool1.tick = BigInt.fromI32(0) + zoraPool1.save() + + const swapFixture1: ZoraSwapFixture = { + id: ZORA_POOL_ID_1, + sender: Address.fromString('0x841B5A0b3DBc473c8A057E2391014aa4C4751351'), + amount0: BigInt.fromString('1'), // Creator token in + amount1: BigInt.fromString('-50'), // Content tokens out + sqrtPriceX96: BigInt.fromString('79228162514264337593543950336'), + liquidity: BigInt.fromString('1000000000000000000'), + tick: 0, + fee: 500, + } + + const swapEvent1 = new Swap( + MOCK_EVENT.address, + BigInt.fromI32(3), + MOCK_EVENT.transactionLogIndex, + MOCK_EVENT.logType, + MOCK_EVENT.block, + MOCK_EVENT.transaction, + [ + new ethereum.EventParam('id', ethereum.Value.fromFixedBytes(Bytes.fromHexString(swapFixture1.id))), + new ethereum.EventParam('sender', ethereum.Value.fromAddress(swapFixture1.sender)), + new ethereum.EventParam('amount0', ethereum.Value.fromSignedBigInt(swapFixture1.amount0)), + new ethereum.EventParam('amount1', ethereum.Value.fromSignedBigInt(swapFixture1.amount1)), + new ethereum.EventParam('sqrtPriceX96', ethereum.Value.fromSignedBigInt(swapFixture1.sqrtPriceX96)), + new ethereum.EventParam('liquidity', ethereum.Value.fromSignedBigInt(swapFixture1.liquidity)), + new ethereum.EventParam('tick', ethereum.Value.fromI32(swapFixture1.tick as i32)), + new ethereum.EventParam('fee', ethereum.Value.fromI32(swapFixture1.fee as i32)), + ], + MOCK_EVENT.receipt, + ) + + // Before swap: content token should have no whitelist + const contentToken1Before = Token.load(CONTENT_TOKEN_1_ADDRESS)! + assert.assertTrue(contentToken1Before.whitelistPools.length == 0) + assert.assertTrue(contentToken1Before.derivedETH.equals(ZERO_BD)) + + // Execute swap + handleSwapHelper(swapEvent1, TEST_CONFIG) + + // Fix creator token pricing - in real world it would have established pricing + const creatorToken1Fixed = Token.load(CREATOR_TOKEN_1_ADDRESS)! + creatorToken1Fixed.derivedETH = BigDecimal.fromString('0.002') + creatorToken1Fixed.save() + + // Re-run pricing for content token now that creator token has proper derivedETH + const contentToken1ToUpdate = Token.load(CONTENT_TOKEN_1_ADDRESS)! + contentToken1ToUpdate.derivedETH = findNativePerToken( + contentToken1ToUpdate, + TEST_CONFIG.wrappedNativeAddress, + TEST_CONFIG.stablecoinAddresses, + TEST_CONFIG.minimumNativeLocked, + ) + contentToken1ToUpdate.save() + + // After swap: content token (token1) should be whitelisted + const contentToken1After = Token.load(CONTENT_TOKEN_1_ADDRESS)! + + assert.assertTrue(contentToken1After.whitelistPools.length == 1) + // Most importantly: content token should have pricing + assert.assertTrue(contentToken1After.derivedETH.gt(ZERO_BD)) + }) +}) + +function arrayIncludes(array: string[], item: string): boolean { + for (let i = 0; i < array.length; i++) { + log.info('Comparing array[{}]: "{}" (length: {}) vs item: "{}" (length: {})', [ + i.toString(), + array[i], + array[i].length.toString(), + item, + item.length.toString(), + ]) + if (array[i] == item) { + log.info('Match found!', []) + return true + } + } + log.info('No match found', []) + return false +}