From 4608739922059b1fb1ba92dae8d9287c18ecac06 Mon Sep 17 00:00:00 2001 From: Sniezka Date: Wed, 19 Mar 2025 15:33:40 +0100 Subject: [PATCH 1/3] Add scripts for validating balance & getting reserves pubkeys --- scripts/nucleus/nucleus.ts | 30 ++++- sdk/src/utils.ts | 234 ++++++++++++++++++++++++++++++++++--- 2 files changed, 243 insertions(+), 21 deletions(-) diff --git a/scripts/nucleus/nucleus.ts b/scripts/nucleus/nucleus.ts index 75eb30cc43..d48991f9e7 100644 --- a/scripts/nucleus/nucleus.ts +++ b/scripts/nucleus/nucleus.ts @@ -2,6 +2,12 @@ import { AnchorProvider } from "@coral-xyz/anchor"; import { PublicKey } from "@solana/web3.js"; import fs from "fs"; import { getEffectiveTETHBalances } from "../../sdk/src"; +import { + getReservePubkeys, + validateEffectiveBalance, +} from "../../sdk/src/utils"; +import { getBalance } from "@invariant-labs/sdk-eclipse/lib/utils"; +import assert from "assert"; require("dotenv").config(); @@ -13,10 +19,24 @@ const ADDRESSES: PublicKey[] = [ ]; const main = async () => { - const data = await getEffectiveTETHBalances(connection, ADDRESSES); - fs.writeFileSync( - `./scripts/nucleus/data/${new Date().getTime()}.json`, - JSON.stringify(data, null, 2) - ); + const addresses = await getReservePubkeys(connection); + let b = 0; + for (const address of addresses) { + const balance = await getBalance( + connection, + address, + new PublicKey("TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb") + ); + b += balance.toNumber() / 1e9; + } + + const isOk = await validateEffectiveBalance(connection, b); + assert(isOk, "Balance equation failed"); + + // const data = await getEffectiveTETHBalances(connection, ADDRESSES); + // fs.writeFileSync( + // `./scripts/nucleus/data/${new Date().getTime()}.json`, + // JSON.stringify(data, null, 2) + // ); }; main(); diff --git a/sdk/src/utils.ts b/sdk/src/utils.ts index d11dce0351..7fb8828d92 100644 --- a/sdk/src/utils.ts +++ b/sdk/src/utils.ts @@ -1,12 +1,16 @@ -import { priceToTick } from "@invariant-labs/sdk-eclipse/lib/math"; +import { getX, getY, priceToTick } from "@invariant-labs/sdk-eclipse/lib/math"; import { BN } from "@coral-xyz/anchor"; import { bs58 } from "@coral-xyz/anchor/dist/cjs/utils/bytes"; -import { Market, Network } from "@invariant-labs/sdk-eclipse"; +import { Market, Network, Pair } from "@invariant-labs/sdk-eclipse"; import { + parsePool, parsePosition, + parseTick, PoolStructure, Position, + PositionStructure, RawPosition, + Tick, } from "@invariant-labs/sdk-eclipse/lib/market"; import { calculatePriceSqrt, @@ -19,7 +23,12 @@ import { Transaction, VersionedTransaction, } from "@solana/web3.js"; -import { isActive } from "@invariant-labs/sdk-eclipse/lib/utils"; +import { + calculateClaimAmount, + calculateTokensOwed, + DENOMINATOR, + isActive, +} from "@invariant-labs/sdk-eclipse/lib/utils"; const ONE_DAY = new BN(86400); @@ -86,6 +95,7 @@ const TETH_DECIMALS = 9; const TETH_DENOMINATOR = 10 ** TETH_DECIMALS; const MAX_RETRIES = 25; const RETRY_DELAY = 2000; +const EPSILON = 1 / 1e3; export const NUCLEUS_WHITELISTED_POOLS: PublicKey[] = [ new PublicKey("FvVsbwsbGVo6PVfimkkPhpcRfBrRitiV946nMNNuz7f9"), @@ -101,6 +111,131 @@ interface ResultEntry { vault_token_address: string; } +export const getReservePubkeys = async ( + connection: Connection +): Promise => { + const market = Market.build( + Network.MAIN, + { + async signTransaction( + tx: T + ): Promise { + return tx; + }, + + async signAllTransactions( + txs: T[] + ): Promise { + return txs; + }, + + publicKey: PublicKey.default, + }, + connection + ); + + const addresses: PublicKey[] = []; + + const pools = ( + await market.program.account.pool.fetchMultiple(NUCLEUS_WHITELISTED_POOLS) + ).map((p) => parsePool(p as any)); + + for (const pool of pools) { + const isTokenX = pool.tokenX.equals(TETH_PUBKEY); + addresses.push(isTokenX ? pool.tokenXReserve : pool.tokenYReserve); + } + + return addresses; +}; + +export const validateEffectiveBalance = async ( + connection: Connection, + expectedBalance: number +): Promise => { + const market = Market.build( + Network.MAIN, + { + async signTransaction( + tx: T + ): Promise { + return tx; + }, + + async signAllTransactions( + txs: T[] + ): Promise { + return txs; + }, + + publicKey: PublicKey.default, + }, + connection + ); + + let effectiveBalance = 0; + + const poolFees: Record = {}; + const inactiveBalances: Record = {}; + + for (const pool of NUCLEUS_WHITELISTED_POOLS) { + const poolKey = pool.toBase58(); + + const { poolState, allPositions, ticks } = await retryOperation( + queryStatesAndTicks(connection, market, pool) + ); + + const isTokenX = poolState.tokenX.equals(TETH_PUBKEY); + + const tETHProtocolFee = isTokenX + ? poolState.feeProtocolTokenX + : poolState.feeProtocolTokenY; + + poolFees[poolKey] = tETHProtocolFee.toNumber() / TETH_DENOMINATOR; + inactiveBalances[poolKey] = 0; + + for (const position of allPositions) { + const tETH = calculateTETH(position, poolState); + + const lowerTick = ticks[position.lowerTickIndex]; + const upperTick = ticks[position.upperTickIndex]; + + const [bnX, bnY] = calculateClaimAmount({ + position, + tickLower: lowerTick, + tickUpper: upperTick, + tickCurrent: poolState.currentTickIndex, + feeGrowthGlobalX: poolState.feeGrowthGlobalX, + feeGrowthGlobalY: poolState.feeGrowthGlobalY, + }); + + const fee = + (isTokenX ? bnX.toNumber() : bnY.toNumber()) / TETH_DENOMINATOR; + + poolFees[poolKey] += fee; + + if ( + !isActive( + position.lowerTickIndex, + position.upperTickIndex, + poolState.currentTickIndex + ) + ) { + inactiveBalances[poolKey] += tETH.toNumber() / TETH_DENOMINATOR; + continue; + } + + effectiveBalance += tETH.toNumber() / TETH_DENOMINATOR; + } + } + + const sum = + effectiveBalance + + Object.values(poolFees).reduce((acc, fee) => acc + fee, 0) + + Object.values(inactiveBalances).reduce((acc, fee) => acc + fee, 0); + + return expectedBalance - sum < EPSILON; +}; + export const getEffectiveTETHBalances = async ( connection: Connection, addresses?: PublicKey[] @@ -153,19 +288,7 @@ export const getEffectiveTETHBalances = async ( continue; } - const tETH = isTokenX - ? getDeltaX( - poolState.sqrtPrice, - calculatePriceSqrt(position.upperTickIndex), - position.liquidity, - false - ) ?? new BN(0) - : getDeltaY( - calculatePriceSqrt(position.lowerTickIndex), - poolState.sqrtPrice, - position.liquidity, - false - ) ?? new BN(0); + const tETH = calculateTETH(position, poolState); const ownerKey = position.owner.toBase58(); @@ -194,6 +317,85 @@ export const getEffectiveTETHBalances = async ( return data; }; +const calculateTETH = (position: Position, pool: PoolStructure): BN => { + const isTokenX = pool.tokenX.equals(TETH_PUBKEY); + + return isTokenX + ? getX( + position.liquidity, + calculatePriceSqrt(position.upperTickIndex), + pool.sqrtPrice, + calculatePriceSqrt(position.lowerTickIndex) + ) ?? new BN(0) + : getY( + position.liquidity, + calculatePriceSqrt(position.upperTickIndex), + pool.sqrtPrice, + calculatePriceSqrt(position.lowerTickIndex) + ) ?? new BN(0); +}; + +const queryStatesAndTicks = async ( + connection: Connection, + market: Market, + pool: PublicKey +): Promise<{ + allPositions: Position[]; + poolState: PoolStructure; + ticks: Record; +}> => { + const lastSignature = await getLatestTxHash(pool, connection); + + const poolState = await market.getPoolByAddress(pool); + + const allPositions = ( + await market.program.account.position.all([ + { + memcmp: { bytes: bs58.encode(pool.toBuffer()), offset: 40 }, + }, + ]) + ).map((p) => parsePosition(p.account)); + + const pair = new Pair(poolState.tokenX, poolState.tokenY, { + fee: poolState.fee, + tickSpacing: poolState.tickSpacing, + }); + + const tickIndexes = Array.from( + new Set( + allPositions.map((p) => [p.lowerTickIndex, p.upperTickIndex]).flat() + ) + ); + + const tickAddresses = tickIndexes.map( + (t: number) => market.getTickAddress(pair, t).tickAddress + ); + + const tickStates = ( + await market.program.account.tick.fetchMultiple(tickAddresses) + ).map((t) => parseTick(t as any)); + + const ticks = tickStates.reduce( + (acc: Record, tick: Tick, index: number) => { + acc[tickIndexes[index]] = tick; + return acc; + }, + {} + ); + + const recentSig = await getLatestTxHash(pool, connection); + + if (lastSignature !== recentSig) { + throw new Error("State inconsistency, please try again"); + } + + return { + allPositions, + poolState, + ticks, + }; +}; + const queryStates = async ( connection: Connection, market: Market, From 4c8a863f387146b7a501f46f3bb6e42558baaa3c Mon Sep 17 00:00:00 2001 From: Sniezka Date: Wed, 19 Mar 2025 15:41:22 +0100 Subject: [PATCH 2/3] Simplify math --- scripts/nucleus/nucleus.ts | 17 ++++++++++++----- sdk/src/utils.ts | 38 ++++++++------------------------------ 2 files changed, 20 insertions(+), 35 deletions(-) diff --git a/scripts/nucleus/nucleus.ts b/scripts/nucleus/nucleus.ts index d48991f9e7..142e4e62e1 100644 --- a/scripts/nucleus/nucleus.ts +++ b/scripts/nucleus/nucleus.ts @@ -13,27 +13,34 @@ require("dotenv").config(); const provider = AnchorProvider.local("https://eclipse.helius-rpc.com"); const connection = provider.connection; +const TETH_DENOMINATOR = 1e9; const ADDRESSES: PublicKey[] = [ // new PublicKey("6Wpj1RUs1hBwKuAzfFPGeHtgiQp5Fx4PVkDwfgzkLYCu"), ]; const main = async () => { + const data = await getEffectiveTETHBalances(connection, ADDRESSES); + const totalEffectiveBalances = data.reduce( + (acc, entry) => acc + entry.effective_balance, + 0 + ); + console.log("Total effective TETH: ", totalEffectiveBalances); const addresses = await getReservePubkeys(connection); - let b = 0; + console.log("Reserve Addresses: ", addresses); + let total = 0; for (const address of addresses) { const balance = await getBalance( connection, address, - new PublicKey("TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb") + new PublicKey("TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb") // TOKEN_2022_PROGRAM_ID ); - b += balance.toNumber() / 1e9; + total += balance.toNumber() / TETH_DENOMINATOR; } - const isOk = await validateEffectiveBalance(connection, b); + const isOk = await validateEffectiveBalance(connection, total); assert(isOk, "Balance equation failed"); - // const data = await getEffectiveTETHBalances(connection, ADDRESSES); // fs.writeFileSync( // `./scripts/nucleus/data/${new Date().getTime()}.json`, // JSON.stringify(data, null, 2) diff --git a/sdk/src/utils.ts b/sdk/src/utils.ts index 7fb8828d92..db91494af4 100644 --- a/sdk/src/utils.ts +++ b/sdk/src/utils.ts @@ -95,7 +95,7 @@ const TETH_DECIMALS = 9; const TETH_DENOMINATOR = 10 ** TETH_DECIMALS; const MAX_RETRIES = 25; const RETRY_DELAY = 2000; -const EPSILON = 1 / 1e3; +const EPSILON = 1 / 1e2; export const NUCLEUS_WHITELISTED_POOLS: PublicKey[] = [ new PublicKey("FvVsbwsbGVo6PVfimkkPhpcRfBrRitiV946nMNNuz7f9"), @@ -172,14 +172,10 @@ export const validateEffectiveBalance = async ( connection ); - let effectiveBalance = 0; - - const poolFees: Record = {}; - const inactiveBalances: Record = {}; + let poolFees = 0; + let userBalances = 0; for (const pool of NUCLEUS_WHITELISTED_POOLS) { - const poolKey = pool.toBase58(); - const { poolState, allPositions, ticks } = await retryOperation( queryStatesAndTicks(connection, market, pool) ); @@ -190,8 +186,7 @@ export const validateEffectiveBalance = async ( ? poolState.feeProtocolTokenX : poolState.feeProtocolTokenY; - poolFees[poolKey] = tETHProtocolFee.toNumber() / TETH_DENOMINATOR; - inactiveBalances[poolKey] = 0; + poolFees += tETHProtocolFee.toNumber() / TETH_DENOMINATOR; for (const position of allPositions) { const tETH = calculateTETH(position, poolState); @@ -208,31 +203,14 @@ export const validateEffectiveBalance = async ( feeGrowthGlobalY: poolState.feeGrowthGlobalY, }); - const fee = - (isTokenX ? bnX.toNumber() : bnY.toNumber()) / TETH_DENOMINATOR; - - poolFees[poolKey] += fee; + const fee = (isTokenX ? bnX : bnY).toNumber() / TETH_DENOMINATOR; + poolFees += fee; - if ( - !isActive( - position.lowerTickIndex, - position.upperTickIndex, - poolState.currentTickIndex - ) - ) { - inactiveBalances[poolKey] += tETH.toNumber() / TETH_DENOMINATOR; - continue; - } - - effectiveBalance += tETH.toNumber() / TETH_DENOMINATOR; + userBalances += tETH.toNumber() / TETH_DENOMINATOR; } } - const sum = - effectiveBalance + - Object.values(poolFees).reduce((acc, fee) => acc + fee, 0) + - Object.values(inactiveBalances).reduce((acc, fee) => acc + fee, 0); - + const sum = userBalances + poolFees; return expectedBalance - sum < EPSILON; }; From de3ff936c31771554cfb1477acd71b41d26b14fb Mon Sep 17 00:00:00 2001 From: Sniezka Date: Fri, 21 Mar 2025 12:46:37 +0100 Subject: [PATCH 3/3] Apply changes --- sdk/src/utils.ts | 102 ++++++++++------------------------------------- 1 file changed, 20 insertions(+), 82 deletions(-) diff --git a/sdk/src/utils.ts b/sdk/src/utils.ts index db91494af4..acc536f5e7 100644 --- a/sdk/src/utils.ts +++ b/sdk/src/utils.ts @@ -10,6 +10,7 @@ import { Position, PositionStructure, RawPosition, + RawTick, Tick, } from "@invariant-labs/sdk-eclipse/lib/market"; import { @@ -114,33 +115,15 @@ interface ResultEntry { export const getReservePubkeys = async ( connection: Connection ): Promise => { - const market = Market.build( - Network.MAIN, - { - async signTransaction( - tx: T - ): Promise { - return tx; - }, - - async signAllTransactions( - txs: T[] - ): Promise { - return txs; - }, - - publicKey: PublicKey.default, - }, - connection - ); - + const market = await Market.buildWithoutProvider(Network.MAIN, connection); const addresses: PublicKey[] = []; - const pools = ( - await market.program.account.pool.fetchMultiple(NUCLEUS_WHITELISTED_POOLS) - ).map((p) => parsePool(p as any)); + const pools = await market.program.account.pool.fetchMultiple( + NUCLEUS_WHITELISTED_POOLS + ); - for (const pool of pools) { + for (const rawPool of pools) { + const pool = parsePool(rawPool as any); const isTokenX = pool.tokenX.equals(TETH_PUBKEY); addresses.push(isTokenX ? pool.tokenXReserve : pool.tokenYReserve); } @@ -152,25 +135,7 @@ export const validateEffectiveBalance = async ( connection: Connection, expectedBalance: number ): Promise => { - const market = Market.build( - Network.MAIN, - { - async signTransaction( - tx: T - ): Promise { - return tx; - }, - - async signAllTransactions( - txs: T[] - ): Promise { - return txs; - }, - - publicKey: PublicKey.default, - }, - connection - ); + const market = await Market.buildWithoutProvider(Network.MAIN, connection); let poolFees = 0; let userBalances = 0; @@ -218,25 +183,7 @@ export const getEffectiveTETHBalances = async ( connection: Connection, addresses?: PublicKey[] ): Promise => { - const market = Market.build( - Network.MAIN, - { - async signTransaction( - tx: T - ): Promise { - return tx; - }, - - async signAllTransactions( - txs: T[] - ): Promise { - return txs; - }, - - publicKey: PublicKey.default, - }, - connection - ); + const market = await Market.buildWithoutProvider(Network.MAIN, connection); const userEntries: Record< string, @@ -334,28 +281,19 @@ const queryStatesAndTicks = async ( ]) ).map((p) => parsePosition(p.account)); - const pair = new Pair(poolState.tokenX, poolState.tokenY, { - fee: poolState.fee, - tickSpacing: poolState.tickSpacing, - }); - - const tickIndexes = Array.from( - new Set( - allPositions.map((p) => [p.lowerTickIndex, p.upperTickIndex]).flat() - ) - ); - - const tickAddresses = tickIndexes.map( - (t: number) => market.getTickAddress(pair, t).tickAddress - ); - - const tickStates = ( - await market.program.account.tick.fetchMultiple(tickAddresses) - ).map((t) => parseTick(t as any)); + const tickStates = (await market.program.account.tick.all([ + { + memcmp: { bytes: bs58.encode(pool.toBuffer()), offset: 8 }, + }, + ])) as { publicKey: PublicKey; account: RawTick }[]; const ticks = tickStates.reduce( - (acc: Record, tick: Tick, index: number) => { - acc[tickIndexes[index]] = tick; + ( + acc: Record, + rawTick: { publicKey: PublicKey; account: RawTick } + ) => { + const tick = parseTick(rawTick.account); + acc[tick.index] = tick; return acc; }, {}