diff --git a/src/pools/index.ts b/src/pools/index.ts index 45db5f49..97e0062e 100644 --- a/src/pools/index.ts +++ b/src/pools/index.ts @@ -10,6 +10,7 @@ import { Gyro3Pool } from './gyro3Pool/gyro3Pool'; import { GyroEPool } from './gyroEPool/gyroEPool'; import { GyroEV2Pool } from './gyroEV2Pool/gyroEV2Pool'; import { FxPool } from './xaveFxPool/fxPool'; +import { MaganedPoolKassandra } from './managedPools/MaganedPoolKassandra'; import { BigNumber as OldBigNumber, INFINITY, @@ -41,6 +42,7 @@ export function parseNewPool( | GyroEPool | GyroEV2Pool | FxPool + | MaganedPoolKassandra | undefined { // We're not interested in any pools which don't allow swapping if (!pool.swapEnabled) return undefined; @@ -57,7 +59,8 @@ export function parseNewPool( | Gyro3Pool | GyroEPool | GyroEV2Pool - | FxPool; + | FxPool + | MaganedPoolKassandra; try { const isLinear = pool.poolType.toString().includes('Linear'); @@ -90,6 +93,8 @@ export function parseNewPool( newPool = GyroEPool.fromPool(pool); } } else if (pool.poolType === 'FX') newPool = FxPool.fromPool(pool); + else if (pool.poolType === 'Managed') + newPool = MaganedPoolKassandra.fromPool(pool); else { console.error( `Unknown pool type or type field missing: ${pool.poolType} ${pool.id}` diff --git a/src/pools/managedPools/MaganedPoolKassandra.ts b/src/pools/managedPools/MaganedPoolKassandra.ts new file mode 100644 index 00000000..6aaf7cc8 --- /dev/null +++ b/src/pools/managedPools/MaganedPoolKassandra.ts @@ -0,0 +1,282 @@ +import { BigNumber, formatFixed, parseFixed } from '@ethersproject/bignumber'; +import { WeiPerEther as ONE, Zero } from '@ethersproject/constants'; +import { getAddress } from '@ethersproject/address'; +import { + BigNumber as OldBigNumber, + ZERO, + bnum, + scale, +} from '../../utils/bignumber'; +import { isSameAddress } from '../../utils'; +import { + PoolBase, + PoolPairBase, + PoolTypes, + SubgraphPoolBase, + SwapTypes, +} from '../../types'; +import { WeightedPoolToken } from '../weightedPool/weightedPool'; +import { + _calcInGivenOut, + _calcOutGivenIn, + _derivativeSpotPriceAfterSwapExactTokenInForTokenOut, + _derivativeSpotPriceAfterSwapTokenInForExactTokenOut, + _spotPriceAfterSwapExactTokenInForTokenOut, + _spotPriceAfterSwapTokenInForExactTokenOut, +} from '../weightedPool/weightedMath'; +import { universalNormalizedLiquidity } from '../liquidity'; + +export type ManagedPoolPairData = PoolPairBase & { + weightIn: BigNumber; + weightOut: BigNumber; +}; + +export class MaganedPoolKassandra implements PoolBase { + poolType: PoolTypes = PoolTypes.Managed; + id: string; + address: string; + tokensList: string[]; + tokens: WeightedPoolToken[]; + totalWeight: BigNumber; + totalShares: BigNumber; + swapFee: BigNumber; + MAX_IN_RATIO = parseFixed('0.3', 18); + MAX_OUT_RATIO = parseFixed('0.3', 18); + mainIndex?: number | undefined; + isLBP?: boolean | undefined; + + constructor( + id: string, + address: string, + tokenList: string[], + tokens: WeightedPoolToken[], + totalShares: string, + swapFee: string + ) { + this.id = id; + this.address = address; + this.tokensList = tokenList; + this.tokens = tokens; + this.totalShares = parseFixed(totalShares, 18); + this.totalWeight = parseFixed('1', 18); + this.swapFee = parseFixed(swapFee, 18); + } + + static fromPool(pool: SubgraphPoolBase): MaganedPoolKassandra { + return new MaganedPoolKassandra( + pool.id, + pool.address, + pool.tokensList, + pool.tokens as WeightedPoolToken[], + pool.totalShares, + pool.swapFee + ); + } + + parsePoolPairData(tokenIn: string, tokenOut: string): ManagedPoolPairData { + if ( + isSameAddress(tokenIn, this.address) || + isSameAddress(tokenOut, this.address) + ) { + throw new Error('Token cannot be BPT'); + } + const tokenIndexIn = this.tokens.findIndex( + (t) => getAddress(t.address) === getAddress(tokenIn) + ); + if (tokenIndexIn < 0) throw 'Pool does not contain tokenIn'; + const tI = this.tokens[tokenIndexIn]; + const balanceIn = tI.balance; + const decimalsIn = tI.decimals; + const weightIn = parseFixed(tI.weight, 18) + .mul(ONE) + .div(this.totalWeight); + const tokenIndexOut = this.tokens.findIndex( + (t) => getAddress(t.address) === getAddress(tokenOut) + ); + if (tokenIndexOut < 0) throw 'Pool does not contain tokenOut'; + const tO = this.tokens[tokenIndexOut]; + const balanceOut = tO.balance; + const decimalsOut = tO.decimals; + const weightOut = parseFixed(tO.weight, 18) + .mul(ONE) + .div(this.totalWeight); + + const poolPairData: ManagedPoolPairData = { + id: this.id, + address: this.address, + poolType: this.poolType, + swapFee: this.swapFee, + tokenIn: tokenIn, + tokenOut: tokenOut, + decimalsIn: Number(decimalsIn), + decimalsOut: Number(decimalsOut), + balanceIn: parseFixed(balanceIn, decimalsIn), + balanceOut: parseFixed(balanceOut, decimalsOut), + weightIn, + weightOut, + }; + + return poolPairData; + } + + getNormalizedLiquidity(poolPairData: ManagedPoolPairData): OldBigNumber { + return universalNormalizedLiquidity( + this._derivativeSpotPriceAfterSwapExactTokenInForTokenOut( + poolPairData, + ZERO + ) + ); + } + + getLimitAmountSwap( + poolPairData: PoolPairBase, + swapType: SwapTypes + ): OldBigNumber { + if (swapType === SwapTypes.SwapExactIn) { + return bnum( + formatFixed( + poolPairData.balanceIn.mul(this.MAX_IN_RATIO).div(ONE), + poolPairData.decimalsIn + ) + ); + } else { + return bnum( + formatFixed( + poolPairData.balanceOut.mul(this.MAX_OUT_RATIO).div(ONE), + poolPairData.decimalsOut + ) + ); + } + } + + updateTokenBalanceForPool(token: string, newBalance: BigNumber): void { + // token is BPT + if (isSameAddress(this.address, token)) { + this.updateTotalShares(newBalance); + } + // token is underlying in the pool + const T = this.tokens.find((t) => isSameAddress(t.address, token)); + if (!T) throw Error('Pool does not contain this token'); + T.balance = formatFixed(newBalance, T.decimals); + } + + updateTotalShares(newTotalShares: BigNumber): void { + this.totalShares = newTotalShares; + } + + _exactTokenInForTokenOut( + poolPairData: ManagedPoolPairData, + amount: OldBigNumber + ): OldBigNumber { + if (amount.isNaN()) return amount; + const amountIn = parseFixed(amount.dp(18, 1).toString(), 18).toBigInt(); + const decimalsIn = poolPairData.decimalsIn; + const decimalsOut = poolPairData.decimalsOut; + const balanceIn = parseFixed( + poolPairData.balanceIn.toString(), + 18 - decimalsIn + ).toBigInt(); + const balanceOut = parseFixed( + poolPairData.balanceOut.toString(), + 18 - decimalsOut + ).toBigInt(); + const normalizedWeightIn = poolPairData.weightIn.toBigInt(); + const normalizedWeightOut = poolPairData.weightOut.toBigInt(); + const swapFee = poolPairData.swapFee.toBigInt(); + try { + const returnAmt = _calcOutGivenIn( + balanceIn, + normalizedWeightIn, + balanceOut, + normalizedWeightOut, + amountIn, + swapFee + ); + return scale(bnum(returnAmt.toString()), -18); + } catch (err) { + return ZERO; + } + } + + _tokenInForExactTokenOut( + poolPairData: ManagedPoolPairData, + amount: OldBigNumber + ): OldBigNumber { + if (amount.isNaN()) return amount; + const amountOut = parseFixed( + amount.dp(18, 1).toString(), + 18 + ).toBigInt(); + const decimalsIn = poolPairData.decimalsIn; + const decimalsOut = poolPairData.decimalsOut; + const balanceIn = parseFixed( + poolPairData.balanceIn.toString(), + 18 - decimalsIn + ).toBigInt(); + const balanceOut = parseFixed( + poolPairData.balanceOut.toString(), + 18 - decimalsOut + ).toBigInt(); + const normalizedWeightIn = poolPairData.weightIn.toBigInt(); + const normalizedWeightOut = poolPairData.weightOut.toBigInt(); + const swapFee = poolPairData.swapFee.toBigInt(); + try { + const returnAmt = _calcInGivenOut( + balanceIn, + normalizedWeightIn, + balanceOut, + normalizedWeightOut, + amountOut, + swapFee + ); + // return human scaled + return scale(bnum(returnAmt.toString()), -18); + } catch (err) { + return ZERO; + } + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _calcTokensOutGivenExactBptIn(_: BigNumber): BigNumber[] { + return Array(this.tokensList.length).fill(Zero); + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _calcBptOutGivenExactTokensIn(_: BigNumber[]): BigNumber { + return Zero; + } + + _spotPriceAfterSwapExactTokenInForTokenOut( + poolPairData: ManagedPoolPairData, + amount: OldBigNumber + ): OldBigNumber { + return _spotPriceAfterSwapExactTokenInForTokenOut(amount, poolPairData); + } + + _spotPriceAfterSwapTokenInForExactTokenOut( + poolPairData: ManagedPoolPairData, + amount: OldBigNumber + ): OldBigNumber { + return _spotPriceAfterSwapTokenInForExactTokenOut(amount, poolPairData); + } + + _derivativeSpotPriceAfterSwapExactTokenInForTokenOut( + poolPairData: ManagedPoolPairData, + amount: OldBigNumber + ): OldBigNumber { + return _derivativeSpotPriceAfterSwapExactTokenInForTokenOut( + amount, + poolPairData + ); + } + + _derivativeSpotPriceAfterSwapTokenInForExactTokenOut( + poolPairData: ManagedPoolPairData, + amount: OldBigNumber + ): OldBigNumber { + return _derivativeSpotPriceAfterSwapTokenInForExactTokenOut( + amount, + poolPairData + ); + } +} diff --git a/src/pools/weightedPool/weightedMath.ts b/src/pools/weightedPool/weightedMath.ts index 142683a1..8c7b2861 100644 --- a/src/pools/weightedPool/weightedMath.ts +++ b/src/pools/weightedPool/weightedMath.ts @@ -5,6 +5,8 @@ import { MathSol, BZERO } from '../../utils/basicOperations'; const MAX_INVARIANT_RATIO = BigInt('3000000000000000000'); // 3e18 +type WeightedPoolMathPairData = Omit; + // The following function are BigInt versions implemented by Sergio. // BigInt was requested from integrators as it is more efficient. // Swap outcomes formulas should match exactly those from smart contracts. @@ -482,7 +484,7 @@ export function _calcDueProtocolSwapFeeBptAmount( // SwapType = 'swapExactIn' export function _spotPriceAfterSwapExactTokenInForTokenOut( amount: OldBigNumber, - poolPairData: WeightedPoolPairData + poolPairData: WeightedPoolMathPairData ): OldBigNumber { const Bi = parseFloat( formatFixed(poolPairData.balanceIn, poolPairData.decimalsIn) @@ -506,7 +508,7 @@ export function _spotPriceAfterSwapExactTokenInForTokenOut( // SwapType = 'swapExactOut' export function _spotPriceAfterSwapTokenInForExactTokenOut( amount: OldBigNumber, - poolPairData: WeightedPoolPairData + poolPairData: WeightedPoolMathPairData ): OldBigNumber { const Bi = parseFloat( formatFixed(poolPairData.balanceIn, poolPairData.decimalsIn) @@ -530,7 +532,7 @@ export function _spotPriceAfterSwapTokenInForExactTokenOut( // SwapType = 'swapExactIn' export function _spotPriceAfterSwapExactTokenInForBPTOut( amount: OldBigNumber, - poolPairData: WeightedPoolPairData + poolPairData: WeightedPoolMathPairData ): OldBigNumber { const Bi = parseFloat( formatFixed(poolPairData.balanceIn, poolPairData.decimalsIn) @@ -573,7 +575,7 @@ export function _spotPriceAfterSwapBptOutGivenExactTokenInBigInt( // SwapType = 'swapExactIn' export function _spotPriceAfterSwapExactBPTInForTokenOut( amount: OldBigNumber, - poolPairData: WeightedPoolPairData + poolPairData: WeightedPoolMathPairData ): OldBigNumber { const Bbpt = parseFloat( formatFixed(poolPairData.balanceIn, poolPairData.decimalsIn) @@ -597,7 +599,7 @@ export function _spotPriceAfterSwapExactBPTInForTokenOut( // SwapType = 'swapExactOut' export function _spotPriceAfterSwapBPTInForExactTokenOut( amount: OldBigNumber, - poolPairData: WeightedPoolPairData + poolPairData: WeightedPoolMathPairData ): OldBigNumber { const Bbpt = parseFloat(formatFixed(poolPairData.balanceIn, 18)); const Bo = parseFloat( @@ -619,7 +621,7 @@ export function _spotPriceAfterSwapBPTInForExactTokenOut( // SwapType = 'swapExactOut' export function _spotPriceAfterSwapTokenInForExactBPTOut( amount: OldBigNumber, - poolPairData: WeightedPoolPairData + poolPairData: WeightedPoolMathPairData ): OldBigNumber { const Bi = parseFloat( formatFixed(poolPairData.balanceIn, poolPairData.decimalsIn) @@ -642,7 +644,7 @@ export function _spotPriceAfterSwapTokenInForExactBPTOut( // SwapType = 'swapExactIn' export function _derivativeSpotPriceAfterSwapExactTokenInForTokenOut( amount: OldBigNumber, - poolPairData: WeightedPoolPairData + poolPairData: WeightedPoolMathPairData ): OldBigNumber { const Bi = parseFloat( formatFixed(poolPairData.balanceIn, poolPairData.decimalsIn) @@ -661,7 +663,7 @@ export function _derivativeSpotPriceAfterSwapExactTokenInForTokenOut( // SwapType = 'swapExactOut' export function _derivativeSpotPriceAfterSwapTokenInForExactTokenOut( amount: OldBigNumber, - poolPairData: WeightedPoolPairData + poolPairData: WeightedPoolMathPairData ): OldBigNumber { const Bi = parseFloat( formatFixed(poolPairData.balanceIn, poolPairData.decimalsIn) @@ -685,7 +687,7 @@ export function _derivativeSpotPriceAfterSwapTokenInForExactTokenOut( // SwapType = 'swapExactIn' export function _derivativeSpotPriceAfterSwapExactTokenInForBPTOut( amount: OldBigNumber, - poolPairData: WeightedPoolPairData + poolPairData: WeightedPoolMathPairData ): OldBigNumber { const Bi = parseFloat( formatFixed(poolPairData.balanceIn, poolPairData.decimalsIn) @@ -703,7 +705,7 @@ export function _derivativeSpotPriceAfterSwapExactTokenInForBPTOut( // SwapType = 'swapExactOut' export function _derivativeSpotPriceAfterSwapTokenInForExactBPTOut( amount: OldBigNumber, - poolPairData: WeightedPoolPairData + poolPairData: WeightedPoolMathPairData ): OldBigNumber { const Bi = parseFloat( formatFixed(poolPairData.balanceIn, poolPairData.decimalsIn) @@ -726,7 +728,7 @@ export function _derivativeSpotPriceAfterSwapTokenInForExactBPTOut( // SwapType = 'swapExactIn' export function _derivativeSpotPriceAfterSwapExactBPTInForTokenOut( amount: OldBigNumber, - poolPairData: WeightedPoolPairData + poolPairData: WeightedPoolMathPairData ): OldBigNumber { const Bbpt = parseFloat(formatFixed(poolPairData.balanceIn, 18)); const Bo = parseFloat( @@ -747,7 +749,7 @@ export function _derivativeSpotPriceAfterSwapExactBPTInForTokenOut( // SwapType = 'swapExactOut' export function _derivativeSpotPriceAfterSwapBPTInForExactTokenOut( amount: OldBigNumber, - poolPairData: WeightedPoolPairData + poolPairData: WeightedPoolMathPairData ): OldBigNumber { const Bbpt = parseFloat(formatFixed(poolPairData.balanceIn, 18)); const Bo = parseFloat( diff --git a/src/types.ts b/src/types.ts index daced257..6df831f3 100644 --- a/src/types.ts +++ b/src/types.ts @@ -32,6 +32,7 @@ export enum PoolTypes { Gyro3, GyroE, Fx, + Managed, } export interface SwapOptions { @@ -214,6 +215,7 @@ export enum PoolFilter { TetuLinear = 'TetuLinear', YearnLinear = 'YearnLinear', FX = 'FX', + Managed = 'Managed', } export interface PoolBase { diff --git a/test/MaganedPoolKassandra.integration.spec.ts b/test/MaganedPoolKassandra.integration.spec.ts new file mode 100644 index 00000000..09f12700 --- /dev/null +++ b/test/MaganedPoolKassandra.integration.spec.ts @@ -0,0 +1,305 @@ +import dotenv from 'dotenv'; +import { JsonRpcProvider } from '@ethersproject/providers'; +import { SOR, SubgraphPoolBase, SwapTypes } from '../src'; +import { Network, vaultAddr } from './testScripts/constants'; +import { formatFixed, parseFixed } from '@ethersproject/bignumber'; +import { expect } from 'chai'; +import { MaganedPoolKassandra } from '../src/pools/managedPools/MaganedPoolKassandra'; +import { setUp } from './testScripts/utils'; +import { WeightedPoolToken } from '../src/pools/weightedPool/weightedPool'; +import { Vault, Vault__factory } from '@balancer-labs/typechain'; +import { AddressZero } from '@ethersproject/constants'; + +dotenv.config(); + +const testPool: SubgraphPoolBase = { + id: '0x107cb7c6d67ad745c50d7d4627335c1c6a684003000100000000000000000c37', + address: '0x107cb7c6d67ad745c50d7d4627335c1c6a684003', + poolType: 'Managed', + swapFee: '0.003', + swapEnabled: true, + totalWeight: '0', + totalShares: '587.155942710616273381', + tokensList: [ + '0x107cb7c6d67ad745c50d7d4627335c1c6a684003', + '0x172370d5cd63279efa6d502dab29171933a610af', + '0x1a3acf6d19267e2d3e7f898f42803e90c9219062', + '0x50b728d8d964fd00c2d0aad81718b71311fef68a', + '0x6f7c932e7684666c9fd1d44527765433e01ff61d', + '0x8505b9d2254a7ae468c0e9dd10ccea3a837aef5c', + '0x9a71012b13ca4d3d0cdc72a177df3ef03b0e76a3', + '0xb33eaad8d922b1083446dc23f610c2567fb5180f', + '0xc3c7d422809852031b44ab29eec9f1eff2a58756', + '0xd6df932a45c0f255f85145f286ea0b292b21c90b', + ], + tokens: [ + { + address: '0x172370d5cd63279efa6d502dab29171933a610af', + balance: '396.136772427774719658', + decimals: 18, + priceRate: '1', + weight: null, + }, + { + address: '0x1a3acf6d19267e2d3e7f898f42803e90c9219062', + balance: '32.15685999953081616', + decimals: 18, + priceRate: '1', + weight: null, + }, + { + address: '0x50b728d8d964fd00c2d0aad81718b71311fef68a', + balance: '128.635643494144033617', + decimals: 18, + priceRate: '1', + weight: null, + }, + { + address: '0x6f7c932e7684666c9fd1d44527765433e01ff61d', + balance: '0.376577387336367528', + decimals: 18, + priceRate: '1', + weight: null, + }, + { + address: '0x8505b9d2254a7ae468c0e9dd10ccea3a837aef5c', + balance: '3.075712849360558397', + decimals: 18, + priceRate: '1', + weight: null, + }, + { + address: '0x9a71012b13ca4d3d0cdc72a177df3ef03b0e76a3', + balance: '22.14930620288425309', + decimals: 18, + priceRate: '1', + weight: null, + }, + { + address: '0xb33eaad8d922b1083446dc23f610c2567fb5180f', + balance: '300.69117873097954205', + decimals: 18, + priceRate: '1', + weight: null, + }, + { + address: '0xc3c7d422809852031b44ab29eec9f1eff2a58756', + balance: '331.466141781162778932', + decimals: 18, + priceRate: '1', + weight: null, + }, + { + address: '0xd6df932a45c0f255f85145f286ea0b292b21c90b', + balance: '4.675112151574476223', + decimals: 18, + priceRate: '1', + weight: null, + }, + ], +}; + +const networkId = Network.POLYGON; +const jsonRpcUrl = process.env.RPC_URL_POLYGON; +const rpcUrl = 'http://127.0.0.1:8137'; +const blockNumber = 50629622; +const provider = new JsonRpcProvider(rpcUrl, networkId); +let pool: MaganedPoolKassandra; +let sor: SOR; +let vault: Vault; + +const funds = { + sender: AddressZero, + recipient: AddressZero, + fromInternalBalance: false, + toInternalBalance: false, +}; + +describe('Managed', () => { + before(async function () { + sor = await setUp( + networkId, + provider, + [testPool], + jsonRpcUrl as string, + blockNumber + ); + await sor.fetchPools(); + const pools = sor.getPools(); + pool = MaganedPoolKassandra.fromPool(pools[0]); + vault = Vault__factory.connect(vaultAddr, provider); + }); + context('test swaps vs querySwap', () => { + context('single swap', () => { + context('exact in', () => { + it('should calc swap amount out CRV -> FXS', async () => { + const amountIn = parseFixed('1', 18); + + const swapInfo = await sor.getSwaps( + testPool.tokensList[1], + testPool.tokensList[2], + SwapTypes.SwapExactIn, + amountIn + ); + + const response = await vault.callStatic.queryBatchSwap( + SwapTypes.SwapExactIn, + swapInfo.swaps, + swapInfo.tokenAddresses, + funds + ); + expect(swapInfo.swapAmount.toString()).eq( + response[0].toString() + ); + expect(swapInfo.returnAmount.toString()).eq( + response[1].abs().toString() + ); + }); + + it('should calc swap amount out BAL -> UNI', async () => { + const amountIn = parseFixed('0.1', 18); + + const swapInfo = await sor.getSwaps( + testPool.tokensList[6], + testPool.tokensList[7], + SwapTypes.SwapExactIn, + amountIn + ); + + const response = await vault.callStatic.queryBatchSwap( + SwapTypes.SwapExactIn, + swapInfo.swaps, + swapInfo.tokenAddresses, + funds + ); + expect(swapInfo.swapAmount.toString()).eq( + response[0].toString() + ); + expect(swapInfo.returnAmount.toString()).eq( + response[1].abs().toString() + ); + }); + }); + + context('exact out', () => { + it('should calc swap amout in CRV -> FXS', async () => { + const amountIn = parseFixed('1', 18); + + const swapInfo = await sor.getSwaps( + testPool.tokensList[1], + testPool.tokensList[2], + SwapTypes.SwapExactOut, + amountIn + ); + + const response = await vault.callStatic.queryBatchSwap( + SwapTypes.SwapExactOut, + swapInfo.swaps, + swapInfo.tokenAddresses, + funds + ); + expect(swapInfo.swapAmount.toString()).eq( + response[1].abs().toString() + ); + expect(swapInfo.returnAmount.toString()).eq( + response[0].abs().toString() + ); + }); + + it('should calc swap amount in BAL -> UNI', async () => { + const amountIn = parseFixed('1', 18); + + const swapInfo = await sor.getSwaps( + testPool.tokensList[6], + testPool.tokensList[7], + SwapTypes.SwapExactOut, + amountIn + ); + + const response = await vault.callStatic.queryBatchSwap( + SwapTypes.SwapExactOut, + swapInfo.swaps, + swapInfo.tokenAddresses, + funds + ); + expect(swapInfo.swapAmount.toString()).eq( + response[1].abs().toString() + ); + expect(swapInfo.returnAmount.toString()).eq( + response[0].abs().toString() + ); + }); + }); + }); + }); + context('test joins vs queryJoin', () => { + context('join with all tokens', () => { + it('should return zero in calc join call with all tokens', async () => { + const amountsIn = [ + parseFixed('0', 18), + parseFixed('0.123', 18), + parseFixed('0.456', 18), + parseFixed('0.123', 18), + parseFixed('0.456', 18), + parseFixed('0.123', 18), + parseFixed('0.456', 18), + parseFixed('0.123', 18), + parseFixed('0.456', 18), + parseFixed('0.123', 18), + ]; + const bptOut = pool._calcBptOutGivenExactTokensIn(amountsIn); + expect(bptOut.toString()).to.eq('0'); + }); + }); + context('join with single token', () => { + it('should returns zero in calc join call with single token', async () => { + const amountsIn = [parseFixed('0.789', 8), parseFixed('0', 18)]; + const bptOut = pool._calcBptOutGivenExactTokensIn(amountsIn); + + expect(bptOut.toString()).to.eq('0'); + }); + }); + }); + context('test exits vs queryExit', () => { + context('exit to single token', () => { + before(async function () { + const bptAsToken: WeightedPoolToken = { + address: pool.address, + balance: formatFixed(pool.totalShares, 18), + decimals: 18, + weight: '0', + }; + + pool.tokens.push(bptAsToken); + pool.tokensList.push(pool.address); + }); + it('should throw when token is BPT', async () => { + const tokenIndex = 0; + + const response = () => + pool.parsePoolPairData( + pool.address, + pool.tokensList[tokenIndex] + ); + + expect(response).throws('Token cannot be BPT'); + }); + after(async function () { + // Remove BPT that was artifically added to the pool + pool.tokens.pop(); + pool.tokensList.pop(); + }); + }); + context('exit to all tokens', async () => { + it('should return zero when calling calc exit with all tokens', async () => { + const bptIn = parseFixed('0.123', 18); + + const amountsOut = pool._calcTokensOutGivenExactBptIn(bptIn); + + amountsOut.forEach((amount) => { + expect(amount.toString()).to.eq('0'); + }); + }); + }); + }); +}); diff --git a/test/MaganedPoolKassandra.spec.ts b/test/MaganedPoolKassandra.spec.ts new file mode 100644 index 00000000..2a7f82ec --- /dev/null +++ b/test/MaganedPoolKassandra.spec.ts @@ -0,0 +1,174 @@ +import { expect } from 'chai'; +import cloneDeep from 'lodash.clonedeep'; +import { parseFixed } from '@ethersproject/bignumber'; +import { bnum } from '../src/utils/bignumber'; +import { SwapTypes } from '../src'; +import { MaganedPoolKassandra } from '../src/pools/managedPools/MaganedPoolKassandra'; +import managedPools from './testData/managedPools/kassandraManagedPoolsTest.json'; + +const MAX_RATIO = bnum(0.3); + +describe('MaganedPoolKassandra', () => { + context('parsePoolPairData', () => { + it(`should correctly parse USDC > BAL`, async () => { + const pool = cloneDeep(managedPools).pools[0]; + const tokenIn = pool.tokens[0]; + const tokenOut = pool.tokens[1]; + const managedPool = MaganedPoolKassandra.fromPool(pool); + const poolPairData = managedPool.parsePoolPairData( + tokenIn.address, + tokenOut.address + ); + + expect(poolPairData.swapFee.toString()).to.eq( + parseFixed(pool.swapFee, 18).toString() + ); + expect(poolPairData.id).to.eq(pool.id); + expect(poolPairData.tokenIn).to.eq(tokenIn.address); + expect(poolPairData.tokenOut).to.eq(tokenOut.address); + expect(poolPairData.balanceIn.toString()).to.eq( + parseFixed(tokenIn.balance, tokenIn.decimals).toString() + ); + expect(poolPairData.balanceOut.toString()).to.eq( + parseFixed(tokenOut.balance, tokenOut.decimals).toString() + ); + expect(poolPairData.weightIn.toString()).to.eq( + parseFixed(tokenIn.weight, 18).toString() + ); + expect(poolPairData.weightOut.toString()).to.eq( + parseFixed(tokenOut.weight, 18).toString() + ); + }); + }); + + context('limit amounts', () => { + it(`getLimitAmountSwap, BAL to USDC`, async () => { + const pool = cloneDeep(managedPools).pools[0]; + const tokenIn = pool.tokens[0]; + const tokenOut = pool.tokens[1]; + const managedPool = MaganedPoolKassandra.fromPool(pool); + const poolPairData = managedPool.parsePoolPairData( + tokenIn.address, + tokenOut.address + ); + + let amount = managedPool.getLimitAmountSwap( + poolPairData, + SwapTypes.SwapExactIn + ); + + expect(amount.toString()).to.eq( + bnum(tokenIn.balance).times(MAX_RATIO).toString() + ); + + amount = managedPool.getLimitAmountSwap( + poolPairData, + SwapTypes.SwapExactOut + ); + + expect(amount.toString()).to.eq( + bnum(tokenOut.balance).times(MAX_RATIO).toString() + ); + }); + + it(`getLimitAmountSwap, USDC to BAL`, async () => { + const pool = cloneDeep(managedPools).pools[0]; + const tokenIn = pool.tokens[1]; + const tokenOut = pool.tokens[0]; + const managedPool = MaganedPoolKassandra.fromPool(pool); + const poolPairData = managedPool.parsePoolPairData( + tokenIn.address, + tokenOut.address + ); + + let amount = managedPool.getLimitAmountSwap( + poolPairData, + SwapTypes.SwapExactIn + ); + + expect(amount.toString()).to.eq( + bnum(tokenIn.balance).times(MAX_RATIO).toString() + ); + + amount = managedPool.getLimitAmountSwap( + poolPairData, + SwapTypes.SwapExactOut + ); + + expect(amount.toString()).to.eq( + bnum(tokenOut.balance).times(MAX_RATIO).toString() + ); + }); + }); + + context('Test Swaps', () => { + context('_exactTokenInForTokenOut', () => { + it('BAL>USDC', async () => { + const pool = cloneDeep(managedPools).pools[0]; + const tokenIn = pool.tokens[0]; + const tokenOut = pool.tokens[1]; + const amountIn = bnum('1.5'); + const managedPool = MaganedPoolKassandra.fromPool(pool); + const poolPairData = managedPool.parsePoolPairData( + tokenIn.address, + tokenOut.address + ); + const amountOut = managedPool._exactTokenInForTokenOut( + poolPairData, + amountIn + ); + expect(amountOut.toString()).to.eq('5.61881163893890533'); + }); + it('USDC>BAL', async () => { + const pool = cloneDeep(managedPools).pools[0]; + const tokenIn = pool.tokens[1]; + const tokenOut = pool.tokens[0]; + const amountIn = bnum('5.61881163893890533'); + const managedPool = MaganedPoolKassandra.fromPool(pool); + const poolPairData = managedPool.parsePoolPairData( + tokenIn.address, + tokenOut.address + ); + const amountOut = managedPool._exactTokenInForTokenOut( + poolPairData, + amountIn + ); + expect(amountOut.toString()).to.eq('1.489550744765116'); + }); + }); + context('_tokenInForExactTokenOut', () => { + it('BAL>DAI', async () => { + const pool = cloneDeep(managedPools).pools[0]; + const tokenIn = pool.tokens[0]; + const tokenOut = pool.tokens[1]; + const amountOut = bnum('5.5'); + const managedPool = MaganedPoolKassandra.fromPool(pool); + const poolPairData = managedPool.parsePoolPairData( + tokenIn.address, + tokenOut.address + ); + const amountIn = managedPool._tokenInForExactTokenOut( + poolPairData, + amountOut + ); + expect(amountIn.toString()).to.eq('1.468235525276634269'); + }); + it('DAI>BAL', async () => { + const pool = cloneDeep(managedPools).pools[0]; + const tokenIn = pool.tokens[1]; + const tokenOut = pool.tokens[0]; + const amountOut = bnum('1.5'); + const managedPool = MaganedPoolKassandra.fromPool(pool); + const poolPairData = managedPool.parsePoolPairData( + tokenIn.address, + tokenOut.address + ); + const amountIn = managedPool._tokenInForExactTokenOut( + poolPairData, + amountOut + ); + expect(amountIn.toString()).to.eq('5.658287029780740079'); + }); + }); + }); +}); diff --git a/test/lib/onchainData.ts b/test/lib/onchainData.ts index 8313e77a..e3667842 100644 --- a/test/lib/onchainData.ts +++ b/test/lib/onchainData.ts @@ -92,6 +92,7 @@ export async function getOnChainBalances( // TO DO - Make this part of class to make more flexible? if ( pool.poolType === 'Weighted' || + pool.poolType === 'Managed' || pool.poolType === 'LiquidityBootstrapping' || pool.poolType === 'Investment' ) { @@ -277,14 +278,22 @@ export async function getOnChainBalances( subgraphPools[index].swapFee = formatFixed(swapFee, 18); - poolTokens.tokens.forEach((token, i) => { + const tokens = [...poolTokens.tokens]; + const balances = [...poolTokens.balances]; + + if (subgraphPools[index].poolType === 'Managed') { + tokens.shift(); + balances.shift(); + } + + tokens.forEach((token, i) => { const T = subgraphPools[index].tokens.find((t) => isSameAddress(t.address, token) ); if (!T) throw `Pool Missing Expected Token: ${poolId} ${token}`; - T.balance = formatFixed(poolTokens.balances[i], T.decimals); + T.balance = formatFixed(balances[i], T.decimals); if (weights) { // Only expected for WeightedPools diff --git a/test/testData/managedPools/kassandraManagedPoolsTest.json b/test/testData/managedPools/kassandraManagedPoolsTest.json new file mode 100644 index 00000000..008529f7 --- /dev/null +++ b/test/testData/managedPools/kassandraManagedPoolsTest.json @@ -0,0 +1,35 @@ +{ + "pools": [ + { + "id": "kassandra-managed-pool", + "address": "0x0000000000000000000000000000000000000000", + "swapFee": "0.002", + "swapEnabled": true, + "tokens": [ + { + "address": "0xba100000625a3754423978a60c9317c58a424e3d", + "balance": "1000.0", + "decimals": 18, + "weight": "0.5", + "priceRate": "1", + "symbol": "BAL" + }, + { + "address": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + "balance": "3759.0", + "decimals": 6, + "weight": "0.5", + "priceRate": "1", + "symbol": "USDC" + } + ], + "tokensList": [ + "0x6b175474e89094c44da98b954eedeac495271d0f", + "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48" + ], + "totalWeight": "1", + "totalShares": "10000000000000", + "poolType": "ManagedPool" + } + ] +} diff --git a/test/testScripts/constants.ts b/test/testScripts/constants.ts index 1905c381..5367a4b1 100644 --- a/test/testScripts/constants.ts +++ b/test/testScripts/constants.ts @@ -371,6 +371,7 @@ export const ADDRESSES = { }, }, [Network.POLYGON]: { + balancerHelpers: '0xE39B5e3B6D74016b2F6A9673D7d7493B6DF549d5', MATIC: { address: AddressZero, decimals: 18,