UNPKG

@hikaru-fi/sc-calculators

Version:

Package for pool calculations

604 lines (557 loc) 19.5 kB
import BN from 'bn.js'; import web3Utils from 'web3-utils'; import { BuyLimitExceeded, NoExchangeRate, NoPoolAddress, NoPoolTokens, NoSwapFee, NoTokenBalances, NoTokenMultipliers, NoTokenWeights, NoVaultAddress, SellLimitExceeded, SwapIsNotPossible, UnknownPool, UnknownSwapType, UnknownToken, UserLPAmountIsGTTotalSupply, WithdrawLimitSingleToken } from '../../errors/Errors.mjs'; import { getSwapName, isBuy, isSell } from '../../utils/types.mjs'; import { calcInGivenOut, calcOutGivenIn, calculateLPAmountForTokenOut, spotPrice, swapLimit } from './weightedPoolMath.mjs'; const ZERO = web3Utils.toBN(0); const ONE = web3Utils.toBN(1e18); const ONEN = 1e18; const swapLimitCoefficient = web3Utils.toBN(0.3e18); const exchangeRates = {}; /** * @type {Record<String,PoolInfo>} */ const poolInformation = {}; export const poolCalculations = { /** * @param {BackendPoolInfo[]} backendObj */ fromBackendInfo(backendObj) { for (const poolObj of backendObj) { checkPoolInfo(poolObj, poolObj.poolAddress) /** * @type {PoolInfo} */ const poolInfo = { name: poolObj.name, symbol: poolObj.symbol, vaultAddress: poolObj.vaultAddress, poolAddress: poolObj.poolAddress, tokens: poolObj.tokens.copyWithin(), balances: poolObj.balances.map((val) => web3Utils.toBN(val)), exchangeRates: poolObj.exchangeRates.copyWithin(), weights: poolObj.weights.map((val) => web3Utils.toBN(val)), multipliers: poolObj.multipliers.map((val) => web3Utils.toBN(val)), swapFee: web3Utils.toBN(poolObj.swapFee), tokensNames: poolObj.tokensNames.copyWithin(), tokensSymbols: poolObj.tokensSymbols.copyWithin() } poolInfo.normalizedBalances = poolInfo.balances.map((val, index) => val.mul(poolInfo.multipliers[index])); poolInformation[poolInfo.poolAddress] = poolInfo; for (const [id, tokenAddress] of poolInfo.tokens.entries()) { exchangeRates[tokenAddress] = poolInfo.exchangeRates[id]; } } }, /** * @param {String} poolAddress * @param {BN[]} balances */ updatePoolBalance( poolAddress, balances ) { checkPoolInfo(poolInformation[poolAddress], poolAddress); poolInformation[poolAddress].balances = balances.map((val) => val.add(web3Utils.toBN(0))); poolInformation[poolAddress].normalizedBalances = balances.map((val, index) => val.mul(poolInformation[poolAddress].multipliers[index])); }, /** * @param {String} poolAddress * @param {Number[]} exchangeRates */ updatePoolExchangeRate(poolAddress, exchangeRates) { checkPoolInfo(poolInformation[poolAddress], poolAddress); poolInformation[poolAddress].exchangeRates = exchangeRates.copyWithin(); for (const [id, tokenAddress] of poolInformation[poolAddress].tokens.entries()) { exchangeRates[tokenAddress] = poolInformation[poolAddress].exchangeRates[id]; } }, /** * @param {String[]} pools * @param {BN[][]} balances */ updateMultiplePoolBalances( pools, balances ) { for (const [index, poolAddress] of pools.entries()) { this.updatePoolBalance(poolAddress, balances[index]); } }, /** * @param {String[]} pools * @param {Number[][]} exchangeRates */ updateMultiplePoolExchangeRates( pools, exchangeRates ) { for (const [index, poolAddress] of pools.entries()) { this.updatePoolExchangeRate(poolAddress, exchangeRates[index]); } }, /** * @param {String} tokenAddress * @returns {Number} */ getTokenExchangeRate(tokenAddress) { const er = exchangeRates[tokenAddress] return er ? er : 0; }, /** * @param {String} poolAddress * @returns {Number[]} */ getPoolTokenCost( poolAddress ) { const poolInfo = getPoolInfo(poolAddress) return poolInfo.balances.map((val, index) => Number(val.toString(10)) / (ONEN / Number(poolInfo.multipliers[index].toString(10))) * poolInfo.exchangeRates[index]); }, /** * @param {String} poolAddress * @param {String} tokenAddress * @returns {BN} */ getPoolSwapLimitForToken( poolAddress, tokenAddress ) { const poolInfo = getPoolInfo(poolAddress); const tokenIndex = poolInfo.tokens.indexOf(tokenAddress); if (tokenIndex < 0) throw new UnknownToken(poolAddress, tokenAddress); return swapLimit(poolInfo.balances[tokenIndex], swapLimitCoefficient); }, /** * @param {String} poolAddress * @returns {Number} */ getPoolTVL( poolAddress ) { const poolTokenCosts = this.getPoolTokenCost(poolAddress); return poolTokenCosts.reduce((val, currentValue) => currentValue + val, 0); }, /** * @param {String[]} pools * @returns {Number[]} */ getMultiplePoolTVL( pools ) { return pools.map((val) => this.getPoolTVL(val)); }, /** * @param {String} pool * @param {BN} userLPTokens * @param {BN} poolLPTokens */ getUserTokenCosts( pool, userLPTokens, poolLPTokens ) { const poolTokenCosts = this.getPoolTokenCost(pool); return poolLPTokens.eq(ZERO) ? ZERO : poolTokenCosts.map((val) => val * Number(userLPTokens.toString(10)) / Number(poolLPTokens.toString(10))); }, /** * @param {String} pool * @param {BN} userLPTokens * @param {BN} poolLPTokens * @returns {Number} */ getUserUSDAmount( pool, userLPTokens, poolLPTokens ) { return poolLPTokens.eq(ZERO) ? ZERO : this.getPoolTVL(pool) * Number(userLPTokens.toString(10)) / Number(poolLPTokens.toString(10)); }, /** * * @param {String} pool * @returns {BN[]} */ getPoolTokenBalances( pool ) { const poolInfo = getPoolInfo(pool); return poolInfo.balances.map((val) => val.add(ZERO)); }, /** * @param {String} pool * @param {BN} userLPTokens * @param {BN} poolLPTokens * @returns {BN[]} */ getUserTokensInPool( pool, userLPTokens, poolLPTokens ) { const poolInfo = getPoolInfo(pool); if (userLPTokens.gt(poolLPTokens)) throw new UserLPAmountIsGTTotalSupply(); return poolInfo.balances.map( (val) => poolLPTokens.eq(ZERO) ? ZERO : val.mul(userLPTokens).div(poolLPTokens) ); }, /** * @param {RouteInfo} swapRequest * @param {String | BN} swapAmount * @param {Number | String} slippagePercentage * @returns {SwapInfo} */ calculateSwapResult( swapRequest, swapAmount, slippagePercentage ) { if (getSwapName(swapRequest) === undefined) throw new UnknownSwapType(swapRequest.swapType); swapAmount = web3Utils.toBN(swapAmount); let swapAmountWithoutFee = web3Utils.toBN(swapAmount); slippagePercentage = Number(slippagePercentage); /**@type {BN} */ let amountWithFee = swapAmount.clone(); /**@type {BN} */ let amountWithoutFee = swapAmountWithoutFee.clone(); for (let id = 0; id < swapRequest.swapRoute.length; id++) { const swap = swapRequest.swapRoute[isSell(swapRequest) ? id : swapRequest.swapRoute.length - 1 - id]; amountWithFee = isSell(swapRequest) ? sellTokens(swap.pool, swap.tokenIn, swap.tokenOut, amountWithFee) : buyTokens(swap.pool, swap.tokenIn, swap.tokenOut, amountWithFee); amountWithoutFee = isSell(swapRequest) ? sellTokens(swap.pool, swap.tokenIn, swap.tokenOut, amountWithoutFee, true) : buyTokens(swap.pool, swap.tokenIn, swap.tokenOut, amountWithoutFee, true); } const priceImpact = calculatePriceImpact( getVirtualSpotPrice(swapRequest.swapRoute), getVirtualSpotPrice( swapRequest.swapRoute, isSell(swapRequest) ? swapAmount : amountWithFee, isSell(swapRequest) ? amountWithFee : swapAmount ) ) const swapResultWithSlippage = calculateSlippageAmount( amountWithFee, slippagePercentage, isBuy(swapRequest) ); const [firstSwap, lastSwap] = [swapRequest.swapRoute[0], swapRequest.swapRoute[swapRequest.swapRoute.length - 1]]; const effectivePrice = getEffectivePrice( firstSwap.pool, firstSwap.tokenIn, isSell(swapRequest) ? swapAmount : amountWithFee, lastSwap.pool, lastSwap.tokenOut, isSell(swapRequest) ? amountWithFee : swapAmount ); return { swapResult: amountWithFee, swapResultWithoutFee: amountWithoutFee, swapFee: isSell(swapRequest) ? amountWithoutFee.sub(amountWithFee) : amountWithFee.sub(amountWithoutFee), priceImpact, swapResultWithSlippage, effectivePrice } }, getPoolInfo, calculateLPTokensForSingleToken, isSell, isBuy, getSwapName, getSpotPrice } /** * @param {PoolInfo} poolInfo * @param {String} poolAddress * @returns {Boolean} */ function checkPoolInfo(poolInfo, poolAddress) { if (!poolInfo) throw new UnknownPool(poolAddress); if (!poolInfo.vaultAddress) throw new NoVaultAddress(poolAddress); if (!poolInfo.poolAddress) throw new NoPoolAddress(poolAddress); if (!poolInfo.tokens) throw new NoPoolTokens(poolAddress); if (!poolInfo.balances) throw new NoTokenBalances(poolAddress); if (!poolInfo.exchangeRates) throw new NoExchangeRate(poolAddress); if (!poolInfo.weights) throw new NoTokenWeights(poolAddress); if (!poolInfo.multipliers) throw new NoTokenMultipliers(poolAddress); if (!poolInfo.swapFee) throw new NoSwapFee(poolAddress); } /** * @param {String} poolAddress * @param {String} tokenIn * @param {String} tokenOut * @param {BN} amountIn * @param {Boolean?} disableFee * @returns {BN} */ function sellTokens( poolAddress, tokenIn, tokenOut, amountIn, disableFee ) { const poolInfo = getPoolInfo(poolAddress); const [inIndex, outIndex] = getTokenIndexes(poolInfo, [tokenIn, tokenOut]); if (swapLimit(poolInfo.balances[inIndex], swapLimitCoefficient).lt(amountIn)) throw new SellLimitExceeded(); return calcOutGivenIn( poolInfo.normalizedBalances[inIndex], poolInfo.weights[inIndex], poolInfo.normalizedBalances[outIndex], poolInfo.weights[outIndex], amountIn.mul(poolInfo.multipliers[inIndex]), disableFee ? ZERO : poolInfo.swapFee ).div(poolInfo.multipliers[outIndex]); } /** * @param {String} poolAddress * @param {String} tokenIn * @param {String} tokenOut * @param {BN} amountOut * @param {Boolean?} disableFee * @returns {BN} */ function buyTokens( poolAddress, tokenIn, tokenOut, amountOut, disableFee ) { const poolInfo = getPoolInfo(poolAddress); const [inIndex, outIndex] = getTokenIndexes(poolInfo, [tokenIn, tokenOut]); if (swapLimit(poolInfo.balances[outIndex], swapLimitCoefficient).lt(amountOut)) throw new BuyLimitExceeded(); return calcInGivenOut( poolInfo.normalizedBalances[inIndex], poolInfo.weights[inIndex], poolInfo.normalizedBalances[outIndex], poolInfo.weights[outIndex], amountOut.mul(poolInfo.multipliers[outIndex]), disableFee ? ZERO : poolInfo.swapFee ).div(poolInfo.multipliers[inIndex]); } /** * @param {String} poolAddress * @param {String} tokenIn * @param {String} tokenOut * @param {BN} balanceInDelta * @param {BN} balanceOutDelta */ function getSpotPrice( poolAddress, tokenIn, tokenOut, balanceInDelta, balanceOutDelta ) { const poolInfo = getPoolInfo(poolAddress); const [inIndex, outIndex] = getTokenIndexes(poolInfo, [tokenIn, tokenOut]); balanceInDelta = balanceInDelta ? balanceInDelta.mul(poolInfo.multipliers[inIndex]) : ZERO; balanceOutDelta = balanceOutDelta ? balanceOutDelta.mul(poolInfo.multipliers[outIndex]) : ZERO; return spotPrice( poolInfo.normalizedBalances[inIndex].add(balanceInDelta), poolInfo.weights[inIndex], poolInfo.normalizedBalances[outIndex].sub(balanceOutDelta), poolInfo.weights[outIndex], poolInfo.swapFee ).div(poolInfo.multipliers[outIndex]); } /** * @param {VirtualSwapInfo[]} path * @param {BN} balanceInDelta * @param {BN} balanceOutDelta */ function getVirtualSpotPrice( path, balanceInDelta, balanceOutDelta ) { const firstPoolInfo = getPoolInfo(path[0].pool); const [inIndex] = getTokenIndexes(firstPoolInfo, [path[0].tokenIn]); const lastPoolInfo = getPoolInfo(path[path.length - 1].pool); const [outIndex] = getTokenIndexes(lastPoolInfo, [path[path.length - 1].tokenOut]); balanceInDelta = balanceInDelta ? balanceInDelta.mul(firstPoolInfo.multipliers[inIndex]) : ZERO; balanceOutDelta = balanceOutDelta ? balanceOutDelta.mul(lastPoolInfo.multipliers[outIndex]) : ZERO; const priceWithoutFee = spotPrice( firstPoolInfo.normalizedBalances[inIndex].add(balanceInDelta), firstPoolInfo.weights[inIndex], lastPoolInfo.normalizedBalances[outIndex].sub(balanceOutDelta), lastPoolInfo.weights[outIndex], ZERO ); let price = priceWithoutFee; for (const swap of path) { const poolInfo = getPoolInfo(swap.pool); price = price.mul(ONE.div(ONE.sub(poolInfo.swapFee))); } return price.div(lastPoolInfo.multipliers[outIndex]); } /** * Effective price = amountOut / amountIn * @param {String} poolIn * @param {String} tokenIn * @param {BN} amountIn * @param {String} poolOut * @param {String} tokenOut * @param {BN} amountOut * @returns {Number} */ function getEffectivePrice( poolIn, tokenIn, amountIn, poolOut, tokenOut, amountOut ) { const poolInInfo = getPoolInfo(poolIn); const [inIndex] = getTokenIndexes(poolInInfo, [tokenIn]); const poolOutInfo = getPoolInfo(poolOut); const [outIndex] = getTokenIndexes(poolOutInfo, [tokenOut]); const amountInAdj = amountIn.mul(poolInInfo.multipliers[inIndex]); const amountOutAdj = amountOut.mul(poolOutInfo.multipliers[outIndex]); return Number(amountOutAdj.toString()) / Number(amountInAdj.toString()); } /** * * @param {String} pool * @param {String} tokenOut * @param {BN} amountOut * @param {BN} poolLPTokens */ function calculateLPTokensForSingleToken( pool, tokenOut, amountOut, poolLPTokens ) { amountOut = web3Utils.toBN(amountOut); poolLPTokens = web3Utils.toBN(poolLPTokens); const poolInfo = getPoolInfo(pool); const [tokenIndex] = getTokenIndexes(poolInfo, [tokenOut]); if (amountOut.gt(poolInfo.balances[tokenIndex].mul(web3Utils.toBN(7)).div(web3Utils.toBN(10)))) throw new WithdrawLimitSingleToken(); return calculateLPAmountForTokenOut( amountOut.mul(poolInfo.multipliers[tokenIndex]), poolInfo.normalizedBalances[tokenIndex], poolInfo.weights[tokenIndex], poolLPTokens, poolInfo.swapFee ); } /** * @param {PoolInfo} poolInfo * @param {String[]} tokens * @returns {Number[]} */ function getTokenIndexes( poolInfo, tokens ) { const indexes = []; for (const token of tokens) { const index = poolInfo.tokens.indexOf(token); if (index < 0) throw new UnknownToken(poolInfo.poolAddress, token); indexes.push(index); } return indexes; } /** * @param {String} poolAddress * @returns {PoolInfo} */ function getPoolInfo(poolAddress) { const poolInfo = poolInformation[poolAddress]; checkPoolInfo(poolInfo, poolAddress); return poolInfo; } /** * @notice Price impact = (priceBefore - priceAfter)/priceBefore * 100 * @param {BN} before * @param {BN} after * @returns {Number} */ function calculatePriceImpact(before, after) { return Math.abs((Number(before.sub(after).toString())/Number(before.toString()))*100); } /** * @notice Price with slippage = amountOut * (1 - slippage)^(swaps); * @param {BN} amount * @param {Number | String } slippage * @param {Boolean} plus * @param {number?} amountOfSwaps * @returns {BN} */ function calculateSlippageAmount(amount, slippage, plus, amountOfSwaps) { slippage = Number(slippage) / 100; slippage = plus ? 1 + slippage : 1 - slippage; const slippageCoefficient = web3Utils.toBN(Math.floor(Math.pow(slippage, amountOfSwaps ? amountOfSwaps : 1)*ONEN)); return amount.mul(slippageCoefficient).div(ONE); } /** * @typedef VirtualSwapInfo * @property {String} pool * @property {String} tokenIn * @property {String} tokenOut */ /** * @typedef RouteInfo * @property {String} vault * @property {VirtualSwapInfo[]} swapRoute * @property {Number} swapType * @property {String} swapAmount * @property {String} minMaxAmount * @property {String} receiver * @property {String} deadline */ /** * @typedef PoolAndTokens * @property {String} poolAddress * @property {String} tokenIn * @property {String} tokenOut */ /** * @typedef BackendPoolInfo * @property {String} name * @property {String} symbol * @property {String} vaultAddress * @property {String} poolAddress * @property {String[]} tokens * @property {String[]} balances * @property {Number[]} exchangeRates * @property {String[]} weights * @property {String[]} multipliers * @property {String} swapFee * @property {String[]} tokensNames * @property {String[]} tokensSymbols */ /** * @typedef PoolInfo * @property {String} name * @property {String} symbol * @property {String} vaultAddress * @property {String} poolAddress * @property {String[]} tokens * @property {BN[]} balances * @property {BN[]} normalizedBalances * @property {Number[]} exchangeRates * @property {BN[]} weights * @property {BN[]} multipliers * @property {String} swapFee * @property {String[]} tokensNames * @property {String[]} tokensSymbols */ /** * @typedef SwapInfo * @property {BN} swapResult * @property {BN} swapResultWithoutFee * @property {BN} swapFee * @property {Number} priceImpact * @property {BN} swapResultWithSlippage * @property {Number} effectivePrice */