diff --git a/README.md b/README.md index d0d02d72..f39088f1 100644 --- a/README.md +++ b/README.md @@ -57,3 +57,9 @@ Example: PRICE_ERROR_TOLERANCE=0.00001 INFINITESIMAL=0.000001 ``` + +## Note on Licensing + +Except where indicated otherwise, the code in this repository is licensed GPLv3. + +Superluminal Labs Ltd. is the owner of the directories `balancer-sor/src/pools/gyro2Pool/`, `balancer-sor/src/pools/gyro3Pool/` and `balancer-sor/src/pools/gyroEPool/` and any accompanying files contained herein (collectively, these “Software”). Use of these Software is exclusively subject to the [Gyroscope Pool License](./src/pools/gyroEPool/LICENSE), which is available at the provided link (the “Gyroscope Pool License”). These Software are not covered by the General Public License and do not confer any rights to the user other than the limited rights specified in the Gyroscope Pool License. A special hybrid license between Superluminal Labs Ltd and Balancer Labs OÜ governs Superluminal Labs Ltd's use of the Balancer Labs OÜ code [Special License](./src/pools/gyroEPool/GyroscopeBalancerLicense.pdf), which is available at the provided link. By using these Software, you agree to be bound by the terms and conditions of the Gyroscope Pool License. If you do not agree to all terms and conditions of the Gyroscope Pool License, do not use any of these Software. diff --git a/package.json b/package.json index 5763ea3d..15b3ce95 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@balancer-labs/sor", - "version": "4.0.1-beta.11", + "version": "4.0.1-beta.12", "license": "GPL-3.0-only", "main": "dist/index.js", "module": "dist/index.esm.js", diff --git a/src/pools/gyro2Pool/LICENSE b/src/pools/gyro2Pool/LICENSE new file mode 100644 index 00000000..c506bbda --- /dev/null +++ b/src/pools/gyro2Pool/LICENSE @@ -0,0 +1,5 @@ +(c) 2022 Superluminal Labs Ltd. All rights reserved. + +All moral, intellectual property, and other rights (including rights to all inventions, codes, designs, and protocols) associated with the software code published by Superluminal Labs Ltd. and residing in this repository (this “Software”) are reserved by its right holder(s) except as otherwise provided in this Gyroscope Pool User Contract. This Software has been published for informational purposes and may be run only with the Balancer automated portfolio manager and trading platform solely for testing and internal evaluation purposes only; no license under patents, copyright, trademark, or any other intellectual property right (other than the limited license to run this Software for testing and internal evaluation) is granted or implied. A special hybrid license between Superluminal Labs Ltd and Balancer Labs OÜ governs Superluminal Labs Ltd.'s use of the Balancer Labs OÜ code [Special License](../gyroEPool/GyroscopeBalancerLicense.pdf), which is available at the provided link. + +THE SOFTWARE AND INTELLECTUAL PROPERTY INCLUDED IN THIS SOFTWARE IS PROVIDED BY THE RIGHT HOLDER(S) "AS IS," AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE RIGHT HOLDER(S) BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE OR INTELLECTUAL PROPERTY (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION), HOWEVER CAUSED OR CLAIMED (WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)), EVEN IF SUCH DAMAGES WERE REASONABLY FORESEEABLE OR THE RIGHT HOLDER(S) WERE ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/src/pools/gyro2Pool/gyro2Math.ts b/src/pools/gyro2Pool/gyro2Math.ts index 8c2979ac..c3ecfe07 100644 --- a/src/pools/gyro2Pool/gyro2Math.ts +++ b/src/pools/gyro2Pool/gyro2Math.ts @@ -1,6 +1,12 @@ import { BigNumber } from '@ethersproject/bignumber'; import { WeiPerEther as ONE } from '@ethersproject/constants'; -import { _sqrt, mulUp, divUp, mulDown, divDown } from './helpers'; +import { + sqrt, + mulUp, + divUp, + mulDown, + divDown, +} from '../gyroHelpers/gyroSignedFixedPoint'; ///////// /// Virtual Parameter calculations @@ -91,7 +97,7 @@ export function _calculateQuadratic( 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 = _sqrt(radicand, BigNumber.from(5)); + 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 = divDown(numerator, denominator); diff --git a/src/pools/gyro2Pool/gyro2Pool.ts b/src/pools/gyro2Pool/gyro2Pool.ts index b8c80936..20b24cda 100644 --- a/src/pools/gyro2Pool/gyro2Pool.ts +++ b/src/pools/gyro2Pool/gyro2Pool.ts @@ -1,6 +1,6 @@ import { getAddress } from '@ethersproject/address'; import { WeiPerEther as ONE } from '@ethersproject/constants'; -import { parseFixed, formatFixed, BigNumber } from '@ethersproject/bignumber'; +import { formatFixed, BigNumber } from '@ethersproject/bignumber'; import { BigNumber as OldBigNumber, bnum } from '../../utils/bignumber'; import { @@ -11,7 +11,7 @@ import { SwapTypes, SubgraphPoolBase, } from '../../types'; -import { isSameAddress } from '../../utils'; +import { isSameAddress, safeParseFixed } from '../../utils'; import { _calculateInvariant, _calcOutGivenIn, @@ -26,10 +26,9 @@ import { _normalizeBalances, _reduceFee, _addFee, - mulDown, - divDown, -} from './helpers'; -import { SWAP_LIMIT_FACTOR } from './constants'; +} from '../gyroHelpers/helpers'; +import { mulDown, divDown } from '../gyroHelpers/gyroSignedFixedPoint'; +import { SWAP_LIMIT_FACTOR } from '../gyroHelpers/constants'; export type Gyro2PoolPairData = PoolPairBase & { sqrtAlpha: BigNumber; @@ -52,10 +51,6 @@ export class Gyro2Pool implements PoolBase { 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.sqrtAlpha || !pool.sqrtBeta) throw new Error( @@ -86,12 +81,12 @@ export class Gyro2Pool implements PoolBase { ) { this.id = id; this.address = address; - this.swapFee = parseFixed(swapFee, 18); - this.totalShares = parseFixed(totalShares, 18); + this.swapFee = safeParseFixed(swapFee, 18); + this.totalShares = safeParseFixed(totalShares, 18); this.tokens = tokens; this.tokensList = tokensList; - this.sqrtAlpha = parseFixed(sqrtAlpha, 18); - this.sqrtBeta = parseFixed(sqrtBeta, 18); + this.sqrtAlpha = safeParseFixed(sqrtAlpha, 18); + this.sqrtBeta = safeParseFixed(sqrtBeta, 18); } parsePoolPairData(tokenIn: string, tokenOut: string): Gyro2PoolPairData { @@ -121,8 +116,8 @@ export class Gyro2Pool implements PoolBase { tokenOut: tokenOut, decimalsIn: Number(decimalsIn), decimalsOut: Number(decimalsOut), - balanceIn: parseFixed(balanceIn, decimalsIn), - balanceOut: parseFixed(balanceOut, decimalsOut), + balanceIn: safeParseFixed(balanceIn, decimalsIn), + balanceOut: safeParseFixed(balanceOut, decimalsOut), swapFee: this.swapFee, sqrtAlpha: tokenInIsToken0 ? this.sqrtAlpha @@ -137,11 +132,10 @@ export class Gyro2Pool implements PoolBase { getNormalizedLiquidity(poolPairData: Gyro2PoolPairData): OldBigNumber { const balances = [poolPairData.balanceIn, poolPairData.balanceOut]; - const normalizedBalances = _normalizeBalances( - balances, + const normalizedBalances = _normalizeBalances(balances, [ poolPairData.decimalsIn, - poolPairData.decimalsOut - ); + poolPairData.decimalsOut, + ]); const invariant = _calculateInvariant( normalizedBalances, poolPairData.sqrtAlpha, @@ -166,11 +160,10 @@ export class Gyro2Pool implements PoolBase { ): OldBigNumber { if (swapType === SwapTypes.SwapExactIn) { const balances = [poolPairData.balanceIn, poolPairData.balanceOut]; - const normalizedBalances = _normalizeBalances( - balances, + const normalizedBalances = _normalizeBalances(balances, [ poolPairData.decimalsIn, - poolPairData.decimalsOut - ); + poolPairData.decimalsOut, + ]); const invariant = _calculateInvariant( normalizedBalances, poolPairData.sqrtAlpha, @@ -224,11 +217,10 @@ export class Gyro2Pool implements PoolBase { ): OldBigNumber { try { const balances = [poolPairData.balanceIn, poolPairData.balanceOut]; - const normalizedBalances = _normalizeBalances( - balances, + const normalizedBalances = _normalizeBalances(balances, [ poolPairData.decimalsIn, - poolPairData.decimalsOut - ); + poolPairData.decimalsOut, + ]); const invariant = _calculateInvariant( normalizedBalances, poolPairData.sqrtAlpha, @@ -239,7 +231,7 @@ export class Gyro2Pool implements PoolBase { poolPairData.sqrtAlpha, poolPairData.sqrtBeta ); - const inAmount = parseFixed(amount.toString(), 18); + const inAmount = safeParseFixed(amount.toString(), 18); const inAmountLessFee = _reduceFee(inAmount, poolPairData.swapFee); const outAmount = _calcOutGivenIn( @@ -261,13 +253,12 @@ export class Gyro2Pool implements PoolBase { amount: OldBigNumber ): OldBigNumber { try { - const outAmount = parseFixed(amount.toString(), 18); + const outAmount = safeParseFixed(amount.toString(), 18); const balances = [poolPairData.balanceIn, poolPairData.balanceOut]; - const normalizedBalances = _normalizeBalances( - balances, + const normalizedBalances = _normalizeBalances(balances, [ poolPairData.decimalsIn, - poolPairData.decimalsOut - ); + poolPairData.decimalsOut, + ]); const invariant = _calculateInvariant( normalizedBalances, poolPairData.sqrtAlpha, @@ -299,11 +290,10 @@ export class Gyro2Pool implements PoolBase { ): OldBigNumber { try { const balances = [poolPairData.balanceIn, poolPairData.balanceOut]; - const normalizedBalances = _normalizeBalances( - balances, + const normalizedBalances = _normalizeBalances(balances, [ poolPairData.decimalsIn, - poolPairData.decimalsOut - ); + poolPairData.decimalsOut, + ]); const invariant = _calculateInvariant( normalizedBalances, poolPairData.sqrtAlpha, @@ -314,7 +304,7 @@ export class Gyro2Pool implements PoolBase { poolPairData.sqrtAlpha, poolPairData.sqrtBeta ); - const inAmount = parseFixed(amount.toString(), 18); + const inAmount = safeParseFixed(amount.toString(), 18); const inAmountLessFee = _reduceFee(inAmount, poolPairData.swapFee); const outAmount = _calcOutGivenIn( normalizedBalances[0], @@ -342,13 +332,12 @@ export class Gyro2Pool implements PoolBase { amount: OldBigNumber ): OldBigNumber { try { - const outAmount = parseFixed(amount.toString(), 18); + const outAmount = safeParseFixed(amount.toString(), 18); const balances = [poolPairData.balanceIn, poolPairData.balanceOut]; - const normalizedBalances = _normalizeBalances( - balances, + const normalizedBalances = _normalizeBalances(balances, [ poolPairData.decimalsIn, - poolPairData.decimalsOut - ); + poolPairData.decimalsOut, + ]); const invariant = _calculateInvariant( normalizedBalances, poolPairData.sqrtAlpha, @@ -388,11 +377,10 @@ export class Gyro2Pool implements PoolBase { ): OldBigNumber { try { const balances = [poolPairData.balanceIn, poolPairData.balanceOut]; - const normalizedBalances = _normalizeBalances( - balances, + const normalizedBalances = _normalizeBalances(balances, [ poolPairData.decimalsIn, - poolPairData.decimalsOut - ); + poolPairData.decimalsOut, + ]); const invariant = _calculateInvariant( normalizedBalances, poolPairData.sqrtAlpha, @@ -403,7 +391,7 @@ export class Gyro2Pool implements PoolBase { poolPairData.sqrtAlpha, poolPairData.sqrtBeta ); - const inAmount = parseFixed(amount.toString(), 18); + const inAmount = safeParseFixed(amount.toString(), 18); const inAmountLessFee = _reduceFee(inAmount, poolPairData.swapFee); const outAmount = _calcOutGivenIn( normalizedBalances[0], @@ -430,13 +418,12 @@ export class Gyro2Pool implements PoolBase { amount: OldBigNumber ): OldBigNumber { try { - const outAmount = parseFixed(amount.toString(), 18); + const outAmount = safeParseFixed(amount.toString(), 18); const balances = [poolPairData.balanceIn, poolPairData.balanceOut]; - const normalizedBalances = _normalizeBalances( - balances, + const normalizedBalances = _normalizeBalances(balances, [ poolPairData.decimalsIn, - poolPairData.decimalsOut - ); + poolPairData.decimalsOut, + ]); const invariant = _calculateInvariant( normalizedBalances, poolPairData.sqrtAlpha, diff --git a/src/pools/gyro3Pool/LICENSE b/src/pools/gyro3Pool/LICENSE new file mode 100644 index 00000000..c506bbda --- /dev/null +++ b/src/pools/gyro3Pool/LICENSE @@ -0,0 +1,5 @@ +(c) 2022 Superluminal Labs Ltd. All rights reserved. + +All moral, intellectual property, and other rights (including rights to all inventions, codes, designs, and protocols) associated with the software code published by Superluminal Labs Ltd. and residing in this repository (this “Software”) are reserved by its right holder(s) except as otherwise provided in this Gyroscope Pool User Contract. This Software has been published for informational purposes and may be run only with the Balancer automated portfolio manager and trading platform solely for testing and internal evaluation purposes only; no license under patents, copyright, trademark, or any other intellectual property right (other than the limited license to run this Software for testing and internal evaluation) is granted or implied. A special hybrid license between Superluminal Labs Ltd and Balancer Labs OÜ governs Superluminal Labs Ltd.'s use of the Balancer Labs OÜ code [Special License](../gyroEPool/GyroscopeBalancerLicense.pdf), which is available at the provided link. + +THE SOFTWARE AND INTELLECTUAL PROPERTY INCLUDED IN THIS SOFTWARE IS PROVIDED BY THE RIGHT HOLDER(S) "AS IS," AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE RIGHT HOLDER(S) BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE OR INTELLECTUAL PROPERTY (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION), HOWEVER CAUSED OR CLAIMED (WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)), EVEN IF SUCH DAMAGES WERE REASONABLY FORESEEABLE OR THE RIGHT HOLDER(S) WERE ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/src/pools/gyro3Pool/constants.ts b/src/pools/gyro3Pool/constants.ts index d5ec0b65..c824d5c5 100644 --- a/src/pools/gyro3Pool/constants.ts +++ b/src/pools/gyro3Pool/constants.ts @@ -1,17 +1,5 @@ import { BigNumber } from '@ethersproject/bignumber'; -// 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 @@ -24,6 +12,3 @@ export const MIDDECIMAL = BigNumber.from(10).pow(9); // splits the fixed point d // 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; - -// Swap Limit factor -export const SWAP_LIMIT_FACTOR = BigNumber.from('999999000000000000'); diff --git a/src/pools/gyro3Pool/gyro3Math.ts b/src/pools/gyro3Pool/gyro3Math.ts index 783e7bb5..c48b6d01 100644 --- a/src/pools/gyro3Pool/gyro3Math.ts +++ b/src/pools/gyro3Pool/gyro3Math.ts @@ -5,14 +5,14 @@ import { _INVARIANT_MIN_ITERATIONS, _INVARIANT_SHRINKING_FACTOR_PER_STEP, } from './constants'; +import { _safeLargePow3ADown } from './helpers'; import { mulUp, divUp, mulDown, divDown, - newtonSqrt, - _safeLargePow3ADown, -} from './helpers'; + sqrt, +} from '../gyroHelpers/gyroSignedFixedPoint'; ///////// /// Invariant Calculation @@ -85,7 +85,7 @@ export function _calculateCubicStartingPoint( ): BigNumber { 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)) + divUp(sqrt(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. diff --git a/src/pools/gyro3Pool/gyro3Pool.ts b/src/pools/gyro3Pool/gyro3Pool.ts index 986fe257..84fdcb95 100644 --- a/src/pools/gyro3Pool/gyro3Pool.ts +++ b/src/pools/gyro3Pool/gyro3Pool.ts @@ -1,6 +1,6 @@ import { getAddress } from '@ethersproject/address'; import { WeiPerEther as ONE } from '@ethersproject/constants'; -import { parseFixed, formatFixed, BigNumber } from '@ethersproject/bignumber'; +import { formatFixed, BigNumber } from '@ethersproject/bignumber'; import { BigNumber as OldBigNumber, bnum } from '../../utils/bignumber'; import { @@ -11,7 +11,7 @@ import { SwapTypes, SubgraphPoolBase, } from '../../types'; -import { isSameAddress } from '../../utils'; +import { isSameAddress, safeParseFixed } from '../../utils'; import { _calculateInvariant, _calcOutGivenIn, @@ -26,10 +26,9 @@ import { _normalizeBalances, _reduceFee, _addFee, - mulDown, - divDown, -} from './helpers'; -import { SWAP_LIMIT_FACTOR } from './constants'; +} from '../gyroHelpers/helpers'; +import { mulDown, divDown } from '../gyroHelpers/gyroSignedFixedPoint'; +import { SWAP_LIMIT_FACTOR } from '../gyroHelpers/constants'; export type Gyro3PoolPairData = PoolPairBase & { balanceTertiary: BigNumber; // Balance of the unchanged asset @@ -51,10 +50,6 @@ export class Gyro3Pool implements PoolBase { totalShares: BigNumber; root3Alpha: BigNumber; - // Max In/Out Ratios - MAX_IN_RATIO = parseFixed('0.3', 18); - MAX_OUT_RATIO = parseFixed('0.3', 18); - private static findToken(list, tokenAddress, error) { const token = list.find( (t) => getAddress(t.address) === getAddress(tokenAddress) @@ -67,8 +62,8 @@ export class Gyro3Pool implements PoolBase { if (!pool.root3Alpha) throw new Error('Pool missing root3Alpha'); if ( - parseFixed(pool.root3Alpha, 18).lte(0) || - parseFixed(pool.root3Alpha, 18).gte(ONE) + safeParseFixed(pool.root3Alpha, 18).lte(0) || + safeParseFixed(pool.root3Alpha, 18).gte(ONE) ) throw new Error('Invalid root3Alpha parameter'); @@ -97,11 +92,11 @@ export class Gyro3Pool implements PoolBase { ) { this.id = id; this.address = address; - this.swapFee = parseFixed(swapFee, 18); - this.totalShares = parseFixed(totalShares, 18); + this.swapFee = safeParseFixed(swapFee, 18); + this.totalShares = safeParseFixed(totalShares, 18); this.tokens = tokens; this.tokensList = tokensList; - this.root3Alpha = parseFixed(root3Alpha, 18); + this.root3Alpha = safeParseFixed(root3Alpha, 18); } parsePoolPairData(tokenIn: string, tokenOut: string): Gyro3PoolPairData { @@ -142,9 +137,9 @@ export class Gyro3Pool implements PoolBase { decimalsIn: Number(decimalsIn), decimalsOut: Number(decimalsOut), decimalsTertiary: Number(decimalsTertiary), - balanceIn: parseFixed(balanceIn, decimalsIn), - balanceOut: parseFixed(balanceOut, decimalsOut), - balanceTertiary: parseFixed(balanceTertiary, decimalsTertiary), + balanceIn: safeParseFixed(balanceIn, decimalsIn), + balanceOut: safeParseFixed(balanceOut, decimalsOut), + balanceTertiary: safeParseFixed(balanceTertiary, decimalsTertiary), swapFee: this.swapFee, }; @@ -266,7 +261,7 @@ export class Gyro3Pool implements PoolBase { ); const virtualOffsetInOut = mulDown(invariant, this.root3Alpha); - const inAmount = parseFixed(amount.toString(), 18); + const inAmount = safeParseFixed(amount.toString(), 18); const inAmountLessFee = _reduceFee(inAmount, poolPairData.swapFee); const outAmount = _calcOutGivenIn( @@ -286,7 +281,7 @@ export class Gyro3Pool implements PoolBase { amount: OldBigNumber ): OldBigNumber { try { - const outAmount = parseFixed(amount.toString(), 18); + const outAmount = safeParseFixed(amount.toString(), 18); const balances = [ poolPairData.balanceIn, poolPairData.balanceOut, @@ -344,7 +339,7 @@ export class Gyro3Pool implements PoolBase { const virtualOffsetInOut = mulDown(invariant, this.root3Alpha); - const inAmount = parseFixed(amount.toString(), 18); + const inAmount = safeParseFixed(amount.toString(), 18); const inAmountLessFee = _reduceFee(inAmount, poolPairData.swapFee); const outAmount = _calcOutGivenIn( @@ -372,7 +367,7 @@ export class Gyro3Pool implements PoolBase { amount: OldBigNumber ): OldBigNumber { try { - const outAmount = parseFixed(amount.toString(), 18); + const outAmount = safeParseFixed(amount.toString(), 18); const balances = [ poolPairData.balanceIn, poolPairData.balanceOut, @@ -438,7 +433,7 @@ export class Gyro3Pool implements PoolBase { const virtualOffsetInOut = mulDown(invariant, this.root3Alpha); - const inAmount = parseFixed(amount.toString(), 18); + const inAmount = safeParseFixed(amount.toString(), 18); const inAmountLessFee = _reduceFee(inAmount, poolPairData.swapFee); const outAmount = _calcOutGivenIn( @@ -465,7 +460,7 @@ export class Gyro3Pool implements PoolBase { amount: OldBigNumber ): OldBigNumber { try { - const outAmount = parseFixed(amount.toString(), 18); + const outAmount = safeParseFixed(amount.toString(), 18); const balances = [ poolPairData.balanceIn, poolPairData.balanceOut, diff --git a/src/pools/gyro3Pool/helpers.ts b/src/pools/gyro3Pool/helpers.ts index a4eb0a5b..f8a1ef97 100644 --- a/src/pools/gyro3Pool/helpers.ts +++ b/src/pools/gyro3Pool/helpers.ts @@ -1,148 +1,8 @@ -import { BigNumber, parseFixed } from '@ethersproject/bignumber'; -import bn from 'bignumber.js'; +import { BigNumber } 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, - _SAFE_LARGE_POW3_THRESHOLD, - MIDDECIMAL, -} from './constants'; +import { _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, @@ -189,30 +49,3 @@ export function _safeLargePow3ADown( } 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/gyroEPool/GyroscopeBalancerLicense.pdf b/src/pools/gyroEPool/GyroscopeBalancerLicense.pdf new file mode 100644 index 00000000..531fd47e Binary files /dev/null and b/src/pools/gyroEPool/GyroscopeBalancerLicense.pdf differ diff --git a/src/pools/gyroEPool/LICENSE b/src/pools/gyroEPool/LICENSE new file mode 100644 index 00000000..f22ac9e4 --- /dev/null +++ b/src/pools/gyroEPool/LICENSE @@ -0,0 +1,5 @@ +(c) 2022 Superluminal Labs Ltd. All rights reserved. + +All moral, intellectual property, and other rights (including rights to all inventions, codes, designs, and protocols) associated with the software code published by Superluminal Labs Ltd. and residing in this repository (this “Software”) are reserved by its right holder(s) except as otherwise provided in this Gyroscope Pool User Contract. This Software has been published for informational purposes and may be run only with the Balancer automated portfolio manager and trading platform solely for testing and internal evaluation purposes only; no license under patents, copyright, trademark, or any other intellectual property right (other than the limited license to run this Software for testing and internal evaluation) is granted or implied. A special hybrid license between Superluminal Labs Ltd and Balancer Labs OÜ governs Superluminal Labs Ltd.'s use of the Balancer Labs OÜ code [Special License](./GyroscopeBalancerLicense.pdf), which is available at the provided link. + +THE SOFTWARE AND INTELLECTUAL PROPERTY INCLUDED IN THIS SOFTWARE IS PROVIDED BY THE RIGHT HOLDER(S) "AS IS," AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE RIGHT HOLDER(S) BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE OR INTELLECTUAL PROPERTY (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION), HOWEVER CAUSED OR CLAIMED (WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)), EVEN IF SUCH DAMAGES WERE REASONABLY FORESEEABLE OR THE RIGHT HOLDER(S) WERE ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/src/pools/gyroEPool/gyroEMath/constants.ts b/src/pools/gyroEPool/gyroEMath/constants.ts new file mode 100644 index 00000000..558f164e --- /dev/null +++ b/src/pools/gyroEPool/gyroEMath/constants.ts @@ -0,0 +1,6 @@ +import { BigNumber } from '@ethersproject/bignumber'; + +export const MAX_BALANCES = BigNumber.from(10).pow(34); // 1e16 in normal precision + +// Invariant calculation +export const MAX_INVARIANT = BigNumber.from(10).pow(37).mul(3); // 3e19 in normal precision diff --git a/src/pools/gyroEPool/gyroEMath/gyroEMath.ts b/src/pools/gyroEPool/gyroEMath/gyroEMath.ts new file mode 100644 index 00000000..839587af --- /dev/null +++ b/src/pools/gyroEPool/gyroEMath/gyroEMath.ts @@ -0,0 +1,248 @@ +import { BigNumber, formatFixed } from '@ethersproject/bignumber'; +import { WeiPerEther as ONE } from '@ethersproject/constants'; +import { MAX_BALANCES, MAX_INVARIANT } from './constants'; +import { ONE_XP, SMALL } from '../../gyroHelpers/constants'; +import { + GyroEParams, + DerivedGyroEParams, + Vector2, + calcAtAChi, + calcInvariantSqrt, + calcAChiAChiInXp, + calcXGivenY, + calcYGivenX, + checkAssetBounds, +} from './gyroEMathHelpers'; +import { + mulDown, + divDown, + mulUpMagU, + divUpMagU, + mulUpXpToNpU, + mulDownXpToNpU, + divXpU, + sqrt, +} from '../../gyroHelpers/gyroSignedFixedPoint'; +import { + normalizedLiquidityXIn, + normalizedLiquidityYIn, + calcSpotPriceXGivenY, + calcSpotPriceYGivenX, + dPxDXOut, + dPxDYIn, + dPyDXIn, + dPyDYOut, +} from './gyroEMathFunctions'; + +export function calculateNormalizedLiquidity( + balances: BigNumber[], + params: GyroEParams, + derived: DerivedGyroEParams, + r: Vector2, + fee: BigNumber, + tokenInIsToken0: boolean +) { + if (tokenInIsToken0) { + return normalizedLiquidityXIn(balances, params, derived, fee, r); + } else { + return normalizedLiquidityYIn(balances, params, derived, fee, r); + } +} + +export function calculateInvariantWithError( + balances: BigNumber[], + params: GyroEParams, + derived: DerivedGyroEParams +) { + const [x, y] = balances; + + if (x.add(y).gt(MAX_BALANCES)) throw new Error('MAX ASSETS EXCEEDED'); + const AtAChi = calcAtAChi(x, y, params, derived); + + let [square_root, err] = calcInvariantSqrt(x, y, params, derived); + + if (square_root.gt(0)) { + err = divUpMagU(err.add(1), square_root.mul(2)); + } else { + err = err.gt(0) + ? sqrt(err, BigNumber.from(5)) + : BigNumber.from(10).pow(9); + } + + err = mulUpMagU(params.lambda, x.add(y)) + .div(ONE_XP) + .add(err) + .add(1) + .mul(20); + + const mulDenominator = divXpU( + ONE_XP, + calcAChiAChiInXp(params, derived).sub(ONE_XP) + ); + const invariant = mulDownXpToNpU( + AtAChi.add(square_root).sub(err), + mulDenominator + ); + err = mulUpXpToNpU(err, mulDenominator); + + err = err + .add( + mulUpXpToNpU(invariant, mulDenominator) + .mul( + params.lambda + .mul(params.lambda) + .div(BigNumber.from(10).pow(36)) + ) + .mul(40) + .div(ONE_XP) + ) + .add(1); + + if (invariant.add(err).gt(MAX_INVARIANT)) + throw new Error('MAX INVARIANT EXCEEDED'); + + return [invariant, err]; +} + +export function calcOutGivenIn( + balances: BigNumber[], + amountIn: BigNumber, + tokenInIsToken0: boolean, + params: GyroEParams, + derived: DerivedGyroEParams, + invariant: Vector2 +): BigNumber { + if (amountIn.lt(SMALL)) return BigNumber.from(0); + + const ixIn = Number(!tokenInIsToken0); + const ixOut = Number(tokenInIsToken0); + + const calcGiven = tokenInIsToken0 ? calcYGivenX : calcXGivenY; + + const balInNew = balances[ixIn].add(amountIn); + + checkAssetBounds(params, derived, invariant, balInNew, ixIn); + const balOutNew = calcGiven(balInNew, params, derived, invariant); + const amountOut = balances[ixOut].sub(balOutNew); + if (amountOut.lt(0)) { + // Should never happen; check anyways to catch a numerical bug. + throw new Error('ASSET BOUNDS EXCEEDED 1'); + } + + return amountOut; +} + +export function calcInGivenOut( + balances: BigNumber[], + amountOut: BigNumber, + tokenInIsToken0: boolean, + params: GyroEParams, + derived: DerivedGyroEParams, + invariant: Vector2 +): BigNumber { + if (amountOut.lt(SMALL)) return BigNumber.from(0); + + const ixIn = Number(!tokenInIsToken0); + const ixOut = Number(tokenInIsToken0); + + const calcGiven = tokenInIsToken0 ? calcXGivenY : calcYGivenX; + + if (amountOut.gt(balances[ixOut])) + throw new Error('ASSET BOUNDS EXCEEDED 2'); + const balOutNew = balances[ixOut].sub(amountOut); + + const balInNew = calcGiven(balOutNew, params, derived, invariant); + checkAssetBounds(params, derived, invariant, balInNew, ixIn); + const amountIn = balInNew.sub(balances[ixIn]); + + if (amountIn.lt(0)) + // Should never happen; check anyways to catch a numerical bug. + throw new Error('ASSET BOUNDS EXCEEDED 3'); + return amountIn; +} + +export function calcSpotPriceAfterSwapOutGivenIn( + balances: BigNumber[], + amountIn: BigNumber, + tokenInIsToken0: boolean, + params: GyroEParams, + derived: DerivedGyroEParams, + invariant: Vector2, + swapFee: BigNumber +): BigNumber { + const ixIn = Number(!tokenInIsToken0); + const f = ONE.sub(swapFee); + + const calcSpotPriceGiven = tokenInIsToken0 + ? calcSpotPriceYGivenX + : calcSpotPriceXGivenY; + + const balInNew = balances[ixIn].add(amountIn); + const newSpotPriceFactor = calcSpotPriceGiven( + balInNew, + params, + derived, + invariant + ); + return divDown(ONE, mulDown(newSpotPriceFactor, f)); +} + +export function calcSpotPriceAfterSwapInGivenOut( + balances: BigNumber[], + amountOut: BigNumber, + tokenInIsToken0: boolean, + params: GyroEParams, + derived: DerivedGyroEParams, + invariant: Vector2, + swapFee: BigNumber +): BigNumber { + const ixOut = Number(tokenInIsToken0); + const f = ONE.sub(swapFee); + + const calcSpotPriceGiven = tokenInIsToken0 + ? calcSpotPriceXGivenY + : calcSpotPriceYGivenX; + + const balOutNew = balances[ixOut].sub(amountOut); + const newSpotPriceFactor = calcSpotPriceGiven( + balOutNew, + params, + derived, + invariant + ); + return divDown(newSpotPriceFactor, f); +} + +export function calcDerivativePriceAfterSwapOutGivenIn( + balances: BigNumber[], + tokenInIsToken0: boolean, + params: GyroEParams, + derived: DerivedGyroEParams, + invariant: Vector2, + swapFee: BigNumber +): BigNumber { + const ixIn = Number(!tokenInIsToken0); + + const newDerivativeSpotPriceFactor = ixIn + ? dPxDYIn(balances, params, derived, swapFee, invariant) + : dPyDXIn(balances, params, derived, swapFee, invariant); + + return newDerivativeSpotPriceFactor; +} + +export function calcDerivativeSpotPriceAfterSwapInGivenOut( + balances: BigNumber[], + tokenInIsToken0: boolean, + params: GyroEParams, + derived: DerivedGyroEParams, + invariant: Vector2, + swapFee: BigNumber +): BigNumber { + const ixIn = Number(!tokenInIsToken0); + + const newDerivativeSpotPriceFactor = ixIn + ? dPxDXOut(balances, params, derived, swapFee, invariant) + : dPyDYOut(balances, params, derived, swapFee, invariant); + + return newDerivativeSpotPriceFactor; +} diff --git a/src/pools/gyroEPool/gyroEMath/gyroEMathFunctions.ts b/src/pools/gyroEPool/gyroEMath/gyroEMathFunctions.ts new file mode 100644 index 00000000..0283a837 --- /dev/null +++ b/src/pools/gyroEPool/gyroEMath/gyroEMathFunctions.ts @@ -0,0 +1,411 @@ +import { BigNumber } from '@ethersproject/bignumber'; +import { WeiPerEther as ONE } from '@ethersproject/constants'; +import { + GyroEParams, + DerivedGyroEParams, + Vector2, + QParams, + virtualOffset0, + virtualOffset1, +} from './gyroEMathHelpers'; +import { ONE_XP } from '../../gyroHelpers/constants'; +import { + mulDown, + divDown, + mulDownMagU, + divDownMagU, + mulUpMagU, + divUpMagU, + mulUpXpToNpU, + mulDownXpToNpU, + divXpU, + sqrt, +} from '../../gyroHelpers/gyroSignedFixedPoint'; +import { calcXpXpDivLambdaLambda } from './gyroEMathHelpers'; + +///////// +/// SPOT PRICE AFTER SWAP CALCULATIONS +///////// + +export function calcSpotPriceYGivenX( + x: BigNumber, + params: GyroEParams, + d: DerivedGyroEParams, + r: Vector2 +) { + const ab: Vector2 = { + x: virtualOffset0(params, d, r), + y: virtualOffset1(params, d, r), + }; + const newSpotPriceFactor = solveDerivativeQuadraticSwap( + params.lambda, + x, + params.s, + params.c, + r, + ab, + d.tauBeta, + d.dSq + ); + return newSpotPriceFactor; +} + +export function calcSpotPriceXGivenY( + y: BigNumber, + params: GyroEParams, + d: DerivedGyroEParams, + r: Vector2 +) { + const ba: Vector2 = { + x: virtualOffset1(params, d, r), + y: virtualOffset0(params, d, r), + }; + const newSpotPriceFactor = solveDerivativeQuadraticSwap( + params.lambda, + y, + params.c, + params.s, + r, + ba, + { + x: d.tauAlpha.x.mul(-1), + y: d.tauAlpha.y, + }, + d.dSq + ); + return newSpotPriceFactor; +} + +function solveDerivativeQuadraticSwap( + lambda: BigNumber, + x: BigNumber, + s: BigNumber, + c: BigNumber, + r: Vector2, + ab: Vector2, + tauBeta: Vector2, + dSq: BigNumber +): BigNumber { + const lamBar: Vector2 = { + x: ONE_XP.sub(divDownMagU(divDownMagU(ONE_XP, lambda), lambda)), + y: ONE_XP.sub(divUpMagU(divUpMagU(ONE_XP, lambda), lambda)), + }; + const q: QParams = { + a: BigNumber.from(0), + b: BigNumber.from(0), + c: BigNumber.from(0), + }; + const xp = x.sub(ab.x); + q.b = mulUpXpToNpU(mulDownMagU(s, c), divXpU(lamBar.y, dSq)); + + const sTerm: Vector2 = { + x: divXpU(mulDownMagU(mulDownMagU(lamBar.y, s), s), dSq), + y: divXpU(mulUpMagU(mulUpMagU(lamBar.x, s), s), dSq.add(1)).add(1), + }; + sTerm.x = ONE_XP.sub(sTerm.x); + sTerm.y = ONE_XP.sub(sTerm.y); + + q.c = calcXpXpDivLambdaLambda(x, r, lambda, s, c, tauBeta, dSq).mul(-1); + q.c = q.c.add(mulDownXpToNpU(mulDownMagU(r.y, r.y), sTerm.y)); // r.y === currentInv + err + q.c = q.c.gt(BigNumber.from(0)) + ? sqrt(q.c, BigNumber.from(5)) + : BigNumber.from(0); + + q.c = mulDown(mulDown(q.c, lambda), lambda); + q.c = divDown(xp, q.c); + + if (q.b.sub(q.c).gt(BigNumber.from(0))) { + q.a = mulUpXpToNpU(q.b.sub(q.c), divXpU(ONE_XP, sTerm.y).add(1)); + } else { + q.a = mulUpXpToNpU(q.b.sub(q.c), divXpU(ONE_XP, sTerm.x)); + } + return q.a; +} + +///////// +/// SPOT PRICE DERIVATIVE CALCULATIONS +///////// + +function setup( + balances, + params: GyroEParams, + derived: DerivedGyroEParams, + fee: BigNumber, + rVec: Vector2, + ixVar: number +) { + const r = rVec.y; + const { c, s, lambda } = params; + const [x0, y0] = balances; + const a = virtualOffset0(params, derived, rVec); + const b = virtualOffset1(params, derived, rVec); + const ls = ONE.sub(divDown(ONE, mulDown(lambda, lambda))); + const f = ONE.sub(fee); + + let R: BigNumber; + if (ixVar === 0) { + R = sqrt( + mulDown(mulDown(r, r), ONE.sub(mulDown(ls, mulDown(s, s)))).sub( + divDown(mulDown(x0.sub(a), x0.sub(a)), mulDown(lambda, lambda)) + ), + BigNumber.from(5) + ); + } else { + R = sqrt( + mulDown(mulDown(r, r), ONE.sub(mulDown(ls, mulDown(c, c)))).sub( + divDown(mulDown(y0.sub(b), y0.sub(b)), mulDown(lambda, lambda)) + ), + BigNumber.from(5) + ); + } + + return { x0, y0, c, s, lambda, a, b, ls, f, r, R }; +} + +export function normalizedLiquidityYIn( + balances: BigNumber[], + params: GyroEParams, + derived: DerivedGyroEParams, + fee: BigNumber, + rVec: Vector2 +) { + const { y0, c, s, lambda, b, ls, R } = setup( + balances, + params, + derived, + fee, + rVec, + 1 + ); + + const returnValue = divDown( + mulDown( + divDown(ONE, ONE.sub(mulDown(ls, mulDown(c, c)))), + mulDown( + R, + mulDown( + mulDown( + mulDown( + mulDown(mulDown(ls, s), c), + mulDown(lambda, lambda) + ), + R + ).sub(y0.sub(b)), + mulDown( + mulDown( + mulDown(mulDown(ls, s), c), + mulDown(lambda, lambda) + ), + R + ).sub(y0.sub(b)) + ) + ) + ), + mulDown(mulDown(lambda, lambda), mulDown(R, R)).add( + mulDown(y0.sub(b), y0.sub(b)) + ) + ); + + return returnValue; +} + +export function normalizedLiquidityXIn( + balances: BigNumber[], + params: GyroEParams, + derived: DerivedGyroEParams, + fee: BigNumber, + rVec: Vector2 +) { + const { x0, c, s, lambda, a, ls, R } = setup( + balances, + params, + derived, + fee, + rVec, + 0 + ); + + const returnValue = divDown( + mulDown( + divDown(ONE, ONE.sub(mulDown(ls, mulDown(s, s)))), + mulDown( + R, + mulDown( + mulDown( + mulDown( + mulDown(mulDown(ls, s), c), + mulDown(lambda, lambda) + ), + R + ).sub(x0.sub(a)), + mulDown( + mulDown( + mulDown(mulDown(ls, s), c), + mulDown(lambda, lambda) + ), + R + ).sub(x0.sub(a)) + ) + ) + ), + mulDown(mulDown(lambda, lambda), mulDown(R, R)).add( + mulDown(x0.sub(a), x0.sub(a)) + ) + ); + + return returnValue; +} + +export function dPyDXIn( + balances: BigNumber[], + params: GyroEParams, + derived: DerivedGyroEParams, + fee: BigNumber, + rVec: Vector2 +) { + const { x0, c, s, lambda, a, ls, R } = setup( + balances, + params, + derived, + fee, + rVec, + 0 + ); + + const returnValue = divDown( + mulDown( + ONE.sub(mulDown(ls, mulDown(s, s))), + divDown(ONE, mulDown(mulDown(lambda, lambda), R)).add( + divDown( + mulDown(x0.sub(a), x0.sub(a)), + mulDown( + mulDown( + mulDown(lambda, lambda), + mulDown(lambda, lambda) + ), + mulDown(R, mulDown(R, R)) + ) + ) + ) + ), + mulDown( + mulDown(mulDown(ls, s), c).sub( + divDown(x0.sub(a), mulDown(mulDown(lambda, lambda), R)) + ), + mulDown(mulDown(ls, s), c).sub( + divDown(x0.sub(a), mulDown(mulDown(lambda, lambda), R)) + ) + ) + ); + + return returnValue; +} + +export function dPxDYIn( + balances: BigNumber[], + params: GyroEParams, + derived: DerivedGyroEParams, + fee: BigNumber, + rVec: Vector2 +) { + const { y0, c, s, lambda, b, ls, R } = setup( + balances, + params, + derived, + fee, + rVec, + 1 + ); + + const returnValue = divDown( + mulDown( + ONE.sub(mulDown(ls, mulDown(c, c))), + divDown(ONE, mulDown(mulDown(lambda, lambda), R)).add( + divDown( + mulDown(y0.sub(b), y0.sub(b)), + mulDown( + mulDown( + mulDown(lambda, lambda), + mulDown(lambda, lambda) + ), + mulDown(R, mulDown(R, R)) + ) + ) + ) + ), + mulDown( + mulDown(mulDown(ls, s), c).sub( + divDown(y0.sub(b), mulDown(mulDown(lambda, lambda), R)) + ), + mulDown(mulDown(ls, s), c).sub( + divDown(y0.sub(b), mulDown(mulDown(lambda, lambda), R)) + ) + ) + ); + + return returnValue; +} + +export function dPxDXOut( + balances: BigNumber[], + params: GyroEParams, + derived: DerivedGyroEParams, + fee: BigNumber, + rVec: Vector2 +) { + const { x0, s, lambda, a, ls, R, f } = setup( + balances, + params, + derived, + fee, + rVec, + 0 + ); + + const returnValue = mulDown( + divDown(ONE, mulDown(f, ONE.sub(mulDown(ls, mulDown(s, s))))), + divDown(ONE, mulDown(mulDown(lambda, lambda), R)).add( + divDown( + mulDown(x0.sub(a), x0.sub(a)), + mulDown( + mulDown(mulDown(lambda, lambda), mulDown(lambda, lambda)), + mulDown(mulDown(R, R), R) + ) + ) + ) + ); + + return returnValue; +} + +export function dPyDYOut( + balances: BigNumber[], + params: GyroEParams, + derived: DerivedGyroEParams, + fee: BigNumber, + rVec: Vector2 +) { + const { y0, c, lambda, b, ls, R, f } = setup( + balances, + params, + derived, + fee, + rVec, + 1 + ); + + const returnValue = mulDown( + divDown(ONE, mulDown(f, ONE.sub(mulDown(ls, mulDown(c, c))))), + divDown(ONE, mulDown(mulDown(lambda, lambda), R)).add( + divDown( + mulDown(y0.sub(b), y0.sub(b)), + mulDown( + mulDown(mulDown(lambda, lambda), mulDown(lambda, lambda)), + mulDown(mulDown(R, R), R) + ) + ) + ) + ); + + return returnValue; +} diff --git a/src/pools/gyroEPool/gyroEMath/gyroEMathHelpers.ts b/src/pools/gyroEPool/gyroEMath/gyroEMathHelpers.ts new file mode 100644 index 00000000..c0b6f2a0 --- /dev/null +++ b/src/pools/gyroEPool/gyroEMath/gyroEMathHelpers.ts @@ -0,0 +1,498 @@ +import { BigNumber, formatFixed, parseFixed } from '@ethersproject/bignumber'; +import { WeiPerEther as ONE } from '@ethersproject/constants'; +import { ONE_XP } from '../../gyroHelpers/constants'; +import { + mulDown, + divDown, + mulDownMagU, + divDownMagU, + mulUpMagU, + divUpMagU, + mulUpXpToNpU, + mulDownXpToNpU, + mulXpU, + divXpU, + sqrt, +} from '../../gyroHelpers/gyroSignedFixedPoint'; +import { MAX_BALANCES } from './constants'; + +///////// +/// TYPES +///////// + +export type GyroEParams = { + alpha: BigNumber; + beta: BigNumber; + c: BigNumber; + s: BigNumber; + lambda: BigNumber; +}; + +// terms in this struct are stored in extra precision (38 decimals) with final decimal rounded down +export type DerivedGyroEParams = { + tauAlpha: Vector2; + tauBeta: Vector2; + u: BigNumber; + v: BigNumber; + w: BigNumber; + z: BigNumber; + dSq: BigNumber; +}; + +export type Vector2 = { + x: BigNumber; + y: BigNumber; +}; + +export type QParams = { + a: BigNumber; + b: BigNumber; + c: BigNumber; +}; + +///////// +/// FEE CALCULATION +///////// + +export function reduceFee(amountIn: BigNumber, swapFee: BigNumber): BigNumber { + const feeAmount = mulDown(amountIn, swapFee); + return amountIn.sub(feeAmount); +} + +export function addFee(amountIn: BigNumber, swapFee: BigNumber): BigNumber { + return divDown(amountIn, ONE.sub(swapFee)); +} + +//////// +/// BALANCE CALCULATION +//////// +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]) + ); +} + +export function balancesFromTokenInOut( + balanceTokenIn: BigNumber, + balanceTokenOut: BigNumber, + tokenInIsToken0: boolean +) { + return tokenInIsToken0 + ? [balanceTokenIn, balanceTokenOut] + : [balanceTokenOut, balanceTokenIn]; +} + +///////// +/// INVARIANT CALC +///////// + +export function calcAtAChi( + x: BigNumber, + y: BigNumber, + p: GyroEParams, + d: DerivedGyroEParams +) { + const dSq2 = mulXpU(d.dSq, d.dSq); + + // (cx - sy) * (w/lambda + z) / lambda + // account for 2 factors of dSq (4 s,c factors) + const termXp = divXpU( + divDownMagU(divDownMagU(d.w, p.lambda).add(d.z), p.lambda), + dSq2 + ); + + let val = mulDownXpToNpU( + mulDownMagU(x, p.c).sub(mulDownMagU(y, p.s)), + termXp + ); + + // (x lambda s + y lambda c) * u, note u > 0 + let termNp = mulDownMagU(mulDownMagU(x, p.lambda), p.s).add( + mulDownMagU(mulDownMagU(y, p.lambda), p.c) + ); + val = val.add(mulDownXpToNpU(termNp, divXpU(d.u, dSq2))); + + // (sx+cy) * v, note v > 0 + termNp = mulDownMagU(x, p.s).add(mulDownMagU(y, p.c)); + val = val.add(mulDownXpToNpU(termNp, divXpU(d.v, dSq2))); + + return val; +} + +export function calcInvariantSqrt( + x: BigNumber, + y: BigNumber, + p: GyroEParams, + d: DerivedGyroEParams +) { + let val = calcMinAtxAChiySqPlusAtxSq(x, y, p, d).add( + calc2AtxAtyAChixAChiy(x, y, p, d) + ); + val = val.add(calcMinAtyAChixSqPlusAtySq(x, y, p, d)); + const err = mulUpMagU(x, x).add(mulUpMagU(y, y)).div(ONE_XP); + val = val.gt(0) ? sqrt(val, BigNumber.from(5)) : BigNumber.from(0); + return [val, err]; +} + +function calcMinAtxAChiySqPlusAtxSq( + x: BigNumber, + y: BigNumber, + p: GyroEParams, + d: DerivedGyroEParams +) { + let termNp = mulUpMagU(mulUpMagU(mulUpMagU(x, x), p.c), p.c).add( + mulUpMagU(mulUpMagU(mulUpMagU(y, y), p.s), p.s) + ); + termNp = termNp.sub( + mulDownMagU(mulDownMagU(mulDownMagU(x, y), p.c.mul(2)), p.s) + ); + let termXp = mulXpU(d.u, d.u) + .add(divDownMagU(mulXpU(d.u.mul(2), d.v), p.lambda)) + .add(divDownMagU(divDownMagU(mulXpU(d.v, d.v), p.lambda), p.lambda)); + + let val = mulDownXpToNpU(termNp.mul(-1), termXp); + val = val.add( + mulDownXpToNpU( + divDownMagU(divDownMagU(termNp.sub(9), p.lambda), p.lambda), + divXpU(ONE_XP, d.dSq) + ) + ); + return val; +} + +function calc2AtxAtyAChixAChiy( + x: BigNumber, + y: BigNumber, + p: GyroEParams, + d: DerivedGyroEParams +) { + let termNp = mulDownMagU( + mulDownMagU(mulDownMagU(x, x).sub(mulUpMagU(y, y)), p.c.mul(2)), + p.s + ); + + const xy = mulDownMagU(y, x.mul(2)); + termNp = termNp + .add(mulDownMagU(mulDownMagU(xy, p.c), p.c)) + .sub(mulDownMagU(mulDownMagU(xy, p.s), p.s)); + let termXp = mulXpU(d.z, d.u).add( + divDownMagU(divDownMagU(mulXpU(d.w, d.v), p.lambda), p.lambda) + ); + termXp = termXp.add( + divDownMagU(mulXpU(d.w, d.u).add(mulXpU(d.z, d.v)), p.lambda) + ); + termXp = divXpU(termXp, mulXpU(mulXpU(mulXpU(d.dSq, d.dSq), d.dSq), d.dSq)); + const val = mulDownXpToNpU(termNp, termXp); + return val; +} + +function calcMinAtyAChixSqPlusAtySq( + x: BigNumber, + y: BigNumber, + p: GyroEParams, + d: DerivedGyroEParams +) { + let termNp = mulUpMagU(mulUpMagU(mulUpMagU(x, x), p.s), p.s).add( + mulUpMagU(mulUpMagU(mulUpMagU(y, y), p.c), p.c) + ); + termNp = termNp.add(mulUpMagU(mulUpMagU(mulUpMagU(x, y), p.s.mul(2)), p.c)); + let termXp = mulXpU(d.z, d.z).add( + divDownMagU(divDownMagU(mulXpU(d.w, d.w), p.lambda), p.lambda) + ); + termXp = termXp.add(divDownMagU(mulXpU(d.z.mul(2), d.w), p.lambda)); + termXp = divXpU(termXp, mulXpU(mulXpU(mulXpU(d.dSq, d.dSq), d.dSq), d.dSq)); + let val = mulDownXpToNpU(termNp.mul(-1), termXp); + val = val.add(mulDownXpToNpU(termNp.sub(9), divXpU(ONE_XP, d.dSq))); + return val; +} + +export function calcAChiAChiInXp(p: GyroEParams, d: DerivedGyroEParams) { + const dSq3 = mulXpU(mulXpU(d.dSq, d.dSq), d.dSq); + let val = mulUpMagU(p.lambda, divXpU(mulXpU(d.u.mul(2), d.v), dSq3)); + val = val.add( + mulUpMagU( + mulUpMagU(divXpU(mulXpU(d.u.add(1), d.u.add(1)), dSq3), p.lambda), + p.lambda + ) + ); + val = val.add(divXpU(mulXpU(d.v, d.v), dSq3)); + const termXp = divUpMagU(d.w, p.lambda).add(d.z); + val = val.add(divXpU(mulXpU(termXp, termXp), dSq3)); + return val; +} + +///////// +/// SWAP AMOUNT CALC +///////// + +export function checkAssetBounds( + params: GyroEParams, + derived: DerivedGyroEParams, + invariant: Vector2, + newBal: BigNumber, + assetIndex: number +) { + if (assetIndex === 0) { + const xPlus = maxBalances0(params, derived, invariant); + if (newBal.gt(MAX_BALANCES) || newBal.gt(xPlus)) + throw new Error('ASSET BOUNDS EXCEEDED'); + } else { + const yPlus = maxBalances1(params, derived, invariant); + if (newBal.gt(MAX_BALANCES) || newBal.gt(yPlus)) + throw new Error('ASSET BOUNDS EXCEEDED'); + } +} + +function maxBalances0(p: GyroEParams, d: DerivedGyroEParams, r: Vector2) { + const termXp1 = divXpU(d.tauBeta.x.sub(d.tauAlpha.x), d.dSq); + const termXp2 = divXpU(d.tauBeta.y.sub(d.tauAlpha.y), d.dSq); + let xp = mulDownXpToNpU( + mulDownMagU(mulDownMagU(r.y, p.lambda), p.c), + termXp1 + ); + xp = xp.add( + termXp2.gt(BigNumber.from(0)) + ? mulDownMagU(r.y, p.s) + : mulDownXpToNpU(mulUpMagU(r.x, p.s), termXp2) + ); + return xp; +} + +function maxBalances1(p: GyroEParams, d: DerivedGyroEParams, r: Vector2) { + const termXp1 = divXpU(d.tauBeta.x.sub(d.tauAlpha.x), d.dSq); + const termXp2 = divXpU(d.tauBeta.y.sub(d.tauAlpha.y), d.dSq); + let yp = mulDownXpToNpU( + mulDownMagU(mulDownMagU(r.y, p.lambda), p.s), + termXp1 + ); + yp = yp.add( + termXp2.gt(BigNumber.from(0)) + ? mulDownMagU(r.y, p.c) + : mulDownXpToNpU(mulUpMagU(r.x, p.c), termXp2) + ); + return yp; +} + +export function calcYGivenX( + x: BigNumber, + params: GyroEParams, + d: DerivedGyroEParams, + r: Vector2 +) { + const ab: Vector2 = { + x: virtualOffset0(params, d, r), + y: virtualOffset1(params, d, r), + }; + + const y = solveQuadraticSwap( + params.lambda, + x, + params.s, + params.c, + r, + ab, + d.tauBeta, + d.dSq + ); + return y; +} + +export function calcXGivenY( + y: BigNumber, + params: GyroEParams, + d: DerivedGyroEParams, + r: Vector2 +) { + const ba: Vector2 = { + x: virtualOffset1(params, d, r), + y: virtualOffset0(params, d, r), + }; + const x = solveQuadraticSwap( + params.lambda, + y, + params.c, + params.s, + r, + ba, + { + x: d.tauAlpha.x.mul(-1), + y: d.tauAlpha.y, + }, + d.dSq + ); + return x; +} + +export function virtualOffset0( + p: GyroEParams, + d: DerivedGyroEParams, + r: Vector2, + switchTau?: boolean +): BigNumber { + const tauValue = switchTau ? d.tauAlpha : d.tauBeta; + const termXp = divXpU(tauValue.x, d.dSq); + + let a = tauValue.x.gt(BigNumber.from(0)) + ? mulUpXpToNpU(mulUpMagU(mulUpMagU(r.x, p.lambda), p.c), termXp) + : mulUpXpToNpU(mulDownMagU(mulDownMagU(r.y, p.lambda), p.c), termXp); + + a = a.add(mulUpXpToNpU(mulUpMagU(r.x, p.s), divXpU(tauValue.y, d.dSq))); + + return a; +} + +export function virtualOffset1( + p: GyroEParams, + d: DerivedGyroEParams, + r: Vector2, + switchTau?: boolean +): BigNumber { + const tauValue = switchTau ? d.tauBeta : d.tauAlpha; + const termXp = divXpU(tauValue.x, d.dSq); + + let b = tauValue.x.lt(BigNumber.from(0)) + ? mulUpXpToNpU(mulUpMagU(mulUpMagU(r.x, p.lambda), p.s), termXp.mul(-1)) + : mulUpXpToNpU( + mulDownMagU(mulDownMagU(r.y.mul(-1), p.lambda), p.s), + termXp + ); + + b = b.add(mulUpXpToNpU(mulUpMagU(r.x, p.c), divXpU(tauValue.y, d.dSq))); + return b; +} + +function solveQuadraticSwap( + lambda: BigNumber, + x: BigNumber, + s: BigNumber, + c: BigNumber, + r: Vector2, + ab: Vector2, + tauBeta: Vector2, + dSq: BigNumber +): BigNumber { + const lamBar: Vector2 = { + x: ONE_XP.sub(divDownMagU(divDownMagU(ONE_XP, lambda), lambda)), + y: ONE_XP.sub(divUpMagU(divUpMagU(ONE_XP, lambda), lambda)), + }; + const q: QParams = { + a: BigNumber.from(0), + b: BigNumber.from(0), + c: BigNumber.from(0), + }; + const xp = x.sub(ab.x); + if (xp.gt(BigNumber.from(0))) { + q.b = mulUpXpToNpU( + mulDownMagU(mulDownMagU(xp.mul(-1), s), c), + divXpU(lamBar.y, dSq) + ); + } else { + q.b = mulUpXpToNpU( + mulUpMagU(mulUpMagU(xp.mul(-1), s), c), + divXpU(lamBar.x, dSq).add(1) + ); + } + const sTerm: Vector2 = { + x: divXpU(mulDownMagU(mulDownMagU(lamBar.y, s), s), dSq), + y: divXpU(mulUpMagU(mulUpMagU(lamBar.x, s), s), dSq.add(1)).add(1), + }; + sTerm.x = ONE_XP.sub(sTerm.x); + sTerm.y = ONE_XP.sub(sTerm.y); + + q.c = calcXpXpDivLambdaLambda(x, r, lambda, s, c, tauBeta, dSq).mul(-1); + q.c = q.c.add(mulDownXpToNpU(mulDownMagU(r.y, r.y), sTerm.y)); // r.y === currentInv + err + q.c = q.c.gt(BigNumber.from(0)) + ? sqrt(q.c, BigNumber.from(5)) + : BigNumber.from(0); + if (q.b.sub(q.c).gt(BigNumber.from(0))) { + q.a = mulUpXpToNpU(q.b.sub(q.c), divXpU(ONE_XP, sTerm.y).add(1)); + } else { + q.a = mulUpXpToNpU(q.b.sub(q.c), divXpU(ONE_XP, sTerm.x)); + } + return q.a.add(ab.y); +} + +export function calcXpXpDivLambdaLambda( + x: BigNumber, + r: Vector2, + lambda: BigNumber, + s: BigNumber, + c: BigNumber, + tauBeta: Vector2, + dSq: BigNumber +): BigNumber { + const sqVars = { + x: mulXpU(dSq, dSq), + y: mulUpMagU(r.x, r.x), + }; + const q: QParams = { + a: BigNumber.from(0), + b: BigNumber.from(0), + c: BigNumber.from(0), + }; + let termXp = divXpU(mulXpU(tauBeta.x, tauBeta.y), sqVars.x); + if (termXp.gt(BigNumber.from(0))) { + q.a = mulUpMagU(sqVars.y, s.mul(2)); + q.a = mulUpXpToNpU(mulUpMagU(q.a, c), termXp.add(7)); + } else { + q.a = mulDownMagU(mulDownMagU(r.y, r.y), s.mul(2)); // r.y === currentInv + err + q.a = mulUpXpToNpU(mulDownMagU(q.a, c), termXp); + } + + if (tauBeta.x.lt(BigNumber.from(0))) { + q.b = mulUpXpToNpU( + mulUpMagU(mulUpMagU(r.x, x), c.mul(2)), + divXpU(tauBeta.x, dSq).mul(-1).add(3) + ); + } else { + q.b = mulUpXpToNpU( + mulDownMagU(mulDownMagU(r.y.mul(-1), x), c.mul(2)), + divXpU(tauBeta.x, dSq) + ); + } + q.a = q.a.add(q.b); + termXp = divXpU(mulXpU(tauBeta.y, tauBeta.y), sqVars.x).add(7); + q.b = mulUpMagU(sqVars.y, s); + q.b = mulUpXpToNpU(mulUpMagU(q.b, s), termXp); + + q.c = mulUpXpToNpU( + mulDownMagU(mulDownMagU(r.y.mul(-1), x), s.mul(2)), + divXpU(tauBeta.y, dSq) + ); + q.b = q.b.add(q.c).add(mulUpMagU(x, x)); + q.b = q.b.gt(BigNumber.from(0)) + ? divUpMagU(q.b, lambda) + : divDownMagU(q.b, lambda); + + q.a = q.a.add(q.b); + q.a = q.a.gt(BigNumber.from(0)) + ? divUpMagU(q.a, lambda) + : divDownMagU(q.a, lambda); + + termXp = divXpU(mulXpU(tauBeta.x, tauBeta.x), sqVars.x).add(7); + const val = mulUpMagU(mulUpMagU(sqVars.y, c), c); + return mulUpXpToNpU(val, termXp).add(q.a); +} + +///////// +/// LINEAR ALGEBRA OPERATIONS +///////// + +export function mulA(params: GyroEParams, tp: Vector2): Vector2 { + return { + x: divDownMagU(mulDownMagU(params.c, tp.x), params.lambda).sub( + divDownMagU(mulDownMagU(params.s, tp.y), params.lambda) + ), + y: mulDownMagU(params.s, tp.x).add(mulDownMagU(params.c, tp.y)), + }; +} + +export function scalarProd(t1: Vector2, t2: Vector2) { + const ret = mulDownMagU(t1.x, t2.x).add(mulDownMagU(t1.y, t2.y)); + return ret; +} diff --git a/src/pools/gyroEPool/gyroEPool.ts b/src/pools/gyroEPool/gyroEPool.ts new file mode 100644 index 00000000..83525722 --- /dev/null +++ b/src/pools/gyroEPool/gyroEPool.ts @@ -0,0 +1,542 @@ +import { getAddress } from '@ethersproject/address'; +import { WeiPerEther as ONE } from '@ethersproject/constants'; +import { formatFixed, BigNumber } from '@ethersproject/bignumber'; +import { BigNumber as OldBigNumber, bnum } from '../../utils/bignumber'; + +import { + PoolBase, + PoolPairBase, + PoolTypes, + SubgraphToken, + SwapTypes, + SubgraphPoolBase, +} from '../../types'; +import { + GyroEParams, + DerivedGyroEParams, + Vector2, + normalizeBalances, + balancesFromTokenInOut, + reduceFee, + addFee, + virtualOffset0, + virtualOffset1, +} from './gyroEMath/gyroEMathHelpers'; +import { isSameAddress, safeParseFixed } from '../../utils'; +import { mulDown, divDown } from '../gyroHelpers/gyroSignedFixedPoint'; +import { + calculateInvariantWithError, + calcOutGivenIn, + calcInGivenOut, + calcSpotPriceAfterSwapOutGivenIn, + calcSpotPriceAfterSwapInGivenOut, + calcDerivativePriceAfterSwapOutGivenIn, + calcDerivativeSpotPriceAfterSwapInGivenOut, + calculateNormalizedLiquidity, +} from './gyroEMath/gyroEMath'; +import { SWAP_LIMIT_FACTOR } from '../gyroHelpers/constants'; + +export type GyroEPoolPairData = PoolPairBase & { + tokenInIsToken0: boolean; +}; + +export type GyroEPoolToken = Pick< + SubgraphToken, + 'address' | 'balance' | 'decimals' +>; + +type GyroEParamsFromSubgraph = { + alpha: string; + beta: string; + c: string; + s: string; + lambda: string; +}; +type DerivedGyroEParamsFromSubgraph = { + tauAlphaX: string; + tauAlphaY: string; + tauBetaX: string; + tauBetaY: string; + u: string; + v: string; + w: string; + z: string; + dSq: string; +}; + +export class GyroEPool implements PoolBase { + poolType: PoolTypes = PoolTypes.GyroE; + id: string; + address: string; + tokensList: string[]; + tokens: GyroEPoolToken[]; + swapFee: BigNumber; + totalShares: BigNumber; + gyroEParams: GyroEParams; + derivedGyroEParams: DerivedGyroEParams; + + static fromPool(pool: SubgraphPoolBase): GyroEPool { + const { + alpha, + beta, + c, + s, + lambda, + tauAlphaX, + tauAlphaY, + tauBetaX, + tauBetaY, + u, + v, + w, + z, + dSq, + } = pool; + + const gyroEParams = { + alpha, + beta, + c, + s, + lambda, + }; + + const derivedGyroEParams = { + tauAlphaX, + tauAlphaY, + tauBetaX, + tauBetaY, + u, + v, + w, + z, + dSq, + }; + + if ( + !Object.values(gyroEParams).every((el) => el) || + !Object.values(derivedGyroEParams).every((el) => el) + ) + throw new Error( + 'Pool missing GyroE params and/or GyroE derived params' + ); + + return new GyroEPool( + pool.id, + pool.address, + pool.swapFee, + pool.totalShares, + pool.tokens as GyroEPoolToken[], + pool.tokensList, + gyroEParams as GyroEParamsFromSubgraph, + derivedGyroEParams as DerivedGyroEParamsFromSubgraph + ); + } + + constructor( + id: string, + address: string, + swapFee: string, + totalShares: string, + tokens: GyroEPoolToken[], + tokensList: string[], + gyroEParams: GyroEParamsFromSubgraph, + derivedGyroEParams: DerivedGyroEParamsFromSubgraph + ) { + this.id = id; + this.address = address; + this.swapFee = safeParseFixed(swapFee, 18); + this.totalShares = safeParseFixed(totalShares, 18); + this.tokens = tokens; + this.tokensList = tokensList; + + this.gyroEParams = { + alpha: safeParseFixed(gyroEParams.alpha, 18), + beta: safeParseFixed(gyroEParams.beta, 18), + c: safeParseFixed(gyroEParams.c, 18), + s: safeParseFixed(gyroEParams.s, 18), + lambda: safeParseFixed(gyroEParams.lambda, 18), + }; + + this.derivedGyroEParams = { + tauAlpha: { + x: safeParseFixed(derivedGyroEParams.tauAlphaX, 38), + y: safeParseFixed(derivedGyroEParams.tauAlphaY, 38), + }, + tauBeta: { + x: safeParseFixed(derivedGyroEParams.tauBetaX, 38), + y: safeParseFixed(derivedGyroEParams.tauBetaY, 38), + }, + u: safeParseFixed(derivedGyroEParams.u, 38), + v: safeParseFixed(derivedGyroEParams.v, 38), + w: safeParseFixed(derivedGyroEParams.w, 38), + z: safeParseFixed(derivedGyroEParams.z, 38), + dSq: safeParseFixed(derivedGyroEParams.dSq, 38), + }; + } + + parsePoolPairData(tokenIn: string, tokenOut: string): GyroEPoolPairData { + const tokenInIndex = this.tokens.findIndex( + (t) => getAddress(t.address) === getAddress(tokenIn) + ); + if (tokenInIndex < 0) throw 'Pool does not contain tokenIn'; + const tI = this.tokens[tokenInIndex]; + const balanceIn = tI.balance; + const decimalsIn = tI.decimals; + + const tokenOutIndex = this.tokens.findIndex( + (t) => getAddress(t.address) === getAddress(tokenOut) + ); + if (tokenOutIndex < 0) throw 'Pool does not contain tokenOut'; + const tO = this.tokens[tokenOutIndex]; + const balanceOut = tO.balance; + const decimalsOut = tO.decimals; + + const tokenInIsToken0 = tokenInIndex === 0; + + const poolPairData: GyroEPoolPairData = { + id: this.id, + address: this.address, + poolType: this.poolType, + tokenIn: tokenIn, + tokenOut: tokenOut, + decimalsIn: Number(decimalsIn), + decimalsOut: Number(decimalsOut), + balanceIn: safeParseFixed(balanceIn, decimalsIn), + balanceOut: safeParseFixed(balanceOut, decimalsOut), + swapFee: this.swapFee, + tokenInIsToken0, + }; + + return poolPairData; + } + + getNormalizedLiquidity(poolPairData: GyroEPoolPairData) { + const normalizedBalances = normalizeBalances( + [poolPairData.balanceIn, poolPairData.balanceOut], + [poolPairData.decimalsIn, poolPairData.decimalsOut] + ); + + const orderedNormalizedBalances = balancesFromTokenInOut( + normalizedBalances[0], + normalizedBalances[1], + poolPairData.tokenInIsToken0 + ); + + const [currentInvariant, invErr] = calculateInvariantWithError( + orderedNormalizedBalances, + this.gyroEParams, + this.derivedGyroEParams + ); + + const invariant: Vector2 = { + x: currentInvariant.add(invErr.mul(2)), + y: currentInvariant, + }; + + const normalizedLiquidity = calculateNormalizedLiquidity( + orderedNormalizedBalances, + this.gyroEParams, + this.derivedGyroEParams, + invariant, + this.swapFee, + poolPairData.tokenInIsToken0 + ); + + return bnum(formatFixed(normalizedLiquidity, 18)); + } + + getLimitAmountSwap( + poolPairData: GyroEPoolPairData, + swapType: SwapTypes + ): OldBigNumber { + if (swapType === SwapTypes.SwapExactIn) { + const normalizedBalances = normalizeBalances( + [poolPairData.balanceIn, poolPairData.balanceOut], + [poolPairData.decimalsIn, poolPairData.decimalsOut] + ); + const orderedNormalizedBalances = balancesFromTokenInOut( + normalizedBalances[0], + normalizedBalances[1], + poolPairData.tokenInIsToken0 + ); + const [currentInvariant, invErr] = calculateInvariantWithError( + orderedNormalizedBalances, + this.gyroEParams, + this.derivedGyroEParams + ); + const invariant: Vector2 = { + x: currentInvariant.add(invErr.mul(2)), + y: currentInvariant, + }; + const virtualOffsetFunc = poolPairData.tokenInIsToken0 + ? virtualOffset0 + : virtualOffset1; + const maxAmountInAssetInPool = virtualOffsetFunc( + this.gyroEParams, + this.derivedGyroEParams, + invariant + ).sub( + virtualOffsetFunc( + this.gyroEParams, + this.derivedGyroEParams, + invariant, + true + ) + ); + const limitAmountIn = maxAmountInAssetInPool.sub( + normalizedBalances[0] + ); + const limitAmountInPlusSwapFee = divDown( + limitAmountIn, + ONE.sub(poolPairData.swapFee) + ); + return bnum( + formatFixed( + mulDown(limitAmountInPlusSwapFee, SWAP_LIMIT_FACTOR), + 18 + ) + ); + } else { + return bnum( + formatFixed( + mulDown(poolPairData.balanceOut, SWAP_LIMIT_FACTOR), + poolPairData.decimalsOut + ) + ); + } + } + + // Updates the balance of a given token for the pool + updateTokenBalanceForPool(token: string, newBalance: BigNumber): void { + // token is BPT + if (this.address == token) { + this.totalShares = newBalance; + } else { + // token is underlying in the pool + const T = this.tokens.find((t) => isSameAddress(t.address, token)); + if (!T) throw Error('Pool does not contain this token'); + T.balance = formatFixed(newBalance, T.decimals); + } + } + + _exactTokenInForTokenOut( + poolPairData: GyroEPoolPairData, + amount: OldBigNumber + ): OldBigNumber { + const normalizedBalances = normalizeBalances( + [poolPairData.balanceIn, poolPairData.balanceOut], + [poolPairData.decimalsIn, poolPairData.decimalsOut] + ); + const orderedNormalizedBalances = balancesFromTokenInOut( + normalizedBalances[0], + normalizedBalances[1], + poolPairData.tokenInIsToken0 + ); + const [currentInvariant, invErr] = calculateInvariantWithError( + orderedNormalizedBalances, + this.gyroEParams, + this.derivedGyroEParams + ); + + const invariant: Vector2 = { + x: currentInvariant.add(invErr.mul(2)), + y: currentInvariant, + }; + const inAmount = safeParseFixed(amount.toString(), 18); + const inAmountLessFee = reduceFee(inAmount, poolPairData.swapFee); + const outAmount = calcOutGivenIn( + orderedNormalizedBalances, + inAmountLessFee, + poolPairData.tokenInIsToken0, + this.gyroEParams, + this.derivedGyroEParams, + invariant + ); + return bnum(formatFixed(outAmount, 18)); + } + + _tokenInForExactTokenOut( + poolPairData: GyroEPoolPairData, + amount: OldBigNumber + ): OldBigNumber { + const normalizedBalances = normalizeBalances( + [poolPairData.balanceIn, poolPairData.balanceOut], + [poolPairData.decimalsIn, poolPairData.decimalsOut] + ); + const orderedNormalizedBalances = balancesFromTokenInOut( + normalizedBalances[0], + normalizedBalances[1], + poolPairData.tokenInIsToken0 + ); + const [currentInvariant, invErr] = calculateInvariantWithError( + orderedNormalizedBalances, + this.gyroEParams, + this.derivedGyroEParams + ); + const invariant: Vector2 = { + x: currentInvariant.add(invErr.mul(2)), + y: currentInvariant, + }; + const outAmount = safeParseFixed(amount.toString(), 18); + + const inAmountLessFee = calcInGivenOut( + orderedNormalizedBalances, + outAmount, + poolPairData.tokenInIsToken0, + this.gyroEParams, + this.derivedGyroEParams, + invariant + ); + const inAmount = addFee(inAmountLessFee, poolPairData.swapFee); + return bnum(formatFixed(inAmount, 18)); + } + + _spotPriceAfterSwapExactTokenInForTokenOut( + poolPairData: GyroEPoolPairData, + amount: OldBigNumber + ): OldBigNumber { + const normalizedBalances = normalizeBalances( + [poolPairData.balanceIn, poolPairData.balanceOut], + [poolPairData.decimalsIn, poolPairData.decimalsOut] + ); + const orderedNormalizedBalances = balancesFromTokenInOut( + normalizedBalances[0], + normalizedBalances[1], + poolPairData.tokenInIsToken0 + ); + const [currentInvariant, invErr] = calculateInvariantWithError( + orderedNormalizedBalances, + this.gyroEParams, + this.derivedGyroEParams + ); + const invariant: Vector2 = { + x: currentInvariant.add(invErr.mul(2)), + y: currentInvariant, + }; + const inAmount = safeParseFixed(amount.toString(), 18); + const inAmountLessFee = reduceFee(inAmount, poolPairData.swapFee); + const newSpotPrice = calcSpotPriceAfterSwapOutGivenIn( + orderedNormalizedBalances, + inAmountLessFee, + poolPairData.tokenInIsToken0, + this.gyroEParams, + this.derivedGyroEParams, + invariant, + poolPairData.swapFee + ); + return bnum(formatFixed(newSpotPrice, 18)); + } + + _spotPriceAfterSwapTokenInForExactTokenOut( + poolPairData: GyroEPoolPairData, + amount: OldBigNumber + ): OldBigNumber { + const normalizedBalances = normalizeBalances( + [poolPairData.balanceIn, poolPairData.balanceOut], + [poolPairData.decimalsIn, poolPairData.decimalsOut] + ); + const orderedNormalizedBalances = balancesFromTokenInOut( + normalizedBalances[0], + normalizedBalances[1], + poolPairData.tokenInIsToken0 + ); + const [currentInvariant, invErr] = calculateInvariantWithError( + orderedNormalizedBalances, + this.gyroEParams, + this.derivedGyroEParams + ); + const invariant: Vector2 = { + x: currentInvariant.add(invErr.mul(2)), + y: currentInvariant, + }; + const outAmount = safeParseFixed(amount.toString(), 18); + const newSpotPrice = calcSpotPriceAfterSwapInGivenOut( + orderedNormalizedBalances, + outAmount, + poolPairData.tokenInIsToken0, + this.gyroEParams, + this.derivedGyroEParams, + invariant, + poolPairData.swapFee + ); + return bnum(formatFixed(newSpotPrice, 18)); + } + + _derivativeSpotPriceAfterSwapExactTokenInForTokenOut( + poolPairData: GyroEPoolPairData, + amount: OldBigNumber + ): OldBigNumber { + const inAmount = safeParseFixed(amount.toString(), 18); + const normalizedBalances = normalizeBalances( + [poolPairData.balanceIn, poolPairData.balanceOut], + [poolPairData.decimalsIn, poolPairData.decimalsOut] + ); + const orderedNormalizedBalances = balancesFromTokenInOut( + normalizedBalances[0], + normalizedBalances[1], + poolPairData.tokenInIsToken0 + ); + const [currentInvariant, invErr] = calculateInvariantWithError( + orderedNormalizedBalances, + this.gyroEParams, + this.derivedGyroEParams + ); + const invariant: Vector2 = { + x: currentInvariant.add(invErr.mul(2)), + y: currentInvariant, + }; + + const derivative = calcDerivativePriceAfterSwapOutGivenIn( + [ + orderedNormalizedBalances[0].add( + reduceFee(inAmount, poolPairData.swapFee) + ), + orderedNormalizedBalances[1], + ], + poolPairData.tokenInIsToken0, + this.gyroEParams, + this.derivedGyroEParams, + invariant, + poolPairData.swapFee + ); + return bnum(formatFixed(derivative, 18)); + } + + _derivativeSpotPriceAfterSwapTokenInForExactTokenOut( + poolPairData: GyroEPoolPairData, + amount: OldBigNumber + ): OldBigNumber { + const normalizedBalances = normalizeBalances( + [poolPairData.balanceIn, poolPairData.balanceOut], + [poolPairData.decimalsIn, poolPairData.decimalsOut] + ); + const orderedNormalizedBalances = balancesFromTokenInOut( + normalizedBalances[0], + normalizedBalances[1], + poolPairData.tokenInIsToken0 + ); + const [currentInvariant, invErr] = calculateInvariantWithError( + orderedNormalizedBalances, + this.gyroEParams, + this.derivedGyroEParams + ); + const invariant: Vector2 = { + x: currentInvariant.add(invErr.mul(2)), + y: currentInvariant, + }; + const outAmount = safeParseFixed(amount.toString(), 18); + const derivative = calcDerivativeSpotPriceAfterSwapInGivenOut( + [ + orderedNormalizedBalances[0], + orderedNormalizedBalances[1].sub(outAmount), + ], + poolPairData.tokenInIsToken0, + this.gyroEParams, + this.derivedGyroEParams, + invariant, + poolPairData.swapFee + ); + return bnum(formatFixed(derivative, 18)); + } +} diff --git a/src/pools/gyroEPool/testingHelpers.ts b/src/pools/gyroEPool/testingHelpers.ts new file mode 100644 index 00000000..7f8a4492 --- /dev/null +++ b/src/pools/gyroEPool/testingHelpers.ts @@ -0,0 +1,104 @@ +import { parseFixed, BigNumber } from '@ethersproject/bignumber'; +import { DerivedGyroEParams, GyroEParams } from './gyroEMath/gyroEMathHelpers'; + +const ONE_100 = parseFixed('1', 100); + +export function calculateDerivedValues(p: GyroEParams): DerivedGyroEParams { + let { alpha, beta, s, c, lambda } = p; + [alpha, beta, s, c, lambda] = [alpha, beta, s, c, lambda].map((bn) => + scale(bn, 18, 100) + ); + + const dSq = mul(c, c).add(mul(s, s)); + const d = sqrtArbitraryDecimal(dSq, 100); + + const cOverDPlusAlphaOverSD = div(c, d).add(div(mul(alpha, s), d)); + const lambdaSquared = mul(lambda, lambda); + const alphaCOverDMinusSOverD = div(mul(alpha, c), d).sub(div(s, d)); + + const dAlpha = div( + ONE_100, + sqrtArbitraryDecimal( + div( + mul(cOverDPlusAlphaOverSD, cOverDPlusAlphaOverSD), + lambdaSquared + ).add(mul(alphaCOverDMinusSOverD, alphaCOverDMinusSOverD)), + 100 + ) + ); + + const cOverDPlusBetaSOverD = div(c, d).add(div(mul(beta, s), d)); + const betaCOverDMinusSOverD = div(mul(beta, c), d).sub(div(s, d)); + + const dBeta = div( + ONE_100, + sqrtArbitraryDecimal( + div( + mul(cOverDPlusBetaSOverD, cOverDPlusBetaSOverD), + lambdaSquared + ).add(mul(betaCOverDMinusSOverD, betaCOverDMinusSOverD)), + 100 + ) + ); + + let tauAlpha: BigNumber[] = []; + tauAlpha.push(mul(mul(alpha, c).sub(s), dAlpha)); + tauAlpha.push(mul(c.add(mul(s, alpha)), div(dAlpha, lambda))); + + let tauBeta: BigNumber[] = []; + + tauBeta.push(mul(mul(beta, c).sub(s), dBeta)); + tauBeta.push(mul(c.add(mul(s, beta)), div(dBeta, lambda))); + + let w = mul(mul(s, c), tauBeta[1].sub(tauAlpha[1])); + let z = mul(mul(c, c), tauBeta[0]).add(mul(mul(s, s), tauAlpha[0])); + let u = mul(mul(s, c), tauBeta[0].sub(tauAlpha[0])); + let v = mul(mul(s, s), tauBeta[1]).add(mul(mul(c, c), tauAlpha[1])); + + const derived = { + tauAlpha: { + x: scale(tauAlpha[0], 100, 38), + y: scale(tauAlpha[1], 100, 38), + }, + tauBeta: { + x: scale(tauBeta[0], 100, 38), + y: scale(tauBeta[1], 100, 38), + }, + u: scale(u, 100, 38), + v: scale(v, 100, 38), + w: scale(w, 100, 38), + z: scale(z, 100, 38), + dSq: scale(dSq, 100, 38), + }; + + return derived; +} + +function scale(bn: BigNumber, decimalsIn: number, decimalsOut: number) { + return bn + .mul(parseFixed('1', decimalsOut)) + .div(parseFixed('1', decimalsIn)); +} + +export function sqrtArbitraryDecimal(input: BigNumber, decimals: number) { + if (input.isZero()) { + return BigNumber.from(0); + } + let guess = input.gt(parseFixed('1', decimals)) ? input.div(2) : input; + + // 100 iterations + for (let i of new Array(100).fill(0)) { + guess = guess + .add(input.mul(parseFixed('1', decimals)).div(guess)) + .div(2); + } + + return guess; +} + +function mul(x: BigNumber, y: BigNumber) { + return x.mul(y).div(ONE_100); +} +function div(x: BigNumber, y: BigNumber) { + return x.mul(ONE_100).div(y); +} diff --git a/src/pools/gyro2Pool/constants.ts b/src/pools/gyroHelpers/constants.ts similarity index 75% rename from src/pools/gyro2Pool/constants.ts rename to src/pools/gyroHelpers/constants.ts index 9f2fac1f..1c84b617 100644 --- a/src/pools/gyro2Pool/constants.ts +++ b/src/pools/gyroHelpers/constants.ts @@ -1,6 +1,6 @@ import { BigNumber } from '@ethersproject/bignumber'; -// Swap limits: amounts swapped may not be larger than this percentage of total balance. +// SQRT constants export const SQRT_1E_NEG_1 = BigNumber.from('316227766016837933'); export const SQRT_1E_NEG_3 = BigNumber.from('31622776601683793'); @@ -12,5 +12,11 @@ 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'); +// High precision +export const ONE_XP = BigNumber.from(10).pow(38); // 38 decimal places + +// Small number to prevent rounding errors +export const SMALL = BigNumber.from(10).pow(8); // 1e-10 in normal precision + // Swap Limit factor export const SWAP_LIMIT_FACTOR = BigNumber.from('999999000000000000'); diff --git a/src/pools/gyro2Pool/helpers.ts b/src/pools/gyroHelpers/gyroSignedFixedPoint.ts similarity index 55% rename from src/pools/gyro2Pool/helpers.ts rename to src/pools/gyroHelpers/gyroSignedFixedPoint.ts index 318eb406..424ca000 100644 --- a/src/pools/gyro2Pool/helpers.ts +++ b/src/pools/gyroHelpers/gyroSignedFixedPoint.ts @@ -1,4 +1,4 @@ -import { BigNumber, parseFixed } from '@ethersproject/bignumber'; +import { BigNumber } from '@ethersproject/bignumber'; import { WeiPerEther as ONE } from '@ethersproject/constants'; import { SQRT_1E_NEG_1, @@ -10,40 +10,130 @@ import { SQRT_1E_NEG_13, SQRT_1E_NEG_15, SQRT_1E_NEG_17, + ONE_XP, } from './constants'; -// Helpers +///////// +/// ARITHMETIC HELPERS +///////// + +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 mulXpU(a: BigNumber, b: BigNumber) { + return a.mul(b).div(ONE_XP); +} + +export function divXpU(a: BigNumber, b: BigNumber) { + if (b.isZero()) throw new Error('ZERO DIVISION'); + return a.mul(ONE_XP).div(b); +} + +export function mulDownMagU(a: BigNumber, b: BigNumber) { + return a.mul(b).div(ONE); +} + +export function divDownMagU(a: BigNumber, b: BigNumber) { + if (b.isZero()) throw new Error('ZERO DIVISION'); + return a.mul(ONE).div(b); +} + +export function mulUpMagU(a: BigNumber, b: BigNumber) { + const product = a.mul(b); + if (product.gt(0)) return product.sub(1).div(ONE).add(1); + else if (product.lt(0)) return product.add(1).div(ONE).sub(1); + else return BigNumber.from(0); +} + +export function divUpMagU(a: BigNumber, b: BigNumber) { + if (b.isZero()) throw new Error('ZERO DIVISION'); + if (b.lt(0)) { + b = b.mul(-1); + a = a.mul(-1); + } + if (a.isZero()) { + return BigNumber.from(0); + } else { + if (a.gt(0)) return a.mul(ONE).sub(1).div(b).add(1); + else return a.mul(ONE).add(1).div(b.sub(1)); + } +} + +export function mulUpXpToNpU(a: BigNumber, b: BigNumber) { + const TenPower19 = BigNumber.from(10).pow(19); + const b1 = b.div(TenPower19); + const b2 = b.isNegative() + ? b.mul(-1).mod(TenPower19).mul(-1) + : b.mod(TenPower19); + const prod1 = a.mul(b1); + const prod2 = a.mul(b2); + return prod1.lte(0) && prod2.lte(0) + ? prod1.add(prod2.div(TenPower19)).div(TenPower19) + : prod1.add(prod2.div(TenPower19)).sub(1).div(TenPower19).add(1); +} + +export function mulDownXpToNpU(a: BigNumber, b: BigNumber) { + const TenPower19 = BigNumber.from(10).pow(19); + const b1 = b.div(TenPower19); + const b2 = b.isNegative() + ? b.mul(-1).mod(TenPower19).mul(-1) + : b.mod(TenPower19); + const prod1 = a.mul(b1); + const prod2 = a.mul(b2); + return prod1.gte(0) && prod2.gte(0) + ? prod1.add(prod2.div(TenPower19)).div(TenPower19) + : prod1.add(prod2.div(TenPower19)).add(1).div(TenPower19).sub(1); +} -export function _sqrt(input: BigNumber, tolerance: BigNumber) { +///////// +/// SQUARE ROOT +///////// + +export function sqrt(input: BigNumber, tolerance: BigNumber) { if (input.isZero()) { return BigNumber.from(0); } - let guess = _makeInitialGuess(input); + 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 + // Check square is more or less correct (in some epsilon range) 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'); + throw new Error('GyroEPool: sqrt failed'); return guess; } -function _makeInitialGuess(input: BigNumber) { +function makeInitialGuess(input: BigNumber) { if (input.gte(ONE)) { return BigNumber.from(2) - .pow(_intLog2Halved(input.div(ONE))) + .pow(intLog2Halved(input.div(ONE))) .mul(ONE); } else { if (input.lte('10')) { @@ -101,7 +191,7 @@ function _makeInitialGuess(input: BigNumber) { } } -function _intLog2Halved(x: BigNumber) { +function intLog2Halved(x: BigNumber) { let n = 0; for (let i = 128; i >= 2; i = i / 2) { @@ -114,51 +204,3 @@ function _intLog2Halved(x: BigNumber) { 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/gyroHelpers/helpers.ts b/src/pools/gyroHelpers/helpers.ts new file mode 100644 index 00000000..98b15368 --- /dev/null +++ b/src/pools/gyroHelpers/helpers.ts @@ -0,0 +1,29 @@ +import { BigNumber, parseFixed } from '@ethersproject/bignumber'; +import { WeiPerEther as ONE } from '@ethersproject/constants'; + +//////// +/// 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]) + ); +} + +///////// +/// 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/index.ts b/src/pools/index.ts index 37c0dcda..55e500dd 100644 --- a/src/pools/index.ts +++ b/src/pools/index.ts @@ -19,6 +19,7 @@ import { PoolPairBase, PoolTypes, } from '../types'; +import { GyroEPool } from './gyroEPool/gyroEPool'; export function parseNewPool( pool: SubgraphPoolBase, @@ -32,6 +33,7 @@ export function parseNewPool( | PhantomStablePool | Gyro2Pool | Gyro3Pool + | GyroEPool | undefined { // We're not interested in any pools which don't allow swapping if (!pool.swapEnabled) return undefined; @@ -44,7 +46,8 @@ export function parseNewPool( | MetaStablePool | PhantomStablePool | Gyro2Pool - | Gyro3Pool; + | Gyro3Pool + | GyroEPool; try { if (pool.poolType === 'Weighted' || pool.poolType === 'Investment') { @@ -65,8 +68,9 @@ export function parseNewPool( pool.poolType === 'ComposableStable' ) newPool = PhantomStablePool.fromPool(pool); - else if (pool.poolType === 'Gyro2') newPool = Gyro2Pool.fromPool(pool); - else if (pool.poolType === 'Gyro3') newPool = Gyro3Pool.fromPool(pool); + // else if (pool.poolType === 'Gyro2') newPool = Gyro2Pool.fromPool(pool); + // else if (pool.poolType === 'Gyro3') newPool = Gyro3Pool.fromPool(pool); + // else if (pool.poolType === 'GyroE') newPool = GyroEPool.fromPool(pool); else { console.error( `Unknown pool type or type field missing: ${pool.poolType} ${pool.id}` diff --git a/src/routeProposal/filtering.ts b/src/routeProposal/filtering.ts index 95d09bab..8628425f 100644 --- a/src/routeProposal/filtering.ts +++ b/src/routeProposal/filtering.ts @@ -179,7 +179,6 @@ export function getBoostedGraph( const phantomPools: PoolBase[] = []; const relevantRaisingTokens: string[] = []; // Here we add all linear pools, take note of phantom pools, - // add pools with tokenIn or tokenOut with weth, // add LBP pools with tokenIn or tokenOut and take note of the // corresponding raising tokens. for (const id in poolsAllDict) { @@ -196,22 +195,6 @@ export function getBoostedGraph( if (tokensList.includes(pool.address)) { phantomPools.push(pool); } - // adds pools having tokenIn or tokenOut with weth - if ( - tokenIn != wethAddress && - tokenOut != wethAddress && - tokensList.includes(wethAddress) - ) { - if ( - // This is a heuristic condition that prevents the graph - // from growing too large - tokensList.length <= 4 && - (tokensList.includes(tokenIn) || - tokensList.includes(tokenOut)) - ) { - graphPoolsSet.add(pool); - } - } if (config.lbpRaisingTokens) { const raisingTokens = config.lbpRaisingTokens.map((address) => address.toLowerCase() @@ -239,6 +222,23 @@ export function getBoostedGraph( } } } + // add highest liquidity pools with tokenIn and weth or tokenOut and weth + const bestTokenInToWeth = getHighestLiquidityPool( + tokenIn, + wethAddress, + poolsAllDict + ); + if (bestTokenInToWeth) { + graphPoolsSet.add(poolsAllDict[bestTokenInToWeth]); + } + const bestWethToTokenOut = getHighestLiquidityPool( + wethAddress, + tokenOut, + poolsAllDict + ); + if (bestWethToTokenOut) { + graphPoolsSet.add(poolsAllDict[bestWethToTokenOut]); + } if (linearPools.length == 0) return {}; const linearPoolsAddresses = linearPools.map((pool) => pool.address); const secondStepPoolsSet: Set = new Set(); diff --git a/src/types.ts b/src/types.ts index e21113a8..07e7d46d 100644 --- a/src/types.ts +++ b/src/types.ts @@ -28,6 +28,7 @@ export enum PoolTypes { Linear, Gyro2, Gyro3, + GyroE, } export interface SwapOptions { @@ -92,12 +93,28 @@ export interface SubgraphPoolBase { lowerTarget?: string; upperTarget?: string; - // Gyro2 specific field + // Gyro2 specific fields sqrtAlpha?: string; sqrtBeta?: string; // Gyro3 specific field root3Alpha?: string; + + // GyroE specific fields + alpha?: string; + beta?: string; + c?: string; + s?: string; + lambda?: string; + tauAlphaX?: string; + tauAlphaY?: string; + tauBetaX?: string; + tauBetaY?: string; + u?: string; + v?: string; + w?: string; + z?: string; + dSq?: string; } export type SubgraphToken = { @@ -165,9 +182,10 @@ export enum PoolFilter { AaveLinear = 'AaveLinear', StablePhantom = 'StablePhantom', ERC4626Linear = 'ERC4626Linear', + ComposableStable = 'ComposableStable', Gyro2 = 'Gyro2', Gyro3 = 'Gyro3', - ComposableStable = 'ComposableStable', + GyroE = 'GyroE', } export interface PoolBase { diff --git a/src/utils/index.ts b/src/utils/index.ts index c9607ff5..9f0b5b04 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,4 +1,16 @@ import { getAddress } from '@ethersproject/address'; +import { parseFixed, BigNumber } from '@ethersproject/bignumber'; export const isSameAddress = (address1: string, address2: string): boolean => getAddress(address1) === getAddress(address2); + +/// Parses a fixed-point decimal string into a BigNumber +/// If we do not have enough decimals to express the number, we truncate it +export function safeParseFixed(value: string, decimals: number = 0): BigNumber { + const [integer, fraction] = value.split('.'); + if (!fraction) { + return parseFixed(value, decimals); + } + const safeValue = integer + '.' + fraction.slice(0, decimals); + return parseFixed(safeValue, decimals); +} diff --git a/test/gyro2Math.spec.ts b/test/gyro2Math.spec.ts index c4ae4f70..ebe9f313 100644 --- a/test/gyro2Math.spec.ts +++ b/test/gyro2Math.spec.ts @@ -16,7 +16,7 @@ import { _addFee, _reduceFee, _normalizeBalances, -} from '../src/pools/gyro2Pool/helpers'; +} from '../src/pools/gyroHelpers/helpers'; describe('gyro2Math tests', () => { const testPool: any = cloneDeep(testPools).pools[0]; @@ -43,8 +43,7 @@ describe('gyro2Math tests', () => { it(`should correctly calculate invariant`, async () => { const normalizedBalances = _normalizeBalances( [poolPairData.balanceIn, poolPairData.balanceOut], - poolPairData.decimalsIn, - poolPairData.decimalsOut + [poolPairData.decimalsIn, poolPairData.decimalsOut] ); const [a, mb, bSquare, mc] = _calculateQuadraticTerms( normalizedBalances, diff --git a/test/gyro2Pool.spec.ts b/test/gyro2Pool.spec.ts index 8373c8ad..104f51a8 100644 --- a/test/gyro2Pool.spec.ts +++ b/test/gyro2Pool.spec.ts @@ -149,53 +149,53 @@ describe('Gyro2Pool tests USDC > DAI', () => { }); }); - 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); - }); - }); + // 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/gyro3Math.spec.ts b/test/gyro3Math.spec.ts index c4ae4f70..a0852faf 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, BigNumber } from '@ethersproject/bignumber'; +import { formatFixed, parseFixed } from '@ethersproject/bignumber'; import { WeiPerEther as ONE } from '@ethersproject/constants'; import { USDC, DAI } from './lib/constants'; // Add new PoolType @@ -16,7 +16,7 @@ import { _addFee, _reduceFee, _normalizeBalances, -} from '../src/pools/gyro2Pool/helpers'; +} from '../src/pools/gyroHelpers/helpers'; describe('gyro2Math tests', () => { const testPool: any = cloneDeep(testPools).pools[0]; @@ -43,8 +43,7 @@ describe('gyro2Math tests', () => { it(`should correctly calculate invariant`, async () => { const normalizedBalances = _normalizeBalances( [poolPairData.balanceIn, poolPairData.balanceOut], - poolPairData.decimalsIn, - poolPairData.decimalsOut + [poolPairData.decimalsIn, poolPairData.decimalsOut] ); const [a, mb, bSquare, mc] = _calculateQuadraticTerms( normalizedBalances, diff --git a/test/gyroE.integration.spec.ts b/test/gyroE.integration.spec.ts new file mode 100644 index 00000000..88d92ed8 --- /dev/null +++ b/test/gyroE.integration.spec.ts @@ -0,0 +1,192 @@ +// TS_NODE_PROJECT='tsconfig.testing.json' npx mocha -r ts-node/register test/gyroE.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 GOERLI + * - Update hardhat.config.js with chainId = 5 + * - Update ALCHEMY_URL on .env with a goerli api key + * - Run goerli 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, 5); +const vault = Vault__factory.connect(vaultAddr, provider); + +const gyroEPool: SubgraphPoolBase = { + id: '0xe0711573a96806182c01ef6c349948edc6635b040002000000000000000002ab', + address: '0xe0711573a96806182c01ef6c349948edc6635b04', + poolType: 'GyroE', + swapFee: '0.0002', + totalShares: '0.001132693078136504', + tokens: [ + { + address: '0x2a7fa61d84db003a999bf4623942f235bff659a8', + balance: '1', + decimals: 18, + weight: null, + priceRate: '1', + }, + { + address: '0x4ac0909d762f20dfee3efc6a5ce1029c78812648', + balance: '1', + decimals: 6, + weight: null, + priceRate: '1', + }, + ], + tokensList: [ + '0x2a7fa61d84db003a999bf4623942f235bff659a8', + '0x4ac0909d762f20dfee3efc6a5ce1029c78812648', + ], + totalWeight: '0', + swapEnabled: true, + wrappedIndex: 0, + mainIndex: 0, + alpha: '0.98', + beta: '1.020408163265306122', + c: '0.707106781186547524', + s: '0.707106781186547524', + lambda: '2500', + tauAlphaX: '-0.9992168409687262363026689301701759', + tauAlphaY: '0.03956898690236155895758568963473897', + tauBetaX: '0.9992168409687262362685980644343916', + tauBetaY: '0.03956898690236155981796108700303143', + u: '0.9992168409687262351527623443756247', + v: '0.0395689869023615593429116906629895', + w: '0.0000000000000000004301876986841462313', + z: '-0.00000000000000000001703543286789219094', + dSq: '0.9999999999999999988662409334210612', +}; + +// 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: [gyroEPool], + }); + + 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('gyroE integration tests', () => { + context('test swaps vs queryBatchSwap', () => { + const tokenIn = '0x2a7fa61d84db003a999bf4623942f235bff659a8'; + const tokenOut = '0x4ac0909d762f20dfee3efc6a5ce1029c78812648'; + 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: 7934310, + }, + }, + ]); + + const networkId = Network.GOERLI; + sor = setUp(networkId, provider); + await sor.fetchPools(); + }); + + it('ExactIn', async () => { + const swapType = SwapTypes.SwapExactIn; + + const swapInfo = await sor.getSwaps( + tokenIn, + tokenOut, + swapType, + parseFixed('0.1', 18) + ); + + 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, + parseFixed('0.1', 6) + ); + + 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/gyroEMath.spec.ts b/test/gyroEMath.spec.ts new file mode 100644 index 00000000..031a73f1 --- /dev/null +++ b/test/gyroEMath.spec.ts @@ -0,0 +1,138 @@ +// TS_NODE_PROJECT='tsconfig.testing.json' npx mocha -r ts-node/register test/gyroEMath.spec.ts + +import { GyroEPoolPairData } from '../src/pools/gyroEPool/gyroEPool'; +import { WeiPerEther as ONE } from '@ethersproject/constants'; +import { parseFixed, formatFixed } from '@ethersproject/bignumber'; +import { expect } from 'chai'; +import { calculateInvariantWithError } from '../src/pools/gyroEPool/gyroEMath/gyroEMath'; +import { calculateDerivedValues } from '../src/pools/gyroEPool/testingHelpers'; +import { + normalizeBalances, + addFee, + reduceFee, + virtualOffset0, + virtualOffset1, + Vector2, +} from '../src/pools/gyroEPool/gyroEMath/gyroEMathHelpers'; +import { GyroEPool } from '../src/pools/gyroEPool/gyroEPool'; + +const GYRO_E_PARAMS = { + alpha: parseFixed('0.050000000000020290', 18), + beta: parseFixed('0.397316269897841178', 18), + c: parseFixed('0.9551573261744535', 18), + s: parseFixed('0.29609877111408056', 18), + lambda: parseFixed('748956.475000000000000000', 18), +}; + +const DERIVED_GYRO_E_PARAMS = calculateDerivedValues(GYRO_E_PARAMS); + +const TEST_POOL_PAIR_DATA: GyroEPoolPairData = { + id: '123', + address: '123', + poolType: 1, + swapFee: ONE.mul(9).div(100), + tokenIn: '123', + tokenOut: '123', + decimalsIn: 18, + decimalsOut: 18, + balanceIn: ONE.mul(100), + balanceOut: ONE.mul(100), + tokenInIsToken0: true, +}; + +describe('gyroEMath tests', () => { + const poolPairData = TEST_POOL_PAIR_DATA; + + context('add and remove swap fee', () => { + const amountIn = parseFixed('28492.48453', 18); + const swapFee = poolPairData.swapFee; + it(`should correctly add swap fee`, async () => { + expect( + Number(formatFixed(addFee(amountIn, swapFee), 18)) + ).to.be.approximately(31310.4225604, 0.00001); + }); + it(`should correctly reduce by swap fee`, async () => { + expect( + Number(formatFixed(reduceFee(amountIn, swapFee), 18)) + ).to.be.approximately(25928.1609223, 0.00001); + }); + }); + + context('invariant', () => { + it(`should correctly calculate invariant`, async () => { + const balances = [poolPairData.balanceIn, poolPairData.balanceOut]; + const normalizedBalances = normalizeBalances(balances, [ + poolPairData.decimalsIn, + poolPairData.decimalsOut, + ]); + const [currentInvariant, invErr] = calculateInvariantWithError( + normalizedBalances, + GYRO_E_PARAMS, + DERIVED_GYRO_E_PARAMS + ); + + expect(currentInvariant.toString()).to.equal('295358168772127'); + expect(invErr.toString()).to.equal('2'); + }); + }); + + context('calculate virtual parameters', () => { + it(`should correctly calculate virtual offset 0 (a)`, async () => { + const balances = [poolPairData.balanceIn, poolPairData.balanceOut]; + const normalizedBalances = normalizeBalances(balances, [ + poolPairData.decimalsIn, + poolPairData.decimalsOut, + ]); + const [currentInvariant, invErr] = calculateInvariantWithError( + normalizedBalances, + GYRO_E_PARAMS, + DERIVED_GYRO_E_PARAMS + ); + + const invariant: Vector2 = { + x: currentInvariant.add(invErr.mul(2)), + y: currentInvariant, + }; + + const a = virtualOffset0( + GYRO_E_PARAMS, + DERIVED_GYRO_E_PARAMS, + invariant + ); + expect(Number(formatFixed(a, 18))).to.be.approximately( + 211.290746521816255142, + 0.00001 + ); + }); + }); + + context('calculate virtual parameters', () => { + it(`should correctly calculate virtual offset 1 (b)`, async () => { + const balances = [poolPairData.balanceIn, poolPairData.balanceOut]; + const normalizedBalances = normalizeBalances(balances, [ + poolPairData.decimalsIn, + poolPairData.decimalsOut, + ]); + const [currentInvariant, invErr] = calculateInvariantWithError( + normalizedBalances, + GYRO_E_PARAMS, + DERIVED_GYRO_E_PARAMS + ); + + const invariant: Vector2 = { + x: currentInvariant.add(invErr.mul(2)), + y: currentInvariant, + }; + + const b = virtualOffset1( + GYRO_E_PARAMS, + DERIVED_GYRO_E_PARAMS, + invariant + ); + expect(Number(formatFixed(b, 18))).to.be.approximately( + 65.500131431538418723, + 0.00001 + ); + }); + }); +}); diff --git a/test/gyroEPool.spec.ts b/test/gyroEPool.spec.ts new file mode 100644 index 00000000..aeea721d --- /dev/null +++ b/test/gyroEPool.spec.ts @@ -0,0 +1,195 @@ +// TS_NODE_PROJECT='tsconfig.testing.json' npx mocha -r ts-node/register test/gyroEPool.spec.ts + +import { GyroEPoolPairData } from '../src/pools/gyroEPool/gyroEPool'; +import { WeiPerEther as ONE } from '@ethersproject/constants'; +import { formatFixed, parseFixed } from '@ethersproject/bignumber'; +import { expect } from 'chai'; +import { GyroEPool } from '../src/pools/gyroEPool/gyroEPool'; +import { SwapTypes } from '../src/types'; +import { bnum } from '../src/utils/bignumber'; +import { reduceFee } from '../src/pools/gyroEPool/gyroEMath/gyroEMathHelpers'; + +const TEST_POOL_PAIR_DATA: GyroEPoolPairData = { + id: '123', + address: '123', + poolType: 1, + swapFee: ONE.mul(9).div(100), + tokenIn: '123', + tokenOut: '123', + decimalsIn: 18, + decimalsOut: 18, + balanceIn: ONE.mul(100), + balanceOut: ONE.mul(100), + tokenInIsToken0: true, +}; + +const POOL = GyroEPool.fromPool({ + id: '1', + address: '1', + poolType: 'GyroE', + swapFee: '0.09', + swapEnabled: true, + totalShares: '100', + tokens: [ + { + address: '1', + balance: '100', + decimals: 18, + priceRate: '1', + weight: null, + }, + { + address: '2', + balance: '100', + decimals: 18, + priceRate: '1', + weight: null, + }, + ], + tokensList: ['1', '2'], + // GYRO E-CLP PARAMS + alpha: '0.050000000000020290', + beta: '0.397316269897841178', + c: '0.9551573261744535', + s: '0.29609877111408056', + lambda: '748956.475000000000000000', + // GYRO E-CLP DERIVED PARAMS + tauAlphaX: '-0.99999999998640216827321822090250869512', + tauAlphaY: '0.00000521494821273352387635736307999088', + tauBetaX: '0.99999999985251225321221463296419833612', + tauBetaY: '0.00001717485123551095031292618834391386', + u: '0.56564182095617502122541689600223111041', + v: '0.00000626352651807875756896296543790835', + w: '0.00000338251066240397957902003753652350', + z: '0.82465103535609803284538786438983276111', + dSq: '1.00000000000000002140811391783216360000', +}); + +describe('gyroEPool tests', () => { + const poolPairData = TEST_POOL_PAIR_DATA; + + context('normalized liquidity', () => { + it(`should correctly calculate normalized liquidity`, async () => { + const normalizedLiquidity = + POOL.getNormalizedLiquidity(poolPairData); + + expect(Number(normalizedLiquidity)).to.be.approximately( + 8521784.492644514672267378, + 0.00001 + ); + }); + }); + + context('limit amount swap', () => { + it(`should correctly calculate limit amount for swap exact in`, async () => { + const limitAmount = POOL.getLimitAmountSwap( + poolPairData, + SwapTypes.SwapExactIn + ); + + expect(Number(limitAmount)).to.be.approximately( + 354.48480273457726733583, + 0.00001 + ); + }); + + it(`should correctly calculate limit amount for swap exact out`, async () => { + const limitAmount = POOL.getLimitAmountSwap( + poolPairData, + SwapTypes.SwapExactOut + ); + + expect(Number(limitAmount)).to.be.approximately(99.9999, 0.00001); + }); + }); + + context('swap amounts', () => { + it(`should correctly calculate swap amount for swap exact in`, async () => { + const swapAmount = POOL._exactTokenInForTokenOut( + poolPairData, + bnum('10') + ); + + expect(Number(swapAmount)).to.be.approximately( + 2.821007799187925949, + 0.00001 + ); + }); + + it(`should correctly calculate swap amount for swap exact out`, async () => { + const swapAmount = POOL._tokenInForExactTokenOut( + poolPairData, + bnum('10') + ); + + const reduced = formatFixed( + reduceFee( + parseFixed(swapAmount.toString(), 18), + poolPairData.swapFee + ), + 18 + ); + + expect(Number(reduced)).to.be.approximately( + 32.257987339909373037, + 0.00001 + ); + }); + }); + + context('prices', () => { + it(`should correctly calculate price after swap exact in`, async () => { + const priceAfterSwap = + POOL._spotPriceAfterSwapExactTokenInForTokenOut( + poolPairData, + bnum('10') + ); + + expect(Number(priceAfterSwap)).to.be.approximately( + 3.544833007099248968, + 0.00001 + ); + }); + + it(`should correctly calculate price after swap exact out`, async () => { + const priceAfterSwap = + POOL._spotPriceAfterSwapTokenInForExactTokenOut( + poolPairData, + bnum('10') + ); + + expect(Number(priceAfterSwap)).to.be.approximately( + 3.544835504848199306, + 0.00001 + ); + }); + }); + + context('derivative of price', () => { + it(`should correctly calculate derivative of price after swap exact in`, async () => { + const priceDerivative = + POOL._derivativeSpotPriceAfterSwapExactTokenInForTokenOut( + poolPairData, + bnum('10') + ); + + expect(Number(priceDerivative)).to.be.approximately( + 1.07491448987e-7, + 0.00001 + ); + }); + + it(`should correctly calculate derivative of price after swap exact out`, async () => { + const priceDerivative = + POOL._derivativeSpotPriceAfterSwapTokenInForExactTokenOut( + poolPairData, + bnum('10') + ); + + expect(Number(priceDerivative)).to.be.approximately( + 3.2030926701e-7, + 0.00001 + ); + }); + }); +}); diff --git a/test/lib/subgraphPoolDataService.ts b/test/lib/subgraphPoolDataService.ts index 739b98db..bbe95afa 100644 --- a/test/lib/subgraphPoolDataService.ts +++ b/test/lib/subgraphPoolDataService.ts @@ -38,6 +38,20 @@ const queryWithLinear = ` sqrtAlpha sqrtBeta root3Alpha + alpha + beta + c + s + lambda + tauAlphaX + tauAlphaY + tauBetaX + tauBetaY + u + v + w + z + dSq } pool1000: pools( first: 1000, @@ -73,6 +87,20 @@ const queryWithLinear = ` sqrtAlpha sqrtBeta root3Alpha + alpha + beta + c + s + lambda + tauAlphaX + tauAlphaY + tauBetaX + tauBetaY + u + v + w + z + dSq } } `; diff --git a/test/linear.spec.ts b/test/linear.spec.ts index a080c029..e5939597 100644 --- a/test/linear.spec.ts +++ b/test/linear.spec.ts @@ -327,17 +327,17 @@ describe('linear pool tests', () => { ['linearDAI', 'bbaUSD-Pool', 'linearUSDC'], // eslint-disable-next-line prettier/prettier [ - 'weightedDaiWeth', - 'weightedWeth-BBausd', + 'linearDAI', 'bbaUSD-Pool', - 'linearUSDC', + 'weightedWeth-BBausd', + 'weightedUsdcWeth', ], // eslint-disable-next-line prettier/prettier [ - 'linearDAI', - 'bbaUSD-Pool', + 'weightedDaiWeth', 'weightedWeth-BBausd', - 'weightedUsdcWeth', + 'bbaUSD-Pool', + 'linearUSDC', ], ]; for (let i = 0; i < 3; i++) { @@ -718,8 +718,7 @@ describe('linear pool tests', () => { fullKovanPools.pools, sorConfigFullKovan ); - // 6605808981785744500 - expect(returnAmount).to.eq('20111716378263652638'); + expect(returnAmount).to.eq('6606146264948964392'); }); it('BAL>USDT, SwapExactIn', async () => { @@ -757,7 +756,7 @@ describe('linear pool tests', () => { fullKovanPools.pools, sorConfigFullKovan ); - expect(returnAmount).to.eq('221067'); + expect(returnAmount).to.eq('702055'); }); it('BAL>USDT, SwapExactOut', async () => { @@ -769,8 +768,7 @@ describe('linear pool tests', () => { fullKovanPools.pools, sorConfigFullKovan ); - // from worse path: 81899098582251741376 - expect(returnAmount).to.eq('653098636918112'); + expect(returnAmount).to.eq('81899098582251741376'); }); }); diff --git a/test/testScripts/constants.ts b/test/testScripts/constants.ts index 4282778a..177ce2ab 100644 --- a/test/testScripts/constants.ts +++ b/test/testScripts/constants.ts @@ -150,7 +150,7 @@ export const ADDRESSES = { symbol: 'waUSDC', }, bbausd2: { - address: '0x9b532ab955417afd0d012eb9f7389457cd0ea712', + address: '0xA13a9247ea42D743238089903570127DdA72fE44', decimals: 18, symbol: 'bbausd2', }, @@ -159,6 +159,11 @@ export const ADDRESSES = { decimals: 18, symbol: 'bb-a-dai2', }, + bbausdt2: { + address: '0x2F4eb100552ef93840d5aDC30560E5513DFfFACb', + decimals: 18, + symbol: 'bb-a-usdt2', + }, RPL: { address: '0xD33526068D116cE69F19A9ee46F0bd304F21A51f', decimals: 18, @@ -333,7 +338,7 @@ export const ADDRESSES = { symbol: 'dUSD', }, TUSD: { - address: '0x2e1ad108ff1d8c782fcbbb89aad783ac49586756', + address: '0x2e1AD108fF1D8C782fcBbB89AAd783aC49586756', decimals: 18, symbol: 'TUSD', }, diff --git a/test/testScripts/swapExample.ts b/test/testScripts/swapExample.ts index ea3d8275..55ac76c6 100644 --- a/test/testScripts/swapExample.ts +++ b/test/testScripts/swapExample.ts @@ -65,10 +65,10 @@ export async function swap(): Promise { const gasPrice = BigNumber.from('40000000000'); // This determines the max no of pools the SOR will use to swap. const maxPools = 4; - const tokenIn = ADDRESSES[networkId].WETHBAL; - const tokenOut = ADDRESSES[networkId].TETUBAL; + const tokenIn = ADDRESSES[networkId].USDT; + const tokenOut = ADDRESSES[networkId].USDC; const swapType: SwapTypes = SwapTypes.SwapExactIn; - const swapAmount = parseFixed('7', 18); + const swapAmount = parseFixed('100', 6); const sor = setUp(networkId, provider);