From e70e220835e0bb8d3d2447795102ad4c91974180 Mon Sep 17 00:00:00 2001 From: franz Date: Thu, 1 Feb 2024 10:40:44 +0100 Subject: [PATCH 01/14] wip --- modules/pool/lib/pool-normalized-liquidty.ts | 238 ++++++++++++++++++ .../pool/lib/pool-on-chain-data.service.ts | 8 + 2 files changed, 246 insertions(+) create mode 100644 modules/pool/lib/pool-normalized-liquidty.ts diff --git a/modules/pool/lib/pool-normalized-liquidty.ts b/modules/pool/lib/pool-normalized-liquidty.ts new file mode 100644 index 000000000..24d75c91c --- /dev/null +++ b/modules/pool/lib/pool-normalized-liquidty.ts @@ -0,0 +1,238 @@ +import { formatEther, formatUnits } from 'ethers/lib/utils'; +import { Multicaller3 } from '../../web3/multicaller3'; +import { PrismaPoolType } from '@prisma/client'; +import { BigNumber, formatFixed } from '@ethersproject/bignumber'; +import ElementPoolAbi from '../abi/ConvergentCurvePool.json'; +import LinearPoolAbi from '../abi/LinearPool.json'; +import LiquidityBootstrappingPoolAbi from '../abi/LiquidityBootstrappingPool.json'; +import ComposableStablePoolAbi from '../abi/ComposableStablePool.json'; +import GyroEV2Abi from '../abi/GyroEV2.json'; +import VaultAbi from '../abi/Vault.json'; +import aTokenRateProvider from '../abi/StaticATokenRateProvider.json'; +import WeightedPoolAbi from '../abi/WeightedPool.json'; +import StablePoolAbi from '../abi/StablePool.json'; +import MetaStablePoolAbi from '../abi/MetaStablePool.json'; +import StablePhantomPoolAbi from '../abi/StablePhantomPool.json'; +import FxPoolAbi from '../abi/FxPool.json'; +import { JsonFragment } from '@ethersproject/abi'; + +interface PoolInput { + id: string; + address: string; + type: PrismaPoolType | 'COMPOSABLE_STABLE'; + tokens: { + address: string; + token: { + decimals: number; + }; + }[]; + version: number; +} + +interface OnchainData { + poolTokens: [string[], BigNumber[]]; + totalSupply: BigNumber; + swapFee: BigNumber; + swapEnabled?: boolean; + protocolYieldFeePercentageCache?: BigNumber; + protocolSwapFeePercentageCache?: BigNumber; + rate?: BigNumber; + weights?: BigNumber[]; + targets?: [BigNumber, BigNumber]; + wrappedTokenRate?: BigNumber; + amp?: [BigNumber, boolean, BigNumber]; + tokenRates?: [BigNumber, BigNumber]; + tokenRate?: BigNumber[]; + metaPriceRateCache?: [BigNumber, BigNumber, BigNumber][]; +} + +const abi: JsonFragment[] = Object.values( + // Remove duplicate entries using their names + Object.fromEntries( + [ + ...ElementPoolAbi, + ...LinearPoolAbi, + ...LiquidityBootstrappingPoolAbi, + ...ComposableStablePoolAbi, + ...GyroEV2Abi, + ...VaultAbi, + ...aTokenRateProvider, + ...WeightedPoolAbi, + ...StablePoolAbi, + ...StablePhantomPoolAbi, + ...MetaStablePoolAbi, + ...ComposableStablePoolAbi, + ...FxPoolAbi, + //...WeightedPoolV2Abi, + ].map((row) => [row.name, row]), + ), +); + +const getSwapFeeFn = (type: string) => { + if (type === 'ELEMENT') { + return 'percentFee'; + } else if (type === 'FX') { + return 'protocolPercentFee'; + } else { + return 'getSwapFeePercentage'; + } +}; + +const getTotalSupplyFn = (type: PoolInput['type'], version: number) => { + if (['LINEAR'].includes(type) || (type === 'COMPOSABLE_STABLE' && version === 0)) { + return 'getVirtualSupply'; + } else if ( + type === 'COMPOSABLE_STABLE' || + (type === 'WEIGHTED' && version > 1) || + (type === 'UNKNOWN' && version > 1) + ) { + return 'getActualSupply'; + } else { + return 'totalSupply'; + } +}; + +const addDefaultCallsToMulticaller = ( + { id, address, type, version }: PoolInput, + vaultAddress: string, + multicaller: Multicaller3, +) => { + multicaller.call(`${id}.poolTokens`, vaultAddress, 'getPoolTokens', [id]); + multicaller.call(`${id}.totalSupply`, address, getTotalSupplyFn(type, version)); + multicaller.call(`${id}.swapFee`, address, getSwapFeeFn(type)); + multicaller.call(`${id}.rate`, address, 'getRate'); + multicaller.call(`${id}.protocolSwapFeePercentageCache`, address, 'getProtocolFeePercentageCache', [0]); + multicaller.call(`${id}.protocolYieldFeePercentageCache`, address, 'getProtocolFeePercentageCache', [2]); +}; + +const weightedCalls = ({ id, address }: PoolInput, multicaller: Multicaller3) => { + multicaller.call(`${id}.weights`, address, 'getNormalizedWeights'); +}; + +const lbpAndInvestmentCalls = ({ id, address }: PoolInput, multicaller: Multicaller3) => { + multicaller.call(`${id}.weights`, address, 'getNormalizedWeights'); + multicaller.call(`${id}.swapEnabled`, address, 'getSwapEnabled'); +}; + +const linearCalls = ({ id, address }: PoolInput, multicaller: Multicaller3) => { + multicaller.call(`${id}.targets`, address, 'getTargets'); + multicaller.call(`${id}.wrappedTokenRate`, address, 'getWrappedTokenRate'); +}; + +const stableCalls = ({ id, address, tokens }: PoolInput, multicaller: Multicaller3) => { + multicaller.call(`${id}.amp`, address, 'getAmplificationParameter'); + + tokens.forEach(({ address: tokenAddress }, i) => { + multicaller.call(`${id}.tokenRate[${i}]`, address, 'getTokenRate', [tokenAddress]); + }); +}; + +const metaStableCalls = ({ id, address, tokens }: PoolInput, multicaller: Multicaller3) => { + multicaller.call(`${id}.amp`, address, 'getAmplificationParameter'); + + tokens.forEach(({ address: tokenAddress }, i) => { + multicaller.call(`${id}.metaPriceRateCache[${i}]`, address, 'getPriceRateCache', [tokenAddress]); + }); +}; + +const gyroECalls = ({ id, address }: PoolInput, multicaller: Multicaller3) => { + multicaller.call(`${id}.tokenRates`, address, 'getTokenRates'); +}; + +const addPoolTypeSpecificCallsToMulticaller = (type: PoolInput['type'], version = 1) => { + const do_nothing = () => ({}); + switch (type) { + case 'WEIGHTED': + return weightedCalls; + case 'LIQUIDITY_BOOTSTRAPPING': + case 'INVESTMENT': + return lbpAndInvestmentCalls; + case 'STABLE': + case 'PHANTOM_STABLE': + case 'COMPOSABLE_STABLE': + return stableCalls; + case 'META_STABLE': + return metaStableCalls; + case 'GYROE': + if (version === 2) { + return gyroECalls; + } else { + return do_nothing; + } + case 'LINEAR': + return linearCalls; + default: + return do_nothing; + } +}; + +const parse = (result: OnchainData, decimalsLookup: { [address: string]: number }) => ({ + amp: result.amp ? formatFixed(result.amp[0], String(result.amp[2]).length - 1) : undefined, + swapFee: formatEther(result.swapFee ?? '0'), + totalShares: formatEther(result.totalSupply || '0'), + weights: result.weights?.map(formatEther), + targets: result.targets?.map(String), + poolTokens: result.poolTokens + ? { + tokens: result.poolTokens[0].map((token) => token.toLowerCase()), + balances: result.poolTokens[1].map((balance, i) => + formatUnits(balance, decimalsLookup[result.poolTokens[0][i].toLowerCase()]), + ), + rates: result.poolTokens[0].map((_, i) => + result.tokenRate && result.tokenRate[i] + ? formatEther(result.tokenRate[i]) + : result.tokenRates && result.tokenRates[i] + ? formatEther(result.tokenRates[i]) + : result.metaPriceRateCache && result.metaPriceRateCache[i][0].gt(0) + ? formatEther(result.metaPriceRateCache[i][0]) + : undefined, + ), + } + : { tokens: [], balances: [], rates: [] }, + wrappedTokenRate: result.wrappedTokenRate ? formatEther(result.wrappedTokenRate) : '1.0', + rate: result.rate ? formatEther(result.rate) : '1.0', + swapEnabled: result.swapEnabled, + protocolYieldFeePercentageCache: result.protocolYieldFeePercentageCache + ? formatEther(result.protocolYieldFeePercentageCache) + : undefined, + protocolSwapFeePercentageCache: result.protocolSwapFeePercentageCache + ? formatEther(result.protocolSwapFeePercentageCache) + : undefined, +}); + +export const fetchNormalizedLiquidity = async ( + pools: PoolInput[], + balancerQueriesAddress: string, + batchSize = 1024, +) => { + if (pools.length === 0) { + return {}; + } + + const multicaller = new Multicaller3(abi, batchSize); + + // only inlcude pools with TVL >=$1000 + // for each pool, get pairs + // for each pair per pool, swap $500 amount from a->b + // for each pair per pool, swap result of above back b->a + // calc normalizedLiquidity + + pools.forEach((pool) => { + addDefaultCallsToMulticaller(pool, balancerQueriesAddress, multicaller); + addPoolTypeSpecificCallsToMulticaller(pool.type, pool.version)(pool, multicaller); + }); + + const results = (await multicaller.execute()) as { + [id: string]: OnchainData; + }; + + const decimalsLookup = Object.fromEntries( + pools.flatMap((pool) => pool.tokens.map(({ address, token }) => [address, token.decimals])), + ); + + const parsed = Object.fromEntries( + Object.entries(results).map(([key, result]) => [key, parse(result, decimalsLookup)]), + ); + + return parsed; +}; diff --git a/modules/pool/lib/pool-on-chain-data.service.ts b/modules/pool/lib/pool-on-chain-data.service.ts index 8243749eb..0180f6f85 100644 --- a/modules/pool/lib/pool-on-chain-data.service.ts +++ b/modules/pool/lib/pool-on-chain-data.service.ts @@ -9,6 +9,8 @@ import { fetchOnChainPoolState } from './pool-onchain-state'; import { fetchOnChainPoolData } from './pool-onchain-data'; import { fetchOnChainGyroFees } from './pool-onchain-gyro-fee'; import { networkContext } from '../../network/network-context.service'; +import { filter } from 'lodash'; +import { fetchNormalizedLiquidity } from './pool-normalized-liquidty'; const SUPPORTED_POOL_TYPES: PrismaPoolType[] = [ 'WEIGHTED', @@ -32,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, @@ -103,6 +106,11 @@ export class PoolOnChainDataService { this.options.vaultAddress, networkContext.chain === 'ZKEVM' ? 190 : 1024, ); + const normalizedLiquidityResults = await fetchNormalizedLiquidity( + 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 })); From 1684d6f3b20d01b281ecfdd97b6232fc44548892 Mon Sep 17 00:00:00 2001 From: franz Date: Fri, 2 Feb 2024 14:16:51 +0100 Subject: [PATCH 02/14] wip --- modules/pool/lib/pool-normalized-liquidty.ts | 126 +------------------ modules/pool/pool.prisma | 8 ++ 2 files changed, 14 insertions(+), 120 deletions(-) diff --git a/modules/pool/lib/pool-normalized-liquidty.ts b/modules/pool/lib/pool-normalized-liquidty.ts index 24d75c91c..6a5a20ab5 100644 --- a/modules/pool/lib/pool-normalized-liquidty.ts +++ b/modules/pool/lib/pool-normalized-liquidty.ts @@ -19,13 +19,16 @@ import { JsonFragment } from '@ethersproject/abi'; interface PoolInput { id: string; address: string; - type: PrismaPoolType | 'COMPOSABLE_STABLE'; + type: PrismaPoolType; tokens: { address: string; token: { decimals: number; }; }[]; + dynamicData:{ + totalLiquidity: string; + }; version: number; } @@ -46,125 +49,6 @@ interface OnchainData { metaPriceRateCache?: [BigNumber, BigNumber, BigNumber][]; } -const abi: JsonFragment[] = Object.values( - // Remove duplicate entries using their names - Object.fromEntries( - [ - ...ElementPoolAbi, - ...LinearPoolAbi, - ...LiquidityBootstrappingPoolAbi, - ...ComposableStablePoolAbi, - ...GyroEV2Abi, - ...VaultAbi, - ...aTokenRateProvider, - ...WeightedPoolAbi, - ...StablePoolAbi, - ...StablePhantomPoolAbi, - ...MetaStablePoolAbi, - ...ComposableStablePoolAbi, - ...FxPoolAbi, - //...WeightedPoolV2Abi, - ].map((row) => [row.name, row]), - ), -); - -const getSwapFeeFn = (type: string) => { - if (type === 'ELEMENT') { - return 'percentFee'; - } else if (type === 'FX') { - return 'protocolPercentFee'; - } else { - return 'getSwapFeePercentage'; - } -}; - -const getTotalSupplyFn = (type: PoolInput['type'], version: number) => { - if (['LINEAR'].includes(type) || (type === 'COMPOSABLE_STABLE' && version === 0)) { - return 'getVirtualSupply'; - } else if ( - type === 'COMPOSABLE_STABLE' || - (type === 'WEIGHTED' && version > 1) || - (type === 'UNKNOWN' && version > 1) - ) { - return 'getActualSupply'; - } else { - return 'totalSupply'; - } -}; - -const addDefaultCallsToMulticaller = ( - { id, address, type, version }: PoolInput, - vaultAddress: string, - multicaller: Multicaller3, -) => { - multicaller.call(`${id}.poolTokens`, vaultAddress, 'getPoolTokens', [id]); - multicaller.call(`${id}.totalSupply`, address, getTotalSupplyFn(type, version)); - multicaller.call(`${id}.swapFee`, address, getSwapFeeFn(type)); - multicaller.call(`${id}.rate`, address, 'getRate'); - multicaller.call(`${id}.protocolSwapFeePercentageCache`, address, 'getProtocolFeePercentageCache', [0]); - multicaller.call(`${id}.protocolYieldFeePercentageCache`, address, 'getProtocolFeePercentageCache', [2]); -}; - -const weightedCalls = ({ id, address }: PoolInput, multicaller: Multicaller3) => { - multicaller.call(`${id}.weights`, address, 'getNormalizedWeights'); -}; - -const lbpAndInvestmentCalls = ({ id, address }: PoolInput, multicaller: Multicaller3) => { - multicaller.call(`${id}.weights`, address, 'getNormalizedWeights'); - multicaller.call(`${id}.swapEnabled`, address, 'getSwapEnabled'); -}; - -const linearCalls = ({ id, address }: PoolInput, multicaller: Multicaller3) => { - multicaller.call(`${id}.targets`, address, 'getTargets'); - multicaller.call(`${id}.wrappedTokenRate`, address, 'getWrappedTokenRate'); -}; - -const stableCalls = ({ id, address, tokens }: PoolInput, multicaller: Multicaller3) => { - multicaller.call(`${id}.amp`, address, 'getAmplificationParameter'); - - tokens.forEach(({ address: tokenAddress }, i) => { - multicaller.call(`${id}.tokenRate[${i}]`, address, 'getTokenRate', [tokenAddress]); - }); -}; - -const metaStableCalls = ({ id, address, tokens }: PoolInput, multicaller: Multicaller3) => { - multicaller.call(`${id}.amp`, address, 'getAmplificationParameter'); - - tokens.forEach(({ address: tokenAddress }, i) => { - multicaller.call(`${id}.metaPriceRateCache[${i}]`, address, 'getPriceRateCache', [tokenAddress]); - }); -}; - -const gyroECalls = ({ id, address }: PoolInput, multicaller: Multicaller3) => { - multicaller.call(`${id}.tokenRates`, address, 'getTokenRates'); -}; - -const addPoolTypeSpecificCallsToMulticaller = (type: PoolInput['type'], version = 1) => { - const do_nothing = () => ({}); - switch (type) { - case 'WEIGHTED': - return weightedCalls; - case 'LIQUIDITY_BOOTSTRAPPING': - case 'INVESTMENT': - return lbpAndInvestmentCalls; - case 'STABLE': - case 'PHANTOM_STABLE': - case 'COMPOSABLE_STABLE': - return stableCalls; - case 'META_STABLE': - return metaStableCalls; - case 'GYROE': - if (version === 2) { - return gyroECalls; - } else { - return do_nothing; - } - case 'LINEAR': - return linearCalls; - default: - return do_nothing; - } -}; const parse = (result: OnchainData, decimalsLookup: { [address: string]: number }) => ({ amp: result.amp ? formatFixed(result.amp[0], String(result.amp[2]).length - 1) : undefined, @@ -211,6 +95,8 @@ export const fetchNormalizedLiquidity = async ( const multicaller = new Multicaller3(abi, batchSize); + for + // only inlcude pools with TVL >=$1000 // for each pool, get pairs // for each pair per pool, swap $500 amount from a->b diff --git a/modules/pool/pool.prisma b/modules/pool/pool.prisma index 4cce260b6..7743159e6 100644 --- a/modules/pool/pool.prisma +++ b/modules/pool/pool.prisma @@ -175,6 +175,14 @@ model PrismaPoolDynamicData { fees24hAthTimestamp Int @default(0) fees24hAtl Float @default(0) fees24hAtlTimestamp Int @default(0) + + tokenPairsData TokenPair[] +} + +model PrismaTokenPair { + + id String + poolId } model PrismaPoolStableDynamicData { From 6395a4ca65b9b12aeebf53e7d3bb67d81851d09b Mon Sep 17 00:00:00 2001 From: franz Date: Wed, 7 Feb 2024 16:13:04 +0100 Subject: [PATCH 03/14] wip --- modules/pool/abi/BalancerQueries.json | 309 +++++++++++++++++++ modules/pool/lib/pool-normalized-liquidty.ts | 224 +++++++++----- 2 files changed, 456 insertions(+), 77 deletions(-) create mode 100644 modules/pool/abi/BalancerQueries.json 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-normalized-liquidty.ts b/modules/pool/lib/pool-normalized-liquidty.ts index 6a5a20ab5..70cdc09c6 100644 --- a/modules/pool/lib/pool-normalized-liquidty.ts +++ b/modules/pool/lib/pool-normalized-liquidty.ts @@ -13,112 +13,182 @@ import WeightedPoolAbi from '../abi/WeightedPool.json'; import StablePoolAbi from '../abi/StablePool.json'; import MetaStablePoolAbi from '../abi/MetaStablePool.json'; import StablePhantomPoolAbi from '../abi/StablePhantomPool.json'; -import FxPoolAbi from '../abi/FxPool.json'; -import { JsonFragment } from '@ethersproject/abi'; +import BalancerQueries from '../abi/BalancerQueries.json'; +import { filter, result } from 'lodash'; +import { ZERO_ADDRESS } from '@balancer/sdk'; +import { parseUnits } from 'viem'; interface PoolInput { id: string; address: string; - type: PrismaPoolType; tokens: { address: string; token: { decimals: number; }; + dynamicData: { + balance: string; + balanceUSD: number; + } | null; }[]; - dynamicData:{ - totalLiquidity: string; - }; - version: number; + dynamicData: { + totalLiquidity: number; + } | null; } -interface OnchainData { - poolTokens: [string[], BigNumber[]]; - totalSupply: BigNumber; - swapFee: BigNumber; - swapEnabled?: boolean; - protocolYieldFeePercentageCache?: BigNumber; - protocolSwapFeePercentageCache?: BigNumber; - rate?: BigNumber; - weights?: BigNumber[]; - targets?: [BigNumber, BigNumber]; - wrappedTokenRate?: BigNumber; - amp?: [BigNumber, boolean, BigNumber]; - tokenRates?: [BigNumber, BigNumber]; - tokenRate?: BigNumber[]; - metaPriceRateCache?: [BigNumber, BigNumber, BigNumber][]; +interface PoolOutput { + id: string; + tokenPairs: TokenPair[]; +} + +interface TokenPair { + poolId: string; + tokenA: Token; + tokenB: Token; + normalizedLiqudity: string; + aToBPrice: string; + bToAPrice: string; + spotPrice: string; + effectivePrice: string; } +interface Token { + address: string; + decimals: number; + balance: string; + balanceUsd: number; +} -const parse = (result: OnchainData, decimalsLookup: { [address: string]: number }) => ({ - amp: result.amp ? formatFixed(result.amp[0], String(result.amp[2]).length - 1) : undefined, - swapFee: formatEther(result.swapFee ?? '0'), - totalShares: formatEther(result.totalSupply || '0'), - weights: result.weights?.map(formatEther), - targets: result.targets?.map(String), - poolTokens: result.poolTokens - ? { - tokens: result.poolTokens[0].map((token) => token.toLowerCase()), - balances: result.poolTokens[1].map((balance, i) => - formatUnits(balance, decimalsLookup[result.poolTokens[0][i].toLowerCase()]), - ), - rates: result.poolTokens[0].map((_, i) => - result.tokenRate && result.tokenRate[i] - ? formatEther(result.tokenRate[i]) - : result.tokenRates && result.tokenRates[i] - ? formatEther(result.tokenRates[i]) - : result.metaPriceRateCache && result.metaPriceRateCache[i][0].gt(0) - ? formatEther(result.metaPriceRateCache[i][0]) - : undefined, - ), - } - : { tokens: [], balances: [], rates: [] }, - wrappedTokenRate: result.wrappedTokenRate ? formatEther(result.wrappedTokenRate) : '1.0', - rate: result.rate ? formatEther(result.rate) : '1.0', - swapEnabled: result.swapEnabled, - protocolYieldFeePercentageCache: result.protocolYieldFeePercentageCache - ? formatEther(result.protocolYieldFeePercentageCache) - : undefined, - protocolSwapFeePercentageCache: result.protocolSwapFeePercentageCache - ? formatEther(result.protocolSwapFeePercentageCache) - : undefined, -}); - -export const fetchNormalizedLiquidity = async ( - pools: PoolInput[], - balancerQueriesAddress: string, - batchSize = 1024, -) => { +interface OnchainData { + effectivePrice: BigNumber; + aToBPrice: BigNumber; + bToAPrice: BigNumber; +} + +export async function fetchNormalizedLiquidity(pools: PoolInput[], balancerQueriesAddress: string, batchSize = 1024) { if (pools.length === 0) { return {}; } - const multicaller = new Multicaller3(abi, batchSize); - - for + const multicaller = new Multicaller3(BalancerQueries, batchSize); // only inlcude pools with TVL >=$1000 // for each pool, get pairs - // for each pair per pool, swap $500 amount from a->b - // for each pair per pool, swap result of above back b->a - // calc normalizedLiquidity + // 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 - pools.forEach((pool) => { - addDefaultCallsToMulticaller(pool, balancerQueriesAddress, multicaller); - addPoolTypeSpecificCallsToMulticaller(pool.type, pool.version)(pool, multicaller); + // remove pools that have <$1000 TVL or a token without a balance or USD balance + const filteredPools = pools.filter( + (pool) => + pool.dynamicData?.totalLiquidity || + 0 >= 1000 || + pool.tokens.some((token) => token.dynamicData?.balance || '0' === '0') || + pool.tokens.some((token) => token.dynamicData?.balanceUSD || 0 === 0), + ); + const tokenPairs = generateTokenPairs(filteredPools); + + tokenPairs.forEach((tokenPair) => { + addEffectivePriceCallsToMulticaller(tokenPair, balancerQueriesAddress, multicaller); + addAToBePriceCallsToMulticaller(tokenPair, balancerQueriesAddress, multicaller); }); - const results = (await multicaller.execute()) as { + const resultOne = (await multicaller.execute()) as { [id: string]: OnchainData; }; - const decimalsLookup = Object.fromEntries( - pools.flatMap((pool) => pool.tokens.map(({ address, token }) => [address, token.decimals])), + tokenPairs.forEach((tokenPair) => { + tokenPair.aToBPrice = getAtoBPriceForPair(tokenPair, resultOne); + tokenPair.effectivePrice = getEffectivePriceForPair(tokenPair, resultOne); + }); + + console.log(resultOne); +} + +function generateTokenPairs(filteredPools: PoolInput[]): TokenPair[] { + const tokenPairs: TokenPair[] = []; + + for (const pool of filteredPools) { + // search for and delete phantom BPT if present + let index: number | undefined = undefined; + pool.tokens.forEach((poolToken, i) => { + if (poolToken.address === pool.address) { + index = i; + } + }); + if (index) { + pool.tokens.splice(index, 1); + } + + // 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++) { + tokenPairs.push({ + poolId: pool.id, + 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: '0', + spotPrice: '0', + aToBPrice: '0', + bToAPrice: '0', + effectivePrice: '0', + }); + } + } + } + return tokenPairs; +} + +// call querySwap from tokenA->tokenB with 100USD worth of tokenA +function addEffectivePriceCallsToMulticaller( + tokenPair: TokenPair, + balancerQueriesAddress: string, + multicaller: Multicaller3, +) { + const oneHundredUsdOfTokenA = (parseFloat(tokenPair.tokenA.balance) / tokenPair.tokenA.balanceUsd) * 100; + const amountScaled = parseUnits(`${oneHundredUsdOfTokenA}`, tokenPair.tokenA.decimals); + + multicaller.call( + `${tokenPair.poolId}-${tokenPair.tokenA.address}-${tokenPair.tokenB.address}.effectivePrice`, + balancerQueriesAddress, + 'querySwap', + [ + [tokenPair.poolId, 0, tokenPair.tokenA.address, tokenPair.tokenB.address, `${amountScaled}`, ZERO_ADDRESS], + [ZERO_ADDRESS, false, ZERO_ADDRESS, false], + ], ); +} - const parsed = Object.fromEntries( - Object.entries(results).map(([key, result]) => [key, parse(result, decimalsLookup)]), +// call querySwap from tokenA->tokenB with 1% of tokenA balance +function addAToBePriceCallsToMulticaller( + tokenPair: TokenPair, + balancerQueriesAddress: string, + multicaller: Multicaller3, +) { + const amountScaled = parseUnits(tokenPair.tokenA.balance, tokenPair.tokenA.decimals) / 100n; + + multicaller.call( + `${tokenPair.poolId}-${tokenPair.tokenA.address}-${tokenPair.tokenB.address}.aToBPrice`, + balancerQueriesAddress, + 'querySwap', + [ + [tokenPair.poolId, 0, tokenPair.tokenA.address, tokenPair.tokenB.address, `${amountScaled}`, ZERO_ADDRESS], + [ZERO_ADDRESS, false, ZERO_ADDRESS, false], + ], ); +} +function getAtoBPriceForPair(tokenPair: TokenPair, resultOne: { [id: string]: OnchainData }): string {} - return parsed; -}; +function getEffectivePriceForPair(tokenPair: TokenPair, resultOne: { [id: string]: OnchainData }): string { + throw new Error('Function not implemented.'); +} From 7f8400e3663f15b530b83f5dc1f1c3a486f9cc08 Mon Sep 17 00:00:00 2001 From: franz Date: Wed, 7 Feb 2024 21:15:49 +0100 Subject: [PATCH 04/14] on chain fetching --- modules/pool/lib/pool-normalized-liquidty.ts | 205 +++++++++++++++---- 1 file changed, 162 insertions(+), 43 deletions(-) diff --git a/modules/pool/lib/pool-normalized-liquidty.ts b/modules/pool/lib/pool-normalized-liquidty.ts index 70cdc09c6..2fa2fcb6a 100644 --- a/modules/pool/lib/pool-normalized-liquidty.ts +++ b/modules/pool/lib/pool-normalized-liquidty.ts @@ -15,7 +15,7 @@ import MetaStablePoolAbi from '../abi/MetaStablePool.json'; import StablePhantomPoolAbi from '../abi/StablePhantomPool.json'; import BalancerQueries from '../abi/BalancerQueries.json'; import { filter, result } from 'lodash'; -import { ZERO_ADDRESS } from '@balancer/sdk'; +import { MathSol, WAD, ZERO_ADDRESS } from '@balancer/sdk'; import { parseUnits } from 'viem'; interface PoolInput { @@ -36,20 +36,31 @@ interface PoolInput { } | null; } -interface PoolOutput { - id: string; - tokenPairs: TokenPair[]; +interface PoolTokenPairsOutput { + [poolId: string]: { + tokenPairs: { + id: string; + normalizedLiquidity: string; + spotPrice: string; + }[]; + }; } interface TokenPair { poolId: string; + poolTvl: number; + valid: boolean; tokenA: Token; tokenB: Token; - normalizedLiqudity: string; - aToBPrice: string; - bToAPrice: string; - spotPrice: string; - effectivePrice: string; + normalizedLiqudity: bigint; + spotPrice: bigint; + aToBPrice: bigint; + aToBAmountIn: bigint; + aToBAmountOut: bigint; + bToAPrice: bigint; + bToAAmountOut: bigint; + effectivePrice: bigint; + effectivePriceAmountIn: bigint; } interface Token { @@ -60,9 +71,9 @@ interface Token { } interface OnchainData { - effectivePrice: BigNumber; - aToBPrice: BigNumber; - bToAPrice: BigNumber; + effectivePriceAmountOut: BigNumber; + aToBAmountOut: BigNumber; + bToAAmountOut: BigNumber; } export async function fetchNormalizedLiquidity(pools: PoolInput[], balancerQueriesAddress: string, batchSize = 1024) { @@ -70,6 +81,8 @@ export async function fetchNormalizedLiquidity(pools: PoolInput[], balancerQueri return {}; } + const poolsOutput: PoolTokenPairsOutput = {}; + const multicaller = new Multicaller3(BalancerQueries, batchSize); // only inlcude pools with TVL >=$1000 @@ -78,19 +91,20 @@ export async function fetchNormalizedLiquidity(pools: PoolInput[], balancerQueri // 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 - // remove pools that have <$1000 TVL or a token without a balance or USD balance - const filteredPools = pools.filter( - (pool) => - pool.dynamicData?.totalLiquidity || - 0 >= 1000 || - pool.tokens.some((token) => token.dynamicData?.balance || '0' === '0') || - pool.tokens.some((token) => token.dynamicData?.balanceUSD || 0 === 0), - ); - const tokenPairs = generateTokenPairs(filteredPools); + const tokenPairs = generateTokenPairs(pools); tokenPairs.forEach((tokenPair) => { - addEffectivePriceCallsToMulticaller(tokenPair, balancerQueriesAddress, multicaller); - addAToBePriceCallsToMulticaller(tokenPair, balancerQueriesAddress, multicaller); + 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 { @@ -98,11 +112,46 @@ export async function fetchNormalizedLiquidity(pools: PoolInput[], balancerQueri }; tokenPairs.forEach((tokenPair) => { - tokenPair.aToBPrice = getAtoBPriceForPair(tokenPair, resultOne); - tokenPair.effectivePrice = getEffectivePriceForPair(tokenPair, resultOne); + if (tokenPair.valid) { + getAmountOutAndEffectivePriceFromResult(tokenPair, resultOne); + } + }); + + tokenPairs.forEach((tokenPair) => { + if (tokenPair.valid) { + addBToAPriceCallsToMulticaller(tokenPair, balancerQueriesAddress, multicaller); + } }); - console.log(resultOne); + 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 (!poolsOutput[pool.id]) { + poolsOutput[pool.id] = { + tokenPairs: [], + }; + } + poolsOutput[pool.id].tokenPairs.push({ + id: `${pool.id}-${tokenPair.tokenA.address}-${tokenPair.tokenB.address}`, + normalizedLiquidity: tokenPair.normalizedLiqudity.toString(), + spotPrice: tokenPair.spotPrice.toString(), + }); + } + }); + }); + + return poolsOutput; } function generateTokenPairs(filteredPools: PoolInput[]): TokenPair[] { @@ -125,6 +174,13 @@ function generateTokenPairs(filteredPools: PoolInput[]): TokenPair[] { for (let j = i + 1; j < pool.tokens.length; j++) { 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, @@ -137,11 +193,15 @@ function generateTokenPairs(filteredPools: PoolInput[]): TokenPair[] { balance: pool.tokens[j].dynamicData?.balance || '0', balanceUsd: pool.tokens[j].dynamicData?.balanceUSD || 0, }, - normalizedLiqudity: '0', - spotPrice: '0', - aToBPrice: '0', - bToAPrice: '0', - effectivePrice: '0', + normalizedLiqudity: 0n, + spotPrice: 0n, + aToBPrice: 0n, + aToBAmountIn: 0n, + aToBAmountOut: 0n, + bToAPrice: 0n, + bToAAmountOut: 0n, + effectivePrice: 0n, + effectivePriceAmountIn: 0n, }); } } @@ -155,40 +215,99 @@ function addEffectivePriceCallsToMulticaller( balancerQueriesAddress: string, multicaller: Multicaller3, ) { - const oneHundredUsdOfTokenA = (parseFloat(tokenPair.tokenA.balance) / tokenPair.tokenA.balanceUsd) * 100; - const amountScaled = parseUnits(`${oneHundredUsdOfTokenA}`, tokenPair.tokenA.decimals); - multicaller.call( - `${tokenPair.poolId}-${tokenPair.tokenA.address}-${tokenPair.tokenB.address}.effectivePrice`, + `${tokenPair.poolId}-${tokenPair.tokenA.address}-${tokenPair.tokenB.address}.effectivePriceAmountOut`, balancerQueriesAddress, 'querySwap', [ - [tokenPair.poolId, 0, tokenPair.tokenA.address, tokenPair.tokenB.address, `${amountScaled}`, ZERO_ADDRESS], + [ + 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 addAToBePriceCallsToMulticaller( +function addAToBPriceCallsToMulticaller( tokenPair: TokenPair, balancerQueriesAddress: string, multicaller: Multicaller3, ) { - const amountScaled = parseUnits(tokenPair.tokenA.balance, tokenPair.tokenA.decimals) / 100n; + 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}.aToBPrice`, + `${tokenPair.poolId}-${tokenPair.tokenA.address}-${tokenPair.tokenB.address}.bToAAmountOut`, balancerQueriesAddress, 'querySwap', [ - [tokenPair.poolId, 0, tokenPair.tokenA.address, tokenPair.tokenB.address, `${amountScaled}`, ZERO_ADDRESS], + [ + tokenPair.poolId, + 0, + tokenPair.tokenB.address, + tokenPair.tokenA.address, + `${tokenPair.aToBAmountOut}`, + ZERO_ADDRESS, + ], [ZERO_ADDRESS, false, ZERO_ADDRESS, false], ], ); } -function getAtoBPriceForPair(tokenPair: TokenPair, resultOne: { [id: string]: OnchainData }): string {} -function getEffectivePriceForPair(tokenPair: TokenPair, resultOne: { [id: string]: OnchainData }): string { - throw new Error('Function not implemented.'); +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()); + tokenPair.effectivePrice = MathSol.divDownFixed( + tokenPair.effectivePriceAmountIn, + BigInt(result.effectivePriceAmountOut.toString()), + ); + } +} + +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) { + const priceAtoB = MathSol.divDownFixed(tokenPair.aToBAmountIn, tokenPair.aToBAmountOut); + const priceBtoA = MathSol.divDownFixed(tokenPair.aToBAmountOut, tokenPair.bToAAmountOut); + tokenPair.spotPrice = MathSol.powDownFixed(MathSol.divDownFixed(priceAtoB, priceBtoA), WAD / 2n); +} + +function calculateNormalizedLiquidity(tokenPair: TokenPair) { + const priceRatio = MathSol.divDownFixed(tokenPair.spotPrice, tokenPair.effectivePrice); + const priceImpact = WAD - priceRatio; + tokenPair.normalizedLiqudity = MathSol.divDownFixed(WAD, priceImpact); } From d880eb89d528b0a061bd41386522c1d4bd957978 Mon Sep 17 00:00:00 2001 From: franz Date: Thu, 8 Feb 2024 08:57:33 +0100 Subject: [PATCH 05/14] fix scaling --- modules/pool/lib/pool-normalized-liquidty.ts | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/modules/pool/lib/pool-normalized-liquidty.ts b/modules/pool/lib/pool-normalized-liquidty.ts index 2fa2fcb6a..54bfbbd8f 100644 --- a/modules/pool/lib/pool-normalized-liquidty.ts +++ b/modules/pool/lib/pool-normalized-liquidty.ts @@ -54,10 +54,8 @@ interface TokenPair { tokenB: Token; normalizedLiqudity: bigint; spotPrice: bigint; - aToBPrice: bigint; aToBAmountIn: bigint; aToBAmountOut: bigint; - bToAPrice: bigint; bToAAmountOut: bigint; effectivePrice: bigint; effectivePriceAmountIn: bigint; @@ -110,7 +108,7 @@ export async function fetchNormalizedLiquidity(pools: PoolInput[], balancerQueri const resultOne = (await multicaller.execute()) as { [id: string]: OnchainData; }; - + ``; tokenPairs.forEach((tokenPair) => { if (tokenPair.valid) { getAmountOutAndEffectivePriceFromResult(tokenPair, resultOne); @@ -195,10 +193,8 @@ function generateTokenPairs(filteredPools: PoolInput[]): TokenPair[] { }, normalizedLiqudity: 0n, spotPrice: 0n, - aToBPrice: 0n, aToBAmountIn: 0n, aToBAmountOut: 0n, - bToAPrice: 0n, bToAAmountOut: 0n, effectivePrice: 0n, effectivePriceAmountIn: 0n, @@ -286,9 +282,10 @@ function getAmountOutAndEffectivePriceFromResult(tokenPair: TokenPair, onchainRe if (result) { tokenPair.aToBAmountOut = BigInt(result.aToBAmountOut.toString()); + // MathSol expects all values with 18 decimals, need to scale them tokenPair.effectivePrice = MathSol.divDownFixed( - tokenPair.effectivePriceAmountIn, - BigInt(result.effectivePriceAmountOut.toString()), + parseUnits(tokenPair.effectivePriceAmountIn.toString(), 18 - tokenPair.tokenA.decimals), + parseUnits(result.effectivePriceAmountOut.toString(), 18 - tokenPair.tokenB.decimals), ); } } @@ -301,12 +298,17 @@ function getBToAAmountFromResult(tokenPair: TokenPair, onchainResults: { [id: st } } function calculateSpotPrice(tokenPair: TokenPair) { - const priceAtoB = MathSol.divDownFixed(tokenPair.aToBAmountIn, tokenPair.aToBAmountOut); - const priceBtoA = MathSol.divDownFixed(tokenPair.aToBAmountOut, tokenPair.bToAAmountOut); + // 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 const priceRatio = MathSol.divDownFixed(tokenPair.spotPrice, tokenPair.effectivePrice); const priceImpact = WAD - priceRatio; tokenPair.normalizedLiqudity = MathSol.divDownFixed(WAD, priceImpact); From 33918d8ff5d55fdb2eaac3105b9efcf93721f9a9 Mon Sep 17 00:00:00 2001 From: franz Date: Thu, 8 Feb 2024 16:33:06 +0100 Subject: [PATCH 06/14] cap priceRatio to make sure normalizedLiquditiy is always >0 --- modules/pool/lib/pool-normalized-liquidty.ts | 29 ++++++++------------ 1 file changed, 12 insertions(+), 17 deletions(-) diff --git a/modules/pool/lib/pool-normalized-liquidty.ts b/modules/pool/lib/pool-normalized-liquidty.ts index 54bfbbd8f..83da95b5e 100644 --- a/modules/pool/lib/pool-normalized-liquidty.ts +++ b/modules/pool/lib/pool-normalized-liquidty.ts @@ -1,22 +1,9 @@ -import { formatEther, formatUnits } from 'ethers/lib/utils'; import { Multicaller3 } from '../../web3/multicaller3'; -import { PrismaPoolType } from '@prisma/client'; -import { BigNumber, formatFixed } from '@ethersproject/bignumber'; -import ElementPoolAbi from '../abi/ConvergentCurvePool.json'; -import LinearPoolAbi from '../abi/LinearPool.json'; -import LiquidityBootstrappingPoolAbi from '../abi/LiquidityBootstrappingPool.json'; -import ComposableStablePoolAbi from '../abi/ComposableStablePool.json'; -import GyroEV2Abi from '../abi/GyroEV2.json'; -import VaultAbi from '../abi/Vault.json'; -import aTokenRateProvider from '../abi/StaticATokenRateProvider.json'; -import WeightedPoolAbi from '../abi/WeightedPool.json'; -import StablePoolAbi from '../abi/StablePool.json'; -import MetaStablePoolAbi from '../abi/MetaStablePool.json'; -import StablePhantomPoolAbi from '../abi/StablePhantomPool.json'; +import { BigNumber } from '@ethersproject/bignumber'; import BalancerQueries from '../abi/BalancerQueries.json'; -import { filter, result } from 'lodash'; import { MathSol, WAD, ZERO_ADDRESS } from '@balancer/sdk'; -import { parseUnits } from 'viem'; +import { parseEther, parseUnits } from 'viem'; +import * as Sentry from '@sentry/node'; interface PoolInput { id: string; @@ -309,7 +296,15 @@ function calculateSpotPrice(tokenPair: TokenPair) { function calculateNormalizedLiquidity(tokenPair: TokenPair) { // spotPrice and effective price are already scaled to 18 decimals by the MathSol output - const priceRatio = MathSol.divDownFixed(tokenPair.spotPrice, tokenPair.effectivePrice); + 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); } From c3a12071c3107477bb3fb622fea9c18c06765eae Mon Sep 17 00:00:00 2001 From: franz Date: Thu, 8 Feb 2024 17:05:29 +0100 Subject: [PATCH 07/14] wip --- .../pool/lib/pool-on-chain-data.service.ts | 6 +++-- ...dty.ts => pool-on-chain-tokenpair-data.ts} | 24 ++++++++++--------- modules/pool/pool.prisma | 8 +------ prisma/schema.prisma | 4 +++- 4 files changed, 21 insertions(+), 21 deletions(-) rename modules/pool/lib/{pool-normalized-liquidty.ts => pool-on-chain-tokenpair-data.ts} (95%) diff --git a/modules/pool/lib/pool-on-chain-data.service.ts b/modules/pool/lib/pool-on-chain-data.service.ts index 145ad6b90..2cbd8aeaf 100644 --- a/modules/pool/lib/pool-on-chain-data.service.ts +++ b/modules/pool/lib/pool-on-chain-data.service.ts @@ -10,7 +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 { fetchNormalizedLiquidity } from './pool-normalized-liquidty'; +import { TokenPair, TokenPairData, fetchTokenPairData } from './pool-on-chain-tokenpair-data'; const SUPPORTED_POOL_TYPES: PrismaPoolType[] = [ 'WEIGHTED', @@ -103,7 +103,7 @@ export class PoolOnChainDataService { this.options.vaultAddress, networkContext.chain === 'ZKEVM' ? 190 : 1024, ); - const normalizedLiquidityResults = await fetchNormalizedLiquidity( + const tokenPairData = await fetchTokenPairData( filteredPools, this.options.balancerQueriesAddress, networkContext.chain === 'ZKEVM' ? 190 : 1024, @@ -115,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 { @@ -203,6 +204,7 @@ export class PoolOnChainDataService { protocolYieldFee: yieldProtocolFeePercentage, protocolSwapFee: swapProtocolFeePercentage, blockNumber, + tokenPairsData: tokenPairs, }, }), ); diff --git a/modules/pool/lib/pool-normalized-liquidty.ts b/modules/pool/lib/pool-on-chain-tokenpair-data.ts similarity index 95% rename from modules/pool/lib/pool-normalized-liquidty.ts rename to modules/pool/lib/pool-on-chain-tokenpair-data.ts index 83da95b5e..e26257df0 100644 --- a/modules/pool/lib/pool-normalized-liquidty.ts +++ b/modules/pool/lib/pool-on-chain-tokenpair-data.ts @@ -25,14 +25,16 @@ interface PoolInput { interface PoolTokenPairsOutput { [poolId: string]: { - tokenPairs: { - id: string; - normalizedLiquidity: string; - spotPrice: string; - }[]; + tokenPairs: TokenPairData[]; }; } +export interface TokenPairData { + id: string; + normalizedLiquidity: string; + spotPrice: string; +} + interface TokenPair { poolId: string; poolTvl: number; @@ -61,12 +63,12 @@ interface OnchainData { bToAAmountOut: BigNumber; } -export async function fetchNormalizedLiquidity(pools: PoolInput[], balancerQueriesAddress: string, batchSize = 1024) { +export async function fetchTokenPairData(pools: PoolInput[], balancerQueriesAddress: string, batchSize = 1024) { if (pools.length === 0) { return {}; } - const poolsOutput: PoolTokenPairsOutput = {}; + const tokenPairOutput: PoolTokenPairsOutput = {}; const multicaller = new Multicaller3(BalancerQueries, batchSize); @@ -122,12 +124,12 @@ export async function fetchNormalizedLiquidity(pools: PoolInput[], balancerQueri // prepare output pools.forEach((pool) => { if (pool.id === tokenPair.poolId) { - if (!poolsOutput[pool.id]) { - poolsOutput[pool.id] = { + if (!tokenPairOutput[pool.id]) { + tokenPairOutput[pool.id] = { tokenPairs: [], }; } - poolsOutput[pool.id].tokenPairs.push({ + tokenPairOutput[pool.id].tokenPairs.push({ id: `${pool.id}-${tokenPair.tokenA.address}-${tokenPair.tokenB.address}`, normalizedLiquidity: tokenPair.normalizedLiqudity.toString(), spotPrice: tokenPair.spotPrice.toString(), @@ -136,7 +138,7 @@ export async function fetchNormalizedLiquidity(pools: PoolInput[], balancerQueri }); }); - return poolsOutput; + return tokenPairOutput; } function generateTokenPairs(filteredPools: PoolInput[]): TokenPair[] { diff --git a/modules/pool/pool.prisma b/modules/pool/pool.prisma index d10157bb2..1d3227782 100644 --- a/modules/pool/pool.prisma +++ b/modules/pool/pool.prisma @@ -116,13 +116,7 @@ model PrismaPoolDynamicData { fees24hAtl Float @default(0) fees24hAtlTimestamp Int @default(0) - tokenPairsData TokenPair[] -} - -model PrismaTokenPair { - - id String - poolId + tokenPairsData Json @default("[]") } model PrismaPoolToken { diff --git a/prisma/schema.prisma b/prisma/schema.prisma index fd64f7536..acce1b090 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -64,7 +64,7 @@ model PrismaPool { vaultVersion Int @default(2) - typeData Json @default("{}") + typeData Json @default("{}") tokens PrismaPoolToken[] @@ -162,6 +162,8 @@ model PrismaPoolDynamicData { fees24hAthTimestamp Int @default(0) fees24hAtl Float @default(0) fees24hAtlTimestamp Int @default(0) + + tokenPairsData Json @default("[]") } model PrismaPoolToken { From 196b53e7e81d6eaabe31384f1f55d8bdae4ab42b Mon Sep 17 00:00:00 2001 From: franz Date: Thu, 8 Feb 2024 17:23:28 +0100 Subject: [PATCH 08/14] wip --- modules/pool/lib/pool-on-chain-data.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/pool/lib/pool-on-chain-data.service.ts b/modules/pool/lib/pool-on-chain-data.service.ts index 2cbd8aeaf..2c20afed4 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'; From 75c87017de16f7911be4b14e264cd847235d1c7b Mon Sep 17 00:00:00 2001 From: franz Date: Thu, 8 Feb 2024 17:58:40 +0100 Subject: [PATCH 09/14] add to sor --- .../pool/lib/pool-on-chain-data.service.ts | 2 +- .../pool/lib/pool-on-chain-tokenpair-data.ts | 10 ++++--- modules/sor/sorV2/lib/pools/fx/fxPool.ts | 18 +++++++++++-- .../sor/sorV2/lib/pools/gyro2/gyro2Pool.ts | 18 +++++++++++-- .../sor/sorV2/lib/pools/gyro3/gyro3Pool.ts | 18 +++++++++++-- .../sor/sorV2/lib/pools/gyroE/gyroEPool.ts | 18 +++++++++++-- .../lib/pools/metastable/metastablePool.ts | 27 ++++++++++++++++--- .../sor/sorV2/lib/pools/stable/stablePool.ts | 18 +++++++++++-- .../sorV2/lib/pools/weighted/weightedPool.ts | 16 ++++++++++- 9 files changed, 126 insertions(+), 19 deletions(-) diff --git a/modules/pool/lib/pool-on-chain-data.service.ts b/modules/pool/lib/pool-on-chain-data.service.ts index 2c20afed4..2a8622795 100644 --- a/modules/pool/lib/pool-on-chain-data.service.ts +++ b/modules/pool/lib/pool-on-chain-data.service.ts @@ -10,7 +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 { TokenPair, TokenPairData, fetchTokenPairData } from './pool-on-chain-tokenpair-data'; +import { fetchTokenPairData } from './pool-on-chain-tokenpair-data'; const SUPPORTED_POOL_TYPES: PrismaPoolType[] = [ 'WEIGHTED', diff --git a/modules/pool/lib/pool-on-chain-tokenpair-data.ts b/modules/pool/lib/pool-on-chain-tokenpair-data.ts index e26257df0..52c7acdb3 100644 --- a/modules/pool/lib/pool-on-chain-tokenpair-data.ts +++ b/modules/pool/lib/pool-on-chain-tokenpair-data.ts @@ -29,11 +29,12 @@ interface PoolTokenPairsOutput { }; } -export interface TokenPairData { - id: string; +export type TokenPairData = { + tokenA: string; + tokenB: string; normalizedLiquidity: string; spotPrice: string; -} +}; interface TokenPair { poolId: string; @@ -130,7 +131,8 @@ export async function fetchTokenPairData(pools: PoolInput[], balancerQueriesAddr }; } tokenPairOutput[pool.id].tokenPairs.push({ - id: `${pool.id}-${tokenPair.tokenA.address}-${tokenPair.tokenB.address}`, + tokenA: tokenPair.tokenA.address, + tokenB: tokenPair.tokenB.address, normalizedLiquidity: tokenPair.normalizedLiqudity.toString(), spotPrice: tokenPair.spotPrice.toString(), }); 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..5b68cde4a 100644 --- a/modules/sor/sorV2/lib/pools/metastable/metastablePool.ts +++ b/modules/sor/sorV2/lib/pools/metastable/metastablePool.ts @@ -7,6 +7,7 @@ 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; @@ -16,6 +17,7 @@ export class MetaStablePool implements BasePool { public readonly amp: bigint; public readonly swapFee: bigint; public readonly tokens: StablePoolToken[]; + public readonly tokenPairs: TokenPairData[]; private readonly tokenMap: Map; private readonly tokenIndexMap: Map; @@ -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: StablePoolToken[], + 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/stable/stablePool.ts b/modules/sor/sorV2/lib/pools/stable/stablePool.ts index 83dce24a3..0cf6249cb 100644 --- a/modules/sor/sorV2/lib/pools/stable/stablePool.ts +++ b/modules/sor/sorV2/lib/pools/stable/stablePool.ts @@ -14,6 +14,7 @@ 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 { public readonly rate: bigint; @@ -47,6 +48,7 @@ export class StablePool implements BasePool { public readonly amp: bigint; public readonly swapFee: bigint; public readonly bptIndex: number; + public readonly tokenPairs: TokenPairData[]; public totalShares: bigint; public tokens: StablePoolToken[]; @@ -91,6 +93,7 @@ export class StablePool implements BasePool { parseEther(pool.dynamicData.swapFee), poolTokens, totalShares, + pool.dynamicData.tokenPairsData as TokenPairData[], ); } @@ -102,6 +105,7 @@ export class StablePool implements BasePool { swapFee: bigint, tokens: StablePoolToken[], 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/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 { From a7734e1123e506c777e4c2b0fa2a9d8f366ab245 Mon Sep 17 00:00:00 2001 From: franz Date: Thu, 8 Feb 2024 18:01:06 +0100 Subject: [PATCH 10/14] always update tokenpairs --- modules/pool/lib/pool-on-chain-data.service.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/modules/pool/lib/pool-on-chain-data.service.ts b/modules/pool/lib/pool-on-chain-data.service.ts index 2a8622795..fc53696f8 100644 --- a/modules/pool/lib/pool-on-chain-data.service.ts +++ b/modules/pool/lib/pool-on-chain-data.service.ts @@ -210,6 +210,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)); From 1920814946e2e6f421465e8aff12e10cd9329b71 Mon Sep 17 00:00:00 2001 From: franz Date: Fri, 9 Feb 2024 09:54:38 +0100 Subject: [PATCH 11/14] add migration, fix pool filter --- modules/pool/lib/pool-on-chain-tokenpair-data.ts | 4 ++-- .../20240209084438_add_tokenpair_data/migration.sql | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) create mode 100644 prisma/migrations/20240209084438_add_tokenpair_data/migration.sql diff --git a/modules/pool/lib/pool-on-chain-tokenpair-data.ts b/modules/pool/lib/pool-on-chain-tokenpair-data.ts index 52c7acdb3..bfbf6e60d 100644 --- a/modules/pool/lib/pool-on-chain-tokenpair-data.ts +++ b/modules/pool/lib/pool-on-chain-tokenpair-data.ts @@ -167,8 +167,8 @@ function generateTokenPairs(filteredPools: PoolInput[]): TokenPair[] { // 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), + !pool.tokens.some((token) => token.dynamicData?.balance || '0' === '0') && + !pool.tokens.some((token) => token.dynamicData?.balanceUSD || 0 === 0), tokenA: { address: pool.tokens[i].address, 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 '[]'; From ec770c42c1c6541210e4031d4040e094df4395ec Mon Sep 17 00:00:00 2001 From: franz Date: Fri, 9 Feb 2024 12:13:33 +0100 Subject: [PATCH 12/14] fix bug --- modules/pool/lib/pool-on-chain-data.service.ts | 1 - modules/pool/lib/pool-on-chain-tokenpair-data.ts | 13 ++----------- 2 files changed, 2 insertions(+), 12 deletions(-) diff --git a/modules/pool/lib/pool-on-chain-data.service.ts b/modules/pool/lib/pool-on-chain-data.service.ts index fc53696f8..66f5e1dbb 100644 --- a/modules/pool/lib/pool-on-chain-data.service.ts +++ b/modules/pool/lib/pool-on-chain-data.service.ts @@ -204,7 +204,6 @@ export class PoolOnChainDataService { protocolYieldFee: yieldProtocolFeePercentage, protocolSwapFee: swapProtocolFeePercentage, blockNumber, - tokenPairsData: tokenPairs, }, }), ); diff --git a/modules/pool/lib/pool-on-chain-tokenpair-data.ts b/modules/pool/lib/pool-on-chain-tokenpair-data.ts index bfbf6e60d..72789d230 100644 --- a/modules/pool/lib/pool-on-chain-tokenpair-data.ts +++ b/modules/pool/lib/pool-on-chain-tokenpair-data.ts @@ -147,20 +147,11 @@ function generateTokenPairs(filteredPools: PoolInput[]): TokenPair[] { const tokenPairs: TokenPair[] = []; for (const pool of filteredPools) { - // search for and delete phantom BPT if present - let index: number | undefined = undefined; - pool.tokens.forEach((poolToken, i) => { - if (poolToken.address === pool.address) { - index = i; - } - }); - if (index) { - pool.tokens.splice(index, 1); - } - // 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, From 78223fced8defd7f40916f90c0abffafa1f98373 Mon Sep 17 00:00:00 2001 From: franz Date: Fri, 9 Feb 2024 12:13:37 +0100 Subject: [PATCH 13/14] cleanup --- modules/pool/lib/pool-gql-loader.service.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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 { From 9547467bd298b40495768661d8122bb603b14e47 Mon Sep 17 00:00:00 2001 From: franz Date: Fri, 9 Feb 2024 12:13:45 +0100 Subject: [PATCH 14/14] rename stable to composablestable --- .../composableStablePool.ts} | 20 +++++++++---------- .../stableMath.ts | 0 .../lib/pools/metastable/metastablePool.ts | 14 ++++++------- modules/sor/sorV2/lib/static.ts | 4 ++-- 4 files changed, 19 insertions(+), 19 deletions(-) rename modules/sor/sorV2/lib/pools/{stable/stablePool.ts => composableStable/composableStablePool.ts} (95%) rename modules/sor/sorV2/lib/pools/{stable => composableStable}/stableMath.ts (100%) diff --git a/modules/sor/sorV2/lib/pools/stable/stablePool.ts b/modules/sor/sorV2/lib/pools/composableStable/composableStablePool.ts similarity index 95% rename from modules/sor/sorV2/lib/pools/stable/stablePool.ts rename to modules/sor/sorV2/lib/pools/composableStable/composableStablePool.ts index 0cf6249cb..272457f92 100644 --- a/modules/sor/sorV2/lib/pools/stable/stablePool.ts +++ b/modules/sor/sorV2/lib/pools/composableStable/composableStablePool.ts @@ -16,7 +16,7 @@ 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; @@ -40,24 +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'); @@ -73,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), @@ -85,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, @@ -103,7 +103,7 @@ export class StablePool implements BasePool { chain: Chain, amp: bigint, swapFee: bigint, - tokens: StablePoolToken[], + tokens: ComposableStablePoolToken[], totalShares: bigint, tokenPairs: TokenPairData[], ) { 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/metastable/metastablePool.ts b/modules/sor/sorV2/lib/pools/metastable/metastablePool.ts index 5b68cde4a..281f04a2e 100644 --- a/modules/sor/sorV2/lib/pools/metastable/metastablePool.ts +++ b/modules/sor/sorV2/lib/pools/metastable/metastablePool.ts @@ -1,8 +1,8 @@ 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'; @@ -16,14 +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'); @@ -39,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), @@ -67,7 +67,7 @@ export class MetaStablePool implements BasePool { chain: Chain, amp: bigint, swapFee: bigint, - tokens: StablePoolToken[], + tokens: ComposableStablePoolToken[], tokenPairs: TokenPairData[], ) { this.id = id; 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));