UNPKG

@syncswap/sdk

Version:

SyncSwap TypeScript SDK for building DeFi applications

1,263 lines 86.7 kB
import { BigNumber, Contract } from "ethers"; import { TokenRegistry, getWETHAddress } from "./tokens/tokenRegistry.js"; import { Numbers } from "./utils/numbers.js"; import { PoolTypes, RouteDirection, } from "./router/types.js"; import { ZERO, ZERO_ADDRESS, DECIMALS_2, DECIMALS_5, DECIMALS_6, ETHER, FOUR, ONE, THREE, TWO, UINT128_MAX, UINT256_MAX, DECIMALS_14, DECIMALS_16, DECIMALS_17, DECIMALS_18, DECIMALS_20, DECIMALS_21, DECIMALS_24, DECIMALS_26, DECIMALS_3, DECIMALS_33, DECIMALS_4, DECIMALS_9, Q_96, ZERO_FEE_DATA, } from "./utils/constants.js"; import ContractRegistry from "./contracts/contractRegistry.js"; import invariant from "tiny-invariant"; import { Token } from "./tokens/token.js"; import { defaultAbiCoder, splitSignature } from "ethers/lib/utils.js"; import { stateStore } from "./statestore/statestore.js"; import { utils } from "zksync-web3"; export const SOPHON_MAINNET_USDT = "0x6386da73545ae4e2b2e0393688fa8b65bb9a7169"; export const SOPHON_MAINNET_USDC = "0x9aa0f72392b5784ad86c6f3e899bcc053d00db4f"; export const ZKSYNC_TESTNET = "zkSyncTestnet"; export const ZKSYNC_MAINNET = "zkSyncMainnet"; export const SCROLL_MAINNET = "scrollMainnet"; export const SOPHON_MAINNET = "sophonMainnet"; export const SOPHON_TESTNET = "sophonTestnet"; export const ZKCANDY_MAINNET = "zkcandyMainnet"; export const LINEA_MAINNET = "lineaMainnet"; export const ZKSYNC_SEPOLIA_TESTNET = "zkSyncSepoliaTestnet"; const GAS_CLASSIC_STEP = BigNumber.from(4697000); // 4697727 const GAS_STABLE_STEP = BigNumber.from(4700000); // 4716100 const GAS_AQUA_STEP = BigNumber.from(5000000); // dummy const GAS_EXTRA_PATH = BigNumber.from(4000000); // 4634420 const GAS_SWAP_BASE = BigNumber.from(7230000); // 7230698 const ETHER_x_4 = ETHER.mul(4); const LIQUIDITY_MIN_RESERVE = BigNumber.from(100000); const SOPHON_MAINNET_WSOPH = "0x2b1a859de6a55c553520d7780bc5805712b128f9"; const SOPHON_MAINNET_RTOKENS = [ SOPHON_MAINNET_WSOPH, "0x72af9f169b619d85a47dfa8fefbcd39de55c567d", // ETH SOPHON_MAINNET_USDC, SOPHON_MAINNET_USDT, ]; export function getRouteTokens() { const network = stateStore().network; if (network === SOPHON_MAINNET) { return SOPHON_MAINNET_RTOKENS; } return [getWETHAddress()]; } export function sortTokens(tokenA, tokenB) { return Number(tokenA) < Number(tokenB) ? [tokenA, tokenB] : [tokenB, tokenA]; } export function isETH(token) { if (typeof token === 'string' || token instanceof String) { return token.toLowerCase() === Token.ETHER.address; } else { return token.address === Token.ETHER.address; } } export function getPoolKey(address1, address2) { if (address1 < address2) { return `${address1}:${address2}`; } else { return `${address2}:${address1}`; } } export function createPath(stepDataArray) { const steps = []; for (let i = 0; i < stepDataArray.length; i++) { const data = stepDataArray[i]; const pool = data.pool; //console.log('createPath, pool', pool, data); const tokenOut = data.tokenIn === pool.tokenA ? pool.tokenB : pool.tokenA; //console.log('createPath, tokenOut', tokenOut, 'data.tokenIn', data.tokenIn, 'pool.tokenA', pool.tokenA, 'tokenOut', tokenOut, 'zeroForOne', data.tokenIn < tokenOut); const step = { pool: pool, tokenIn: data.tokenIn, swapFee: data.tokenIn === pool.tokenA ? pool.swapFeeAB : pool.swapFeeBA, tokenOut: tokenOut, zeroForOne: data.tokenIn < tokenOut, }; steps.push(step); } return steps; } const EXTRA_USD_STABLECOIN_SYMBOLS = [ // tokens with 'USD' in name are handled automatically. "USDC.e", "USDC", "USDT", "DAI", "FRAX", "MAI", "DOLA", "MIM", "ONEZ", "USX", //"wTBT", "GRAI", "axlUSDC", "axlUSDT", "wDAI", "USDM", "wUSDM", //"MIMATIC", //BOB //HAY //BEAN //"USX", //"FEI". //"RSV", //"XAI", //"Hue", ]; /* function hasKeywordOnToken(token: Token, keyword: string): boolean { return token.name.includes(keyword) || token.symbol.includes(keyword); } */ export function isUSDStablecoin(token) { if (!token) { return false; } // check symbol if (token.symbol) { if (token.symbol === "USDC" || token.symbol === "USDT" || token.symbol === "USDC.e" || token.symbol === "DAI" || token.symbol === "USN") { return true; } // wrapped tokens if (token.symbol === "hsUSDC" || token.symbol === "sDAI") { return false; } // wrapped tokens by symbol if (token.symbol.startsWith("w")) { return false; } if (token.symbol.includes("USD")) { return true; } if (EXTRA_USD_STABLECOIN_SYMBOLS.indexOf(token.symbol) !== -1) { return true; } } if (token.name) { if (token.name.includes("rapped")) { // Wrapped return false; } if (token.name.includes("USD")) { return true; } const name = token.name.toLowerCase(); return name.includes("dollar") || name.includes("stable"); } return false; } export function hasLiquidity(data, tokenIn, amountIn) { if (tokenIn && amountIn) { const reserveIn = data.tokenA.toLowerCase() === tokenIn.toLowerCase() ? data.reserveA : data.reserveB; return reserveIn.gt(amountIn.div(10)); } else { if (tokenIn) { const isTokenAIn = data.tokenA.toLowerCase() === tokenIn.toLowerCase(); const tokenInData = TokenRegistry.getTokenByAddress(isTokenAIn ? data.tokenA : data.tokenB); if (tokenInData && !tokenInData.symbol.includes("USN") && isUSDStablecoin(tokenInData)) { const tokenInReserves = isTokenAIn ? data.reserveA : data.reserveB; return tokenInReserves.gte(Numbers.pow(tokenInData.decimals).mul(100)); } } return (data.reserveA.gte(LIQUIDITY_MIN_RESERVE) && data.reserveB.gte(LIQUIDITY_MIN_RESERVE)); } } const MAX_LOOP_LIMIT = 256; const MAX_FEE = BigNumber.from(100000); // 1e5 const TWO_MAX_FEE = MAX_FEE.mul(2); // 1e5 const MINIMUM_LIQUIDITY = BigNumber.from(1000); const ZERO_STEP_AMOUNT_OUT = [ZERO, ZERO, null, null]; let cacheAmountOutForStepMap = new Map(); let pathAmountsCacheMapIn = new Map(); // copy a swap step and its pool data function copyStep(step) { const pool = step.pool; const cryptoPoolData = []; if (pool.poolType === PoolTypes.AQUA && pool.cryptoPoolData) { const data = pool.cryptoPoolData[0]; cryptoPoolData.push({ gamma: data.gamma, futureParamsTime: data.futureParamsTime, invariantLast: data.invariantLast, priceScale: data.priceScale, virtualPrice: data.virtualPrice, totalSupply: data.totalSupply, }); } const rangePoolData = []; if (pool.poolType === PoolTypes.UNISWAP_V3 && pool.rangePoolData) { const data = pool.rangePoolData[0]; rangePoolData.push({ ...data, }); } return { pool: { isV2: pool.isV2, isV3: pool.isV3, isUniswap: pool.isUniswap, //vault: pool.vault, pool: pool.pool, tokenA: pool.tokenA, tokenB: pool.tokenB, poolType: pool.poolType, reserveA: BigNumber.from(pool.reserveA), reserveB: BigNumber.from(pool.reserveB), swapFeeAB: pool.swapFeeAB, swapFeeBA: pool.swapFeeBA, // aqua pool params a: pool.a, cryptoPoolData: cryptoPoolData, // uniswap v3 pool params rangePoolData: rangePoolData, }, //tokenA: step.pool.tokenA, //tokenB: step.pool.tokenB, tokenIn: step.tokenIn, swapFee: step.swapFee, tokenOut: step.tokenOut, zeroForOne: step.zeroForOne, }; } function hashAmountForStep(step, amountIn) { let hash = `${step.pool.reserveA?.toHexString()}:${step.tokenIn}:${amountIn?.toHexString()}`; if (step.pool.cryptoPoolData && step.pool.cryptoPoolData.length > 0) { hash += step.pool.cryptoPoolData[0]?.priceScale?.toHexString(); } if (step.pool.rangePoolData && step.pool.rangePoolData.length > 0) { hash += step.pool.rangePoolData[0]?.sqrtPriceX96?.toHexString(); } return hash; } function getAmountOutClassic(params, checkOverflow) { const amountIn = params.amount; const reserveIn = params.reserveIn; if (checkOverflow && reserveIn.add(amountIn).gt(UINT128_MAX)) { //console.log('getAmountOutClassic overflow 0'); throw Error("overflow"); } const swapFee = params.swapFee.maxFee; const amountInWithFee = amountIn.mul(MAX_FEE.sub(swapFee)); if (checkOverflow && amountInWithFee.gt(UINT256_MAX)) { //console.log('getAmountOutClassic overflow 1'); throw Error("overflow"); } const numerator = amountInWithFee.mul(params.reserveOut); if (checkOverflow && numerator.gt(UINT256_MAX)) { //console.log('getAmountOutClassic overflow 2'); throw Error("overflow"); } const denominator = params.reserveIn.mul(MAX_FEE).add(amountInWithFee); if (checkOverflow && denominator.gt(UINT256_MAX)) { //console.log('getAmountOutClassic overflow 3'); throw Error("overflow"); } return numerator.div(denominator); } const MAX_XP = UINT128_MAX; //BigNumber.from('3802571709128108338056982581425910818'); const DEFAULT_STABLE_POOL_A = BigNumber.from(1000); export function getBlockTimestamp() { return BigNumber.from(Math.floor(Date.now() / 1000)); } function getY(A, x, d) { const nA = A.mul(TWO); const c = d.mul(d).div(x.mul(TWO)).mul(d).div(nA.mul(TWO)); const b = d.div(nA).add(x); let yPrev; let y = d; for (let i = 0; i < MAX_LOOP_LIMIT; i++) { yPrev = y; y = y.mul(y).add(c).div(y.mul(TWO).add(b).sub(d)); if (y.sub(yPrev).abs().lte(ONE)) { break; } } return y; } function getAmountOutStable(params, checkOverflow) { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const adjustedReserveIn = params.reserveIn.mul(params.tokenInPrecisionMultiplier); if (checkOverflow && adjustedReserveIn.gt(MAX_XP)) { throw Error("overflow"); } // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const adjustedReserveOut = params.reserveOut.mul(params.tokenOutPrecisionMultiplier); if (checkOverflow && adjustedReserveOut.gt(MAX_XP)) { throw Error("overflow"); } const amountIn = params.amount; const swapFee = params.swapFee.maxFee; const feeDeductedAmountIn = amountIn.sub(amountIn.mul(swapFee).div(MAX_FEE)); const a = params.a ?? DEFAULT_STABLE_POOL_A; const d = computeDFromAdjustedBalances(a, adjustedReserveIn, adjustedReserveOut, checkOverflow); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const x = adjustedReserveIn.add(feeDeductedAmountIn.mul(params.tokenInPrecisionMultiplier)); const y = getY(a, x, d); const dy = adjustedReserveOut.sub(y).sub(1); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const amountOut = dy.div(params.tokenOutPrecisionMultiplier); //console.log('getAmountOutStable: amountOut', amountOut.toString(), 'amountIn', amountIn.toString(), 'reserveIn', params.reserveIn.toString(), 'reserveOut', params.reserveOut.toString(), 'tokenInPrecisionMultiplier', params.tokenInPrecisionMultiplier?.toString(), 'tokenOutPrecisionMultiplier', params.tokenOutPrecisionMultiplier?.toString()); return amountOut; } export function sqrtBN(x) { let z = x.add(ONE).div(TWO); let y = x; while (z.sub(y).isNegative()) { y = z; z = x.div(z).add(z).div(TWO); } return y; } export function computeInvariant(poolType, reserveA, reserveB, tokenAPrecisionMultiplier, tokenBPrecisionMultiplier, a, gamma) { if (poolType === PoolTypes.STABLE) { invariant(tokenAPrecisionMultiplier && tokenBPrecisionMultiplier && a, "multiplier"); const adjustedReserveA = reserveA.mul(tokenAPrecisionMultiplier); const adjustedReserveB = reserveB.mul(tokenBPrecisionMultiplier); return computeDFromAdjustedBalances(a, adjustedReserveA, adjustedReserveB, true); } if (poolType === PoolTypes.AQUA) { invariant(tokenAPrecisionMultiplier && tokenBPrecisionMultiplier && a && gamma, "multiplier"); const adjustedReserveA = reserveA.mul(tokenAPrecisionMultiplier); const adjustedReserveB = reserveB.mul(tokenBPrecisionMultiplier); return cryptoMathComputeD(a, gamma, adjustedReserveA, adjustedReserveB); } if (poolType === PoolTypes.CLASSIC) { if (reserveA.gt(UINT128_MAX) || reserveB.gt(UINT128_MAX)) { throw Error("overflow"); } return sqrtBN(reserveA.mul(reserveB)); } return ZERO; } export function getUnbalancedMintFee(isV2, amountA, amountB, reserveA, reserveB, swapFeeAB, swapFeeBA) { if (reserveA.isZero() || reserveB.isZero()) { return [ZERO, ZERO]; } const amountBOptimal = amountA.mul(reserveB).div(reserveA); if (amountB.gte(amountBOptimal)) { // swap B for A const tokenBFee = swapFeeBA .mul(amountB.sub(amountBOptimal)) .div(isV2 ? MAX_FEE : TWO_MAX_FEE); return [ZERO, tokenBFee]; } else { const amountAOptimal = amountB.mul(reserveA).div(reserveB); const tokenAFee = swapFeeAB .mul(amountA.sub(amountAOptimal)) .div(isV2 ? MAX_FEE : TWO_MAX_FEE); return [tokenAFee, ZERO]; } } export function getXCP(invariant, priceScale) { return geometricMean(invariant.div(2), invariant.mul(ETHER).div(priceScale.mul(2))); } export function getCryptoFee(feeData, xp0, xp1) { const gamma = feeData.gamma; const minFee = BigNumber.from(feeData.minFee); const maxFee = BigNumber.from(feeData.maxFee); let f = xp0.add(xp1); f = gamma .mul(ETHER) .div(gamma.add(ETHER).sub(ETHER_x_4.mul(xp1).mul(xp0).div(f).div(f))); const fee = minFee .mul(f) .add(maxFee.mul(ETHER.sub(f))) .div(ETHER); return fee; } export function calculateCryptoLiquidityFee(feeData, amount0, amount1, xp0, xp1) { const fee = getCryptoFee(feeData, xp0, xp1).div(2); const sum = amount0.add(amount1); if (sum.isZero()) { return ZERO; } const avg = sum.div(2); let sdiff; if (amount0.gt(avg)) { sdiff = amount0.sub(avg); } else { sdiff = avg.sub(amount0); } if (amount1.gt(avg)) { sdiff = sdiff.add(amount1.sub(avg)); } else { sdiff = sdiff.add(avg.sub(amount1)); } return fee.mul(sdiff).div(sum).add(1); } // compute liquidity amount by pool states and input amounts, returns expected and quote export function computeLiquidity(isV2, poolType, amountA, amountB, reserveA, reserveB, swapFeeAB, swapFeeBA, totalSupply, isTokenA0, tokenAPrecisionMultiplier, tokenBPrecisionMultiplier, a, cryptoData) { if (poolType === PoolTypes.UNISWAP_V3) { return [ZERO, ZERO]; } // Aqua Pools if (poolType === PoolTypes.AQUA) { const newReserveA = reserveA.add(amountA); const newReserveB = reserveB.add(amountB); if (newReserveA.gt(UINT128_MAX) || newReserveB.gt(UINT128_MAX)) { throw Error("overflow"); } invariant(tokenAPrecisionMultiplier && tokenBPrecisionMultiplier && a && cryptoData, "multiplier"); const priceScale = cryptoData.priceScale; const xpA = isTokenA0 ? newReserveA.mul(tokenAPrecisionMultiplier) : newReserveA.mul(priceScale).mul(tokenAPrecisionMultiplier).div(ETHER); //const priceScaleB = priceScale.mul(tokenBPrecisionMultiplier); const xpB = isTokenA0 ? newReserveB.mul(priceScale).mul(tokenBPrecisionMultiplier).div(ETHER) : newReserveB.mul(tokenBPrecisionMultiplier); const oldXpA = isTokenA0 ? reserveA.mul(tokenAPrecisionMultiplier) : reserveA.mul(priceScale).mul(tokenAPrecisionMultiplier).div(ETHER); const oldXpB = isTokenA0 ? reserveB.mul(priceScale).mul(tokenBPrecisionMultiplier).div(ETHER) : reserveB.mul(tokenBPrecisionMultiplier); let oldInvariant; if (cryptoData.futureParamsTime.gt(getBlockTimestamp())) { oldInvariant = cryptoMathComputeD(a, cryptoData.gamma, oldXpA, oldXpB); } else { oldInvariant = cryptoData.invariantLast; } const newInvariant = cryptoMathComputeD(a, cryptoData.gamma, xpA, xpB); let liquidity; if (!totalSupply.isZero() && !oldInvariant.isZero()) { liquidity = totalSupply .mul(newInvariant) .div(oldInvariant) .sub(totalSupply); //console.log("computeLiquidity: oldInvariant is not zero, totalSupply", totalSupply.toString(), "newInvariant", newInvariant.toString(), "totalSupply.mul(newInvariant)", totalSupply.mul(newInvariant).toString()); } else { liquidity = getXCP(newInvariant, priceScale); if (!newReserveB.isZero() && !newReserveA.isZero()) { const newPriceScale = isTokenA0 ? ETHER.mul(xpA).div(newReserveB.mul(tokenBPrecisionMultiplier)) : ETHER.mul(xpB).div(newReserveA.mul(tokenAPrecisionMultiplier)); if (newPriceScale.gt(priceScale.mul(2)) || newPriceScale.lt(priceScale.div(2))) { console.log("loss newPriceScale", newPriceScale.toString(), "priceScale", priceScale.toString(), "isTokenA0", isTokenA0, "xpA", xpA.toString(), "xpB", xpB.toString()); throw Error("loss"); } } } //console.log("computeLiquidity: newInvariant", newInvariant.toString(), "oldInvariant", oldInvariant.toString(), "liquidity", liquidity.toString(), "cryptoData.priceScale", cryptoData.priceScale.toString(), "cryptoData.invariantLast", cryptoData.invariantLast.toString()); let liquidityWithoutFee = liquidity; let mintFee; const amountBOptimal = reserveA.isZero() ? ZERO : amountA.mul(reserveB).div(reserveA); if (amountB.lt(amountBOptimal)) { mintFee = swapFeeAB; } else { mintFee = swapFeeBA; } if (!oldInvariant.isZero()) { const swapFee = calculateCryptoLiquidityFee(mintFee, xpA.sub(oldXpA), xpB.sub(oldXpB), xpA, xpB); let feeAmount = liquidity.mul(swapFee).div(DECIMALS_5).add(1); if (feeAmount.lt(ZERO)) { feeAmount = ZERO; } liquidity = liquidity.sub(feeAmount); } if (liquidity.lt(ZERO)) { liquidity = ZERO; } if (liquidityWithoutFee.lt(ZERO)) { liquidityWithoutFee = ZERO; } return [liquidity, liquidityWithoutFee]; } // Classic / Stable Pools // fees are added to old invariant const [feeA, feeB] = getUnbalancedMintFee(isV2, amountA, amountB, reserveA, reserveB, BigNumber.from(swapFeeAB.maxFee), BigNumber.from(swapFeeBA.maxFee)); // add fees onto old reserves const reserveAWithFee = reserveA.add(feeA); const reserveBWithFee = reserveB.add(feeB); // compute old invariant const oldInvariant = computeInvariant(poolType, reserveAWithFee, reserveBWithFee, tokenAPrecisionMultiplier, tokenBPrecisionMultiplier, a, cryptoData?.gamma); const oldInvariantWithoutFee = computeInvariant(poolType, reserveA, reserveB, tokenAPrecisionMultiplier, tokenBPrecisionMultiplier, a, cryptoData?.gamma); // compute new invariant const newReserveA = reserveA.add(amountA); const newReserveB = reserveB.add(amountB); if (newReserveA.gt(UINT128_MAX) || newReserveB.gt(UINT128_MAX)) { throw Error("overflow"); } const newInvariant = computeInvariant(poolType, newReserveA, newReserveB, tokenAPrecisionMultiplier, tokenBPrecisionMultiplier, a, cryptoData?.gamma); if (totalSupply.isZero()) { // sub min liquidity from first mint const liquidity = newInvariant.sub(MINIMUM_LIQUIDITY); return [liquidity, liquidity]; } else { // calculate liquidity with invariant delta const liquidity = newInvariant .sub(oldInvariant) .mul(totalSupply) .div(oldInvariant); const liquidityWithoutFee = newInvariant .sub(oldInvariantWithoutFee) .mul(totalSupply) .div(oldInvariantWithoutFee); return [liquidity, liquidityWithoutFee]; } } function computeDFromAdjustedBalances(A, xp0, xp1, checkOverflow) { const s = xp0.add(xp1); if (s.isZero()) { return ZERO; } else { let prevD; let d = s; const nA = A.mul(TWO); for (let i = 0; i < MAX_LOOP_LIMIT; i++) { const dSq = d.mul(d); if (checkOverflow && dSq.gt(UINT256_MAX)) { throw Error("overflow"); } const d2 = dSq.div(xp0).mul(d); if (checkOverflow && d2.gt(UINT256_MAX)) { throw Error("overflow"); } const dP = d2.div(xp1).div(FOUR); prevD = d; const d0 = nA.mul(s).add(dP.mul(TWO)).mul(d); if (checkOverflow && d0.gt(UINT256_MAX)) { throw Error("overflow"); } d = d0.div(nA.sub(ONE).mul(d).add(dP.mul(THREE))); if (d.sub(prevD).abs().lte(ONE)) { return d; } } return d; } } const A_MULTIPLIER = BigNumber.from(10000); const N_COINS = BigNumber.from(2); function hashGetY(ANN, gamma, x0, x1, D, i) { return (ANN.toHexString() + gamma.toHexString() + x0.toHexString() + x1.toHexString() + D.toHexString() + i); } let getYCacheMap = new Map(); export function cryptoMathGetY(ANN, gamma, x0, x1, D, i) { //invariant(D.gte(DECIMALS_17) && D.lte(DECIMALS_33), "unsafe values D"); if (!(D.gte(DECIMALS_17) && D.lte(DECIMALS_33))) { //console.warn("cryptoMathGetY: unsafe values D", D.toString()); return ZERO; } // cache const hash = hashGetY(ANN, gamma, x0, x1, D, i); const cached = getYCacheMap.get(hash); if (cached !== undefined) { //console.log('[cryptoMathGetY] hit cache, size', getYCacheMap.size); return cached; } let x_j = x0; if (i === 0) { x_j = x1; } let y = D.pow(TWO).div(x_j.mul(N_COINS.pow(TWO))); const K0_i = ETHER.mul(N_COINS).mul(x_j).div(D); //invariant(K0_i.gte(DECIMALS_16.mul(N_COINS)) && K0_i.lte(DECIMALS_20.mul(N_COINS)), "cryptoMathGetY: unsafe values x[i]"); if (!(K0_i.gte(DECIMALS_16.mul(N_COINS)) && K0_i.lte(DECIMALS_20.mul(N_COINS)))) { //console.warn("cryptoMathGetY: unsafe values x[i]", K0_i.toString()); return ZERO; } const convergence_limit = max(max(x_j.div(DECIMALS_14), D.div(DECIMALS_14)), DECIMALS_2); const __g1k0 = gamma.add(ETHER); for (let j = 0; j < 255; j++) { const y_prev = y; const K0 = K0_i.mul(y).mul(N_COINS).div(D); const S = x_j.add(y); let _g1k0 = __g1k0; if (_g1k0.gt(K0)) { _g1k0 = _g1k0.sub(K0).add(1); } else { _g1k0 = K0.sub(_g1k0).add(1); } const mul1 = DECIMALS_18.mul(D) .div(gamma) .mul(_g1k0) .div(gamma) .mul(_g1k0) .mul(A_MULTIPLIER) .div(ANN); const mul2 = DECIMALS_18.add(TWO.mul(DECIMALS_18).mul(K0)).div(_g1k0); let yfprime = DECIMALS_18.mul(y).add(S.mul(mul2)).add(mul1); const _dyfprime = D.mul(mul2); if (yfprime.lt(_dyfprime)) { y = y_prev.div(TWO); continue; } else { yfprime = yfprime.sub(_dyfprime); } const fprime = yfprime.div(y); const y_minus_temp = mul1.div(fprime); const y_plus = yfprime .add(ETHER.mul(D)) .div(fprime) .add(y_minus_temp.mul(ETHER).div(K0)); const y_minus = y_minus_temp.add(DECIMALS_18.mul(S).div(fprime)); if (y_plus.lt(y_minus)) { y = y_prev.div(2); } else { y = y_plus.sub(y_minus); } let diff; if (y.gt(y_prev)) { diff = y.sub(y_prev); } else { diff = y_prev.sub(y); } if (diff.lt(max(convergence_limit, y.div(DECIMALS_14)))) { const frac = y.mul(DECIMALS_18).div(D); //invariant(frac.gte(DECIMALS_16) && frac.lte(DECIMALS_20), "cryptoMathGetY: unsafe value for y"); if (!(frac.gte(DECIMALS_16) && frac.lte(DECIMALS_20))) { //console.warn("cryptoMathGetY: unsafe values for y", frac.toString()); return ZERO; } if (getYCacheMap.size > 256) { getYCacheMap = new Map(); } getYCacheMap.set(hash, y); return y; } } //throw new Error("Did not converge"); console.warn("Did not converge"); return ZERO; } function hashComputeD(ANN, gamma, x0, x1) { return (ANN.toHexString() + gamma.toHexString() + x0.toHexString() + x1.toHexString()); } let computeDCacheMap = new Map(); export function cryptoMathComputeD(ANN, gamma, x0, x1) { //console.log('cryptoMathComputeD ANN', ANN.toString(), 'gamma', gamma.toString(), 'x0', x0.toString(), 'x1', x1.toString()); if (x0.isZero() || x1.isZero()) { return ZERO; } // Initial value of invariant D is that for constant-product invariant let x; //[x0, x1].sort((a, b) => b.sub(a).isNegative() ? -1 : 1); if (x0.lt(x1)) { x = [x1, x0]; } else { x = [x0, x1]; } if (!(x[0].gte(DECIMALS_9) && x[0].lte(DECIMALS_33))) { //console.warn("cryptoMathComputeD: unsafe values x[0]", x[0].toString()); return ZERO; } if (!x[1].mul(DECIMALS_18).div(x[0]).gte(DECIMALS_14)) { //console.warn("cryptoMathComputeD: unsafe values x[i] (input)", x[1].toString()); return ZERO; } //invariant(x[0].gte(DECIMALS_9) && x[0].lte(DECIMALS_33), "cryptoMathComputeD: unsafe values x[0]"); //invariant(x[1].mul(DECIMALS_18).div(x[0]).gte(DECIMALS_14), "cryptoMathComputeD: unsafe values x[i] (input)"); // cache const hash = hashComputeD(ANN, gamma, x0, x1); const cached = computeDCacheMap.get(hash); if (cached !== undefined) { //console.log('[cryptoMathComputeD] hit cache, size', computeDCacheMap.size); return cached; } let D = N_COINS.mul(geometricMean(x0, x1)); const S = x0.add(x1); const __g1k0 = gamma.add(DECIMALS_18); for (let i = 0; i < 255; i++) { const D_prev = D; //invariant(D.gt(ZERO), "Unsafe value D"); if (D.lte(ZERO)) { //console.warn("Unsafe value D", D.toString()); return ZERO; } const K0 = DECIMALS_18.mul(N_COINS.pow(2)) .mul(x[0]) .div(D) .mul(x[1]) .div(D); let _g1k0 = __g1k0; if (_g1k0.gt(K0)) { _g1k0 = _g1k0.sub(K0).add(1); } else { _g1k0 = K0.sub(_g1k0).add(1); } const mul1 = DECIMALS_18.mul(D) .div(gamma) .mul(_g1k0) .div(gamma) .mul(_g1k0) .mul(A_MULTIPLIER) .div(ANN); const mul2 = TWO.mul(DECIMALS_18).mul(N_COINS).mul(K0).div(_g1k0); const neg_fprime = S.add(S.mul(mul2).div(DECIMALS_18)) .add(mul1.mul(N_COINS).div(K0)) .sub(mul2.mul(D).div(DECIMALS_18)); const D_plus = D.mul(neg_fprime.add(S)).div(neg_fprime); let D_minus = D.mul(D).div(neg_fprime); if (DECIMALS_18.gt(K0)) { D_minus = D_minus.add(D.mul(mul1.div(neg_fprime)) .div(DECIMALS_18) .mul(DECIMALS_18.sub(K0)) .div(K0)); } else { D_minus = D_minus.sub(D.mul(mul1.div(neg_fprime)) .div(DECIMALS_18) .mul(K0.sub(DECIMALS_18)) .div(K0)); } if (D_plus.gt(D_minus)) { D = D_plus.sub(D_minus); } else { D = D_minus.sub(D_plus).div(2); } const diff = D.gt(D_prev) ? D.sub(D_prev) : D_prev.sub(D); if (diff.mul(DECIMALS_14).lt(max(DECIMALS_16, D))) { const frac = x0.mul(DECIMALS_18).div(D); if (!(frac.gte(DECIMALS_16) && frac.lte(DECIMALS_20))) { //console.warn("cryptoMathComputeD: unsafe values x[i]-1", frac.toString()); return ZERO; } //nvariant(frac.gte(DECIMALS_16) && frac.lte(DECIMALS_20), "cryptoMathComputeD: unsafe values x[i]-1"); const frac1 = x1.mul(DECIMALS_18).div(D); if (!(frac1.gte(DECIMALS_16) && frac1.lte(DECIMALS_20))) { //console.warn("cryptoMathComputeD: unsafe values x[i]-2", frac1.toString()); return ZERO; } //invariant(frac1.gte(DECIMALS_16) && frac1.lte(DECIMALS_20), "cryptoMathComputeD: unsafe values x[i]-2"); // cache if (computeDCacheMap.size > 256) { computeDCacheMap = new Map(); } computeDCacheMap.set(hash, D); return D; } } //throw new Error("Did not converge"); console.warn("Did not converge"); return ZERO; } function geometricMean(x0, x1) { // Assuming ethers has sqrt method for BigNumber (This may be an oversimplification, and a more detailed algorithm might be needed for accurate square root computation) return sqrtBN(x0.mul(x1)); } function max(x, y) { return x.gt(y) ? x : y; } function estimateTweakPriceAqua(a, gamma, xp0, xp1, newInvariant, oldVirtualPrice, priceScale, totalSupply, futureParamsTime) { //console.log('estimateTweakPriceAqua', a, gamma, xp0, xp1, newInvariant, oldVirtualPrice, priceScale, totalSupply, futureParamsTime); //console.log('estimateTweakPriceAqua futureParamsTime', futureParamsTime.toString(), 'oldVirtualPrice', oldVirtualPrice.toString()); let unadjustedInvariant; if (newInvariant.isZero()) { unadjustedInvariant = cryptoMathComputeD(a, gamma, xp0, xp1); } else { unadjustedInvariant = newInvariant; } if (!oldVirtualPrice.isZero()) { const _xp0 = unadjustedInvariant.div(2); const _xp1 = ETHER.mul(unadjustedInvariant).div(priceScale.mul(2)); const xcp = geometricMean(_xp0, _xp1); const newVirtualPrice = ETHER.mul(xcp).div(totalSupply); if (futureParamsTime.lt(getBlockTimestamp())) { if (newVirtualPrice.lt(oldVirtualPrice)) { console.warn("estimateTweakPriceAqua: loss, newVirtualPrice", newVirtualPrice.toString(), "oldVirtualPrice", oldVirtualPrice.toString(), "xcp", xcp.toString(), "newXp0", _xp0.toString(), "newXp1", _xp1.toString(), "priceScale", priceScale.toString()); throw Error("loss"); } } } } function hashGetAmountOutAquaParams(params) { return `${params.tokenIn}_${params.tokenOut}_${params.amount.toHexString()}_${params.reserveIn?.toHexString()}_${params.reserveOut?.toHexString()}_${params.priceScale?.toHexString()}_${params.virtualPrice?.toHexString()}_${params.invariantLast?.toHexString()}_${params.gamma?.toHexString()}`; } let getAmountOutAquaCacheMap = new Map(); function getCachedAmountOutAquaResult(hash) { const cachedResult = getAmountOutAquaCacheMap.get(hash); if (cachedResult) { if (Date.now() - cachedResult.timestamp <= 60000) { //console.log('[debug] getCachedAmountOutAquaResult: hit cache, size', getAmountOutAquaCacheMap.size); return cachedResult.amountOut; } else { //console.log('[debug] getCachedAmountOutAquaResult: found expired'); } } if (getAmountOutAquaCacheMap.size > 256) { //console.log('[debug] cache cleared'); getAmountOutAquaCacheMap = new Map(); } return null; } function getAmountOutAqua(params, checkOverflow, shouldEstimateTweakPrice) { //console.log('getAmountOutAqua', params, checkOverflow); if (params.reserveIn.isZero() || params.amount.isZero()) { //console.log('getAmountOutAqua reserveIn or amount is zero'); return ZERO; } if (!params.a || !params.gamma || !params.invariantLast || !params.priceScale || !params.futureParamsTime || !params.totalSupply || !params.virtualPrice) { console.warn("getAmountOutAqua: incomplete params", params); return ZERO; } if (params.a?.isZero() || params.gamma?.isZero() || params.invariantLast?.isZero()) { console.warn("getAmountOutAqua: invalid params", params); return ZERO; } const hash = hashGetAmountOutAquaParams(params); const cachedResult = getCachedAmountOutAquaResult(hash); if (cachedResult) { return cachedResult; } // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const tokenInPrecisionMultiplier = params.tokenInPrecisionMultiplier; // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const tokenOutPrecisionMultiplier = params.tokenOutPrecisionMultiplier; // Get XPs. const amountIn = params.amount; const balanceIn = params.reserveIn.add(amountIn); const balanceOut = params.reserveOut; const priceScale = params.priceScale; /* params.swapFee = { gamma: BigNumber.from(10000000000), minFee: 100, maxFee: 100, }; console.log('getAmountOutAqua params', params, 'swapFee', params.swapFee); */ let xpIn; let xpOut; if (params.swap0For1) { xpIn = balanceIn.mul(tokenInPrecisionMultiplier); xpOut = balanceOut .mul(priceScale) .mul(tokenOutPrecisionMultiplier) .div(ETHER); } else { xpIn = balanceIn.mul(priceScale).mul(tokenInPrecisionMultiplier).div(ETHER); xpOut = balanceOut.mul(tokenOutPrecisionMultiplier); } let invariant; if (params.futureParamsTime.gt(getBlockTimestamp())) { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion // eslint-disable-next-line @typescript-eslint/no-non-null-assertion let oldXpIn; if (params.swap0For1) { oldXpIn = params.reserveIn.mul(tokenInPrecisionMultiplier); invariant = cryptoMathComputeD(params.a, params.gamma, oldXpIn, xpOut); } else { oldXpIn = params.reserveIn .mul(priceScale) .mul(tokenInPrecisionMultiplier) .div(ETHER); invariant = cryptoMathComputeD(params.a, params.gamma, xpOut, oldXpIn); } } else { invariant = params.invariantLast; } if (checkOverflow && balanceIn.gt(UINT128_MAX)) { //console.log('getAmountOutAqua overflow'); throw Error("overflow"); } let amountOutAfterFee; let newXpOut; // used for estimateTweakPriceAqua //console.log('getAmountOutAqua: a', params.a.toString(), 'gamma', params.gamma.toString(), 'futureParamsTime', params.futureParamsTime.toString(), 'now', SwapUtils.blockTimestamp().toString()); if (params.swap0For1) { const y = cryptoMathGetY(params.a, params.gamma, xpIn, xpOut, invariant, 1); const yOut = xpOut.sub(y); if (yOut.lte(ONE)) { //console.log('getAmountOutAqua: zero out 1', xpIn.toString(), xpOut.toString(), invariant.toString()); return ZERO; } // modify xp out newXpOut = xpOut.sub(yOut); const intermediate = yOut.sub(ONE).mul(ETHER); const priceScale_x_precision = priceScale.mul(tokenOutPrecisionMultiplier); //const amountOut = yOut.sub(ONE).mul(ETHER).div(priceScale.mul(tokenOutPrecisionMultiplier)); const amountOut = intermediate.div(priceScale_x_precision); const swapFee = getCryptoFee(params.swapFee, xpIn, newXpOut); const amountFee = intermediate .mul(swapFee) .div(priceScale_x_precision) .div(DECIMALS_5); amountOutAfterFee = amountOut.sub(amountFee); //console.log('getAmountOutAqua: swapFee', swapFee.toString(), 'amountFee', amountFee.toString(), '0For1'); if (shouldEstimateTweakPrice) { // update xp out newXpOut = params.reserveOut .sub(amountOutAfterFee) .mul(priceScale_x_precision) .div(ETHER); } } else { const y = cryptoMathGetY(params.a, params.gamma, xpOut, xpIn, invariant, 0); const yOut = xpOut.sub(y); if (yOut.lte(ONE)) { //console.log('getAmountOutAqua: zero out 0', xpOut.toString(), xpIn.toString(), invariant.toString()); return ZERO; } // modify xp out newXpOut = xpOut.sub(yOut); const intermediate = yOut.sub(ONE); //const amountOut = yOut.sub(ONE).div(tokenOutPrecisionMultiplier); const amountOut = intermediate.div(tokenOutPrecisionMultiplier); const swapFee = getCryptoFee(params.swapFee, newXpOut, xpIn); const amountFee = intermediate .mul(swapFee) .div(tokenOutPrecisionMultiplier) .div(DECIMALS_5); amountOutAfterFee = amountOut.sub(amountFee); //console.log('getAmountOutAqua: swapFee', swapFee.toString(), 'amountFee', amountFee.toString(), '1For0'); if (shouldEstimateTweakPrice) { // update xp out newXpOut = params.reserveOut .sub(amountOutAfterFee) .mul(tokenOutPrecisionMultiplier); } } if (shouldEstimateTweakPrice) { if (params.swap0For1) { estimateTweakPriceAqua(params.a, params.gamma, xpIn, newXpOut, ZERO, params.virtualPrice, priceScale, params.totalSupply, params.futureParamsTime); } else { estimateTweakPriceAqua(params.a, params.gamma, newXpOut, xpIn, ZERO, params.virtualPrice, priceScale, params.totalSupply, params.futureParamsTime); } } if (amountOutAfterFee.lte(ZERO)) { getAmountOutAquaCacheMap.set(hash, { amountOut: ZERO, timestamp: Date.now(), }); return ZERO; } else { getAmountOutAquaCacheMap.set(hash, { amountOut: amountOutAfterFee, timestamp: Date.now(), }); return amountOutAfterFee; } } let cachedQuoterInfo = null; let getAmountOutUniswapV3CacheMap = new Map(); function hashGetAmountOutUniswapV3Params(params) { return `${params.tokenIn}_${params.tokenOut}_${params.amount.toHexString()}_${params.tickSpacing}`; } function getCachedAmountOutUniswapV3Result(hash) { const cachedResult = getAmountOutUniswapV3CacheMap.get(hash); if (cachedResult) { if (Date.now() - cachedResult.timestamp <= 10000) { //console.log('[debug] getCachedAmountOutUniswapV3Result: hit cache, size', getAmountOutUniswapV3CacheMap.size); return cachedResult.amountOut; } else { //console.log('[debug] getCachedAmountOutUniswapV3Result: found expired'); } } if (getAmountOutUniswapV3CacheMap.size > 256) { //console.log('[debug] cache cleared'); getAmountOutUniswapV3CacheMap = new Map(); } return null; } async function getAmountOutUniswapV3(params, checkOverflow, shouldEstimateTweakPrice) { if (!params.sqrtPriceX96 || params.sqrtPriceX96.isZero()) { return { amountOut: ZERO, sqrtPriceLimitX96: ZERO, }; } if (params.reserveIn.isZero() || params.amount.isZero()) { //console.log('getAmountOutAqua reserveIn or amount is zero'); return { amountOut: ZERO, sqrtPriceLimitX96: ZERO, }; } const amountIn = params.amount; const balanceIn = params.reserveIn.add(amountIn); if (checkOverflow && balanceIn.gt(UINT128_MAX)) { //console.log('getAmountOutAqua overflow'); throw Error("overflow"); } let sqrtPriceLimitX96; if (params.swap0For1) { sqrtPriceLimitX96 = ZERO; //BigNumber.from(params.sqrtPriceX96).sub(sqrtPriceX96Slippage); } else { sqrtPriceLimitX96 = ZERO; //BigNumber.from(params.sqrtPriceX96).add(sqrtPriceX96Slippage); } let quoterContract; let rangeFactoryAddress; const network = stateStore().network; if (cachedQuoterInfo && cachedQuoterInfo.network === network) { quoterContract = cachedQuoterInfo.quoterContract; rangeFactoryAddress = cachedQuoterInfo.rangeFactoryAddress; } else { quoterContract = ContractRegistry.getContractByName("quoter"); rangeFactoryAddress = ContractRegistry.getAddressByName("ranged_factory"); // cache quoter contract cachedQuoterInfo = { network: network, quoterContract: quoterContract, rangeFactoryAddress: rangeFactoryAddress, }; } let resolved = false; return new Promise((resolve) => { const hash = hashGetAmountOutUniswapV3Params(params); const cachedAmountOut = getCachedAmountOutUniswapV3Result(hash); if (cachedAmountOut) { resolve({ amountOut: cachedAmountOut, sqrtPriceLimitX96: sqrtPriceLimitX96, }); return; } quoterContract.callStatic .quoteExactInputSingle({ factory: rangeFactoryAddress, tokenIn: params.tokenIn, tokenOut: params.tokenOut, amountIn: params.amount, tickSpacing: params.tickSpacing, sqrtPriceLimitX96: ZERO, }) .then((quoteResults) => { resolved = true; resolve({ amountOut: quoteResults.amountOut, sqrtPriceLimitX96: sqrtPriceLimitX96, }); getAmountOutUniswapV3CacheMap.set(hash, { amountOut: quoteResults.amountOut, timestamp: Date.now(), }); }) .catch((e) => { console.warn("Range quoter failed", e); resolved = true; resolve({ amountOut: ZERO, sqrtPriceLimitX96: sqrtPriceLimitX96, }); }); setTimeout(() => { if (!resolved) { resolve({ amountOut: ZERO, sqrtPriceLimitX96: sqrtPriceLimitX96, }); } }, 20000); // 2s timeout }); } export async function calculateAmountOut(params, checkOverflow) { //console.log('calculateAmountOut params', params); if (params.amount.isZero()) { return { amountOut: ZERO, }; } try { switch (params.poolType) { case PoolTypes.CLASSIC: return { amountOut: getAmountOutClassic(params, checkOverflow), }; case PoolTypes.STABLE: return { amountOut: getAmountOutStable(params, checkOverflow), }; case PoolTypes.AQUA: return { amountOut: getAmountOutAqua(params, checkOverflow, true), }; case PoolTypes.UNISWAP_V3: return getAmountOutUniswapV3(params, checkOverflow, true); default: throw Error("Unsupported pool type"); } // eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (error) { if (error.message !== "overflow") { console.log(params.poolType, "error", error); } return { amountOut: ZERO, }; } } function getQuoteOutClassic(params, hasFee) { if (params.reserveIn.isZero() || params.amount.isZero()) { return ZERO; } const amountIn = params.amount; const multiplier = DECIMALS_5; const swapFee = hasFee ? params.swapFee.maxFee : ZERO; const amountInWithFee = amountIn.mul(MAX_FEE.sub(swapFee)); return amountInWithFee .mul(params.reserveOut.mul(multiplier)) .div(params.reserveIn.mul(multiplier).mul(MAX_FEE)); } function getQuoteOutAqua(params, hasFee) { const scale = DECIMALS_3; const adjustAmount = params.amount.div(scale); const quoteOut = getAmountOutAqua({ tokenIn: params.tokenIn, tokenOut: params.tokenOut, poolType: params.poolType, swap0For1: params.swap0For1, amount: adjustAmount, reserve0: params.reserve0, reserve1: params.reserve1, reserveIn: params.reserveIn, reserveOut: params.reserveOut, swapFee: hasFee ? params.swapFee : ZERO_FEE_DATA, tokenInPrecisionMultiplier: params.tokenInPrecisionMultiplier, tokenOutPrecisionMultiplier: params.tokenOutPrecisionMultiplier, a: params.a, gamma: params.gamma, priceScale: params.priceScale, futureParamsTime: params.futureParamsTime, invariantLast: params.invariantLast, virtualPrice: params.virtualPrice, totalSupply: params.totalSupply, }, false, false).mul(scale); return quoteOut; } function getQuoteOutUniswapV3(params, hasFee) { const sqrtPriceX96 = params.sqrtPriceX96; if (!sqrtPriceX96 || sqrtPriceX96.isZero()) { return ZERO; } const squaredPrice = sqrtPriceX96.mul(sqrtPriceX96).div(Q_96); let amountIn = params.amount; if (hasFee) { const amountFee = amountIn.mul(params.swapFee.maxFee).div(DECIMALS_5); amountIn = amountIn.sub(amountFee); } if (params.swap0For1) { return amountIn.mul(squaredPrice).div(Q_96); } else { return amountIn.mul(Q_96).div(squaredPrice); } } function getQuoteOutStable(params, hasFee) { const multiplier = DECIMALS_4; // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const adjustedReserveIn = params.reserveIn .mul(params.tokenInPrecisionMultiplier) .mul(multiplier); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const adjustedReserveOut = params.reserveOut .mul(params.tokenOutPrecisionMultiplier) .mul(multiplier); const amountIn = params.amount; const swapFee = hasFee ? params.swapFee.maxFee : ZERO; const feeDeductedAmountIn = amountIn.sub(amountIn.mul(swapFee).div(MAX_FEE)); const a = params.a ?? DEFAULT_STABLE_POOL_A; const d = computeDFromAdjustedBalances(a, adjustedReserveIn, adjustedReserveOut, false); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const x = adjustedReserveIn.add(feeDeductedAmountIn.mul(params.tokenInPrecisionMultiplier)); const y = getY(a, x, d); const dy = adjustedReserveOut.sub(y).sub(1); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion return dy.div(params.tokenOutPrecisionMultiplier); } export function calculateQuoteOut(params, hasFee) { if (params.amount.isZero()) { return ZERO; } let quoteOut; switch (params.poolType) { case PoolTypes.CLASSIC: quoteOut = getQuoteOutClassic(params, hasFee); break; case PoolTypes.STABLE: quoteOut = getQuoteOutStable(params, hasFee); break; case PoolTypes.AQUA: quoteOut = getQuoteOutAqua(params, hasFee); break; case PoolTypes.UNISWAP_V3: quoteOut = getQuoteOutUniswapV3(params, hasFee); break; default: throw Error("Unsupported pool type"); } return quoteOut; } async function calculateAmountOutForStep(step, amountIn, quoteIn //updateReserves: boolean, ) { // cache const hash = hashAmountForStep(step, amountIn); const cached = cacheAmountOutForStepMap.get(hash); if (cached) { //console.log('[calculateAmountOutForStep] hit cache, size', cacheAmountOutForStepMap.size); return cached.amounts; } const isTokenAIn = step.pool.tokenA === step.tokenIn; const [reserveIn, reserveOut] = isTokenAIn ? [step.pool.reserveA, step.pool.reserveB] : [step.pool.reserveB, step.pool.reserveA]; //console.log('calculateAmountOutForStep tokenA', step.tokenA, 'tokenIn', step.tokenIn); let tokenInPrecisionMultiplier; let tokenOutPrecisionMultiplier; const [tokenInAddress, tokenOutAddress] = isTokenAIn ? [step.pool.tokenA, step.pool.tokenB] : [step.pool.tokenB, step.pool.tokenA]; // create multiplier for stable pools const needMultiplers = step.pool.poolType === PoolTypes.STABLE || step.pool.poolType === PoolTypes.AQUA; if (needMultiplers) { const tokenIn = TokenRegistry.getTokenByAddress(tokenInAddress); const tokenOut = TokenRegistry.getTokenByAddress(tok