diff --git a/package.json b/package.json index c4a658e0..47ce6f7c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@balancer-labs/sor", - "version": "4.0.1-beta.1", + "version": "4.0.1-beta.2", "license": "GPL-3.0-only", "main": "dist/index.js", "module": "dist/index.esm.js", diff --git a/src/pools/gyro2Pool/constants.ts b/src/pools/gyro2Pool/constants.ts new file mode 100644 index 00000000..cd93f639 --- /dev/null +++ b/src/pools/gyro2Pool/constants.ts @@ -0,0 +1,16 @@ +import { BigNumber, parseFixed } from '@ethersproject/bignumber'; + +// Swap limits: amounts swapped may not be larger than this percentage of total balance. + +export const _MAX_IN_RATIO: BigNumber = parseFixed('0.3', 18); +export const _MAX_OUT_RATIO: BigNumber = parseFixed('0.3', 18); + +export const SQRT_1E_NEG_1 = BigNumber.from('316227766016837933'); +export const SQRT_1E_NEG_3 = BigNumber.from('31622776601683793'); +export const SQRT_1E_NEG_5 = BigNumber.from('3162277660168379'); +export const SQRT_1E_NEG_7 = BigNumber.from('316227766016837'); +export const SQRT_1E_NEG_9 = BigNumber.from('31622776601683'); +export const SQRT_1E_NEG_11 = BigNumber.from('3162277660168'); +export const SQRT_1E_NEG_13 = BigNumber.from('316227766016'); +export const SQRT_1E_NEG_15 = BigNumber.from('31622776601'); +export const SQRT_1E_NEG_17 = BigNumber.from('3162277660'); diff --git a/src/pools/gyro2Pool/gyro2Math.ts b/src/pools/gyro2Pool/gyro2Math.ts index c4b88fe0..6a9f5cea 100644 --- a/src/pools/gyro2Pool/gyro2Math.ts +++ b/src/pools/gyro2Pool/gyro2Math.ts @@ -1,46 +1,8 @@ -import { BigNumber, parseFixed } from '@ethersproject/bignumber'; +import { BigNumber } from '@ethersproject/bignumber'; import { WeiPerEther as ONE } from '@ethersproject/constants'; -import bn from 'bignumber.js'; - -// Swap limits: amounts swapped may not be larger than this percentage of total balance. - -const _MAX_IN_RATIO: BigNumber = parseFixed('0.3', 18); -const _MAX_OUT_RATIO: BigNumber = parseFixed('0.3', 18); - -// Helpers -export function _squareRoot(value: BigNumber): BigNumber { - return BigNumber.from( - new bn(value.mul(ONE).toString()).sqrt().toFixed().split('.')[0] - ); -} - -export function _normalizeBalances( - balances: BigNumber[], - decimalsIn: number, - decimalsOut: number -): BigNumber[] { - const scalingFactors = [ - parseFixed('1', decimalsIn), - parseFixed('1', decimalsOut), - ]; - - return balances.map((bal, index) => - bal.mul(ONE).div(scalingFactors[index]) - ); -} +import { _sqrt, mulUp, divUp, mulDown, divDown } from './helpers'; +import { _MAX_IN_RATIO, _MAX_OUT_RATIO } from './constants'; -///////// -/// Fee calculations -///////// - -export function _reduceFee(amountIn: BigNumber, swapFee: BigNumber): BigNumber { - const feeAmount = amountIn.mul(swapFee).div(ONE); - return amountIn.sub(feeAmount); -} - -export function _addFee(amountIn: BigNumber, swapFee: BigNumber): BigNumber { - return amountIn.mul(ONE).div(ONE.sub(swapFee)); -} ///////// /// Virtual Parameter calculations ///////// @@ -50,10 +12,7 @@ export function _findVirtualParams( sqrtAlpha: BigNumber, sqrtBeta: BigNumber ): [BigNumber, BigNumber] { - return [ - invariant.mul(ONE).div(sqrtBeta), - invariant.mul(sqrtAlpha).div(ONE), - ]; + return [divDown(invariant, sqrtBeta), mulDown(invariant, sqrtAlpha)]; } ///////// @@ -76,9 +35,13 @@ export function _calculateInvariant( // 2 * a // // // **********************************************************************************************/ - const [a, mb, mc] = _calculateQuadraticTerms(balances, sqrtAlpha, sqrtBeta); + const [a, mb, bSquare, mc] = _calculateQuadraticTerms( + balances, + sqrtAlpha, + sqrtBeta + ); - const invariant = _calculateQuadratic(a, mb, mc); + const invariant = _calculateQuadratic(a, mb, bSquare, mc); return invariant; } @@ -87,30 +50,52 @@ export function _calculateQuadraticTerms( balances: BigNumber[], sqrtAlpha: BigNumber, sqrtBeta: BigNumber -): [BigNumber, BigNumber, BigNumber] { - const a = ONE.sub(sqrtAlpha.mul(ONE).div(sqrtBeta)); - const bterm0 = balances[1].mul(ONE).div(sqrtBeta); - const bterm1 = balances[0].mul(sqrtAlpha).div(ONE); +): [BigNumber, BigNumber, BigNumber, BigNumber] { + const a = ONE.sub(divDown(sqrtAlpha, sqrtBeta)); + const bterm0 = divDown(balances[1], sqrtBeta); + const bterm1 = mulDown(balances[0], sqrtAlpha); const mb = bterm0.add(bterm1); - const mc = balances[0].mul(balances[1]).div(ONE); + const mc = mulDown(balances[0], balances[1]); - return [a, mb, mc]; + // For better fixed point precision, calculate in expanded form w/ re-ordering of multiplications + // b^2 = x^2 * alpha + x*y*2*sqrt(alpha/beta) + y^2 / beta + let bSquare = mulDown( + mulDown(mulDown(balances[0], balances[0]), sqrtAlpha), + sqrtAlpha + ); + const bSq2 = divDown( + mulDown( + mulDown(mulDown(balances[0], balances[1]), ONE.mul(2)), + sqrtAlpha + ), + sqrtBeta + ); + + const bSq3 = divDown( + mulDown(balances[1], balances[1]), + mulUp(sqrtBeta, sqrtBeta) + ); + + bSquare = bSquare.add(bSq2).add(bSq3); + + return [a, mb, bSquare, mc]; } export function _calculateQuadratic( a: BigNumber, mb: BigNumber, + bSquare: BigNumber, mc: BigNumber ): BigNumber { - const denominator = a.mul(BigNumber.from(2)); - const bSquare = mb.mul(mb).div(ONE); - const addTerm = a.mul(mc.mul(BigNumber.from(4))).div(ONE); + const denominator = mulUp(a, ONE.mul(2)); + // order multiplications for fixed point precision + const addTerm = mulDown(mulDown(mc, ONE.mul(4)), a); // The minus sign in the radicand cancels out in this special case, so we add const radicand = bSquare.add(addTerm); - const sqrResult = _squareRoot(radicand); + const sqrResult = _sqrt(radicand, BigNumber.from(5)); // The minus sign in the numerator cancels out in this special case const numerator = mb.add(sqrResult); - const invariant = numerator.mul(ONE).div(denominator); + const invariant = divDown(numerator, denominator); return invariant; } @@ -125,8 +110,7 @@ export function _calcOutGivenIn( balanceOut: BigNumber, amountIn: BigNumber, virtualParamIn: BigNumber, - virtualParamOut: BigNumber, - currentInvariant: BigNumber + virtualParamOut: BigNumber ): BigNumber { /********************************************************************************************** // Described for X = `in' asset and Y = `out' asset, but equivalent for the other case // @@ -141,15 +125,26 @@ export function _calcOutGivenIn( // Note that -dy > 0 is what the trader receives. // // We exploit the fact that this formula is symmetric up to virtualParam{X,Y}. // **********************************************************************************************/ - if (amountIn.gt(balanceIn.mul(_MAX_IN_RATIO).div(ONE))) + if (amountIn.gt(mulDown(balanceIn, _MAX_IN_RATIO))) throw new Error('Swap Amount Too Large'); - const virtIn = balanceIn.add(virtualParamIn); - const denominator = virtIn.add(amountIn); - const invSquare = currentInvariant.mul(currentInvariant).div(ONE); - const subtrahend = invSquare.mul(ONE).div(denominator); - const virtOut = balanceOut.add(virtualParamOut); - return virtOut.sub(subtrahend); + // The factors in total lead to a multiplicative "safety margin" between the employed virtual offsets + // very slightly larger than 3e-18. + const virtInOver = balanceIn.add(mulUp(virtualParamIn, ONE.add(2))); + const virtOutUnder = balanceOut.add(mulDown(virtualParamOut, ONE.sub(1))); + + const amountOut = divDown( + mulDown(virtOutUnder, amountIn), + virtInOver.add(amountIn) + ); + + if (amountOut.gte(balanceOut)) throw new Error('ASSET_BOUNDS_EXCEEDED'); + + // This in particular ensures amountOut < balanceOut. + if (amountOut.gt(mulDown(balanceOut, _MAX_OUT_RATIO))) + throw new Error('MAX_OUT_RATIO'); + + return amountOut; } // SwapType = 'swapExactOut' export function _calcInGivenOut( @@ -157,8 +152,7 @@ export function _calcInGivenOut( balanceOut: BigNumber, amountOut: BigNumber, virtualParamIn: BigNumber, - virtualParamOut: BigNumber, - currentInvariant: BigNumber + virtualParamOut: BigNumber ): BigNumber { /********************************************************************************************** // dX = incrX = amountIn > 0 // @@ -173,15 +167,23 @@ export function _calcInGivenOut( // Note that dy < 0 < dx. // **********************************************************************************************/ - if (amountOut.gt(balanceOut.mul(_MAX_OUT_RATIO).div(ONE))) + if (amountOut.gt(mulDown(balanceOut, _MAX_OUT_RATIO))) throw new Error('Swap Amount Too Large'); - const virtOut = balanceOut.add(virtualParamOut); - const denominator = virtOut.sub(amountOut); - const invSquare = currentInvariant.mul(currentInvariant).div(ONE); - const term = invSquare.mul(ONE).div(denominator); - const virtIn = balanceIn.add(virtualParamIn); - return term.sub(virtIn); + // The factors in total lead to a multiplicative "safety margin" between the employed virtual offsets + // very slightly larger than 3e-18. + const virtInOver = balanceIn.add(mulUp(virtualParamIn, ONE.add(2))); + const virtOutUnder = balanceOut.add(mulDown(virtualParamOut, ONE.sub(1))); + + const amountIn = divUp( + mulUp(virtInOver, amountOut), + virtOutUnder.sub(amountOut) + ); + + if (amountIn.gt(mulDown(balanceIn, _MAX_IN_RATIO))) + throw new Error('Max_IN_RATIO'); + + return amountIn; } // ///////// @@ -211,10 +213,10 @@ export function _calculateNewSpotPrice( const afterFeeMultiplier = ONE.sub(swapFee); // 1 - s const virtIn = balances[0].add(virtualParamIn); // x + virtualParamX = x' - const numerator = virtIn.add(afterFeeMultiplier.mul(inAmount).div(ONE)); // x' + (1 - s) * dx + const numerator = virtIn.add(mulDown(afterFeeMultiplier, inAmount)); // x' + (1 - s) * dx const virtOut = balances[1].add(virtualParamOut); // y + virtualParamY = y' - const denominator = afterFeeMultiplier.mul(virtOut.sub(outAmount)).div(ONE); // (1 - s) * (y' + dy) - const newSpotPrice = numerator.mul(ONE).div(denominator); + const denominator = mulDown(afterFeeMultiplier, virtOut.sub(outAmount)); // (1 - s) * (y' + dy) + const newSpotPrice = divDown(numerator, denominator); return newSpotPrice; } @@ -245,7 +247,7 @@ export function _derivativeSpotPriceAfterSwapExactTokenInForTokenOut( const virtOut = balances[1].add(virtualParamOut); // y' = y + virtualParamY const denominator = virtOut.sub(outAmount); // y' + dy - const derivative = TWO.mul(ONE).div(denominator); + const derivative = divDown(TWO, denominator); return derivative; } @@ -275,15 +277,12 @@ export function _derivativeSpotPriceAfterSwapTokenInForExactTokenOut( const TWO = BigNumber.from(2).mul(ONE); const afterFeeMultiplier = ONE.sub(swapFee); // 1 - s const virtIn = balances[0].add(virtualParamIn); // x + virtualParamX = x' - const numerator = virtIn.add(afterFeeMultiplier.mul(inAmount).div(ONE)); // x' + (1 - s) * dx + const numerator = virtIn.add(mulDown(afterFeeMultiplier, inAmount)); // x' + (1 - s) * dx const virtOut = balances[1].add(virtualParamOut); // y + virtualParamY = y' - const denominator = virtOut - .sub(outAmount) - .mul(virtOut.sub(outAmount)) - .div(ONE); // (y' + dy)^2 - const factor = TWO.mul(ONE).div(afterFeeMultiplier); // 2 / (1 - s) + const denominator = mulDown(virtOut.sub(outAmount), virtOut.sub(outAmount)); // (y' + dy)^2 + const factor = divDown(TWO, afterFeeMultiplier); // 2 / (1 - s) - const derivative = factor.mul(numerator.mul(ONE).div(denominator)).div(ONE); + const derivative = mulDown(factor, divDown(numerator, denominator)); return derivative; } @@ -308,7 +307,7 @@ export function _getNormalizedLiquidity( const virtIn = balances[0].add(virtualParamIn); const afterFeeMultiplier = ONE.sub(swapFee); - const normalizedLiquidity = virtIn.mul(ONE).div(afterFeeMultiplier); + const normalizedLiquidity = divDown(virtIn, afterFeeMultiplier); return normalizedLiquidity; } diff --git a/src/pools/gyro2Pool/gyro2Pool.ts b/src/pools/gyro2Pool/gyro2Pool.ts index 96dce4be..ad7ba5c6 100644 --- a/src/pools/gyro2Pool/gyro2Pool.ts +++ b/src/pools/gyro2Pool/gyro2Pool.ts @@ -10,23 +10,25 @@ import { SubgraphToken, SwapTypes, SubgraphPoolBase, - Gyro2PriceBounds, } from '../../types'; import { isSameAddress } from '../../utils'; import { - _squareRoot, - _normalizeBalances, _calculateInvariant, _calcOutGivenIn, _calcInGivenOut, _findVirtualParams, _calculateNewSpotPrice, - _reduceFee, - _addFee, _derivativeSpotPriceAfterSwapExactTokenInForTokenOut, _derivativeSpotPriceAfterSwapTokenInForExactTokenOut, _getNormalizedLiquidity, } from './gyro2Math'; +import { + _normalizeBalances, + _reduceFee, + _addFee, + mulDown, + divDown, +} from './helpers'; export type Gyro2PoolPairData = PoolPairBase & { sqrtAlpha: BigNumber; @@ -46,35 +48,18 @@ export class Gyro2Pool implements PoolBase { tokens: Gyro2PoolToken[]; swapFee: BigNumber; totalShares: BigNumber; - priceBounds: Gyro2PriceBounds; + sqrtAlpha: BigNumber; + sqrtBeta: BigNumber; // Max In/Out Ratios MAX_IN_RATIO = parseFixed('0.3', 18); MAX_OUT_RATIO = parseFixed('0.3', 18); static fromPool(pool: SubgraphPoolBase): Gyro2Pool { - if (!pool.gyro2PriceBounds) - throw new Error('Pool missing gyro2PriceBounds'); - - const { lowerBound, upperBound } = pool.gyro2PriceBounds; - if (Number(lowerBound) <= 0 || Number(upperBound) <= 0) - throw new Error('Invalid price bounds in gyro2PriceBounds'); - - const tokenInAddress = pool.gyro2PriceBounds.tokenInAddress; - const tokenOutAddress = pool.gyro2PriceBounds.tokenOutAddress; - - const tokenInIndex = pool.tokens.findIndex( - (t) => getAddress(t.address) === getAddress(tokenInAddress) - ); - const tokenOutIndex = pool.tokens.findIndex( - (t) => getAddress(t.address) === getAddress(tokenOutAddress) - ); - - if (tokenInIndex < 0) - throw new Error('Gyro2Pool priceBounds tokenIn not in tokens'); - - if (tokenOutIndex < 0) - throw new Error('Gyro2Pool priceBounds tokenOut not in tokens'); + if (!pool.sqrtAlpha || !pool.sqrtBeta) + throw new Error( + 'Pool missing Gyro2 sqrtAlpha and/or sqrtBeta params' + ); return new Gyro2Pool( pool.id, @@ -83,7 +68,8 @@ export class Gyro2Pool implements PoolBase { pool.totalShares, pool.tokens as Gyro2PoolToken[], pool.tokensList, - pool.gyro2PriceBounds as Gyro2PriceBounds + pool.sqrtAlpha, + pool.sqrtBeta ); } @@ -94,7 +80,8 @@ export class Gyro2Pool implements PoolBase { totalShares: string, tokens: Gyro2PoolToken[], tokensList: string[], - priceBounds: Gyro2PriceBounds + sqrtAlpha: string, + sqrtBeta: string ) { this.id = id; this.address = address; @@ -102,7 +89,8 @@ export class Gyro2Pool implements PoolBase { this.totalShares = parseFixed(totalShares, 18); this.tokens = tokens; this.tokensList = tokensList; - this.priceBounds = priceBounds; + this.sqrtAlpha = parseFixed(sqrtAlpha, 18); + this.sqrtBeta = parseFixed(sqrtBeta, 18); } parsePoolPairData(tokenIn: string, tokenOut: string): Gyro2PoolPairData { @@ -122,23 +110,7 @@ export class Gyro2Pool implements PoolBase { const balanceOut = tO.balance; const decimalsOut = tO.decimals; - const sqrtAlpha = isSameAddress( - tI.address, - this.priceBounds.tokenInAddress - ) - ? _squareRoot(parseFixed(this.priceBounds.lowerBound, 18)) - : _squareRoot( - ONE.mul(ONE).div(parseFixed(this.priceBounds.upperBound, 18)) - ); - - const sqrtBeta = isSameAddress( - tI.address, - this.priceBounds.tokenInAddress - ) - ? _squareRoot(parseFixed(this.priceBounds.upperBound, 18)) - : _squareRoot( - ONE.mul(ONE).div(parseFixed(this.priceBounds.lowerBound, 18)) - ); + const tokenInIsToken0 = tokenInIndex === 0; const poolPairData: Gyro2PoolPairData = { id: this.id, @@ -151,8 +123,12 @@ export class Gyro2Pool implements PoolBase { balanceIn: parseFixed(balanceIn, decimalsIn), balanceOut: parseFixed(balanceOut, decimalsOut), swapFee: this.swapFee, - sqrtAlpha, - sqrtBeta, + sqrtAlpha: tokenInIsToken0 + ? this.sqrtAlpha + : divDown(ONE, this.sqrtBeta), + sqrtBeta: tokenInIsToken0 + ? this.sqrtBeta + : divDown(ONE, this.sqrtAlpha), }; return poolPairData; @@ -191,14 +167,14 @@ export class Gyro2Pool implements PoolBase { if (swapType === SwapTypes.SwapExactIn) { return bnum( formatFixed( - poolPairData.balanceIn.mul(this.MAX_IN_RATIO).div(ONE), + mulDown(poolPairData.balanceIn, this.MAX_IN_RATIO), poolPairData.decimalsIn ) ); } else { return bnum( formatFixed( - poolPairData.balanceOut.mul(this.MAX_OUT_RATIO).div(ONE), + mulDown(poolPairData.balanceOut, this.MAX_OUT_RATIO), poolPairData.decimalsOut ) ); @@ -246,8 +222,7 @@ export class Gyro2Pool implements PoolBase { normalizedBalances[1], inAmountLessFee, virtualParamIn, - virtualParamOut, - invariant + virtualParamOut ); return bnum(formatFixed(outAmount, 18)); @@ -279,8 +254,7 @@ export class Gyro2Pool implements PoolBase { normalizedBalances[1], outAmount, virtualParamIn, - virtualParamOut, - invariant + virtualParamOut ); const inAmount = _addFee(inAmountLessFee, poolPairData.swapFee); @@ -314,8 +288,7 @@ export class Gyro2Pool implements PoolBase { normalizedBalances[1], inAmountLessFee, virtualParamIn, - virtualParamOut, - invariant + virtualParamOut ); const newSpotPrice = _calculateNewSpotPrice( normalizedBalances, @@ -354,8 +327,7 @@ export class Gyro2Pool implements PoolBase { normalizedBalances[1], outAmount, virtualParamIn, - virtualParamOut, - invariant + virtualParamOut ); const inAmount = _addFee(inAmountLessFee, poolPairData.swapFee); const newSpotPrice = _calculateNewSpotPrice( @@ -397,8 +369,7 @@ export class Gyro2Pool implements PoolBase { normalizedBalances[1], inAmountLessFee, virtualParamIn, - virtualParamOut, - invariant + virtualParamOut ); const derivative = _derivativeSpotPriceAfterSwapExactTokenInForTokenOut( normalizedBalances, @@ -435,8 +406,7 @@ export class Gyro2Pool implements PoolBase { normalizedBalances[1], outAmount, virtualParamIn, - virtualParamOut, - invariant + virtualParamOut ); const inAmount = _addFee(inAmountLessFee, poolPairData.swapFee); diff --git a/src/pools/gyro2Pool/helpers.ts b/src/pools/gyro2Pool/helpers.ts new file mode 100644 index 00000000..318eb406 --- /dev/null +++ b/src/pools/gyro2Pool/helpers.ts @@ -0,0 +1,164 @@ +import { BigNumber, parseFixed } from '@ethersproject/bignumber'; +import { WeiPerEther as ONE } from '@ethersproject/constants'; +import { + SQRT_1E_NEG_1, + SQRT_1E_NEG_3, + SQRT_1E_NEG_5, + SQRT_1E_NEG_7, + SQRT_1E_NEG_9, + SQRT_1E_NEG_11, + SQRT_1E_NEG_13, + SQRT_1E_NEG_15, + SQRT_1E_NEG_17, +} from './constants'; + +// Helpers + +export function _sqrt(input: BigNumber, tolerance: BigNumber) { + if (input.isZero()) { + return BigNumber.from(0); + } + let guess = _makeInitialGuess(input); + + // 7 iterations + for (let i of new Array(7).fill(0)) { + guess = guess.add(input.mul(ONE).div(guess)).div(2); + } + + // Check in some epsilon range + // Check square is more or less correct + const guessSquared = guess.mul(guess).div(ONE); + + if ( + !( + guessSquared.lte(input.add(mulUp(guess, tolerance))) && + guessSquared.gte(input.sub(mulUp(guess, tolerance))) + ) + ) + throw new Error('Gyro2Pool: _sqrt failed'); + + return guess; +} + +function _makeInitialGuess(input: BigNumber) { + if (input.gte(ONE)) { + return BigNumber.from(2) + .pow(_intLog2Halved(input.div(ONE))) + .mul(ONE); + } else { + if (input.lte('10')) { + return SQRT_1E_NEG_17; + } + if (input.lte('100')) { + return BigNumber.from('10000000000'); + } + if (input.lte('1000')) { + return SQRT_1E_NEG_15; + } + if (input.lte('10000')) { + return BigNumber.from('100000000000'); + } + if (input.lte('100000')) { + return SQRT_1E_NEG_13; + } + if (input.lte('1000000')) { + return BigNumber.from('1000000000000'); + } + if (input.lte('10000000')) { + return SQRT_1E_NEG_11; + } + if (input.lte('100000000')) { + return BigNumber.from('10000000000000'); + } + if (input.lte('1000000000')) { + return SQRT_1E_NEG_9; + } + if (input.lte('10000000000')) { + return BigNumber.from('100000000000000'); + } + if (input.lte('100000000000')) { + return SQRT_1E_NEG_7; + } + if (input.lte('1000000000000')) { + return BigNumber.from('1000000000000000'); + } + if (input.lte('10000000000000')) { + return SQRT_1E_NEG_5; + } + if (input.lte('100000000000000')) { + return BigNumber.from('10000000000000000'); + } + if (input.lte('1000000000000000')) { + return SQRT_1E_NEG_3; + } + if (input.lte('10000000000000000')) { + return BigNumber.from('100000000000000000'); + } + if (input.lte('100000000000000000')) { + return SQRT_1E_NEG_1; + } + return input; + } +} + +function _intLog2Halved(x: BigNumber) { + let n = 0; + + for (let i = 128; i >= 2; i = i / 2) { + const factor = BigNumber.from(2).pow(i); + if (x.gte(factor)) { + x = x.div(factor); + n += i / 2; + } + } + + return n; +} + +export function mulUp(a: BigNumber, b: BigNumber) { + const product = a.mul(b); + return product.sub(1).div(ONE).add(1); +} + +export function divUp(a: BigNumber, b: BigNumber) { + const aInflated = a.mul(ONE); + return aInflated.sub(1).div(b).add(1); +} + +export function mulDown(a: BigNumber, b: BigNumber) { + const product = a.mul(b); + return product.div(ONE); +} + +export function divDown(a: BigNumber, b: BigNumber) { + const aInflated = a.mul(ONE); + return aInflated.div(b); +} + +export function _normalizeBalances( + balances: BigNumber[], + decimalsIn: number, + decimalsOut: number +): BigNumber[] { + const scalingFactors = [ + parseFixed('1', decimalsIn), + parseFixed('1', decimalsOut), + ]; + + return balances.map((bal, index) => + bal.mul(ONE).div(scalingFactors[index]) + ); +} + +///////// +/// Fee calculations +///////// + +export function _reduceFee(amountIn: BigNumber, swapFee: BigNumber): BigNumber { + const feeAmount = amountIn.mul(swapFee).div(ONE); + return amountIn.sub(feeAmount); +} + +export function _addFee(amountIn: BigNumber, swapFee: BigNumber): BigNumber { + return amountIn.mul(ONE).div(ONE.sub(swapFee)); +} diff --git a/src/pools/gyro3Pool/constants.ts b/src/pools/gyro3Pool/constants.ts new file mode 100644 index 00000000..3e65a819 --- /dev/null +++ b/src/pools/gyro3Pool/constants.ts @@ -0,0 +1,31 @@ +import { BigNumber, parseFixed } from '@ethersproject/bignumber'; + +// Swap limits: amounts swapped may not be larger than this percentage of total balance. + +export const _MAX_IN_RATIO: BigNumber = parseFixed('0.3', 18); +export const _MAX_OUT_RATIO: BigNumber = parseFixed('0.3', 18); + +// SQRT constants + +export const SQRT_1E_NEG_1 = BigNumber.from('316227766016837933'); +export const SQRT_1E_NEG_3 = BigNumber.from('31622776601683793'); +export const SQRT_1E_NEG_5 = BigNumber.from('3162277660168379'); +export const SQRT_1E_NEG_7 = BigNumber.from('316227766016837'); +export const SQRT_1E_NEG_9 = BigNumber.from('31622776601683'); +export const SQRT_1E_NEG_11 = BigNumber.from('3162277660168'); +export const SQRT_1E_NEG_13 = BigNumber.from('316227766016'); +export const SQRT_1E_NEG_15 = BigNumber.from('31622776601'); +export const SQRT_1E_NEG_17 = BigNumber.from('3162277660'); + +// POW3 constant +// Threshold of x where the normal method of computing x^3 would overflow and we need a workaround. +// Equal to 4.87e13 scaled; 4.87e13 is the point x where x**3 * 10**36 = (x**2 native) * (x native) ~ 2**256 +export const _SAFE_LARGE_POW3_THRESHOLD = BigNumber.from(10).pow(29).mul(487); +export const MIDDECIMAL = BigNumber.from(10).pow(9); // splits the fixed point decimals into two equal parts. + +// Stopping criterion for the Newton iteration that computes the invariant: +// - Stop if the step width doesn't shrink anymore by at least a factor _INVARIANT_SHRINKING_FACTOR_PER_STEP. +// - ... but in any case, make at least _INVARIANT_MIN_ITERATIONS iterations. This is useful to compensate for a +// less-than-ideal starting point, which is important when alpha is small. +export const _INVARIANT_SHRINKING_FACTOR_PER_STEP = 8; +export const _INVARIANT_MIN_ITERATIONS = 5; diff --git a/src/pools/gyro3Pool/gyro3Math.ts b/src/pools/gyro3Pool/gyro3Math.ts index 2737f0c8..c7b288b1 100644 --- a/src/pools/gyro3Pool/gyro3Math.ts +++ b/src/pools/gyro3Pool/gyro3Math.ts @@ -1,54 +1,25 @@ -import { BigNumber, parseFixed } from '@ethersproject/bignumber'; +import { BigNumber } from '@ethersproject/bignumber'; import { WeiPerEther as ONE } from '@ethersproject/constants'; -import bn from 'bignumber.js'; - -// Swap limits: amounts swapped may not be larger than this percentage of total balance. - -const _MAX_IN_RATIO: BigNumber = parseFixed('0.3', 18); -const _MAX_OUT_RATIO: BigNumber = parseFixed('0.3', 18); - -// Helpers -export function _squareRoot(value: BigNumber): BigNumber { - return BigNumber.from( - new bn(value.mul(ONE).toString()).sqrt().toFixed().split('.')[0] - ); -} - -export function _normalizeBalances( - balances: BigNumber[], - decimals: number[] -): BigNumber[] { - const scalingFactors = decimals.map((d) => parseFixed('1', d)); - - return balances.map((bal, index) => - bal.mul(ONE).div(scalingFactors[index]) - ); -} - -///////// -/// Fee calculations -///////// - -export function _reduceFee(amountIn: BigNumber, swapFee: BigNumber): BigNumber { - const feeAmount = amountIn.mul(swapFee).div(ONE); - return amountIn.sub(feeAmount); -} - -export function _addFee(amountIn: BigNumber, swapFee: BigNumber): BigNumber { - return amountIn.mul(ONE).div(ONE.sub(swapFee)); -} +import { + _MAX_IN_RATIO, + _MAX_OUT_RATIO, + _SAFE_LARGE_POW3_THRESHOLD, + _INVARIANT_MIN_ITERATIONS, + _INVARIANT_SHRINKING_FACTOR_PER_STEP, +} from './constants'; +import { + mulUp, + divUp, + mulDown, + divDown, + newtonSqrt, + _safeLargePow3ADown, +} from './helpers'; ///////// /// Invariant Calculation ///////// -// Stopping criterion for the Newton iteration that computes the invariant: -// - Stop if the step width doesn't shrink anymore by at least a factor _INVARIANT_SHRINKING_FACTOR_PER_STEP. -// - ... but in any case, make at least _INVARIANT_MIN_ITERATIONS iterations. This is useful to compensate for a -// less-than-ideal starting point, which is important when alpha is small. -const _INVARIANT_SHRINKING_FACTOR_PER_STEP = 10; -const _INVARIANT_MIN_ITERATIONS = 2; - // Invariant is used to collect protocol swap fees by comparing its value between two times. // So we can round always to the same direction. It is also used to initiate the BPT amount // and, because there is a minimum BPT, we round down the invariant. @@ -67,7 +38,7 @@ export function _calculateInvariant( // taking mb = -b and mc = -c /**********************************************************************************************/ const [a, mb, mc, md] = _calculateCubicTerms(balances, root3Alpha); - return _calculateCubic(a, mb, mc, md); + return _calculateCubic(a, mb, mc, md, root3Alpha); } /** @dev Prepares quadratic terms for input to _calculateCubic @@ -79,18 +50,16 @@ export function _calculateCubicTerms( balances: BigNumber[], root3Alpha: BigNumber ): [BigNumber, BigNumber, BigNumber, BigNumber] { - const alpha23: BigNumber = root3Alpha.mul(root3Alpha).div(ONE); // alpha to the power of (2/3) - const alpha = alpha23.mul(root3Alpha).div(ONE); + const alpha23: BigNumber = mulDown(root3Alpha, root3Alpha); // alpha to the power of (2/3) + const alpha = mulDown(alpha23, root3Alpha); const a = ONE.sub(alpha); const bterm = balances[0].add(balances[1]).add(balances[2]); - const mb = bterm.mul(alpha23).div(ONE); - const cterm = balances[0] - .mul(balances[1]) - .div(ONE) - .add(balances[1].mul(balances[2]).div(ONE)) - .add(balances[2].mul(balances[0]).div(ONE)); - const mc = cterm.mul(root3Alpha).div(ONE); - const md = balances[0].mul(balances[1]).div(ONE).mul(balances[2]).div(ONE); + const mb = mulDown(mulDown(bterm, root3Alpha), root3Alpha); + const cterm = mulDown(balances[0], balances[1]) + .add(mulDown(balances[1], balances[2])) + .add(mulDown(balances[2], balances[0])); + const mc = mulDown(cterm, root3Alpha); + const md = mulDown(mulDown(balances[0], balances[1]), balances[2]); return [a, mb, mc, md]; } @@ -101,21 +70,11 @@ export function _calculateCubic( a: BigNumber, mb: BigNumber, mc: BigNumber, - md: BigNumber + md: BigNumber, + root3Alpha: BigNumber ): BigNumber { - let rootEst: BigNumber; - if (md.isZero()) { - // lower-order special case - const radic = mb - .mul(mb) - .div(ONE) - .add(a.mul(mc).div(ONE).mul(BigNumber.from(4))); - rootEst = mb.add(_squareRoot(radic)).div(BigNumber.from(2).mul(a)); - } else { - rootEst = _calculateCubicStartingPoint(a, mb, mc); - rootEst = _runNewtonIteration(a, mb, mc, md, rootEst); - } - + let rootEst = _calculateCubicStartingPoint(a, mb, mc); + rootEst = _runNewtonIteration(a, mb, mc, md, root3Alpha, rootEst); return rootEst; } @@ -126,17 +85,15 @@ export function _calculateCubicStartingPoint( mb: BigNumber, mc: BigNumber ): BigNumber { - const radic: BigNumber = mb - .mul(mb) - .div(ONE) - .add(a.mul(mc).div(ONE).mul(BigNumber.from(3))); - const lmin = mb - .mul(ONE) - .div(a.mul(BigNumber.from(3))) - .add(_squareRoot(radic).mul(ONE).div(BigNumber.from(3).mul(a))); - // The factor 3/2 is a magic number found experimentally for our invariant. All factors > 1 are safe. - const l0 = lmin.mul(BigNumber.from(3)).div(BigNumber.from(2)); - + const radic = mulUp(mb, mb).add(mulUp(mulUp(a, mc), ONE.mul(3))); + const lmin = divUp(mb, a.mul(3)).add( + divUp(newtonSqrt(radic, BigNumber.from(5)), a.mul(3)) + ); + // This formula has been found experimentally. It is exact for alpha -> 1, where the factor is 1.5. All + // factors > 1 are safe. For small alpha values, it is more efficient to fallback to a larger factor. + const alpha = ONE.sub(a); // We know that a is in [0, 1]. + const factor = alpha.gte(ONE.div(2)) ? ONE.mul(3).div(2) : ONE.mul(2); + const l0 = mulUp(lmin, factor); return l0; } @@ -150,19 +107,28 @@ export function _runNewtonIteration( mb: BigNumber, mc: BigNumber, md: BigNumber, + root3Alpha: BigNumber, rootEst: BigNumber ): BigNumber { let deltaAbsPrev = BigNumber.from(0); for (let iteration = 0; iteration < 255; ++iteration) { // The delta to the next step can be positive or negative, so we represent a positive and a negative part // separately. The signed delta is delta_plus - delta_minus, but we only ever consider its absolute value. - const [deltaAbs, deltaIsPos] = _calcNewtonDelta(a, mb, mc, md, rootEst); + const [deltaAbs, deltaIsPos] = _calcNewtonDelta( + a, + mb, + mc, + md, + root3Alpha, + rootEst + ); + // ^ Note: If we ever set _INVARIANT_MIN_ITERATIONS=0, the following should include `iteration >= 1`. if ( - deltaAbs.isZero() || + deltaAbs.lte(1) || (iteration >= _INVARIANT_MIN_ITERATIONS && deltaIsPos) ) - // Iteration literally stopped or numerical error dominates + // This should mathematically never happen. Thus, the numerical error dominates at this point. return rootEst; if ( iteration >= _INVARIANT_MIN_ITERATIONS && @@ -172,9 +138,8 @@ export function _runNewtonIteration( ) ) ) { - // stalled - // Move one more step to the left to ensure we're underestimating, rather than overestimating, L - return rootEst.sub(deltaAbs); + // The iteration has stalled and isn't making significant progress anymore. + return rootEst; } deltaAbsPrev = deltaAbs; if (deltaIsPos) rootEst = rootEst.add(deltaAbs); @@ -185,33 +150,34 @@ export function _runNewtonIteration( 'Gyro3Pool: Newton Method did not converge on required invariant' ); } -let first = 0; + // -f(l)/f'(l), represented as an absolute value and a sign. Require that l is sufficiently large so that f is strictly increasing. export function _calcNewtonDelta( a: BigNumber, mb: BigNumber, mc: BigNumber, md: BigNumber, + root3Alpha: BigNumber, rootEst: BigNumber ): [BigNumber, boolean] { - const dfRootEst = BigNumber.from(3) - .mul(a) - .mul(rootEst) - .div(ONE) - .sub(BigNumber.from(2).mul(mb)) - .mul(rootEst) - .div(ONE) - .sub(mc); // Does not underflow since rootEst >> 0 by assumption. - // We know that a rootEst^2 / dfRootEst ~ 1. (this is pretty exact actually, see the Mathematica notebook). We use this - // multiplication order to prevent overflows that can otherwise occur when computing l^3 for very large - // reserves. - - let deltaMinus = a.mul(rootEst).div(ONE).mul(rootEst).div(ONE); - deltaMinus = deltaMinus.mul(ONE).div(dfRootEst).mul(rootEst).div(ONE); - // use multiple statements to prevent 'stack too deep'. The order of operations is chosen to prevent overflows - // for very large numbers. - let deltaPlus = mb.mul(rootEst).div(ONE).add(mc).mul(ONE).div(dfRootEst); - deltaPlus = deltaPlus.mul(rootEst).div(ONE).add(md.mul(ONE).div(dfRootEst)); + // The following is equal to dfRootEst^3 * a but with an order of operations optimized for precision. + // Subtraction does not underflow since rootEst is chosen so that it's always above the (only) local minimum. + let dfRootEst = BigNumber.from(0); + + const rootEst2 = mulDown(rootEst, rootEst); + dfRootEst = rootEst2.mul(3); + dfRootEst = dfRootEst.sub( + mulDown(mulDown(mulDown(dfRootEst, root3Alpha), root3Alpha), root3Alpha) + ); + dfRootEst = dfRootEst.sub(mulDown(rootEst, mb).mul(2)).sub(mc); + + const deltaMinus = _safeLargePow3ADown(rootEst, root3Alpha, dfRootEst); + + // NB: We could order the operations here in much the same way we did above to reduce errors. But tests show + // that this has no significant effect, and it would lead to more complex code. + let deltaPlus = mulDown(mulDown(rootEst, rootEst), mb); + deltaPlus = divDown(deltaPlus.add(mulDown(rootEst, mc)), dfRootEst); + deltaPlus = deltaPlus.add(divDown(md, dfRootEst)); const deltaIsPos = deltaPlus.gte(deltaMinus); const deltaAbs = deltaIsPos @@ -233,7 +199,7 @@ export function _calcOutGivenIn( balanceIn: BigNumber, balanceOut: BigNumber, amountIn: BigNumber, - virtualOffsetInOut: BigNumber + virtualOffset: BigNumber ): BigNumber { /********************************************************************************************** // Described for X = `in' asset and Z = `out' asset, but equivalent for the other case // @@ -248,19 +214,21 @@ export function _calcOutGivenIn( // Note that -dz > 0 is what the trader receives. // // We exploit the fact that this formula is symmetric up to virtualParam{X,Y,Z}. // **********************************************************************************************/ - if (amountIn.gt(balanceIn.mul(_MAX_IN_RATIO).div(ONE))) + if (amountIn.gt(mulDown(balanceIn, _MAX_IN_RATIO))) throw new Error('Swap Amount In Too Large'); - const virtIn = balanceIn.add(virtualOffsetInOut); - const virtOut = balanceOut.add(virtualOffsetInOut); - const denominator = virtIn.add(amountIn); - const subtrahend = virtIn.mul(virtOut).div(denominator); - const amountOut = virtOut.sub(subtrahend); + // The factors in total lead to a multiplicative "safety margin" between the employed virtual offsets + // very slightly larger than 3e-18, compensating for the maximum multiplicative error in the invariant + // computation. + + const virtInOver = balanceIn.add(mulUp(virtualOffset, ONE.add(2))); + const virtOutUnder = balanceOut.add(mulDown(virtualOffset, ONE.sub(1))); + const amountOut = virtOutUnder.mul(amountIn).div(virtInOver.add(amountIn)); // Note that this in particular reverts if amountOut > balanceOut, i.e., if the out-amount would be more than // the balance. - if (amountOut.gt(balanceOut.mul(_MAX_OUT_RATIO).div(ONE))) + if (amountOut.gt(mulDown(balanceOut, _MAX_OUT_RATIO))) throw new Error('Resultant Swap Amount Out Too Large'); return amountOut; @@ -273,7 +241,7 @@ export function _calcInGivenOut( balanceIn: BigNumber, balanceOut: BigNumber, amountOut: BigNumber, - virtualOffsetInOut: BigNumber + virtualOffset: BigNumber ): BigNumber { /********************************************************************************************** // Described for X = `in' asset and Z = `out' asset, but equivalent for the other case // @@ -281,27 +249,34 @@ export function _calcInGivenOut( // dZ = incrZ = amountOut < 0 // // x = balanceIn x' = x + virtualOffset // // z = balanceOut z' = z + virtualOffset // - // L = inv.Liq / x' * z' \ // - // dX = | -------------------------- | - x' // - // x' = virtIn \ ( z' + dZ) / // + // L = inv.Liq / x' * z' \ x' * dZ // + // dX = | -------------------------- | - x' = --------------- // + // x' = virtIn \ ( z' + dZ) / z' - dZ // // z' = virtOut // // Note that dz < 0 < dx. // - // We exploit the fact that this formula is symmetric up to virtualParam{X,Y,Z}. // + // We exploit the fact that this formula is symmetric and does not depend on which asset is // + // which. + // We assume that the virtualOffset carries a relative +/- 3e-18 error due to the invariant // + // calculation add an appropriate safety margin. // **********************************************************************************************/ // Note that this in particular reverts if amountOut > balanceOut, i.e., if the trader tries to take more out of // the pool than is in it. - if (amountOut.gt(balanceOut.mul(_MAX_OUT_RATIO).div(ONE))) + if (amountOut.gt(mulDown(balanceOut, _MAX_OUT_RATIO))) throw new Error('Swap Amount Out Too Large'); - const virtIn = balanceIn.add(virtualOffsetInOut); - const virtOut = balanceOut.add(virtualOffsetInOut); - const denominator = virtOut.sub(amountOut); - const minuend = virtIn.mul(virtOut).div(denominator); + // The factors in total lead to a multiplicative "safety margin" between the employed virtual offsets + // very slightly larger than 3e-18, compensating for the maximum multiplicative error in the invariant + // computation. + const virtInOver = balanceIn.add(mulUp(virtualOffset, ONE.add(2))); + const virtOutUnder = balanceOut.add(mulDown(virtualOffset, ONE.sub(1))); - const amountIn = minuend.sub(virtIn); + const amountIn = divUp( + mulUp(virtInOver, amountOut), + virtOutUnder.sub(amountOut) + ); - if (amountIn.gt(balanceIn.mul(_MAX_IN_RATIO).div(ONE))) + if (amountIn.gt(mulDown(balanceIn, _MAX_IN_RATIO))) throw new Error('Resultant Swap Amount In Too Large'); return amountIn; @@ -333,12 +308,12 @@ export function _calculateNewSpotPrice( const afterFeeMultiplier = ONE.sub(swapFee); // 1 - s const virtIn = balances[0].add(virtualOffsetInOut); // x + virtualOffsetInOut = x' - const numerator = virtIn.add(afterFeeMultiplier.mul(inAmount).div(ONE)); // x' + (1 - s) * dx + const numerator = virtIn.add(mulDown(afterFeeMultiplier, inAmount)); // x' + (1 - s) * dx const virtOut = balances[1].add(virtualOffsetInOut); // z + virtualOffsetInOut = y' - const denominator = afterFeeMultiplier.mul(virtOut.sub(outAmount)).div(ONE); // (1 - s) * (z' + dz) + const denominator = mulDown(afterFeeMultiplier, virtOut.sub(outAmount)); // (1 - s) * (z' + dz) - const newSpotPrice = numerator.mul(ONE).div(denominator); + const newSpotPrice = divDown(numerator, denominator); return newSpotPrice; } @@ -369,7 +344,7 @@ export function _derivativeSpotPriceAfterSwapExactTokenInForTokenOut( const virtOut = balances[1].add(virtualOffsetInOut); // z' = z + virtualOffsetInOut const denominator = virtOut.sub(outAmount); // z' + dz - const derivative = TWO.mul(ONE).div(denominator); + const derivative = divDown(TWO, denominator); return derivative; } @@ -398,15 +373,13 @@ export function _derivativeSpotPriceAfterSwapTokenInForExactTokenOut( const TWO = BigNumber.from(2).mul(ONE); const afterFeeMultiplier = ONE.sub(swapFee); // 1 - s const virtIn = balances[0].add(virtualOffsetInOut); // x + virtualOffsetInOut = x' - const numerator = virtIn.add(afterFeeMultiplier.mul(inAmount).div(ONE)); // x' + (1 - s) * dx + const numerator = virtIn.add(mulDown(afterFeeMultiplier, inAmount)); // x' + (1 - s) * dx const virtOut = balances[1].add(virtualOffsetInOut); // z + virtualOffsetInOut = z' - const denominator = virtOut - .sub(outAmount) - .mul(virtOut.sub(outAmount)) - .div(ONE); // (z' + dz)^2 - const factor = TWO.mul(ONE).div(afterFeeMultiplier); // 2 / (1 - s) + const denominator = mulDown(virtOut.sub(outAmount), virtOut.sub(outAmount)); + // (z' + dz)^2 + const factor = divDown(TWO, afterFeeMultiplier); // 2 / (1 - s) - const derivative = factor.mul(numerator.mul(ONE).div(denominator)).div(ONE); + const derivative = mulDown(factor, divDown(numerator, denominator)); return derivative; } @@ -430,10 +403,8 @@ export function _getNormalizedLiquidity( **********************************************************************************************/ const virtIn = balances[0].add(virtualParamIn); - const afterFeeMultiplier = ONE.sub(swapFee); - - const normalizedLiquidity = virtIn.mul(ONE).div(afterFeeMultiplier); + const normalizedLiquidity = divDown(virtIn, afterFeeMultiplier); return normalizedLiquidity; } diff --git a/src/pools/gyro3Pool/gyro3Pool.ts b/src/pools/gyro3Pool/gyro3Pool.ts index 41f702d0..af28828f 100644 --- a/src/pools/gyro3Pool/gyro3Pool.ts +++ b/src/pools/gyro3Pool/gyro3Pool.ts @@ -10,22 +10,26 @@ import { SubgraphToken, SwapTypes, SubgraphPoolBase, - Gyro3PriceBounds, } from '../../types'; import { isSameAddress } from '../../utils'; import { - _normalizeBalances, _calculateInvariant, _calcOutGivenIn, _calcInGivenOut, - _reduceFee, - _addFee, _calculateNewSpotPrice, _derivativeSpotPriceAfterSwapExactTokenInForTokenOut, _derivativeSpotPriceAfterSwapTokenInForExactTokenOut, _getNormalizedLiquidity, } from './gyro3Math'; +import { + _normalizeBalances, + _reduceFee, + _addFee, + mulDown, + divDown, +} from './helpers'; + export type Gyro3PoolPairData = PoolPairBase & { balanceTertiary: BigNumber; // Balance of the unchanged asset decimalsTertiary: number; // Decimals of the unchanged asset @@ -59,12 +63,13 @@ export class Gyro3Pool implements PoolBase { } static fromPool(pool: SubgraphPoolBase): Gyro3Pool { - if (!pool.gyro3PriceBounds) - throw new Error('Pool missing gyro3PriceBounds'); + if (!pool.root3Alpha) throw new Error('Pool missing root3Alpha'); - const { alpha } = pool.gyro3PriceBounds; - if (!(Number(alpha) > 0 && Number(alpha) < 1)) - throw new Error('Invalid alpha price bound in gyro3PriceBounds'); + if ( + parseFixed(pool.root3Alpha, 18).lte(0) || + parseFixed(pool.root3Alpha, 18).gte(ONE) + ) + throw new Error('Invalid root3Alpha parameter'); if (pool.tokens.length !== 3) throw new Error('Gyro3Pool must contain three tokens only'); @@ -76,7 +81,7 @@ export class Gyro3Pool implements PoolBase { pool.totalShares, pool.tokens as Gyro3PoolToken[], pool.tokensList, - pool.gyro3PriceBounds as Gyro3PriceBounds + pool.root3Alpha ); } @@ -87,7 +92,7 @@ export class Gyro3Pool implements PoolBase { totalShares: string, tokens: Gyro3PoolToken[], tokensList: string[], - priceBounds: Gyro3PriceBounds + root3Alpha: string ) { this.id = id; this.address = address; @@ -95,13 +100,7 @@ export class Gyro3Pool implements PoolBase { this.totalShares = parseFixed(totalShares, 18); this.tokens = tokens; this.tokensList = tokensList; - - const root3Alpha = parseFixed( - Math.pow(Number(priceBounds.alpha), 1 / 3).toString(), - 18 - ); - - this.root3Alpha = root3Alpha; + this.root3Alpha = parseFixed(root3Alpha, 18); } parsePoolPairData(tokenIn: string, tokenOut: string): Gyro3PoolPairData { @@ -169,7 +168,7 @@ export class Gyro3Pool implements PoolBase { this.root3Alpha ); - const virtualOffsetInOut = invariant.mul(this.root3Alpha).div(ONE); + const virtualOffsetInOut = mulDown(invariant, this.root3Alpha); const normalisedLiquidity = _getNormalizedLiquidity( normalizedBalances, @@ -187,14 +186,14 @@ export class Gyro3Pool implements PoolBase { if (swapType === SwapTypes.SwapExactIn) { return bnum( formatFixed( - poolPairData.balanceIn.mul(this.MAX_IN_RATIO).div(ONE), + mulDown(poolPairData.balanceIn, this.MAX_IN_RATIO), poolPairData.decimalsIn ) ); } else { return bnum( formatFixed( - poolPairData.balanceOut.mul(this.MAX_OUT_RATIO).div(ONE), + mulDown(poolPairData.balanceOut, this.MAX_OUT_RATIO), poolPairData.decimalsOut ) ); @@ -235,8 +234,7 @@ export class Gyro3Pool implements PoolBase { this.root3Alpha ); - const virtualOffsetInOut = invariant.mul(this.root3Alpha).div(ONE); - + const virtualOffsetInOut = mulDown(invariant, this.root3Alpha); const inAmount = parseFixed(amount.toString(), 18); const inAmountLessFee = _reduceFee(inAmount, poolPairData.swapFee); @@ -246,7 +244,6 @@ export class Gyro3Pool implements PoolBase { inAmountLessFee, virtualOffsetInOut ); - return bnum(formatFixed(outAmount, 18)); } @@ -272,7 +269,7 @@ export class Gyro3Pool implements PoolBase { this.root3Alpha ); - const virtualOffsetInOut = invariant.mul(this.root3Alpha).div(ONE); + const virtualOffsetInOut = mulDown(invariant, this.root3Alpha); const inAmountLessFee = _calcInGivenOut( normalizedBalances[0], @@ -306,7 +303,7 @@ export class Gyro3Pool implements PoolBase { this.root3Alpha ); - const virtualOffsetInOut = invariant.mul(this.root3Alpha).div(ONE); + const virtualOffsetInOut = mulDown(invariant, this.root3Alpha); const inAmount = parseFixed(amount.toString(), 18); const inAmountLessFee = _reduceFee(inAmount, poolPairData.swapFee); @@ -350,7 +347,7 @@ export class Gyro3Pool implements PoolBase { this.root3Alpha ); - const virtualOffsetInOut = invariant.mul(this.root3Alpha).div(ONE); + const virtualOffsetInOut = mulDown(invariant, this.root3Alpha); const inAmountLessFee = _calcInGivenOut( normalizedBalances[0], @@ -392,7 +389,7 @@ export class Gyro3Pool implements PoolBase { this.root3Alpha ); - const virtualOffsetInOut = invariant.mul(this.root3Alpha).div(ONE); + const virtualOffsetInOut = mulDown(invariant, this.root3Alpha); const inAmount = parseFixed(amount.toString(), 18); const inAmountLessFee = _reduceFee(inAmount, poolPairData.swapFee); @@ -434,7 +431,7 @@ export class Gyro3Pool implements PoolBase { this.root3Alpha ); - const virtualOffsetInOut = invariant.mul(this.root3Alpha).div(ONE); + const virtualOffsetInOut = mulDown(invariant, this.root3Alpha); const inAmountLessFee = _calcInGivenOut( normalizedBalances[0], diff --git a/src/pools/gyro3Pool/helpers.ts b/src/pools/gyro3Pool/helpers.ts new file mode 100644 index 00000000..9fd1be6d --- /dev/null +++ b/src/pools/gyro3Pool/helpers.ts @@ -0,0 +1,220 @@ +import { BigNumber, parseFixed } from '@ethersproject/bignumber'; +import bn from 'bignumber.js'; +import { WeiPerEther as ONE } from '@ethersproject/constants'; +import { + _MAX_IN_RATIO, + _MAX_OUT_RATIO, + SQRT_1E_NEG_1, + SQRT_1E_NEG_3, + SQRT_1E_NEG_5, + SQRT_1E_NEG_7, + SQRT_1E_NEG_9, + SQRT_1E_NEG_11, + SQRT_1E_NEG_13, + SQRT_1E_NEG_15, + SQRT_1E_NEG_17, + _SAFE_LARGE_POW3_THRESHOLD, + MIDDECIMAL, +} from './constants'; + +// Helpers +export function _squareRoot(value: BigNumber): BigNumber { + return BigNumber.from( + new bn(value.mul(ONE).toString()).sqrt().toFixed().split('.')[0] + ); +} + +export function mulUp(a: BigNumber, b: BigNumber) { + const product = a.mul(b); + return product.sub(1).div(ONE).add(1); +} + +export function divUp(a: BigNumber, b: BigNumber) { + const aInflated = a.mul(ONE); + return aInflated.sub(1).div(b).add(1); +} + +export function mulDown(a: BigNumber, b: BigNumber) { + const product = a.mul(b); + return product.div(ONE); +} + +export function divDown(a: BigNumber, b: BigNumber) { + const aInflated = a.mul(ONE); + return aInflated.div(b); +} + +export function newtonSqrt(input: BigNumber, tolerance: BigNumber) { + if (input.isZero()) { + return BigNumber.from(0); + } + let guess = _makeInitialGuess(input); + + // 7 iterations + for (let i of new Array(7).fill(0)) { + guess = guess.add(input.mul(ONE).div(guess)).div(2); + } + + // Check in some epsilon range + // Check square is more or less correct + const guessSquared = guess.mul(guess).div(ONE); + + if ( + !( + guessSquared.lte(input.add(mulUp(guess, tolerance))) && + guessSquared.gte(input.sub(mulUp(guess, tolerance))) + ) + ) + throw new Error('Gyro3Pool: newtonSqrt failed'); + + return guess; +} + +function _makeInitialGuess(input: BigNumber) { + if (input.gte(ONE)) { + return BigNumber.from(2) + .pow(_intLog2Halved(input.div(ONE))) + .mul(ONE); + } else { + if (input.lte('10')) { + return SQRT_1E_NEG_17; + } + if (input.lte('100')) { + return BigNumber.from('10000000000'); + } + if (input.lte('1000')) { + return SQRT_1E_NEG_15; + } + if (input.lte('10000')) { + return BigNumber.from('100000000000'); + } + if (input.lte('100000')) { + return SQRT_1E_NEG_13; + } + if (input.lte('1000000')) { + return BigNumber.from('1000000000000'); + } + if (input.lte('10000000')) { + return SQRT_1E_NEG_11; + } + if (input.lte('100000000')) { + return BigNumber.from('10000000000000'); + } + if (input.lte('1000000000')) { + return SQRT_1E_NEG_9; + } + if (input.lte('10000000000')) { + return BigNumber.from('100000000000000'); + } + if (input.lte('100000000000')) { + return SQRT_1E_NEG_7; + } + if (input.lte('1000000000000')) { + return BigNumber.from('1000000000000000'); + } + if (input.lte('10000000000000')) { + return SQRT_1E_NEG_5; + } + if (input.lte('100000000000000')) { + return BigNumber.from('10000000000000000'); + } + if (input.lte('1000000000000000')) { + return SQRT_1E_NEG_3; + } + if (input.lte('10000000000000000')) { + return BigNumber.from('100000000000000000'); + } + if (input.lte('100000000000000000')) { + return SQRT_1E_NEG_1; + } + return input; + } +} + +function _intLog2Halved(x: BigNumber) { + let n = 0; + + for (let i = 128; i >= 2; i = i / 2) { + const factor = BigNumber.from(2).pow(i); + if (x.gte(factor)) { + x = x.div(factor); + n += i / 2; + } + } + + return n; +} + +export function _safeLargePow3ADown( + l: BigNumber, + root3Alpha: BigNumber, + d: BigNumber +) { + let ret = BigNumber.from(0); + if (l.lte(_SAFE_LARGE_POW3_THRESHOLD)) { + // Simple case where there is no overflow + ret = l.mul(l).div(ONE).mul(l).div(ONE); + ret = ret.sub( + ret + .mul(root3Alpha) + .div(ONE) + .mul(root3Alpha) + .div(ONE) + .mul(root3Alpha) + .div(ONE) + ); + ret = ret.mul(ONE).div(d); + } else { + ret = l.mul(l).div(ONE); + + // Compute l^2 * l * (1 - root3Alpha^3) + // The following products split up the factors into different groups of decimal places to reduce temorary + // blowup and prevent overflow. + // No precision is lost. + ret = ret.mul(l.div(ONE)).add(ret.mul(l.mod(ONE)).div(ONE)); + + let x = ret; + + for (let i = 0; i < 3; i++) { + x = x + .mul(root3Alpha.div(MIDDECIMAL)) + .div(MIDDECIMAL) + .add(x.mul(root3Alpha.mod(MIDDECIMAL))); + } + ret = ret.sub(x); + + // We perform half-precision division to reduce blowup. + // In contrast to the above multiplications, this loses precision if d is small. However, tests show that, + // for the l and d values considered here, the precision lost would be below the precision of the fixed + // point type itself, so nothing is actually lost. + ret = ret.mul(MIDDECIMAL).div(d.div(MIDDECIMAL)); + } + return ret; +} + +///////// +/// Fee calculations +///////// + +export function _reduceFee(amountIn: BigNumber, swapFee: BigNumber): BigNumber { + const feeAmount = amountIn.mul(swapFee).div(ONE); + return amountIn.sub(feeAmount); +} + +export function _addFee(amountIn: BigNumber, swapFee: BigNumber): BigNumber { + return amountIn.mul(ONE).div(ONE.sub(swapFee)); +} + +//////// +/// Normalize balances +//////// +export function _normalizeBalances( + balances: BigNumber[], + decimals: number[] +): BigNumber[] { + const scalingFactors = decimals.map((d) => parseFixed('1', d)); + + return balances.map((bal, index) => + bal.mul(ONE).div(scalingFactors[index]) + ); +} diff --git a/src/pools/index.ts b/src/pools/index.ts index 10e7cb51..f6fc832f 100644 --- a/src/pools/index.ts +++ b/src/pools/index.ts @@ -4,6 +4,8 @@ import { MetaStablePool } from './metaStablePool/metaStablePool'; import { LinearPool } from './linearPool/linearPool'; import { ElementPool } from './elementPool/elementPool'; import { PhantomStablePool } from './phantomStablePool/phantomStablePool'; +import { Gyro2Pool } from './gyro2Pool/gyro2Pool'; +import { Gyro3Pool } from './gyro3Pool/gyro3Pool'; import { BigNumber as OldBigNumber, INFINITY, @@ -28,6 +30,8 @@ export function parseNewPool( | LinearPool | MetaStablePool | PhantomStablePool + | Gyro2Pool + | Gyro3Pool | undefined { // We're not interested in any pools which don't allow swapping if (!pool.swapEnabled) return undefined; @@ -38,7 +42,9 @@ export function parseNewPool( | ElementPool | LinearPool | MetaStablePool - | PhantomStablePool; + | PhantomStablePool + | Gyro2Pool + | Gyro3Pool; try { if (pool.poolType === 'Weighted' || pool.poolType === 'Investment') { @@ -56,6 +62,8 @@ export function parseNewPool( newPool = LinearPool.fromPool(pool); else if (pool.poolType === 'StablePhantom') newPool = PhantomStablePool.fromPool(pool); + else if (pool.poolType === 'Gyro2') newPool = Gyro2Pool.fromPool(pool); + else if (pool.poolType === 'Gyro3') newPool = Gyro3Pool.fromPool(pool); else { console.error( `Unknown pool type or type field missing: ${pool.poolType} ${pool.id}` diff --git a/src/router/helpersClass.ts b/src/router/helpersClass.ts index c2910083..1bb2845c 100644 --- a/src/router/helpersClass.ts +++ b/src/router/helpersClass.ts @@ -326,43 +326,32 @@ export function EVMgetOutputAmountSwap( return INFINITY; } if (swapType === SwapTypes.SwapExactIn) { - // TODO we will be able to remove pooltype check once Element EVM maths is available - if ( - pool.poolType === PoolTypes.Weighted || - pool.poolType === PoolTypes.Stable || - pool.poolType === PoolTypes.MetaStable || - pool.poolType === PoolTypes.Linear - ) { - // Will accept/return normalised values - returnAmount = pool._exactTokenInForTokenOut(poolPairData, amount); - } else if (pool.poolType === PoolTypes.Element) { - // TODO this will just be part of above once maths available + if (pool.poolType === PoolTypes.Element) { + // TODO this will just be part of below once maths available returnAmount = getOutputAmountSwap( pool, poolPairData, swapType, amount ); + } else if (pool.poolType in PoolTypes) { + // Will accept/return normalised values + returnAmount = pool._exactTokenInForTokenOut(poolPairData, amount); } else { throw Error('Unsupported swap'); } } else { - // TODO we will be able to remove pooltype check once Element EVM maths is available - if ( - pool.poolType === PoolTypes.Weighted || - pool.poolType === PoolTypes.Stable || - pool.poolType === PoolTypes.MetaStable || - pool.poolType === PoolTypes.Linear - ) { - returnAmount = pool._tokenInForExactTokenOut(poolPairData, amount); - } else if (pool.poolType === PoolTypes.Element) { - // TODO this will just be part of above once maths available + if (pool.poolType === PoolTypes.Element) { + // TODO this will just be part of below once maths available returnAmount = getOutputAmountSwap( pool, poolPairData, swapType, amount ); + } else if (pool.poolType in PoolTypes) { + // Will accept/return normalised values + returnAmount = pool._tokenInForExactTokenOut(poolPairData, amount); } else { throw Error('Unsupported swap'); } diff --git a/src/types.ts b/src/types.ts index c8786ba4..3d6e5fc1 100644 --- a/src/types.ts +++ b/src/types.ts @@ -92,10 +92,11 @@ export interface SubgraphPoolBase { upperTarget?: string; // Gyro2 specific field - gyro2PriceBounds?: Gyro2PriceBounds; + sqrtAlpha?: string; + sqrtBeta?: string; // Gyro3 specific field - gyro3PriceBounds?: Gyro3PriceBounds; + root3Alpha?: string; } export type SubgraphToken = { @@ -162,6 +163,8 @@ export enum PoolFilter { AaveLinear = 'AaveLinear', StablePhantom = 'StablePhantom', ERC4626Linear = 'ERC4626Linear', + Gyro2 = 'Gyro2', + Gyro3 = 'Gyro3', } export interface PoolBase { @@ -224,15 +227,3 @@ export interface TokenPriceService { export interface PoolDataService { getPools(): Promise; } - -export type Gyro2PriceBounds = { - lowerBound: string; - upperBound: string; - tokenInAddress: string; - tokenOutAddress: string; -}; - -export type Gyro3PriceBounds = { - alpha: string; // Assume symmetric price bounds for Gyro 3 pool - // (The price range for any asset pair is equal to [alpha, 1/alpha]) -}; diff --git a/test/gyro2.integration.spec.ts b/test/gyro2.integration.spec.ts new file mode 100644 index 00000000..228ee6b9 --- /dev/null +++ b/test/gyro2.integration.spec.ts @@ -0,0 +1,177 @@ +// TS_NODE_PROJECT='tsconfig.testing.json' npx mocha -r ts-node/register test/gyro2.integration.spec.ts +import dotenv from 'dotenv'; +import { parseFixed } from '@ethersproject/bignumber'; +import { AddressZero } from '@ethersproject/constants'; +import { expect } from 'chai'; +import { JsonRpcProvider } from '@ethersproject/providers'; +import { Vault__factory } from '@balancer-labs/typechain'; +import { vaultAddr } from './lib/constants'; +import { + SOR, + SubgraphPoolBase, + SwapTypes, + TokenPriceService, +} from '../src/index'; +import { Network, MULTIADDR, SOR_CONFIG } from './testScripts/constants'; +import { OnChainPoolDataService } from './lib/onchainData'; + +dotenv.config(); + +/* + * Testing on KOVAN + * - Update hardhat.config.js with chainId = 42 + * - Update ALCHEMY_URL on .env with a kovan api key + * - Run kovan node on terminal: yarn run node + * TO DO - Change this test to mainnet once deployed. + */ +const { ALCHEMY_URL: jsonRpcUrl } = process.env; +const rpcUrl = 'http://127.0.0.1:8545'; +const provider = new JsonRpcProvider(rpcUrl, 42); +const vault = Vault__factory.connect(vaultAddr, provider); + +const gyro2Pool: SubgraphPoolBase = { + id: '0xd9e058f39c11313103229bd481a8453bddd20d81000200000000000000000999', + address: '0xd9e058f39c11313103229bd481a8453bddd20d81', + poolType: 'Gyro2', + swapFee: '0.09', + totalShares: '20950.113635776677649258', + tokens: [ + { + address: '0x2b7c320d7b915d9d10aeb2f93f94720d4f3fff91', + balance: '1000', + decimals: 18, + weight: null, + priceRate: '1', + }, + { + address: '0x6a7ddccff3141a337f8819fa9d0922e33c405d6f', + balance: '1000', + decimals: 18, + weight: null, + priceRate: '1', + }, + ], + tokensList: [ + '0x2b7c320d7b915d9d10aeb2f93f94720d4f3fff91', + '0x6a7ddccff3141a337f8819fa9d0922e33c405d6f', + ], + totalWeight: '0', + swapEnabled: true, + wrappedIndex: 0, + mainIndex: 0, + sqrtAlpha: '0.9', + sqrtBeta: '1.1', +}; + +// Setup SOR with data services +function setUp(networkId: Network, provider: JsonRpcProvider): SOR { + // The SOR needs to fetch pool data from an external source. This provider fetches from Subgraph and onchain calls. + const subgraphPoolDataService = new OnChainPoolDataService({ + vaultAddress: vaultAddr, + multiAddress: MULTIADDR[networkId], + provider, + pools: [gyro2Pool], + }); + + class CoingeckoTokenPriceService implements TokenPriceService { + constructor(private readonly chainId: number) {} + async getNativeAssetPriceInToken( + tokenAddress: string + ): Promise { + return '0'; + } + } + + // Use coingecko to fetch token price information. Used to calculate cost of additonal swaps/hops. + const coingeckoTokenPriceService = new CoingeckoTokenPriceService( + networkId + ); + + return new SOR( + provider, + SOR_CONFIG[networkId], + subgraphPoolDataService, + coingeckoTokenPriceService + ); +} + +let sor: SOR; + +describe('gyro2 integration tests', () => { + context('test swaps vs queryBatchSwap', () => { + const tokenIn = '0x2b7c320d7b915d9d10aeb2f93f94720d4f3fff91'; + const tokenOut = '0x6a7ddccff3141a337f8819fa9d0922e33c405d6f'; + const swapAmount = parseFixed('17.789', 18); + const funds = { + sender: AddressZero, + recipient: AddressZero, + fromInternalBalance: false, + toInternalBalance: false, + }; + // Setup chain + before(async function () { + this.timeout(20000); + + await provider.send('hardhat_reset', [ + { + forking: { + jsonRpcUrl, + blockNumber: 32941193, + }, + }, + ]); + + const networkId = Network.KOVAN; + sor = setUp(networkId, provider); + await sor.fetchPools(); + }); + + it('ExactIn', async () => { + const swapType = SwapTypes.SwapExactIn; + + const swapInfo = await sor.getSwaps( + tokenIn, + tokenOut, + swapType, + swapAmount + ); + + const queryResult = await vault.callStatic.queryBatchSwap( + swapType, + swapInfo.swaps, + swapInfo.tokenAddresses, + funds + ); + expect(queryResult[0].toString()).to.eq( + swapInfo.swapAmount.toString() + ); + expect(queryResult[1].abs().toString()).to.eq( + swapInfo.returnAmount.toString() + ); + }).timeout(10000); + + it('ExactOut', async () => { + const swapType = SwapTypes.SwapExactOut; + + const swapInfo = await sor.getSwaps( + tokenIn, + tokenOut, + swapType, + swapAmount + ); + + const queryResult = await vault.callStatic.queryBatchSwap( + swapType, + swapInfo.swaps, + swapInfo.tokenAddresses, + funds + ); + // Amount out should be exact + expect(queryResult[1].abs().toString()).to.eq( + swapInfo.swapAmount.toString() + ); + const deltaIn = queryResult[0].sub(swapInfo.returnAmount); + expect(deltaIn.toNumber()).to.be.lessThan(2); + }).timeout(10000); + }); +}); diff --git a/test/gyro2Math.spec.ts b/test/gyro2Math.spec.ts index 6aad772a..c4ae4f70 100644 --- a/test/gyro2Math.spec.ts +++ b/test/gyro2Math.spec.ts @@ -1,23 +1,25 @@ import { expect } from 'chai'; import cloneDeep from 'lodash.clonedeep'; -import { formatFixed, parseFixed } from '@ethersproject/bignumber'; +import { formatFixed, parseFixed, BigNumber } from '@ethersproject/bignumber'; +import { WeiPerEther as ONE } from '@ethersproject/constants'; import { USDC, DAI } from './lib/constants'; // Add new PoolType import { Gyro2Pool } from '../src/pools/gyro2Pool/gyro2Pool'; // Add new pool test data in Subgraph Schema format import testPools from './testData/gyro2Pools/gyro2TestPool.json'; import { - _addFee, - _calculateInvariant, - _reduceFee, _calculateQuadratic, _calculateQuadraticTerms, _findVirtualParams, - _normalizeBalances, } from '../src/pools/gyro2Pool/gyro2Math'; +import { + _addFee, + _reduceFee, + _normalizeBalances, +} from '../src/pools/gyro2Pool/helpers'; describe('gyro2Math tests', () => { - const testPool = cloneDeep(testPools).pools[0]; + const testPool: any = cloneDeep(testPools).pools[0]; const pool = Gyro2Pool.fromPool(testPool); const poolPairData = pool.parsePoolPairData(USDC.address, DAI.address); @@ -44,19 +46,22 @@ describe('gyro2Math tests', () => { poolPairData.decimalsIn, poolPairData.decimalsOut ); - const [a, mb, mc] = _calculateQuadraticTerms( + const [a, mb, bSquare, mc] = _calculateQuadraticTerms( normalizedBalances, poolPairData.sqrtAlpha, poolPairData.sqrtBeta ); - expect(formatFixed(a, 18)).to.equal('0.000999500499625375'); - expect(formatFixed(mb, 18)).to.equal('2230.884220610725033295'); + expect(formatFixed(a, 18)).to.equal('0.00099950047470021'); + expect(formatFixed(mb, 18)).to.equal('2230.884220626971757449'); + expect(formatFixed(bSquare, 18)).to.equal( + '4976844.405842411200429555' + ); expect(formatFixed(mc, 18)).to.equal('1232000.0'); - const L = _calculateQuadratic(a, mb, mc); + const L = _calculateQuadratic(a, mb, mb.mul(mb).div(ONE), mc); - expect(formatFixed(L, 18)).to.equal('2232551.215824107930236259'); + expect(formatFixed(L, 18)).to.equal('2232551.271501112084098627'); }); it(`should correctly calculate virtual parameters`, async () => { @@ -66,8 +71,8 @@ describe('gyro2Math tests', () => { poolPairData.sqrtBeta ); - expect(formatFixed(a, 18)).to.equal('2231434.661007672178972479'); - expect(formatFixed(b, 18)).to.equal('2231435.776725839468265783'); + expect(formatFixed(a, 18)).to.equal('2231434.660924038777489798'); + expect(formatFixed(b, 18)).to.equal('2231435.776865147462654764'); }); }); }); diff --git a/test/gyro2Pool.spec.ts b/test/gyro2Pool.spec.ts index 23426779..0198a22c 100644 --- a/test/gyro2Pool.spec.ts +++ b/test/gyro2Pool.spec.ts @@ -1,14 +1,18 @@ +import 'dotenv/config'; // TS_NODE_PROJECT='tsconfig.testing.json' npx mocha -r ts-node/register test/gyro2Pool.spec.ts import { expect } from 'chai'; import cloneDeep from 'lodash.clonedeep'; -import { formatFixed, parseFixed } from '@ethersproject/bignumber'; +import { formatFixed, parseFixed, BigNumber } from '@ethersproject/bignumber'; +import { JsonRpcProvider } from '@ethersproject/providers'; import { bnum } from '../src/utils/bignumber'; -import { USDC, DAI } from './lib/constants'; -import { SwapTypes } from '../src'; +import { USDC, DAI, sorConfigEth } from './lib/constants'; +import { SwapTypes, SOR, SwapInfo } from '../src'; // Add new PoolType import { Gyro2Pool } from '../src/pools/gyro2Pool/gyro2Pool'; // Add new pool test data in Subgraph Schema format import testPools from './testData/gyro2Pools/gyro2TestPool.json'; +import { MockPoolDataService } from './lib/mockPoolDataService'; +import { mockTokenPriceService } from './lib/mockTokenPriceService'; describe('Gyro2Pool tests USDC > DAI', () => { const testPool = cloneDeep(testPools).pools[0]; @@ -72,7 +76,7 @@ describe('Gyro2Pool tests USDC > DAI', () => { pool.getNormalizedLiquidity(poolPairData); expect(Number(normalizedLiquidity.toString())).to.be.approximately( - 2252709.0423891, + 2252709.0984593313, 0.00001 ); }); @@ -82,7 +86,7 @@ describe('Gyro2Pool tests USDC > DAI', () => { pool.getNormalizedLiquidity(poolPairData2); expect(Number(normalizedLiquidity.toString())).to.be.approximately( - 2252944.2752, + 2252944.3314978145, 0.00001 ); }); @@ -97,7 +101,7 @@ describe('Gyro2Pool tests USDC > DAI', () => { poolPairData, amountIn ); - expect(amountOut.toString()).to.eq('13.379816829921106482'); + expect(amountOut.toString()).to.eq('13.379816831223414577'); }); it('should correctly calculate newSpotPrice', async () => { const newSpotPrice = @@ -105,7 +109,7 @@ describe('Gyro2Pool tests USDC > DAI', () => { poolPairData, amountIn ); - expect(newSpotPrice.toString()).to.eq('1.008988469289267733'); + expect(newSpotPrice.toString()).to.eq('1.008988469190824523'); }); it('should correctly calculate derivative of spot price function at newSpotPrice', async () => { const derivative = @@ -113,7 +117,7 @@ describe('Gyro2Pool tests USDC > DAI', () => { poolPairData, amountIn ); - expect(derivative.toString()).to.eq('0.000000895794710891'); + expect(derivative.toString()).to.eq('0.000000895794688507'); }); }); @@ -125,7 +129,7 @@ describe('Gyro2Pool tests USDC > DAI', () => { poolPairData, amountOut ); - expect(amountIn.toString()).to.eq('45.977973900999006919'); + expect(amountIn.toString()).to.eq('45.977973896504501314'); }); it('should correctly calculate newSpotPrice', async () => { const newSpotPrice = @@ -133,7 +137,7 @@ describe('Gyro2Pool tests USDC > DAI', () => { poolPairData, amountOut ); - expect(newSpotPrice.toString()).to.eq('1.009017563096232974'); + expect(newSpotPrice.toString()).to.eq('1.00901756299705875'); }); it('should correctly calculate derivative of spot price function at newSpotPrice', async () => { const derivative = @@ -141,7 +145,56 @@ describe('Gyro2Pool tests USDC > DAI', () => { poolPairData, amountOut ); - expect(derivative.toString()).to.eq('0.000000903885627537'); + expect(derivative.toString()).to.eq('0.000000903885604863'); + }); + }); + + context('FullSwap', () => { + it(`Full Swap - swapExactIn, Token>Token`, async () => { + const pools: any = cloneDeep(testPools.pools); + const tokenIn = USDC.address; + const tokenOut = DAI.address; + const swapType = SwapTypes.SwapExactIn; + const swapAmt = parseFixed('13.5', 6); + + const gasPrice = parseFixed('30', 9); + const maxPools = 4; + const provider = new JsonRpcProvider( + `https://mainnet.infura.io/v3/${process.env.INFURA}` + ); + + const sor = new SOR( + provider, + sorConfigEth, + new MockPoolDataService(pools), + mockTokenPriceService + ); + const fetchSuccess = await sor.fetchPools(); + expect(fetchSuccess).to.be.true; + + const swapInfo: SwapInfo = await sor.getSwaps( + tokenIn, + tokenOut, + swapType, + swapAmt, + { gasPrice, maxPools } + ); + + console.log(`Return amt:`); + console.log(swapInfo.returnAmount.toString()); + // This value is hard coded as sanity check if things unexpectedly change. Taken from V2 test run (with extra fee logic added). + // TO DO - expect(swapInfo.returnAmount.toString()).eq('999603'); + expect(swapInfo.swaps.length).eq(1); + expect(swapInfo.swaps[0].amount.toString()).eq( + swapAmt.toString() + ); + expect(swapInfo.swaps[0].poolId).eq(testPools.pools[0].id); + expect( + swapInfo.tokenAddresses[swapInfo.swaps[0].assetInIndex] + ).eq(tokenIn); + expect( + swapInfo.tokenAddresses[swapInfo.swaps[0].assetOutIndex] + ).eq(tokenOut); }); }); }); diff --git a/test/gyro3.integration.spec.ts b/test/gyro3.integration.spec.ts new file mode 100644 index 00000000..735cd9fe --- /dev/null +++ b/test/gyro3.integration.spec.ts @@ -0,0 +1,184 @@ +// TS_NODE_PROJECT='tsconfig.testing.json' npx mocha -r ts-node/register test/gyro3.integration.spec.ts +import dotenv from 'dotenv'; +import { parseFixed } from '@ethersproject/bignumber'; +import { AddressZero } from '@ethersproject/constants'; +import { expect } from 'chai'; +import { JsonRpcProvider } from '@ethersproject/providers'; +import { Vault__factory } from '@balancer-labs/typechain'; +import { vaultAddr } from './lib/constants'; +import { + SOR, + SubgraphPoolBase, + SwapTypes, + TokenPriceService, +} from '../src/index'; +import { Network, MULTIADDR, SOR_CONFIG } from './testScripts/constants'; +import { OnChainPoolDataService } from './lib/onchainData'; + +dotenv.config(); + +/* + * Testing on KOVAN + * - Update hardhat.config.js with chainId = 42 + * - Update ALCHEMY_URL on .env with a kovan api key + * - Run kovan node on terminal: yarn run node + * TO DO - Change this test to mainnet once deployed. + */ +const { ALCHEMY_URL: jsonRpcUrl } = process.env; +const rpcUrl = 'http://127.0.0.1:8545'; +const provider = new JsonRpcProvider(rpcUrl, 42); +const vault = Vault__factory.connect(vaultAddr, provider); + +const gyro3Pool: SubgraphPoolBase = { + id: '0x24eabc806e3e9b63f41114d1e323a8aa08472ca7000100000000000000000891', + address: '0x24eabc806e3e9b63f41114d1e323a8aa08472ca7', + poolType: 'Gyro3', + swapFee: '0.09', + totalShares: '6583.623489368247136191', + tokens: [ + { + address: '0x11fb9071e69628d804bf0b197cc61eeacd4aaecf', + balance: '9.999481158411540472', + decimals: 18, + weight: null, + priceRate: '1', + }, + { + address: '0x4ea2110a3e277b10c9b098f61d72f58efa8655db', + balance: '9.997571529493261602', + decimals: 18, + weight: null, + priceRate: '1', + }, + { + address: '0x5663082e6d6addf940a38ea312b899a5ec86c2dc', + balance: '9.99848131028051242', + decimals: 18, + weight: null, + priceRate: '1', + }, + ], + tokensList: [ + '0x11fb9071e69628d804bf0b197cc61eeacd4aaecf', + '0x4ea2110a3e277b10c9b098f61d72f58efa8655db', + '0x5663082e6d6addf940a38ea312b899a5ec86c2dc', + ], + totalWeight: '0', + swapEnabled: true, + wrappedIndex: 0, + mainIndex: 0, + root3Alpha: '0.000099997499906244', +}; + +// Setup SOR with data services +function setUp(networkId: Network, provider: JsonRpcProvider): SOR { + // The SOR needs to fetch pool data from an external source. This provider fetches from Subgraph and onchain calls. + const subgraphPoolDataService = new OnChainPoolDataService({ + vaultAddress: vaultAddr, + multiAddress: MULTIADDR[networkId], + provider, + pools: [gyro3Pool], + }); + + class CoingeckoTokenPriceService implements TokenPriceService { + constructor(private readonly chainId: number) {} + async getNativeAssetPriceInToken( + tokenAddress: string + ): Promise { + return '0'; + } + } + + // Use coingecko to fetch token price information. Used to calculate cost of additonal swaps/hops. + const coingeckoTokenPriceService = new CoingeckoTokenPriceService( + networkId + ); + + return new SOR( + provider, + SOR_CONFIG[networkId], + subgraphPoolDataService, + coingeckoTokenPriceService + ); +} + +let sor: SOR; + +describe('gyro3 integration tests', () => { + context('test swaps vs queryBatchSwap', () => { + const tokenIn = '0x11fb9071e69628d804bf0b197cc61eeacd4aaecf'; + const tokenOut = '0x4ea2110a3e277b10c9b098f61d72f58efa8655db'; + const swapAmount = parseFixed('1.7', 18); + const funds = { + sender: AddressZero, + recipient: AddressZero, + fromInternalBalance: false, + toInternalBalance: false, + }; + // Setup chain + before(async function () { + this.timeout(20000); + + await provider.send('hardhat_reset', [ + { + forking: { + jsonRpcUrl, + blockNumber: 32937471, + }, + }, + ]); + + const networkId = Network.KOVAN; + sor = setUp(networkId, provider); + await sor.fetchPools(); + }); + + it('ExactIn', async () => { + const swapType = SwapTypes.SwapExactIn; + + const swapInfo = await sor.getSwaps( + tokenIn, + tokenOut, + swapType, + swapAmount + ); + + const queryResult = await vault.callStatic.queryBatchSwap( + swapType, + swapInfo.swaps, + swapInfo.tokenAddresses, + funds + ); + expect(queryResult[0].toString()).to.eq( + swapInfo.swapAmount.toString() + ); + expect(queryResult[1].abs().toString()).to.eq( + swapInfo.returnAmount.toString() + ); + }).timeout(10000); + + it('ExactOut', async () => { + const swapType = SwapTypes.SwapExactOut; + + const swapInfo = await sor.getSwaps( + tokenIn, + tokenOut, + swapType, + swapAmount + ); + + const queryResult = await vault.callStatic.queryBatchSwap( + swapType, + swapInfo.swaps, + swapInfo.tokenAddresses, + funds + ); + // Amount out should be exact + expect(queryResult[1].abs().toString()).to.eq( + swapInfo.swapAmount.toString() + ); + const deltaIn = queryResult[0].sub(swapInfo.returnAmount); + expect(deltaIn.toNumber()).to.be.lessThan(2); + }).timeout(10000); + }); +}); diff --git a/test/gyro3Math.spec.ts b/test/gyro3Math.spec.ts index 64c1dc0c..d0b74198 100644 --- a/test/gyro3Math.spec.ts +++ b/test/gyro3Math.spec.ts @@ -1,6 +1,6 @@ import { expect } from 'chai'; import cloneDeep from 'lodash.clonedeep'; -import { formatFixed, parseFixed } from '@ethersproject/bignumber'; +import { formatFixed, parseFixed, BigNumber } from '@ethersproject/bignumber'; import { USDC, DAI } from './lib/constants'; // Add new PoolType import { Gyro3Pool } from '../src/pools/gyro3Pool/gyro3Pool'; @@ -11,11 +11,12 @@ import { _calculateCubicStartingPoint, _calculateCubicTerms, _runNewtonIteration, - _normalizeBalances, } from '../src/pools/gyro3Pool/gyro3Math'; +import { _normalizeBalances } from '../src/pools/gyro3Pool/helpers'; describe('gyro3Math tests', () => { - const testPool = cloneDeep(testPools).pools[0]; + const testPool: any = cloneDeep(testPools).pools[0]; + const pool = Gyro3Pool.fromPool(testPool); const poolPairData = pool.parsePoolPairData(USDC.address, DAI.address); @@ -39,11 +40,9 @@ describe('gyro3Math tests', () => { pool.root3Alpha ); - expect(formatFixed(a, 18)).to.equal('0.013000000000000127'); - expect(formatFixed(mb, 18)).to.equal('245387.995391323689889516'); - expect(formatFixed(mc, 18)).to.equal( - '20335328582.7366683195765956' - ); + expect(formatFixed(a, 18)).to.equal('0.013000000252593788'); + expect(formatFixed(mb, 18)).to.equal('245387.995349457123073152'); + expect(formatFixed(mc, 18)).to.equal('20335328581.001924952'); expect(formatFixed(md, 18)).to.equal('561707977531810.0'); }); @@ -54,7 +53,7 @@ describe('gyro3Math tests', () => { const l0 = _calculateCubicStartingPoint(a, mb, mc); - expect(formatFixed(l0, 18)).to.equal('18937948.911434007702525325'); + expect(formatFixed(l0, 18)).to.equal('18937948.911434007702525329'); }); it(`should correctly calculate deltas for Newton method`, async () => { @@ -70,11 +69,12 @@ describe('gyro3Math tests', () => { mb, mc, md, + pool.root3Alpha, rootEst0 ); expect(formatFixed(deltaAbs1, 18)).to.equal( - '20725.034790223169767955' + '20724.666415828607336826' ); expect(deltaIsPos1).to.equal(true); @@ -85,11 +85,12 @@ describe('gyro3Math tests', () => { mb, mc, md, + pool.root3Alpha, rootEst1 ); expect(formatFixed(deltaAbs2, 18)).to.equal( - '45.163851832290322917' + '45.530618963506481754' ); expect(deltaIsPos2).to.equal(false); @@ -100,10 +101,11 @@ describe('gyro3Math tests', () => { mb, mc, md, + pool.root3Alpha, rootEst2 ); - expect(formatFixed(deltaAbs3, 18)).to.equal('0.000214713810115934'); + expect(formatFixed(deltaAbs3, 18)).to.equal('0.366985332320115631'); expect(deltaIsPos3).to.equal(false); const rootEst3 = parseFixed('18958628.782157684771854429', 18); @@ -113,10 +115,11 @@ describe('gyro3Math tests', () => { mb, mc, md, + pool.root3Alpha, rootEst3 ); - expect(formatFixed(deltaAbs4, 18)).to.equal('0.000000000004454138'); + expect(formatFixed(deltaAbs4, 18)).to.equal('0.366770618526583671'); expect(deltaIsPos4).to.equal(false); const rootEst4 = parseFixed('18958628.782157684767400291', 18); @@ -126,29 +129,25 @@ describe('gyro3Math tests', () => { mb, mc, md, + pool.root3Alpha, rootEst4 ); - expect(formatFixed(deltaAbs5, 18)).to.equal('0.000000000004453998'); + expect(formatFixed(deltaAbs5, 18)).to.equal('0.366770618522129533'); expect(deltaIsPos5).to.equal(false); - const finalRootEst = _runNewtonIteration(a, mb, mc, md, rootEst0); + const finalRootEst = _runNewtonIteration( + a, + mb, + mc, + md, + pool.root3Alpha, + rootEst0 + ); expect(formatFixed(finalRootEst, 18)).to.equal( - '18958628.782157684762946293' + '18958628.415387052085178162' ); }); }); }); - -// a = 0.013000000000000127 -// mb = 245387.995391323689889516 -// mc = 20335328582.7366683195765956 -// md = 561707977531810.0 - -// l0 = 18937948.911434007702525325 -// l1 = 18,958,673.94622423087229328 -// l2 = 18,958,628.782372398581970363 -// l3 = 18,958,628.782157684771854429 -// l4 = 18,958,628.782157684767400291 -// lFinal = 18958628.782157684762946293 diff --git a/test/gyro3Pool.spec.ts b/test/gyro3Pool.spec.ts index 5cc3d6c8..b13a1cf4 100644 --- a/test/gyro3Pool.spec.ts +++ b/test/gyro3Pool.spec.ts @@ -1,6 +1,6 @@ import { expect } from 'chai'; import cloneDeep from 'lodash.clonedeep'; -import { formatFixed, parseFixed } from '@ethersproject/bignumber'; +import { formatFixed, parseFixed, BigNumber } from '@ethersproject/bignumber'; import { bnum } from '../src/utils/bignumber'; import { USDC, USDT } from './lib/constants'; import { SwapTypes } from '../src'; @@ -10,7 +10,7 @@ import { Gyro3Pool } from '../src/pools/gyro3Pool/gyro3Pool'; import testPools from './testData/gyro3Pools/gyro3TestPool.json'; describe('Gyro3Pool tests USDC > DAI', () => { - const testPool = cloneDeep(testPools).pools[0]; + const testPool: any = cloneDeep(testPools).pools[0]; const pool = Gyro3Pool.fromPool(testPool); const poolPairData = pool.parsePoolPairData(USDT.address, USDC.address); @@ -55,7 +55,7 @@ describe('Gyro3Pool tests USDC > DAI', () => { pool.getNormalizedLiquidity(poolPairData); expect(normalizedLiquidity.toString()).to.equal( - '19016283.981512596845077192' + '19016283.61041515459756377' ); }); }); @@ -69,7 +69,7 @@ describe('Gyro3Pool tests USDC > DAI', () => { poolPairData, amountIn ); - expect(amountOut.toString()).to.eq('233.62822068392501182'); + expect(amountOut.toString()).to.eq('233.628220683475857751'); }); it('should correctly calculate newSpotPrice', async () => { const newSpotPrice = @@ -77,7 +77,7 @@ describe('Gyro3Pool tests USDC > DAI', () => { poolPairData, amountIn ); - expect(newSpotPrice.toString()).to.eq('1.003120202974438177'); + expect(newSpotPrice.toString()).to.eq('1.003120202976607933'); }); it('should correctly calculate derivative of spot price function at newSpotPrice', async () => { const derivative = @@ -85,7 +85,7 @@ describe('Gyro3Pool tests USDC > DAI', () => { poolPairData, amountIn ); - expect(derivative.toString()).to.eq('0.000000105499880184'); + expect(derivative.toString()).to.eq('0.000000105499882243'); }); }); @@ -97,7 +97,7 @@ describe('Gyro3Pool tests USDC > DAI', () => { poolPairData, amountOut ); - expect(amountIn.toString()).to.eq('4538.618912825809655998'); + expect(amountIn.toString()).to.eq('4538.618912854584519788'); }); it('should correctly calculate newSpotPrice', async () => { @@ -106,7 +106,7 @@ describe('Gyro3Pool tests USDC > DAI', () => { poolPairData, amountOut ); - expect(newSpotPrice.toString()).to.eq('1.003574353766587852'); + expect(newSpotPrice.toString()).to.eq('1.003574353777625146'); }); it('should correctly calculate derivative of spot price function at newSpotPrice', async () => { @@ -115,7 +115,7 @@ describe('Gyro3Pool tests USDC > DAI', () => { poolPairData, amountOut ); - expect(derivative.toString()).to.eq('0.000000105900938637'); + expect(derivative.toString()).to.eq('0.000000105900940706'); }); }); }); diff --git a/test/lib/onchainData.ts b/test/lib/onchainData.ts index 1071c2c3..a3c4d5b0 100644 --- a/test/lib/onchainData.ts +++ b/test/lib/onchainData.ts @@ -1,3 +1,4 @@ +import { JsonRpcProvider } from '@ethersproject/providers'; import { formatFixed } from '@ethersproject/bignumber'; import { Provider } from '@ethersproject/providers'; import { isSameAddress } from '../../src/utils'; @@ -9,7 +10,7 @@ import weightedPoolAbi from '../../src/pools/weightedPool/weightedPoolAbi.json'; import stablePoolAbi from '../../src/pools/stablePool/stablePoolAbi.json'; import elementPoolAbi from '../../src/pools/elementPool/ConvergentCurvePool.json'; import linearPoolAbi from '../../src/pools/linearPool/linearPoolAbi.json'; -import { PoolFilter, SubgraphPoolBase } from '../../src'; +import { PoolFilter, SubgraphPoolBase, PoolDataService } from '../../src'; import { Multicaller } from './multicaller'; export async function getOnChainBalances( @@ -98,6 +99,12 @@ export async function getOnChainBalances( pool.address, 'getWrappedTokenRate' ); + } else if (pool.poolType.toString().includes('Gyro')) { + multiPool.call( + `${pool.id}.swapFee`, + pool.address, + 'getSwapFeePercentage' + ); } }); @@ -209,3 +216,27 @@ export async function getOnChainBalances( return onChainPools; } + +/* +PoolDataService to fetch onchain balances of Gyro3 pool. +(Fetching all pools off a fork is too intensive) +*/ +export class OnChainPoolDataService implements PoolDataService { + constructor( + private readonly config: { + multiAddress: string; + vaultAddress: string; + provider: JsonRpcProvider; + pools: SubgraphPoolBase[]; + } + ) {} + + public async getPools(): Promise { + return getOnChainBalances( + this.config.pools, + this.config.multiAddress, + this.config.vaultAddress, + this.config.provider + ); + } +} diff --git a/test/lib/subgraphPoolDataService.ts b/test/lib/subgraphPoolDataService.ts index a989adbc..d495ee21 100644 --- a/test/lib/subgraphPoolDataService.ts +++ b/test/lib/subgraphPoolDataService.ts @@ -35,6 +35,9 @@ const queryWithLinear = ` mainIndex lowerTarget upperTarget + sqrtAlpha + sqrtBeta + root3Alpha } pool1000: pools( first: 1000, @@ -67,6 +70,9 @@ const queryWithLinear = ` mainIndex lowerTarget upperTarget + sqrtAlpha + sqrtBeta + root3Alpha } } `; diff --git a/test/testData/gyro2Pools/gyro2TestPool.json b/test/testData/gyro2Pools/gyro2TestPool.json index 554e0108..614d8dee 100644 --- a/test/testData/gyro2Pools/gyro2TestPool.json +++ b/test/testData/gyro2Pools/gyro2TestPool.json @@ -38,14 +38,10 @@ ], "totalWeight": "15", "totalShares": "100", - "poolType": "Gyro2Pool", + "poolType": "Gyro2", "swapEnabled": true, - "gyro2PriceBounds": { - "lowerBound": "0.999", - "upperBound": "1.001", - "tokenInAddress": "0x6b175474e89094c44da98b954eedeac495271d0f", - "tokenOutAddress": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48" - } + "sqrtAlpha": "0.9994998749", + "sqrtBeta": "1.000499875" } ] } diff --git a/test/testData/gyro3Pools/gyro3TestPool.json b/test/testData/gyro3Pools/gyro3TestPool.json index 066b4d90..313e1642 100644 --- a/test/testData/gyro3Pools/gyro3TestPool.json +++ b/test/testData/gyro3Pools/gyro3TestPool.json @@ -47,11 +47,9 @@ ], "totalWeight": "15", "totalShares": "100", - "poolType": "Gyro3Pool", + "poolType": "Gyro3", "swapEnabled": true, - "gyro3PriceBounds": { - "alpha": "0.987" - } + "root3Alpha": "0.995647752" } ] }