@hikaru-fi/sc-calculators
Version:
Package for pool calculations
604 lines (557 loc) • 19.5 kB
JavaScript
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
*/