diff --git a/modules/pool/abi/BalancerQueries.json b/modules/pool/abi/BalancerQueries.json new file mode 100644 index 000000000..4f23e1f2b --- /dev/null +++ b/modules/pool/abi/BalancerQueries.json @@ -0,0 +1,309 @@ +[ + { + "inputs": [ + { + "internalType": "contract IVault", + "name": "_vault", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [ + { + "internalType": "enum IVault.SwapKind", + "name": "kind", + "type": "uint8" + }, + { + "components": [ + { + "internalType": "bytes32", + "name": "poolId", + "type": "bytes32" + }, + { + "internalType": "uint256", + "name": "assetInIndex", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "assetOutIndex", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "userData", + "type": "bytes" + } + ], + "internalType": "struct IVault.BatchSwapStep[]", + "name": "swaps", + "type": "tuple[]" + }, + { + "internalType": "contract IAsset[]", + "name": "assets", + "type": "address[]" + }, + { + "components": [ + { + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "internalType": "bool", + "name": "fromInternalBalance", + "type": "bool" + }, + { + "internalType": "address payable", + "name": "recipient", + "type": "address" + }, + { + "internalType": "bool", + "name": "toInternalBalance", + "type": "bool" + } + ], + "internalType": "struct IVault.FundManagement", + "name": "funds", + "type": "tuple" + } + ], + "name": "queryBatchSwap", + "outputs": [ + { + "internalType": "int256[]", + "name": "assetDeltas", + "type": "int256[]" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "poolId", + "type": "bytes32" + }, + { + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "components": [ + { + "internalType": "contract IAsset[]", + "name": "assets", + "type": "address[]" + }, + { + "internalType": "uint256[]", + "name": "minAmountsOut", + "type": "uint256[]" + }, + { + "internalType": "bytes", + "name": "userData", + "type": "bytes" + }, + { + "internalType": "bool", + "name": "toInternalBalance", + "type": "bool" + } + ], + "internalType": "struct IVault.ExitPoolRequest", + "name": "request", + "type": "tuple" + } + ], + "name": "queryExit", + "outputs": [ + { + "internalType": "uint256", + "name": "bptIn", + "type": "uint256" + }, + { + "internalType": "uint256[]", + "name": "amountsOut", + "type": "uint256[]" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "poolId", + "type": "bytes32" + }, + { + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "components": [ + { + "internalType": "contract IAsset[]", + "name": "assets", + "type": "address[]" + }, + { + "internalType": "uint256[]", + "name": "maxAmountsIn", + "type": "uint256[]" + }, + { + "internalType": "bytes", + "name": "userData", + "type": "bytes" + }, + { + "internalType": "bool", + "name": "fromInternalBalance", + "type": "bool" + } + ], + "internalType": "struct IVault.JoinPoolRequest", + "name": "request", + "type": "tuple" + } + ], + "name": "queryJoin", + "outputs": [ + { + "internalType": "uint256", + "name": "bptOut", + "type": "uint256" + }, + { + "internalType": "uint256[]", + "name": "amountsIn", + "type": "uint256[]" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "bytes32", + "name": "poolId", + "type": "bytes32" + }, + { + "internalType": "enum IVault.SwapKind", + "name": "kind", + "type": "uint8" + }, + { + "internalType": "contract IAsset", + "name": "assetIn", + "type": "address" + }, + { + "internalType": "contract IAsset", + "name": "assetOut", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "userData", + "type": "bytes" + } + ], + "internalType": "struct IVault.SingleSwap", + "name": "singleSwap", + "type": "tuple" + }, + { + "components": [ + { + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "internalType": "bool", + "name": "fromInternalBalance", + "type": "bool" + }, + { + "internalType": "address payable", + "name": "recipient", + "type": "address" + }, + { + "internalType": "bool", + "name": "toInternalBalance", + "type": "bool" + } + ], + "internalType": "struct IVault.FundManagement", + "name": "funds", + "type": "tuple" + } + ], + "name": "querySwap", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "vault", + "outputs": [ + { + "internalType": "contract IVault", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + } +] \ No newline at end of file diff --git a/modules/pool/lib/pool-gql-loader.service.ts b/modules/pool/lib/pool-gql-loader.service.ts index 5d6305758..8be100aba 100644 --- a/modules/pool/lib/pool-gql-loader.service.ts +++ b/modules/pool/lib/pool-gql-loader.service.ts @@ -44,10 +44,9 @@ import { networkContext } from '../../network/network-context.service'; import { fixedNumber } from '../../view-helpers/fixed-number'; import { parseUnits } from 'ethers/lib/utils'; import { formatFixed } from '@ethersproject/bignumber'; -import { BalancerChainIds, BeethovenChainIds, chainIdToChain, chainToIdMap } from '../../network/network-config'; +import { BeethovenChainIds, chainToIdMap } from '../../network/network-config'; import { GithubContentService } from '../../content/github-content.service'; import { SanityContentService } from '../../content/sanity-content.service'; -import { FeaturedPool } from '../../content/content-types'; import { ElementData, FxData, GyroData, LinearData, StableData } from '../subgraph-mapper'; export class PoolGqlLoaderService { diff --git a/modules/pool/lib/pool-on-chain-data.service.ts b/modules/pool/lib/pool-on-chain-data.service.ts index 1c23bb203..66f5e1dbb 100644 --- a/modules/pool/lib/pool-on-chain-data.service.ts +++ b/modules/pool/lib/pool-on-chain-data.service.ts @@ -1,5 +1,5 @@ import { formatFixed } from '@ethersproject/bignumber'; -import { PrismaPoolType } from '@prisma/client'; +import { Prisma, PrismaPoolType } from '@prisma/client'; import { isSameAddress } from '@balancer-labs/sdk'; import { prisma } from '../../../prisma/prisma-client'; import { isStablePool } from './pool-utils'; @@ -10,6 +10,7 @@ import { fetchOnChainPoolData } from './pool-onchain-data'; import { fetchOnChainGyroFees } from './pool-onchain-gyro-fee'; import { networkContext } from '../../network/network-context.service'; import { LinearData, StableData } from '../subgraph-mapper'; +import { fetchTokenPairData } from './pool-on-chain-tokenpair-data'; const SUPPORTED_POOL_TYPES: PrismaPoolType[] = [ 'WEIGHTED', @@ -33,6 +34,7 @@ export class PoolOnChainDataService { return { chain: networkContext.chain, vaultAddress: networkContext.data.balancer.v2.vaultAddress, + balancerQueriesAddress: networkContext.data.balancer.v2.balancerQueriesAddress, yieldProtocolFeePercentage: networkContext.data.balancer.v2.defaultSwapFeePercentage, swapProtocolFeePercentage: networkContext.data.balancer.v2.defaultSwapFeePercentage, gyroConfig: networkContext.data.gyro?.config, @@ -101,6 +103,11 @@ export class PoolOnChainDataService { this.options.vaultAddress, networkContext.chain === 'ZKEVM' ? 190 : 1024, ); + const tokenPairData = await fetchTokenPairData( + filteredPools, + this.options.balancerQueriesAddress, + networkContext.chain === 'ZKEVM' ? 190 : 1024, + ); const gyroFees = await (this.options.gyroConfig ? fetchOnChainGyroFees(gyroPools, this.options.gyroConfig, networkContext.chain === 'ZKEVM' ? 190 : 1024) : Promise.resolve({} as { [address: string]: string })); @@ -108,6 +115,7 @@ export class PoolOnChainDataService { const operations = []; for (const pool of filteredPools) { const onchainData = onchainResults[pool.id]; + const { tokenPairs } = tokenPairData[pool.id]; const { amp, poolTokens } = onchainData; try { @@ -201,6 +209,18 @@ export class PoolOnChainDataService { ); } + // always update tokenPair data + if (pool.dynamicData) { + operations.push( + prisma.prismaPoolDynamicData.update({ + where: { id_chain: { id: pool.id, chain: this.options.chain } }, + data: { + tokenPairsData: tokenPairs, + }, + }), + ); + } + for (let i = 0; i < poolTokens.tokens.length; i++) { const tokenAddress = poolTokens.tokens[i]; const poolToken = pool.tokens.find((token) => isSameAddress(token.address, tokenAddress)); diff --git a/modules/pool/lib/pool-on-chain-tokenpair-data.ts b/modules/pool/lib/pool-on-chain-tokenpair-data.ts new file mode 100644 index 000000000..72789d230 --- /dev/null +++ b/modules/pool/lib/pool-on-chain-tokenpair-data.ts @@ -0,0 +1,305 @@ +import { Multicaller3 } from '../../web3/multicaller3'; +import { BigNumber } from '@ethersproject/bignumber'; +import BalancerQueries from '../abi/BalancerQueries.json'; +import { MathSol, WAD, ZERO_ADDRESS } from '@balancer/sdk'; +import { parseEther, parseUnits } from 'viem'; +import * as Sentry from '@sentry/node'; + +interface PoolInput { + id: string; + address: string; + tokens: { + address: string; + token: { + decimals: number; + }; + dynamicData: { + balance: string; + balanceUSD: number; + } | null; + }[]; + dynamicData: { + totalLiquidity: number; + } | null; +} + +interface PoolTokenPairsOutput { + [poolId: string]: { + tokenPairs: TokenPairData[]; + }; +} + +export type TokenPairData = { + tokenA: string; + tokenB: string; + normalizedLiquidity: string; + spotPrice: string; +}; + +interface TokenPair { + poolId: string; + poolTvl: number; + valid: boolean; + tokenA: Token; + tokenB: Token; + normalizedLiqudity: bigint; + spotPrice: bigint; + aToBAmountIn: bigint; + aToBAmountOut: bigint; + bToAAmountOut: bigint; + effectivePrice: bigint; + effectivePriceAmountIn: bigint; +} + +interface Token { + address: string; + decimals: number; + balance: string; + balanceUsd: number; +} + +interface OnchainData { + effectivePriceAmountOut: BigNumber; + aToBAmountOut: BigNumber; + bToAAmountOut: BigNumber; +} + +export async function fetchTokenPairData(pools: PoolInput[], balancerQueriesAddress: string, batchSize = 1024) { + if (pools.length === 0) { + return {}; + } + + const tokenPairOutput: PoolTokenPairsOutput = {}; + + const multicaller = new Multicaller3(BalancerQueries, batchSize); + + // only inlcude pools with TVL >=$1000 + // for each pool, get pairs + // for each pair per pool, create multicall to do a swap with $200 (min liq is $1k, so there should be at least $200 for each token) for effectivePrice calc and a swap with 1% TVL + // then create multicall to do the second swap for each pair using the result of the first 1% swap as input, to calculate the spot price + // https://github.com/balancer/b-sdk/pull/204/files#diff-52e6d86a27aec03f59dd3daee140b625fd99bd9199936bbccc50ee550d0b0806 + + const tokenPairs = generateTokenPairs(pools); + + tokenPairs.forEach((tokenPair) => { + if (tokenPair.valid) { + // prepare swap amounts in + // tokenA->tokenB with 1% of tokenA balance + tokenPair.aToBAmountIn = parseUnits(tokenPair.tokenA.balance, tokenPair.tokenA.decimals) / 100n; + // tokenA->tokenB with 100USD worth of tokenA + const oneHundredUsdOfTokenA = (parseFloat(tokenPair.tokenA.balance) / tokenPair.tokenA.balanceUsd) * 100; + tokenPair.effectivePriceAmountIn = parseUnits(`${oneHundredUsdOfTokenA}`, tokenPair.tokenA.decimals); + + addEffectivePriceCallsToMulticaller(tokenPair, balancerQueriesAddress, multicaller); + addAToBPriceCallsToMulticaller(tokenPair, balancerQueriesAddress, multicaller); + } + }); + + const resultOne = (await multicaller.execute()) as { + [id: string]: OnchainData; + }; + ``; + tokenPairs.forEach((tokenPair) => { + if (tokenPair.valid) { + getAmountOutAndEffectivePriceFromResult(tokenPair, resultOne); + } + }); + + tokenPairs.forEach((tokenPair) => { + if (tokenPair.valid) { + addBToAPriceCallsToMulticaller(tokenPair, balancerQueriesAddress, multicaller); + } + }); + + const resultTwo = (await multicaller.execute()) as { + [id: string]: OnchainData; + }; + + tokenPairs.forEach((tokenPair) => { + if (tokenPair.valid) { + getBToAAmountFromResult(tokenPair, resultTwo); + calculateSpotPrice(tokenPair); + calculateNormalizedLiquidity(tokenPair); + } + + // prepare output + pools.forEach((pool) => { + if (pool.id === tokenPair.poolId) { + if (!tokenPairOutput[pool.id]) { + tokenPairOutput[pool.id] = { + tokenPairs: [], + }; + } + tokenPairOutput[pool.id].tokenPairs.push({ + tokenA: tokenPair.tokenA.address, + tokenB: tokenPair.tokenB.address, + normalizedLiquidity: tokenPair.normalizedLiqudity.toString(), + spotPrice: tokenPair.spotPrice.toString(), + }); + } + }); + }); + + return tokenPairOutput; +} + +function generateTokenPairs(filteredPools: PoolInput[]): TokenPair[] { + const tokenPairs: TokenPair[] = []; + + for (const pool of filteredPools) { + // create all pairs for pool + for (let i = 0; i < pool.tokens.length - 1; i++) { + for (let j = i + 1; j < pool.tokens.length; j++) { + //skip pairs with phantom BPT + if (pool.tokens[i].address === pool.address || pool.tokens[j].address === pool.address) continue; + tokenPairs.push({ + poolId: pool.id, + poolTvl: pool.dynamicData?.totalLiquidity || 0, + // remove pools that have <$1000 TVL or a token without a balance or USD balance + valid: + (pool.dynamicData?.totalLiquidity || 0) >= 1000 && + !pool.tokens.some((token) => token.dynamicData?.balance || '0' === '0') && + !pool.tokens.some((token) => token.dynamicData?.balanceUSD || 0 === 0), + + tokenA: { + address: pool.tokens[i].address, + decimals: pool.tokens[i].token.decimals, + balance: pool.tokens[i].dynamicData?.balance || '0', + balanceUsd: pool.tokens[i].dynamicData?.balanceUSD || 0, + }, + tokenB: { + address: pool.tokens[j].address, + decimals: pool.tokens[j].token.decimals, + balance: pool.tokens[j].dynamicData?.balance || '0', + balanceUsd: pool.tokens[j].dynamicData?.balanceUSD || 0, + }, + normalizedLiqudity: 0n, + spotPrice: 0n, + aToBAmountIn: 0n, + aToBAmountOut: 0n, + bToAAmountOut: 0n, + effectivePrice: 0n, + effectivePriceAmountIn: 0n, + }); + } + } + } + return tokenPairs; +} + +// call querySwap from tokenA->tokenB with 100USD worth of tokenA +function addEffectivePriceCallsToMulticaller( + tokenPair: TokenPair, + balancerQueriesAddress: string, + multicaller: Multicaller3, +) { + multicaller.call( + `${tokenPair.poolId}-${tokenPair.tokenA.address}-${tokenPair.tokenB.address}.effectivePriceAmountOut`, + balancerQueriesAddress, + 'querySwap', + [ + [ + tokenPair.poolId, + 0, + tokenPair.tokenA.address, + tokenPair.tokenB.address, + `${tokenPair.effectivePriceAmountIn}`, + ZERO_ADDRESS, + ], + [ZERO_ADDRESS, false, ZERO_ADDRESS, false], + ], + ); +} + +// call querySwap from tokenA->tokenB with 1% of tokenA balance +function addAToBPriceCallsToMulticaller( + tokenPair: TokenPair, + balancerQueriesAddress: string, + multicaller: Multicaller3, +) { + multicaller.call( + `${tokenPair.poolId}-${tokenPair.tokenA.address}-${tokenPair.tokenB.address}.aToBAmountOut`, + balancerQueriesAddress, + 'querySwap', + [ + [ + tokenPair.poolId, + 0, + tokenPair.tokenA.address, + tokenPair.tokenB.address, + `${tokenPair.aToBAmountIn}`, + ZERO_ADDRESS, + ], + [ZERO_ADDRESS, false, ZERO_ADDRESS, false], + ], + ); +} + +// call querySwap from tokenA->tokenB with AtoB amount out +function addBToAPriceCallsToMulticaller( + tokenPair: TokenPair, + balancerQueriesAddress: string, + multicaller: Multicaller3, +) { + multicaller.call( + `${tokenPair.poolId}-${tokenPair.tokenA.address}-${tokenPair.tokenB.address}.bToAAmountOut`, + balancerQueriesAddress, + 'querySwap', + [ + [ + tokenPair.poolId, + 0, + tokenPair.tokenB.address, + tokenPair.tokenA.address, + `${tokenPair.aToBAmountOut}`, + ZERO_ADDRESS, + ], + [ZERO_ADDRESS, false, ZERO_ADDRESS, false], + ], + ); +} + +function getAmountOutAndEffectivePriceFromResult(tokenPair: TokenPair, onchainResults: { [id: string]: OnchainData }) { + const result = onchainResults[`${tokenPair.poolId}-${tokenPair.tokenA.address}-${tokenPair.tokenB.address}`]; + + if (result) { + tokenPair.aToBAmountOut = BigInt(result.aToBAmountOut.toString()); + // MathSol expects all values with 18 decimals, need to scale them + tokenPair.effectivePrice = MathSol.divDownFixed( + parseUnits(tokenPair.effectivePriceAmountIn.toString(), 18 - tokenPair.tokenA.decimals), + parseUnits(result.effectivePriceAmountOut.toString(), 18 - tokenPair.tokenB.decimals), + ); + } +} + +function getBToAAmountFromResult(tokenPair: TokenPair, onchainResults: { [id: string]: OnchainData }) { + const result = onchainResults[`${tokenPair.poolId}-${tokenPair.tokenA.address}-${tokenPair.tokenB.address}`]; + + if (result) { + tokenPair.bToAAmountOut = BigInt(result.bToAAmountOut.toString()); + } +} +function calculateSpotPrice(tokenPair: TokenPair) { + // MathSol expects all values with 18 decimals, need to scale them + const aToBAmountInScaled = parseUnits(tokenPair.aToBAmountIn.toString(), 18 - tokenPair.tokenA.decimals); + const aToBAmountOutScaled = parseUnits(tokenPair.aToBAmountOut.toString(), 18 - tokenPair.tokenB.decimals); + const bToAAmountOutScaled = parseUnits(tokenPair.bToAAmountOut.toString(), 18 - tokenPair.tokenA.decimals); + const priceAtoB = MathSol.divDownFixed(aToBAmountInScaled, aToBAmountOutScaled); + const priceBtoA = MathSol.divDownFixed(aToBAmountOutScaled, bToAAmountOutScaled); + tokenPair.spotPrice = MathSol.powDownFixed(MathSol.divDownFixed(priceAtoB, priceBtoA), WAD / 2n); +} + +function calculateNormalizedLiquidity(tokenPair: TokenPair) { + // spotPrice and effective price are already scaled to 18 decimals by the MathSol output + let priceRatio = MathSol.divDownFixed(tokenPair.spotPrice, tokenPair.effectivePrice); + // if priceRatio is = 1, normalizedLiquidity becomes infinity, if it is >1, normalized liqudity becomes negative. Need to cap it. + // this happens if you get a "bonus" ie positive price impact. + if (priceRatio > parseEther('0.999999')) { + Sentry.captureException( + `Price ratio was > 0.999999 for token pair ${tokenPair.tokenA.address}/${tokenPair.tokenB.address} in pool ${tokenPair.poolId}.`, + ); + priceRatio = parseEther('0.999999'); + } + const priceImpact = WAD - priceRatio; + tokenPair.normalizedLiqudity = MathSol.divDownFixed(WAD, priceImpact); +} diff --git a/modules/pool/pool.prisma b/modules/pool/pool.prisma index b382b3bf8..1d3227782 100644 --- a/modules/pool/pool.prisma +++ b/modules/pool/pool.prisma @@ -115,6 +115,8 @@ model PrismaPoolDynamicData { fees24hAthTimestamp Int @default(0) fees24hAtl Float @default(0) fees24hAtlTimestamp Int @default(0) + + tokenPairsData Json @default("[]") } model PrismaPoolToken { diff --git a/modules/sor/sorV2/lib/pools/stable/stablePool.ts b/modules/sor/sorV2/lib/pools/composableStable/composableStablePool.ts similarity index 90% rename from modules/sor/sorV2/lib/pools/stable/stablePool.ts rename to modules/sor/sorV2/lib/pools/composableStable/composableStablePool.ts index 83dce24a3..272457f92 100644 --- a/modules/sor/sorV2/lib/pools/stable/stablePool.ts +++ b/modules/sor/sorV2/lib/pools/composableStable/composableStablePool.ts @@ -14,8 +14,9 @@ import { import { BasePool, BigintIsh, PoolType, SwapKind, Token, TokenAmount } from '@balancer/sdk'; import { chainToIdMap } from '../../../../../network/network-config'; import { StableData } from '../../../../../pool/subgraph-mapper'; +import { TokenPairData } from '../../../../../pool/lib/pool-on-chain-tokenpair-data'; -export class StablePoolToken extends TokenAmount { +export class ComposableStablePoolToken extends TokenAmount { public readonly rate: bigint; public readonly index: number; @@ -39,23 +40,24 @@ export class StablePoolToken extends TokenAmount { } } -export class StablePool implements BasePool { +export class ComposableStablePool implements BasePool { public readonly chain: Chain; public readonly id: Hex; public readonly address: string; - public readonly poolType: PoolType = PoolType.MetaStable; + public readonly poolType: PoolType = PoolType.ComposableStable; public readonly amp: bigint; public readonly swapFee: bigint; public readonly bptIndex: number; + public readonly tokenPairs: TokenPairData[]; public totalShares: bigint; - public tokens: StablePoolToken[]; + public tokens: ComposableStablePoolToken[]; - private readonly tokenMap: Map; + private readonly tokenMap: Map; private readonly tokenIndexMap: Map; - static fromPrismaPool(pool: PrismaPoolWithDynamic): StablePool { - const poolTokens: StablePoolToken[] = []; + static fromPrismaPool(pool: PrismaPoolWithDynamic): ComposableStablePool { + const poolTokens: ComposableStablePoolToken[] = []; if (!pool.dynamicData) throw new Error('Stable pool has no dynamic data'); @@ -71,7 +73,7 @@ export class StablePool implements BasePool { const tokenAmount = TokenAmount.fromHumanAmount(token, `${parseFloat(poolToken.dynamicData.balance)}`); poolTokens.push( - new StablePoolToken( + new ComposableStablePoolToken( token, tokenAmount.amount, parseEther(poolToken.dynamicData.priceRate), @@ -83,7 +85,7 @@ export class StablePool implements BasePool { const totalShares = parseEther(pool.dynamicData.totalShares); const amp = parseUnits((pool.typeData as StableData).amp, 3); - return new StablePool( + return new ComposableStablePool( pool.id as Hex, pool.address, pool.chain, @@ -91,6 +93,7 @@ export class StablePool implements BasePool { parseEther(pool.dynamicData.swapFee), poolTokens, totalShares, + pool.dynamicData.tokenPairsData as TokenPairData[], ); } @@ -100,8 +103,9 @@ export class StablePool implements BasePool { chain: Chain, amp: bigint, swapFee: bigint, - tokens: StablePoolToken[], + tokens: ComposableStablePoolToken[], totalShares: bigint, + tokenPairs: TokenPairData[], ) { this.chain = chain; this.id = id; @@ -115,6 +119,7 @@ export class StablePool implements BasePool { this.tokenIndexMap = new Map(this.tokens.map((token) => [token.token.address, token.index])); this.bptIndex = this.tokens.findIndex((t) => t.token.address === this.address); + this.tokenPairs = tokenPairs; } public getNormalizedLiquidity(tokenIn: Token, tokenOut: Token): bigint { @@ -122,8 +127,17 @@ export class StablePool implements BasePool { const tOut = this.tokenMap.get(tokenOut.wrapped); if (!tIn || !tOut) throw new Error('Pool does not contain the tokens provided'); - // TODO: Fix stable normalized liquidity calc - return tOut.amount * this.amp; + + const tokenPair = this.tokenPairs.find( + (tokenPair) => + (tokenPair.tokenA === tIn.token.address && tokenPair.tokenB === tOut.token.address) || + (tokenPair.tokenA === tOut.token.address && tokenPair.tokenB === tIn.token.address), + ); + + if (tokenPair) { + return parseEther(tokenPair.normalizedLiquidity); + } + return 0n; } public swapGivenIn( diff --git a/modules/sor/sorV2/lib/pools/stable/stableMath.ts b/modules/sor/sorV2/lib/pools/composableStable/stableMath.ts similarity index 100% rename from modules/sor/sorV2/lib/pools/stable/stableMath.ts rename to modules/sor/sorV2/lib/pools/composableStable/stableMath.ts diff --git a/modules/sor/sorV2/lib/pools/fx/fxPool.ts b/modules/sor/sorV2/lib/pools/fx/fxPool.ts index 962885078..a0089458a 100644 --- a/modules/sor/sorV2/lib/pools/fx/fxPool.ts +++ b/modules/sor/sorV2/lib/pools/fx/fxPool.ts @@ -9,6 +9,7 @@ import { RAY } from '../../utils/math'; import { FxPoolPairData } from './types'; import { BasePool, PoolType, SwapKind, Token, TokenAmount } from '@balancer/sdk'; import { chainToIdMap } from '../../../../../network/network-config'; +import { TokenPairData } from '../../../../../pool/lib/pool-on-chain-tokenpair-data'; const isUSDC = (address: string): boolean => { return ( @@ -30,6 +31,7 @@ export class FxPool implements BasePool { public readonly delta: bigint; public readonly epsilon: bigint; public readonly tokens: FxPoolToken[]; + public readonly tokenPairs: TokenPairData[]; private readonly tokenMap: Map; @@ -79,6 +81,7 @@ export class FxPool implements BasePool { parseUnits((pool.typeData as FxData).delta as string, 36), parseFixedCurveParam((pool.typeData as FxData).epsilon as string), poolTokens, + pool.dynamicData.tokenPairsData as TokenPairData[], ); } @@ -94,6 +97,7 @@ export class FxPool implements BasePool { delta: bigint, epsilon: bigint, tokens: FxPoolToken[], + tokenPairs: TokenPairData[], ) { this.id = id; this.address = address; @@ -107,6 +111,7 @@ export class FxPool implements BasePool { this.epsilon = epsilon; this.tokens = tokens; this.tokenMap = new Map(this.tokens.map((token) => [token.token.address, token])); + this.tokenPairs = tokenPairs; } public getNormalizedLiquidity(tokenIn: Token, tokenOut: Token): bigint { @@ -114,8 +119,17 @@ export class FxPool implements BasePool { const tOut = this.tokenMap.get(tokenOut.wrapped); if (!tIn || !tOut) throw new Error('Pool does not contain the tokens provided'); - // TODO: Fix fx normalized liquidity calc - return tOut.amount; + + const tokenPair = this.tokenPairs.find( + (tokenPair) => + (tokenPair.tokenA === tIn.token.address && tokenPair.tokenB === tOut.token.address) || + (tokenPair.tokenA === tOut.token.address && tokenPair.tokenB === tIn.token.address), + ); + + if (tokenPair) { + return parseEther(tokenPair.normalizedLiquidity); + } + return 0n; } public swapGivenIn( diff --git a/modules/sor/sorV2/lib/pools/gyro2/gyro2Pool.ts b/modules/sor/sorV2/lib/pools/gyro2/gyro2Pool.ts index 5be64bbda..3de14d818 100644 --- a/modules/sor/sorV2/lib/pools/gyro2/gyro2Pool.ts +++ b/modules/sor/sorV2/lib/pools/gyro2/gyro2Pool.ts @@ -7,6 +7,7 @@ import { SWAP_LIMIT_FACTOR } from '../../utils/gyroHelpers/math'; import { BasePool, BigintIsh, PoolType, SwapKind, Token, TokenAmount } from '@balancer/sdk'; import { chainToIdMap } from '../../../../../network/network-config'; import { GyroData } from '../../../../../pool/subgraph-mapper'; +import { TokenPairData } from '../../../../../pool/lib/pool-on-chain-tokenpair-data'; export class Gyro2PoolToken extends TokenAmount { public readonly index: number; @@ -37,6 +38,7 @@ export class Gyro2Pool implements BasePool { public readonly poolTypeVersion: number; public readonly swapFee: bigint; public readonly tokens: Gyro2PoolToken[]; + public readonly tokenPairs: TokenPairData[]; private readonly sqrtAlpha: bigint; private readonly sqrtBeta: bigint; @@ -76,6 +78,7 @@ export class Gyro2Pool implements BasePool { parseEther(gyroData.sqrtAlpha!), parseEther(gyroData.sqrtBeta!), poolTokens, + pool.dynamicData.tokenPairsData as TokenPairData[], ); } @@ -88,6 +91,7 @@ export class Gyro2Pool implements BasePool { sqrtAlpha: bigint, sqrtBeta: bigint, tokens: Gyro2PoolToken[], + tokenPairs: TokenPairData[], ) { this.id = id; this.address = address; @@ -98,6 +102,7 @@ export class Gyro2Pool implements BasePool { this.sqrtBeta = sqrtBeta; this.tokens = tokens; this.tokenMap = new Map(this.tokens.map((token) => [token.token.address, token])); + this.tokenPairs = tokenPairs; } public getNormalizedLiquidity(tokenIn: Token, tokenOut: Token): bigint { @@ -105,8 +110,17 @@ export class Gyro2Pool implements BasePool { const tOut = this.tokenMap.get(tokenOut.wrapped); if (!tIn || !tOut) throw new Error('Pool does not contain the tokens provided'); - // TODO: Fix gyro normalized liquidity calc - return tOut.amount; + + const tokenPair = this.tokenPairs.find( + (tokenPair) => + (tokenPair.tokenA === tIn.token.address && tokenPair.tokenB === tOut.token.address) || + (tokenPair.tokenA === tOut.token.address && tokenPair.tokenB === tIn.token.address), + ); + + if (tokenPair) { + return parseEther(tokenPair.normalizedLiquidity); + } + return 0n; } public swapGivenIn( diff --git a/modules/sor/sorV2/lib/pools/gyro3/gyro3Pool.ts b/modules/sor/sorV2/lib/pools/gyro3/gyro3Pool.ts index e5f72380e..1ab04963a 100644 --- a/modules/sor/sorV2/lib/pools/gyro3/gyro3Pool.ts +++ b/modules/sor/sorV2/lib/pools/gyro3/gyro3Pool.ts @@ -7,6 +7,7 @@ import { _calcInGivenOut, _calcOutGivenIn, _calculateInvariant } from './gyro3Ma import { BasePool, BigintIsh, PoolType, SwapKind, Token, TokenAmount } from '@balancer/sdk'; import { chainToIdMap } from '../../../../../network/network-config'; import { GyroData } from '../../../../../pool/subgraph-mapper'; +import { TokenPairData } from '../../../../../pool/lib/pool-on-chain-tokenpair-data'; export class Gyro3PoolToken extends TokenAmount { public readonly index: number; @@ -37,6 +38,7 @@ export class Gyro3Pool implements BasePool { public readonly poolTypeVersion: number; public readonly swapFee: bigint; public readonly tokens: Gyro3PoolToken[]; + public readonly tokenPairs: TokenPairData[]; private readonly root3Alpha: bigint; private readonly tokenMap: Map; @@ -72,6 +74,7 @@ export class Gyro3Pool implements BasePool { parseEther(pool.dynamicData.swapFee), parseEther((pool.typeData as GyroData).root3Alpha!), poolTokens, + pool.dynamicData.tokenPairsData as TokenPairData[], ); } constructor( @@ -82,6 +85,7 @@ export class Gyro3Pool implements BasePool { swapFee: bigint, root3Alpha: bigint, tokens: Gyro3PoolToken[], + tokenPairs: TokenPairData[], ) { this.id = id; this.address = address; @@ -91,6 +95,7 @@ export class Gyro3Pool implements BasePool { this.root3Alpha = root3Alpha; this.tokens = tokens; this.tokenMap = new Map(this.tokens.map((token) => [token.token.address, token])); + this.tokenPairs = tokenPairs; } public getNormalizedLiquidity(tokenIn: Token, tokenOut: Token): bigint { @@ -98,8 +103,17 @@ export class Gyro3Pool implements BasePool { const tOut = this.tokenMap.get(tokenOut.wrapped); if (!tIn || !tOut) throw new Error('Pool does not contain the tokens provided'); - // TODO: Fix gyro normalized liquidity calc - return tOut.amount; + + const tokenPair = this.tokenPairs.find( + (tokenPair) => + (tokenPair.tokenA === tIn.token.address && tokenPair.tokenB === tOut.token.address) || + (tokenPair.tokenA === tOut.token.address && tokenPair.tokenB === tIn.token.address), + ); + + if (tokenPair) { + return parseEther(tokenPair.normalizedLiquidity); + } + return 0n; } public swapGivenIn( diff --git a/modules/sor/sorV2/lib/pools/gyroE/gyroEPool.ts b/modules/sor/sorV2/lib/pools/gyroE/gyroEPool.ts index 58bf36c0a..81dc4086d 100644 --- a/modules/sor/sorV2/lib/pools/gyroE/gyroEPool.ts +++ b/modules/sor/sorV2/lib/pools/gyroE/gyroEPool.ts @@ -9,6 +9,7 @@ import { calculateInvariantWithError, calcOutGivenIn, calcInGivenOut } from './g import { BasePool, BigintIsh, PoolType, SwapKind, Token, TokenAmount } from '@balancer/sdk'; import { chainToIdMap } from '../../../../../network/network-config'; import { GyroData } from '../../../../../pool/subgraph-mapper'; +import { TokenPairData } from '../../../../../pool/lib/pool-on-chain-tokenpair-data'; export class GyroEPoolToken extends TokenAmount { public readonly rate: bigint; @@ -44,6 +45,7 @@ export class GyroEPool implements BasePool { public readonly tokens: GyroEPoolToken[]; public readonly gyroEParams: GyroEParams; public readonly derivedGyroEParams: DerivedGyroEParams; + public readonly tokenPairs: TokenPairData[]; private readonly tokenMap: Map; @@ -107,6 +109,7 @@ export class GyroEPool implements BasePool { poolTokens, gyroEParams, derivedGyroEParams, + pool.dynamicData.tokenPairsData as TokenPairData[], ); } @@ -119,6 +122,7 @@ export class GyroEPool implements BasePool { tokens: GyroEPoolToken[], gyroEParams: GyroEParams, derivedGyroEParams: DerivedGyroEParams, + tokenPairs: TokenPairData[], ) { this.id = id; this.address = address; @@ -129,6 +133,7 @@ export class GyroEPool implements BasePool { this.tokenMap = new Map(this.tokens.map((token) => [token.token.address, token])); this.gyroEParams = gyroEParams; this.derivedGyroEParams = derivedGyroEParams; + this.tokenPairs = tokenPairs; } public getNormalizedLiquidity(tokenIn: Token, tokenOut: Token): bigint { @@ -136,8 +141,17 @@ export class GyroEPool implements BasePool { const tOut = this.tokenMap.get(tokenOut.wrapped); if (!tIn || !tOut) throw new Error('Pool does not contain the tokens provided'); - // TODO: Fix gyro normalized liquidity calc - return tOut.amount; + + const tokenPair = this.tokenPairs.find( + (tokenPair) => + (tokenPair.tokenA === tIn.token.address && tokenPair.tokenB === tOut.token.address) || + (tokenPair.tokenA === tOut.token.address && tokenPair.tokenB === tIn.token.address), + ); + + if (tokenPair) { + return parseEther(tokenPair.normalizedLiquidity); + } + return 0n; } public swapGivenIn( diff --git a/modules/sor/sorV2/lib/pools/metastable/metastablePool.ts b/modules/sor/sorV2/lib/pools/metastable/metastablePool.ts index d0479f5ee..281f04a2e 100644 --- a/modules/sor/sorV2/lib/pools/metastable/metastablePool.ts +++ b/modules/sor/sorV2/lib/pools/metastable/metastablePool.ts @@ -1,12 +1,13 @@ import { Chain } from '@prisma/client'; import { Address, Hex, parseEther, parseUnits } from 'viem'; -import { StablePoolToken } from '../stable/stablePool'; +import { ComposableStablePoolToken } from '../composableStable/composableStablePool'; import { PrismaPoolWithDynamic } from '../../../../../../prisma/prisma-types'; -import { _calcInGivenOut, _calcOutGivenIn, _calculateInvariant } from '../stable/stableMath'; +import { _calcInGivenOut, _calcOutGivenIn, _calculateInvariant } from '../composableStable/stableMath'; import { MathSol, WAD } from '../../utils/math'; import { BasePool, PoolType, SwapKind, Token, TokenAmount } from '@balancer/sdk'; import { chainToIdMap } from '../../../../../network/network-config'; import { StableData } from '../../../../../pool/subgraph-mapper'; +import { TokenPairData } from '../../../../../pool/lib/pool-on-chain-tokenpair-data'; export class MetaStablePool implements BasePool { public readonly chain: Chain; @@ -15,13 +16,14 @@ export class MetaStablePool implements BasePool { public readonly poolType: PoolType = PoolType.MetaStable; public readonly amp: bigint; public readonly swapFee: bigint; - public readonly tokens: StablePoolToken[]; + public readonly tokens: ComposableStablePoolToken[]; + public readonly tokenPairs: TokenPairData[]; - private readonly tokenMap: Map; + private readonly tokenMap: Map; private readonly tokenIndexMap: Map; static fromPrismaPool(pool: PrismaPoolWithDynamic): MetaStablePool { - const poolTokens: StablePoolToken[] = []; + const poolTokens: ComposableStablePoolToken[] = []; if (!pool.dynamicData) throw new Error('Stable pool has no dynamic data'); @@ -37,7 +39,7 @@ export class MetaStablePool implements BasePool { const tokenAmount = TokenAmount.fromHumanAmount(token, `${parseFloat(poolToken.dynamicData.balance)}`); poolTokens.push( - new StablePoolToken( + new ComposableStablePoolToken( token, tokenAmount.amount, parseEther(poolToken.dynamicData.priceRate), @@ -55,10 +57,19 @@ export class MetaStablePool implements BasePool { amp, parseEther(pool.dynamicData.swapFee), poolTokens, + pool.dynamicData.tokenPairsData as TokenPairData[], ); } - constructor(id: Hex, address: string, chain: Chain, amp: bigint, swapFee: bigint, tokens: StablePoolToken[]) { + constructor( + id: Hex, + address: string, + chain: Chain, + amp: bigint, + swapFee: bigint, + tokens: ComposableStablePoolToken[], + tokenPairs: TokenPairData[], + ) { this.id = id; this.address = address; this.chain = chain; @@ -68,6 +79,7 @@ export class MetaStablePool implements BasePool { this.tokens = tokens.sort((a, b) => a.index - b.index); this.tokenMap = new Map(this.tokens.map((token) => [token.token.address, token])); this.tokenIndexMap = new Map(this.tokens.map((token) => [token.token.address, token.index])); + this.tokenPairs = tokenPairs; } public getNormalizedLiquidity(tokenIn: Token, tokenOut: Token): bigint { @@ -75,8 +87,17 @@ export class MetaStablePool implements BasePool { const tOut = this.tokenMap.get(tokenOut.address); if (!tIn || !tOut) throw new Error('Pool does not contain the tokens provided'); - // TODO: Fix stable normalized liquidity calc - return tOut.amount * this.amp; + + const tokenPair = this.tokenPairs.find( + (tokenPair) => + (tokenPair.tokenA === tIn.token.address && tokenPair.tokenB === tOut.token.address) || + (tokenPair.tokenA === tOut.token.address && tokenPair.tokenB === tIn.token.address), + ); + + if (tokenPair) { + return parseEther(tokenPair.normalizedLiquidity); + } + return 0n; } public swapGivenIn( diff --git a/modules/sor/sorV2/lib/pools/weighted/weightedPool.ts b/modules/sor/sorV2/lib/pools/weighted/weightedPool.ts index d06fbdacf..d2965bcbf 100644 --- a/modules/sor/sorV2/lib/pools/weighted/weightedPool.ts +++ b/modules/sor/sorV2/lib/pools/weighted/weightedPool.ts @@ -5,6 +5,7 @@ import { MathSol, WAD } from '../../utils/math'; import { Address, Hex, parseEther } from 'viem'; import { BasePool, BigintIsh, SwapKind, Token, TokenAmount } from '@balancer/sdk'; import { chainToIdMap } from '../../../../../network/network-config'; +import { TokenPairData } from '../../../../../pool/lib/pool-on-chain-tokenpair-data'; export class WeightedPoolToken extends TokenAmount { public readonly weight: bigint; @@ -37,6 +38,7 @@ export class WeightedPool implements BasePool { public readonly poolTypeVersion: number; public readonly swapFee: bigint; public readonly tokens: WeightedPoolToken[]; + public readonly tokenPairs: TokenPairData[]; private readonly tokenMap: Map; private readonly MAX_IN_RATIO = 300000000000000000n; // 0.3 @@ -80,6 +82,7 @@ export class WeightedPool implements BasePool { pool.version, parseEther(pool.dynamicData.swapFee), poolTokens, + pool.dynamicData.tokenPairsData as TokenPairData[], ); } @@ -90,6 +93,7 @@ export class WeightedPool implements BasePool { poolTypeVersion: number, swapFee: bigint, tokens: WeightedPoolToken[], + tokenPairs: TokenPairData[], ) { this.chain = chain; this.id = id; @@ -98,12 +102,22 @@ export class WeightedPool implements BasePool { this.swapFee = swapFee; this.tokens = tokens; this.tokenMap = new Map(tokens.map((token) => [token.token.address, token])); + this.tokenPairs = tokenPairs; } public getNormalizedLiquidity(tokenIn: Token, tokenOut: Token): bigint { const { tIn, tOut } = this.getRequiredTokenPair(tokenIn, tokenOut); - return (tIn.amount * tOut.weight) / (tIn.weight + tOut.weight); + const tokenPair = this.tokenPairs.find( + (tokenPair) => + (tokenPair.tokenA === tIn.token.address && tokenPair.tokenB === tOut.token.address) || + (tokenPair.tokenA === tOut.token.address && tokenPair.tokenB === tIn.token.address), + ); + + if (tokenPair) { + return parseEther(tokenPair.normalizedLiquidity); + } + return 0n; } public getLimitAmountSwap(tokenIn: Token, tokenOut: Token, swapKind: SwapKind): bigint { diff --git a/modules/sor/sorV2/lib/static.ts b/modules/sor/sorV2/lib/static.ts index 5817fbc93..ccb0f99a0 100644 --- a/modules/sor/sorV2/lib/static.ts +++ b/modules/sor/sorV2/lib/static.ts @@ -2,13 +2,13 @@ import { Router } from './router'; import { PrismaPoolWithDynamic } from '../../../../prisma/prisma-types'; import { checkInputs } from './utils/helpers'; import { WeightedPool } from './pools/weighted/weightedPool'; -import { StablePool } from './pools/stable/stablePool'; import { MetaStablePool } from './pools/metastable/metastablePool'; import { FxPool } from './pools/fx/fxPool'; import { Gyro2Pool } from './pools/gyro2/gyro2Pool'; import { Gyro3Pool } from './pools/gyro3/gyro3Pool'; import { GyroEPool } from './pools/gyroE/gyroEPool'; import { BasePool, Swap, SwapKind, SwapOptions, Token } from '@balancer/sdk'; +import { ComposableStablePool } from './pools/composableStable/composableStablePool'; export async function sorGetSwapsWithPools( tokenIn: Token, @@ -29,7 +29,7 @@ export async function sorGetSwapsWithPools( break; case 'COMPOSABLE_STABLE': case 'PHANTOM_STABLE': - basePools.push(StablePool.fromPrismaPool(prismaPool)); + basePools.push(ComposableStablePool.fromPrismaPool(prismaPool)); break; case 'META_STABLE': basePools.push(MetaStablePool.fromPrismaPool(prismaPool)); diff --git a/prisma/migrations/20240209084438_add_tokenpair_data/migration.sql b/prisma/migrations/20240209084438_add_tokenpair_data/migration.sql new file mode 100644 index 000000000..da857ecdd --- /dev/null +++ b/prisma/migrations/20240209084438_add_tokenpair_data/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "PrismaPoolDynamicData" ADD COLUMN "tokenPairsData" JSONB NOT NULL DEFAULT '[]'; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index bf06b9fd0..acce1b090 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -162,6 +162,8 @@ model PrismaPoolDynamicData { fees24hAthTimestamp Int @default(0) fees24hAtl Float @default(0) fees24hAtlTimestamp Int @default(0) + + tokenPairsData Json @default("[]") } model PrismaPoolToken {