Skip to content

Commit 05c7ae7

Browse files
committed
fix: select pricing pool by liquidity
1 parent 02a2950 commit 05c7ae7

7 files changed

Lines changed: 227 additions & 52 deletions

File tree

schema.graphql

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,10 @@ type Token @entity(immutable: false) {
6969
derivedETH: BigDecimal!
7070
# pools token is in that are white listed for USD pricing
7171
whitelistPools: [Pool!]!
72+
# cached pool with the most native-equivalent liquidity for USD pricing
73+
pricingPool: Pool
74+
# timestamp of the last full whitelist pool scan
75+
pricingPoolLastScanTimestamp: BigInt
7276
# derived fields
7377
tokenDayData: [TokenDayData!]! @derivedFrom(field: "token")
7478
}
@@ -517,4 +521,4 @@ type ArrakisHook @entity(immutable: false) {
517521
createdAtTimestamp: BigInt!
518522
# block number of creation
519523
createdAtBlockNumber: BigInt!
520-
}
524+
}

src/mappings/poolManager.ts

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -190,13 +190,31 @@ export function handleInitializeHelper(
190190
if (staticNativePriceUSD.gt(ZERO_BD)) {
191191
bundle.ethPriceUSD = staticNativePriceUSD
192192
} else {
193-
bundle.ethPriceUSD = getNativePriceInUSD(stablecoinWrappedNativePoolId, stablecoinIsToken0)
193+
bundle.ethPriceUSD = getNativePriceInUSD(stablecoinWrappedNativePoolId, stablecoinIsToken0, pool)
194194
}
195195
bundle.save()
196196
updatePoolDayData(pool, event)
197197
updatePoolHourData(pool, event)
198-
token1.derivedETH = findNativePerToken(token1, wrappedNativeAddress, stablecoinAddresses, minimumNativeLocked, bundle)
199-
token0.derivedETH = findNativePerToken(token0, wrappedNativeAddress, stablecoinAddresses, minimumNativeLocked, bundle)
198+
token1.derivedETH = findNativePerToken(
199+
token1,
200+
wrappedNativeAddress,
201+
stablecoinAddresses,
202+
minimumNativeLocked,
203+
bundle,
204+
event.block.timestamp,
205+
pool,
206+
token0,
207+
)
208+
token0.derivedETH = findNativePerToken(
209+
token0,
210+
wrappedNativeAddress,
211+
stablecoinAddresses,
212+
minimumNativeLocked,
213+
bundle,
214+
event.block.timestamp,
215+
pool,
216+
token1,
217+
)
200218

201219
token0.save()
202220
token1.save()

src/mappings/swap.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -266,7 +266,7 @@ export function handleSwapHelper(event: SwapEvent, subgraphConfig: SubgraphConfi
266266
const prices = sqrtPriceX96ToTokenPrices(pool.sqrtPrice, token0, token1, nativeTokenDetails)
267267
pool.token0Price = prices[0]
268268
pool.token1Price = prices[1]
269-
bundle.ethPriceUSD = getNativePriceInUSD(stablecoinWrappedNativePoolId, stablecoinIsToken0)
269+
bundle.ethPriceUSD = getNativePriceInUSD(stablecoinWrappedNativePoolId, stablecoinIsToken0, pool)
270270
}
271271

272272
bundle.save()
@@ -276,13 +276,19 @@ export function handleSwapHelper(event: SwapEvent, subgraphConfig: SubgraphConfi
276276
stablecoinAddresses,
277277
minimumNativeLocked,
278278
bundle,
279+
event.block.timestamp,
280+
pool,
281+
token1,
279282
)
280283
token1.derivedETH = findNativePerToken(
281284
token1,
282285
wrappedNativeAddress,
283286
stablecoinAddresses,
284287
minimumNativeLocked,
285288
bundle,
289+
event.block.timestamp,
290+
pool,
291+
token0,
286292
)
287293

288294
/**

src/utils/pricing.ts

Lines changed: 102 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,7 @@ import { NativeTokenDetails } from './nativeTokenDetails'
77

88
const Q192 = BigInt.fromI32(2).pow(192 as u8)
99

10-
/**
11-
* Cap how many whitelist pools findNativePerToken will walk. Tokens with long
12-
* whitelistPools lists otherwise cost O(N) store.gets (Pool + other Token) per
13-
* call, twice per swap. Prefer the first K entries; pool init appends newest
14-
* pools at the end so older/larger pools tend to sit earlier.
15-
*/
16-
const MAX_WHITELIST_POOLS_TO_WALK = 8
10+
const PRICING_POOL_RESCAN_INTERVAL_SECONDS = BigInt.fromI32(86400)
1711

1812
export function sqrtPriceX96ToTokenPrices(
1913
sqrtPriceX96: BigInt,
@@ -35,24 +29,65 @@ export function sqrtPriceX96ToTokenPrices(
3529
return [price0, price1]
3630
}
3731

38-
export function getNativePriceInUSD(stablecoinWrappedNativePoolId: string, stablecoinIsToken0: boolean): BigDecimal {
32+
export function getNativePriceInUSD(
33+
stablecoinWrappedNativePoolId: string,
34+
stablecoinIsToken0: boolean,
35+
loadedPool: Pool | null = null,
36+
): BigDecimal {
3937
// On chains where the native/reference token is itself a USD stablecoin (e.g. Arc, whose native
4038
// gas token is USDC and which has no wrapped-native / reference-stable pool), the native price is
4139
// 1 by definition. Such chains opt in by setting stablecoinWrappedNativePoolId to '' (empty).
4240
if (stablecoinWrappedNativePoolId == '') {
4341
return ONE_BD
4442
}
45-
const stablecoinWrappedNativePool = Pool.load(stablecoinWrappedNativePoolId)
43+
const stablecoinWrappedNativePool =
44+
loadedPool !== null && loadedPool.id == stablecoinWrappedNativePoolId
45+
? loadedPool
46+
: Pool.load(stablecoinWrappedNativePoolId)
4647
if (stablecoinWrappedNativePool !== null) {
4748
return stablecoinIsToken0 ? stablecoinWrappedNativePool.token0Price : stablecoinWrappedNativePool.token1Price
4849
} else {
4950
return ZERO_BD
5051
}
5152
}
5253

54+
function getPoolNativeLiquidityAndPrice(token: Token, pool: Pool, loadedReferenceToken: Token | null): BigDecimal[] {
55+
if (!pool.liquidity.gt(ZERO_BI)) {
56+
return [ZERO_BD, ZERO_BD]
57+
}
58+
59+
let referenceToken = loadedReferenceToken
60+
if (pool.token0 == token.id) {
61+
if (referenceToken === null || referenceToken.id != pool.token1) {
62+
referenceToken = Token.load(pool.token1)
63+
}
64+
if (referenceToken) {
65+
return [
66+
pool.totalValueLockedToken1.times(referenceToken.derivedETH),
67+
pool.token1Price.times(referenceToken.derivedETH),
68+
]
69+
}
70+
} else if (pool.token1 == token.id) {
71+
if (referenceToken === null || referenceToken.id != pool.token0) {
72+
referenceToken = Token.load(pool.token0)
73+
}
74+
if (referenceToken) {
75+
return [
76+
pool.totalValueLockedToken0.times(referenceToken.derivedETH),
77+
pool.token0Price.times(referenceToken.derivedETH),
78+
]
79+
}
80+
}
81+
82+
return [ZERO_BD, ZERO_BD]
83+
}
84+
5385
/**
5486
* Search through graph to find derived Eth per token.
5587
* Callers pass the already-loaded Bundle to avoid a redundant store.get.
88+
* The best pool is cached on Token and compared with the active pool on each
89+
* call. A periodic full scan repairs stale cache entries and seeds grafted
90+
* tokens without assuming whitelistPools creation order reflects liquidity.
5691
* @todo update to be derived ETH (add stablecoin estimates)
5792
**/
5893
export function findNativePerToken(
@@ -61,57 +96,77 @@ export function findNativePerToken(
6196
stablecoinAddresses: string[],
6297
minimumNativeLocked: BigDecimal,
6398
bundle: Bundle,
99+
timestamp: BigInt,
100+
activePool: Pool | null,
101+
activeReferenceToken: Token | null,
64102
): BigDecimal {
65103
if (token.id == wrappedNativeAddress || token.id == ADDRESS_ZERO) {
66104
return ONE_BD
67105
}
106+
// hardcoded fix for incorrect rates
107+
// if whitelist includes token - get the safe price
108+
if (stablecoinAddresses.includes(token.id)) {
109+
return safeDiv(ONE_BD, bundle.ethPriceUSD)
110+
}
111+
68112
const whiteList = token.whitelistPools
69-
// for now just take USD from pool with greatest TVL
70-
// need to update this to actually detect best rate based on liquidity distribution
113+
const cachedPoolId = token.pricingPool
114+
const lastScanTimestamp = token.pricingPoolLastScanTimestamp
115+
let shouldRescan =
116+
lastScanTimestamp === null || timestamp.minus(lastScanTimestamp).ge(PRICING_POOL_RESCAN_INTERVAL_SECONDS)
71117
let largestLiquidityETH = ZERO_BD
72118
let priceSoFar = ZERO_BD
119+
let bestPoolId: string | null = null
120+
121+
if (!shouldRescan && cachedPoolId !== null) {
122+
const cachedPool = activePool !== null && activePool.id == cachedPoolId ? activePool : Pool.load(cachedPoolId)
123+
if (cachedPool !== null) {
124+
const loadedReferenceToken = activePool !== null && activePool.id == cachedPoolId ? activeReferenceToken : null
125+
const cachedValues = getPoolNativeLiquidityAndPrice(token, cachedPool, loadedReferenceToken)
126+
if (cachedValues[0].gt(minimumNativeLocked)) {
127+
largestLiquidityETH = cachedValues[0]
128+
priceSoFar = cachedValues[1]
129+
bestPoolId = cachedPool.id
130+
} else {
131+
shouldRescan = true
132+
}
133+
} else {
134+
shouldRescan = true
135+
}
136+
}
73137

74-
// hardcoded fix for incorrect rates
75-
// if whitelist includes token - get the safe price
76-
if (stablecoinAddresses.includes(token.id)) {
77-
priceSoFar = safeDiv(ONE_BD, bundle.ethPriceUSD)
78-
} else {
79-
const walkLimit = whiteList.length < MAX_WHITELIST_POOLS_TO_WALK ? whiteList.length : MAX_WHITELIST_POOLS_TO_WALK
80-
for (let i = 0; i < walkLimit; ++i) {
138+
if (shouldRescan) {
139+
largestLiquidityETH = ZERO_BD
140+
priceSoFar = ZERO_BD
141+
bestPoolId = null
142+
143+
for (let i = 0; i < whiteList.length; ++i) {
81144
const poolAddress = whiteList[i]
82-
const pool = Pool.load(poolAddress)
83-
84-
if (pool) {
85-
if (pool.liquidity.gt(ZERO_BI)) {
86-
if (pool.token0 == token.id) {
87-
// whitelist token is token1
88-
const token1 = Token.load(pool.token1)
89-
// get the derived ETH in pool
90-
if (token1) {
91-
const ethLocked = pool.totalValueLockedToken1.times(token1.derivedETH)
92-
if (ethLocked.gt(largestLiquidityETH) && ethLocked.gt(minimumNativeLocked)) {
93-
largestLiquidityETH = ethLocked
94-
// token1 per our token * Eth per token1
95-
priceSoFar = pool.token1Price.times(token1.derivedETH as BigDecimal)
96-
}
97-
}
98-
}
99-
if (pool.token1 == token.id) {
100-
const token0 = Token.load(pool.token0)
101-
// get the derived ETH in pool
102-
if (token0) {
103-
const ethLocked = pool.totalValueLockedToken0.times(token0.derivedETH)
104-
if (ethLocked.gt(largestLiquidityETH) && ethLocked.gt(minimumNativeLocked)) {
105-
largestLiquidityETH = ethLocked
106-
// token0 per our token * ETH per token0
107-
priceSoFar = pool.token0Price.times(token0.derivedETH as BigDecimal)
108-
}
109-
}
110-
}
145+
const pool = activePool !== null && activePool.id == poolAddress ? activePool : Pool.load(poolAddress)
146+
if (pool !== null) {
147+
const loadedReferenceToken = activePool !== null && activePool.id == poolAddress ? activeReferenceToken : null
148+
const values = getPoolNativeLiquidityAndPrice(token, pool, loadedReferenceToken)
149+
if (values[0].gt(largestLiquidityETH) && values[0].gt(minimumNativeLocked)) {
150+
largestLiquidityETH = values[0]
151+
priceSoFar = values[1]
152+
bestPoolId = pool.id
111153
}
112154
}
113155
}
156+
token.pricingPoolLastScanTimestamp = timestamp
157+
} else if (
158+
activePool !== null &&
159+
(cachedPoolId === null || activePool.id != cachedPoolId) &&
160+
whiteList.includes(activePool.id)
161+
) {
162+
const values = getPoolNativeLiquidityAndPrice(token, activePool, activeReferenceToken)
163+
if (values[0].gt(largestLiquidityETH) && values[0].gt(minimumNativeLocked)) {
164+
priceSoFar = values[1]
165+
bestPoolId = activePool.id
166+
}
114167
}
168+
169+
token.pricingPool = bestPoolId
115170
return priceSoFar
116171
}
117172

tests/handlers/constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,7 @@ export const createAndStoreTestPool = (poolFixture: PoolFixture): Pool => {
275275
pool.totalValueLockedUSDUntracked = ZERO_BD
276276
pool.liquidityProviderCount = ZERO_BI
277277
pool.hooks = ADDRESS_ZERO
278+
pool.isExternalLiquidity = false
278279

279280
pool.save()
280281
return pool

0 commit comments

Comments
 (0)