From b2aefd33cb0bad062eff41d01a202f10b59565c6 Mon Sep 17 00:00:00 2001 From: johngrantuk Date: Wed, 8 Jan 2025 15:03:22 +0000 Subject: [PATCH 01/16] feat(TS): Add Gyro2CLP support (untested). --- typescript/src/gyro/gyro2CLPData.ts | 11 ++ typescript/src/gyro/gyro2CLPMath.ts | 253 +++++++++++++++++++++++++ typescript/src/gyro/gyro2CLPPool.ts | 274 ++++++++++++++++++++++++++++ typescript/src/gyro/gyroPoolMath.ts | 110 +++++++++++ typescript/src/gyro/index.ts | 3 + typescript/src/index.ts | 1 + typescript/src/utils/math.ts | 2 + 7 files changed, 654 insertions(+) create mode 100644 typescript/src/gyro/gyro2CLPData.ts create mode 100644 typescript/src/gyro/gyro2CLPMath.ts create mode 100644 typescript/src/gyro/gyro2CLPPool.ts create mode 100644 typescript/src/gyro/gyroPoolMath.ts create mode 100644 typescript/src/gyro/index.ts diff --git a/typescript/src/gyro/gyro2CLPData.ts b/typescript/src/gyro/gyro2CLPData.ts new file mode 100644 index 0000000..7ac1276 --- /dev/null +++ b/typescript/src/gyro/gyro2CLPData.ts @@ -0,0 +1,11 @@ +import { BasePoolState } from '@/vault/types'; + +type PoolType = 'GYRO2CLP'; +export type Gyro2CLPImmutable = { + sqrtAlpha: bigint; + sqrtBeta: bigint; +}; + +export type Gyro2CLPState = BasePoolState & { + poolType: PoolType; +} & Gyro2CLPImmutable; diff --git a/typescript/src/gyro/gyro2CLPMath.ts b/typescript/src/gyro/gyro2CLPMath.ts new file mode 100644 index 0000000..29324f3 --- /dev/null +++ b/typescript/src/gyro/gyro2CLPMath.ts @@ -0,0 +1,253 @@ +// The invariant is used to calculate the virtual offsets used in swaps. +// It is also used to collect protocol swap fees by comparing its value between two times. +// We can always round in the same direction. It is also used to initialize the BPT amount and, + +import { FixedPointFunction, MathSol, WAD } from '@/utils/math'; +import { Rounding } from '../vault/types'; +import { GyroPoolMath } from './gyroPoolMath'; + +// because there is a minimum BPT, we round the invariant down. +export function calculateInvariant( + balances: bigint[], + sqrtAlpha: bigint, + sqrtBeta: bigint, + rounding: Rounding, +): bigint { + /********************************************************************************************** + // Calculate with quadratic formula + // 0 = (1-sqrt(alpha/beta)*L^2 - (y/sqrt(beta)+x*sqrt(alpha))*L - x*y) + // 0 = a*L^2 + b*L + c + // here a > 0, b < 0, and c < 0, which is a special case that works well w/o negative numbers + // taking mb = -b and mc = -c: (1/2) + // mb + (mb^2 + 4 * a * mc)^ // + // L = ------------------------------------------ // + // 2 * a // + // // + **********************************************************************************************/ + const { a, mb, bSquare, mc } = calculateQuadraticTerms( + balances, + sqrtAlpha, + sqrtBeta, + rounding, + ); + + return calculateQuadratic(a, mb, bSquare, mc); +} + +/** + * @notice Prepares quadratic terms for input to _calculateQuadratic. + * @dev It uses a special case of the quadratic formula that works nicely without negative numbers, and + * assumes a > 0, b < 0, and c <= 0. + * + * @param balances Pool balances + * @param sqrtAlpha Square root of Gyro's 2CLP alpha parameter + * @param sqrtBeta Square root of Gyro's 2CLP beta parameter + * @param rounding Rounding direction of the invariant, which will be calculated using the quadratic terms + * @return a Bhaskara's `a` term + * @return mb Bhaskara's `b` term, negative (stands for minus b) + * @return bSquare Bhaskara's `b^2` term. The calculation is optimized to be more precise than just b*b + * @return mc Bhaskara's `c` term, negative (stands for minus c) + */ +function calculateQuadraticTerms( + balances: bigint[], + sqrtAlpha: bigint, + sqrtBeta: bigint, + rounding: Rounding, +): { a: bigint; mb: bigint; bSquare: bigint; mc: bigint } { + const _divUpOrDown: FixedPointFunction = + rounding === Rounding.ROUND_DOWN + ? MathSol.divDownFixed + : MathSol.divUpFixed; + + const _mulUpOrDown: FixedPointFunction = + rounding === Rounding.ROUND_DOWN + ? MathSol.mulDownFixed + : MathSol.mulUpFixed; + + const _mulDownOrUp: FixedPointFunction = + rounding === Rounding.ROUND_DOWN + ? MathSol.mulUpFixed + : MathSol.mulDownFixed; + + // `a` follows the opposite rounding than `b` and `c`, since the most significant term is in the + // denominator of Bhaskara's formula. To round the invariant up, we need to round `a` down, which means that + // the division `sqrtAlpha/sqrtBeta` needs to be rounded up. In other words, if the given rounding + // direction is UP, 'a' will be rounded DOWN and vice versa. + const a = WAD - _divUpOrDown(sqrtAlpha, sqrtBeta); + + // `b` is a term in the numerator and should be rounded up if we want to increase the invariant. + const bterm0 = _divUpOrDown(balances[1], sqrtBeta); + const bterm1 = _mulUpOrDown(balances[0], sqrtAlpha); + const mb = bterm0 + bterm1; + // `c` is a term in the numerator and should be rounded up if we want to increase the invariant. + const mc = _mulUpOrDown(balances[0], balances[1]); + + // For better fixed point precision, calculate in expanded form, re-ordering multiplications. + // `b^2 = x^2 * alpha + x*y*2*sqrt(alpha/beta) + y^2 / beta` + let bSquare = _mulUpOrDown( + _mulUpOrDown(_mulUpOrDown(balances[0], balances[0]), sqrtAlpha), + sqrtAlpha, + ); + const bSq2 = _divUpOrDown( + 2n * _mulUpOrDown(_mulUpOrDown(balances[0], balances[1]), sqrtAlpha), + sqrtBeta, + ); + const bSq3 = _divUpOrDown( + _mulUpOrDown(balances[1], balances[1]), + _mulDownOrUp(sqrtBeta, sqrtBeta), + ); + bSquare = bSquare + bSq2 + bSq3; + return { a, mb, bSquare, mc }; +} + +/** + * @dev Calculates the quadratic root for a special case of the quadratic formula. + * assumes a > 0, b < 0, and c <= 0, which is the case for a L^2 + b L + c = 0 + * where a = 1 - sqrt(alpha/beta) + * b = -(y/sqrt(beta) + x*sqrt(alpha)) + * c = -x*y + * The special case works nicely without negative numbers. + * The args use the notation "mb" to represent -b, and "mc" to represent -c + * Note that this calculation underestimates the solution. + */ +function calculateQuadratic( + a: bigint, + mb: bigint, + bSquare: bigint, // b^2 can be calculated separately with more precision + mc: bigint, +): bigint { + const denominator = MathSol.mulUpFixed(a, 2n * WAD); + // Order multiplications for fixed point precision. + const addTerm = MathSol.mulDownFixed(MathSol.mulDownFixed(mc, 4n * WAD), a); + // The minus sign in the radicand cancels out in this special case. + const radicand = bSquare + addTerm; + const sqrResult = GyroPoolMath.sqrt(radicand, 5n); + // The minus sign in the numerator cancels out in this special case. + const numerator = mb + sqrResult; + const invariant = MathSol.divDownFixed(numerator, denominator); + return invariant; +} + +/** + * @dev Computes how many tokens can be taken out of a pool if `amountIn' are sent, given current balances. + * balanceIn = existing balance of input token + * balanceOut = existing balance of requested output token + * virtualParamIn = virtual reserve offset for input token + * virtualParamOut = virtual reserve offset for output token + * Offsets are L/sqrt(beta) and L*sqrt(alpha) depending on what the `in' and `out' tokens are respectively + * Note signs are changed compared to Prop. 4 in Section 2.2.4 Trade (Swap) Execution to account for dy < 0 + * + * The virtualOffset argument depends on the computed invariant. We add a very small margin to ensure that + * potential small errors are not to the detriment of the pool. + * + * There is a corresponding function in the 3CLP, except that there we allow two different virtual "in" and + * "out" assets. + * SOMEDAY: This could be made literally the same function in the pool math library. + */ +export function calcOutGivenIn( + balanceIn: bigint, + balanceOut: bigint, + amountIn: bigint, + virtualOffsetIn: bigint, + virtualOffsetOut: bigint, +): bigint { + /********************************************************************************************** + // Described for X = `in' asset and Y = `out' asset, but equivalent for the other case // + // dX = incrX = amountIn > 0 // + // dY = incrY = amountOut < 0 // + // x = balanceIn x' = x + virtualParamX // + // y = balanceOut y' = y + virtualParamY // + // L = inv.Liq / x' * y' \ y' * dX // + // |dy| = y' - | -------------------------- | = -------------- - // + // x' = virtIn \ ( x' + dX) / x' + dX // + // y' = virtOut // + // Note that -dy > 0 is what the trader receives. // + // We exploit the fact that this formula is symmetric up to virtualOffset{X,Y}. // + // We do not use L^2, but rather x' * y', to prevent a potential accumulation of errors. // + // We add a very small safety margin to compensate for potential errors in the invariant. // + **********************************************************************************************/ + + // The factors in total lead to a multiplicative "safety margin" between the employed virtual offsets + // that is very slightly larger than 3e-18. + const virtInOver = + balanceIn + MathSol.mulUpFixed(virtualOffsetIn, WAD + 2n); + const virtOutUnder = + balanceOut + MathSol.mulDownFixed(virtualOffsetOut, WAD - 1n); + + const amountOut = MathSol.divDownFixed( + MathSol.mulDownFixed(virtOutUnder, amountIn), + virtInOver + amountIn, + ); + + // This ensures amountOut < balanceOut. + if (!(amountOut <= balanceOut)) { + throw Error('AssetBoundsExceeded'); + } + return amountOut; +} + +/** + * @dev Computes how many tokens must be sent to a pool in order to take `amountOut`, given current balances. + * See also _calcOutGivenIn(). Adapted for negative values. + */ +export function calcInGivenOut( + balanceIn: bigint, + balanceOut: bigint, + amountOut: bigint, + virtualOffsetIn: bigint, + virtualOffsetOut: bigint, +): bigint { + /********************************************************************************************** + // dX = incrX = amountIn > 0 // + // dY = incrY = amountOut < 0 // + // x = balanceIn x' = x + virtualParamX // + // y = balanceOut y' = y + virtualParamY // + // x = balanceIn // + // L = inv.Liq / x' * y' \ x' * dy // + // dx = | -------------------------- | - x' = - ----------- // + // x' = virtIn \ y' + dy / y' + dy // + // y' = virtOut // + // Note that dy < 0 < dx. // + // We exploit the fact that this formula is symmetric up to virtualOffset{X,Y}. // + // We do not use L^2, but rather x' * y', to prevent a potential accumulation of errors. // + // We add a very small safety margin to compensate for potential errors in the invariant. // + **********************************************************************************************/ + if (!(amountOut <= balanceOut)) { + throw Error('AssetBoundsExceeded'); + } + + // The factors in total lead to a multiplicative "safety margin" between the employed virtual offsets + // that is very slightly larger than 3e-18. + const virtInOver = + balanceIn + MathSol.mulUpFixed(virtualOffsetIn, WAD + 2n); + const virtOutUnder = + balanceOut + MathSol.mulDownFixed(virtualOffsetOut, WAD - 1n); + + const amountIn = MathSol.divUpFixed( + MathSol.mulUpFixed(virtInOver, amountOut), + virtOutUnder - amountOut, + ); + return amountIn; +} + +/// @dev Calculate the virtual offset `a` for reserves `x`, as in (x+a)*(y+b)=L^2. +export function calculateVirtualParameter0( + invariant: bigint, + _sqrtBeta: bigint, + rounding: Rounding, +): bigint { + return rounding === Rounding.ROUND_DOWN + ? MathSol.divDownFixed(invariant, _sqrtBeta) + : MathSol.divUpFixed(invariant, _sqrtBeta); +} + +/// @dev Calculate the virtual offset `b` for reserves `y`, as in (x+a)*(y+b)=L^2. +export function calculateVirtualParameter1( + invariant: bigint, + _sqrtAlpha: bigint, + rounding: Rounding, +): bigint { + return rounding === Rounding.ROUND_DOWN + ? MathSol.mulDownFixed(invariant, _sqrtAlpha) + : MathSol.mulUpFixed(invariant, _sqrtAlpha); +} diff --git a/typescript/src/gyro/gyro2CLPPool.ts b/typescript/src/gyro/gyro2CLPPool.ts new file mode 100644 index 0000000..871ad91 --- /dev/null +++ b/typescript/src/gyro/gyro2CLPPool.ts @@ -0,0 +1,274 @@ +import { MAX_UINT256, MAX_BALANCE } from '../constants'; +import { + MaxSingleTokenRemoveParams, + MaxSwapParams, + type PoolBase, + Rounding, + SwapKind, + type SwapParams, +} from '../vault/types'; +import { toRawUndoRateRoundDown } from '../vault/utils'; +import { MathSol } from '../utils/math'; +import { Gyro2CLPImmutable } from './gyro2CLPData'; +import { + calcInGivenOut, + calcOutGivenIn, + calculateInvariant, + calculateVirtualParameter0, + calculateVirtualParameter1, +} from './gyro2CLPMath'; + +export class Gyro2CLP implements PoolBase { + public _sqrtAlpha: bigint; + public _sqrtBeta: bigint; + + constructor(poolState: Gyro2CLPImmutable) { + if (poolState.sqrtAlpha >= poolState.sqrtBeta) { + throw Error('SqrtParamsWrong'); + } + + this._sqrtAlpha = poolState.sqrtAlpha; + this._sqrtBeta = poolState.sqrtBeta; + } + + getMaximumInvariantRatio(): bigint { + return MAX_UINT256; + } + + getMinimumInvariantRatio(): bigint { + return 0n; + } + + /** + * Returns the max amount that can be swapped in relation to the swapKind. + * @param maxSwapParams + * @returns GivenIn: Returns the max amount in. GivenOut: Returns the max amount out. + */ + getMaxSwapAmount(maxSwapParams: MaxSwapParams): bigint { + const { + balancesLiveScaled18, + indexIn, + indexOut, + tokenRates, + scalingFactors, + swapKind, + } = maxSwapParams; + if (swapKind === SwapKind.GivenIn) { + // MAX_BALANCE comes from SC limit and is max pool can hold + const diff = MAX_BALANCE - balancesLiveScaled18[indexIn]; + // Scale to token in (and remove rate) + return toRawUndoRateRoundDown( + diff, + scalingFactors[indexIn], + tokenRates[indexIn], + ); + } + // 99% of token out balance + const max = MathSol.mulDownFixed( + 990000000000000000n, + balancesLiveScaled18[indexOut], + ); + // Scale to token out + return toRawUndoRateRoundDown( + max, + scalingFactors[indexOut], + tokenRates[indexOut], + ); + } + + getMaxSingleTokenAddAmount(): bigint { + return MAX_UINT256; + } + + getMaxSingleTokenRemoveAmount( + maxRemoveParams: MaxSingleTokenRemoveParams, + ): bigint { + const { + isExactIn, + totalSupply, + tokenOutBalance, + tokenOutScalingFactor, + tokenOutRate, + } = maxRemoveParams; + return this.getMaxSwapAmount({ + swapKind: isExactIn ? SwapKind.GivenIn : SwapKind.GivenOut, + balancesLiveScaled18: [totalSupply, tokenOutBalance], + tokenRates: [1000000000000000000n, tokenOutRate], + scalingFactors: [1000000000000000000n, tokenOutScalingFactor], + indexIn: 0, + indexOut: 1, + }); + } + + onSwap(swapParams: SwapParams): bigint { + const { + swapKind, + balancesLiveScaled18: balancesScaled18, + indexIn, + indexOut, + amountGivenScaled18, + } = swapParams; + + const tokenInIsToken0 = indexIn == 0; + const balanceTokenInScaled18 = balancesScaled18[indexIn]; + const balanceTokenOutScaled18 = balancesScaled18[indexOut]; + + const { virtualBalanceIn, virtualBalanceOut } = this._getVirtualOffsets( + balanceTokenInScaled18, + balanceTokenOutScaled18, + tokenInIsToken0, + ); + + if (swapKind === SwapKind.GivenIn) { + const amountOutScaled18 = calcOutGivenIn( + balanceTokenInScaled18, + balanceTokenOutScaled18, + amountGivenScaled18, + virtualBalanceIn, + virtualBalanceOut, + ); + return amountOutScaled18; + } + const amountInScaled18 = calcInGivenOut( + balanceTokenInScaled18, + balanceTokenOutScaled18, + amountGivenScaled18, + virtualBalanceIn, + virtualBalanceOut, + ); + + return amountInScaled18; + } + + computeInvariant( + balancesLiveScaled18: bigint[], + rounding: Rounding, + ): bigint { + return calculateInvariant( + balancesLiveScaled18, + this._sqrtAlpha, + this._sqrtBeta, + rounding, + ); + } + + computeBalance( + balancesLiveScaled18: bigint[], + tokenInIndex: number, + invariantRatio: bigint, + ): bigint { + /********************************************************************************************** + // Gyro invariant formula is: + // Lˆ2 = (x + a)(y + b) + // where: + // a = L / _sqrtBeta + // b = L * _sqrtAlpha + // + // In computeBalance, we want to know the new balance of a token, given that the invariant + // changed and the other token balance didn't change. To calculate that for "x", we use: + // + // (L*Lratio)ˆ2 = (newX + (L*Lratio) / _sqrtBeta)(y + (L*Lratio) * _sqrtAlpha) + // + // To simplify, let's rename a few terms: + // + // squareNewInv = (newX + a)(y + b) + // + // Isolating newX: newX = (squareNewInv/(y + b)) - a + // For newY: newY = (squareNewInv/(x + a)) - b + **********************************************************************************************/ + + // `computeBalance` is used to calculate unbalanced adds and removes, when the BPT value is specified. + // A bigger invariant in `computeAddLiquiditySingleTokenExactOut` means that more tokens are required to + // fulfill the trade, and a bigger invariant in `computeRemoveLiquiditySingleTokenExactIn` means that the + // amount out is lower. So, the invariant should always be rounded up. + let invariant = calculateInvariant( + balancesLiveScaled18, + this._sqrtAlpha, + this._sqrtBeta, + Rounding.ROUND_UP, + ); + // New invariant + invariant = MathSol.mulUpFixed(invariant, invariantRatio); + const squareNewInv = invariant * invariant; + // L / sqrt(beta) + const a = MathSol.divDownFixed(invariant, this._sqrtBeta); + // L * sqrt(alpha) + const b = MathSol.mulDownFixed(invariant, this._sqrtAlpha); + + let newBalance = 0n; + if (tokenInIndex === 0) { + // if newBalance = newX + newBalance = + MathSol.divUp(squareNewInv, balancesLiveScaled18[1] + b) - a; + } else { + // if newBalance = newY + newBalance = + MathSol.divUp(squareNewInv, balancesLiveScaled18[0] + a) - b; + } + return newBalance; + } + + /** + * @notice Return the virtual offsets of each token of the 2CLP pool. + * @dev The 2CLP invariant is defined as `L=(x+a)(y+b)`. "x" and "y" are the real balances, and "a" and "b" are + * offsets to concentrate the liquidity of the pool. The sum of real balance and offset is known as + * "virtual balance". Here we return the offsets a and b. + */ + _getVirtualOffsets( + balanceTokenInScaled18: bigint, + balanceTokenOutScaled18: bigint, + tokenInIsToken0: boolean, + ): { virtualBalanceIn: bigint; virtualBalanceOut: bigint } { + const balances = new Array(2).fill(0n); + balances[0] = tokenInIsToken0 + ? balanceTokenInScaled18 + : balanceTokenOutScaled18; + balances[1] = tokenInIsToken0 + ? balanceTokenOutScaled18 + : balanceTokenInScaled18; + + const currentInvariant = calculateInvariant( + balances, + this._sqrtAlpha, + this._sqrtBeta, + Rounding.ROUND_DOWN, + ); + + // virtualBalanceIn is always rounded up, because: + // * If swap is EXACT_IN: a bigger virtualBalanceIn leads to a lower amount out; + // * If swap is EXACT_OUT: a bigger virtualBalanceIn leads to a bigger amount in; + // virtualBalanceOut is always rounded down, because: + // * If swap is EXACT_IN: a lower virtualBalanceOut leads to a lower amount out; + // * If swap is EXACT_OUT: a lower virtualBalanceOut leads to a bigger amount in; + let virtualBalanceIn = 0n; + let virtualBalanceOut = 0n; + if (tokenInIsToken0) { + virtualBalanceIn = calculateVirtualParameter0( + currentInvariant, + this._sqrtBeta, + Rounding.ROUND_UP, + ); + virtualBalanceOut = calculateVirtualParameter1( + currentInvariant, + this._sqrtAlpha, + Rounding.ROUND_DOWN, + ); + } else { + virtualBalanceIn = calculateVirtualParameter1( + currentInvariant, + this._sqrtAlpha, + Rounding.ROUND_UP, + ); + virtualBalanceOut = calculateVirtualParameter0( + currentInvariant, + this._sqrtBeta, + Rounding.ROUND_DOWN, + ); + } + + return { + virtualBalanceIn, + virtualBalanceOut, + }; + } +} diff --git a/typescript/src/gyro/gyroPoolMath.ts b/typescript/src/gyro/gyroPoolMath.ts new file mode 100644 index 0000000..5b8f9ad --- /dev/null +++ b/typescript/src/gyro/gyroPoolMath.ts @@ -0,0 +1,110 @@ +import { MathSol, WAD } from '../utils/math'; + +export class GyroPoolMath { + static _SQRT_1E_NEG_1 = 316227766016837933n; + static _SQRT_1E_NEG_3 = 31622776601683793n; + static _SQRT_1E_NEG_5 = 3162277660168379n; + static _SQRT_1E_NEG_7 = 316227766016837n; + static _SQRT_1E_NEG_9 = 31622776601683n; + static _SQRT_1E_NEG_11 = 3162277660168n; + static _SQRT_1E_NEG_13 = 316227766016n; + static _SQRT_1E_NEG_15 = 31622776601n; + static _SQRT_1E_NEG_17 = 3162277660n; + + /// @dev Implements a square root algorithm using Newton's method and a first-guess optimization. + static sqrt(input: bigint, tolerance: bigint): bigint { + if (input === 0n) { + return 0n; + } + + let guess = this._makeInitialGuess(input); + + // At this point `guess` is an estimation with one bit of precision. We know the true value is a uint128, + // since it is the square root of a uint256. Newton's method converges quadratically (precision doubles at + // every iteration). We thus need at most 7 iterations to turn our partial result with one bit of precision + // into the expected uint128 result. + guess = (guess + (input * WAD) / guess) / 2n; + guess = (guess + (input * WAD) / guess) / 2n; + guess = (guess + (input * WAD) / guess) / 2n; + guess = (guess + (input * WAD) / guess) / 2n; + guess = (guess + (input * WAD) / guess) / 2n; + guess = (guess + (input * WAD) / guess) / 2n; + guess = (guess + (input * WAD) / guess) / 2n; + + // Check that squaredGuess (guess * guess) is close enough from input. `guess` has less than 1 wei error, but + // the loss of precision in the 18 decimal representation causes an error in the squared number, which must be + // less than `(guess * tolerance) / WAD`. Tolerance, in this case, is a very small number (< 10), + // so the tolerance will be very small too. + const guessSquared = MathSol.mulDownFixed(guess, guess); + if ( + !( + guessSquared <= input + MathSol.mulUpFixed(guess, tolerance) && + guessSquared >= input - MathSol.mulUpFixed(guess, tolerance) + ) + ) { + throw Error('_sqrt FAILED'); + } + + return guess; + } + + static _makeInitialGuess(input: bigint): bigint { + if (input >= WAD) { + return (1n << this._intLog2Halved(input / WAD)) * WAD; + } else { + if (input <= 10n) return this._SQRT_1E_NEG_17; + if (input <= 100n) return 10n ** 10n; + if (input <= 1000n) return this._SQRT_1E_NEG_15; + if (input <= 10000n) return 10n ** 11n; + if (input <= 100000n) return this._SQRT_1E_NEG_13; + if (input <= 1000000n) return 10n ** 12n; + if (input <= 10000000n) return this._SQRT_1E_NEG_11; + if (input <= 100000000n) return 10n ** 13n; + if (input <= 1000000000n) return this._SQRT_1E_NEG_9; + if (input <= 10000000000n) return 10n ** 14n; + if (input <= 100000000000n) return this._SQRT_1E_NEG_7; + if (input <= 1000000000000n) return 10n ** 15n; + if (input <= 10000000000000n) return this._SQRT_1E_NEG_5; + if (input <= 100000000000000n) return 10n ** 16n; + if (input <= 1000000000000000n) return this._SQRT_1E_NEG_3; + if (input <= 10000000000000000n) return 10n ** 17n; + if (input <= 100000000000000000n) return this._SQRT_1E_NEG_1; + return input; + } + } + + static _intLog2Halved(x: bigint): bigint { + let n = 0n; // Initialize n as a BigInt + + if (x >= 1n << 128n) { + x >>= 128n; + n += 64n; + } + if (x >= 1n << 64n) { + x >>= 64n; + n += 32n; + } + if (x >= 1n << 32n) { + x >>= 32n; + n += 16n; + } + if (x >= 1n << 16n) { + x >>= 16n; + n += 8n; + } + if (x >= 1n << 8n) { + x >>= 8n; + n += 4n; + } + if (x >= 1n << 4n) { + x >>= 4n; + n += 2n; + } + if (x >= 1n << 2n) { + x >>= 2n; + n += 1n; + } + + return n; + } +} diff --git a/typescript/src/gyro/index.ts b/typescript/src/gyro/index.ts new file mode 100644 index 0000000..0e964c5 --- /dev/null +++ b/typescript/src/gyro/index.ts @@ -0,0 +1,3 @@ +export * from './gyro2CLPData'; +export * from './gyro2CLPMath'; +export * from './gyro2CLPPool'; diff --git a/typescript/src/index.ts b/typescript/src/index.ts index 7afc656..b90809b 100644 --- a/typescript/src/index.ts +++ b/typescript/src/index.ts @@ -4,3 +4,4 @@ export * from './vault/types'; export * from './weighted'; export * from './buffer'; export * from './hooks'; +export * from './gyro'; diff --git a/typescript/src/utils/math.ts b/typescript/src/utils/math.ts index 890fa77..390dc82 100644 --- a/typescript/src/utils/math.ts +++ b/typescript/src/utils/math.ts @@ -17,6 +17,8 @@ const _require = (b: boolean, message: string) => { if (!b) throw new Error(message); }; +export type FixedPointFunction = (a: bigint, b: bigint) => bigint; + export class MathSol { static max(a: bigint, b: bigint): bigint { return a >= b ? a : b; From 452f6ec2b1aaada98658a03051c2c14f4449b476 Mon Sep 17 00:00:00 2001 From: johngrantuk Date: Thu, 9 Jan 2025 15:20:31 +0000 Subject: [PATCH 02/16] fix(TS): Import in gyro maths. --- typescript/src/gyro/gyro2CLPMath.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/typescript/src/gyro/gyro2CLPMath.ts b/typescript/src/gyro/gyro2CLPMath.ts index 29324f3..635046f 100644 --- a/typescript/src/gyro/gyro2CLPMath.ts +++ b/typescript/src/gyro/gyro2CLPMath.ts @@ -2,7 +2,7 @@ // It is also used to collect protocol swap fees by comparing its value between two times. // We can always round in the same direction. It is also used to initialize the BPT amount and, -import { FixedPointFunction, MathSol, WAD } from '@/utils/math'; +import { FixedPointFunction, MathSol, WAD } from '../utils/math'; import { Rounding } from '../vault/types'; import { GyroPoolMath } from './gyroPoolMath'; From e662a33a15f0a7bba15122a5ee7f029e97eaac12 Mon Sep 17 00:00:00 2001 From: johngrantuk Date: Thu, 9 Jan 2025 15:56:11 +0000 Subject: [PATCH 03/16] fix(TS): Use correct API type for Gyro2CLP. --- typescript/src/gyro/gyro2CLPData.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/typescript/src/gyro/gyro2CLPData.ts b/typescript/src/gyro/gyro2CLPData.ts index 7ac1276..dd025ff 100644 --- a/typescript/src/gyro/gyro2CLPData.ts +++ b/typescript/src/gyro/gyro2CLPData.ts @@ -1,6 +1,6 @@ import { BasePoolState } from '@/vault/types'; -type PoolType = 'GYRO2CLP'; +type PoolType = 'GYRO'; export type Gyro2CLPImmutable = { sqrtAlpha: bigint; sqrtBeta: bigint; From 158642a4929d193944c46dbcf56010d85980f489 Mon Sep 17 00:00:00 2001 From: johngrantuk Date: Thu, 9 Jan 2025 15:57:01 +0000 Subject: [PATCH 04/16] feat(TS): Add GyroECLP support (untested). --- typescript/src/gyro/gyroECLPData.ts | 23 + typescript/src/gyro/gyroECLPMath.ts | 919 ++++++++++++++++++++++++ typescript/src/gyro/gyroECLPPool.ts | 236 ++++++ typescript/src/gyro/index.ts | 3 + typescript/src/gyro/signedFixedPoint.ts | 237 ++++++ 5 files changed, 1418 insertions(+) create mode 100644 typescript/src/gyro/gyroECLPData.ts create mode 100644 typescript/src/gyro/gyroECLPMath.ts create mode 100644 typescript/src/gyro/gyroECLPPool.ts create mode 100644 typescript/src/gyro/signedFixedPoint.ts diff --git a/typescript/src/gyro/gyroECLPData.ts b/typescript/src/gyro/gyroECLPData.ts new file mode 100644 index 0000000..34a812f --- /dev/null +++ b/typescript/src/gyro/gyroECLPData.ts @@ -0,0 +1,23 @@ +import { BasePoolState } from '@/vault/types'; + +type PoolType = 'GYROE'; +export type GyroECLPImmutable = { + paramsAlpha: bigint; + paramsBeta: bigint; + paramsC: bigint; + paramsS: bigint; + paramsLambda: bigint; + tauAlphaX: bigint; + tauAlphaY: bigint; + tauBetaX: bigint; + tauBetaY: bigint; + u: bigint; + v: bigint; + w: bigint; + z: bigint; + dSq: bigint; +}; + +export type GyroECLPState = BasePoolState & { + poolType: PoolType; +} & GyroECLPImmutable; diff --git a/typescript/src/gyro/gyroECLPMath.ts b/typescript/src/gyro/gyroECLPMath.ts new file mode 100644 index 0000000..3ebfb2d --- /dev/null +++ b/typescript/src/gyro/gyroECLPMath.ts @@ -0,0 +1,919 @@ +import { GyroPoolMath } from './gyroPoolMath'; +import { SignedFixedPoint } from './signedFixedPoint'; + +export interface Vector2 { + x: bigint; + y: bigint; +} + +interface QParams { + a: bigint; + b: bigint; + c: bigint; +} + +class MaxBalancesExceededError extends Error { + constructor() { + super('Max assets exceeded'); + this.name = 'MaxBalancesExceededError'; + } +} + +class MaxInvariantExceededError extends Error { + constructor() { + super('Max invariant exceeded'); + this.name = 'MaxInvariantExceededError'; + } +} + +// Structs as interfaces +export interface EclpParams { + alpha: bigint; + beta: bigint; + c: bigint; + s: bigint; + lambda: bigint; +} + +export interface DerivedEclpParams { + tauAlpha: Vector2; + tauBeta: Vector2; + u: bigint; + v: bigint; + w: bigint; + z: bigint; + dSq: bigint; +} + +export class GyroECLPMath { + static readonly _ONEHALF = BigInt('500000000000000000'); // 0.5e18 + static readonly _ONE = BigInt('1000000000000000000'); // 1e18 + static readonly _ONE_XP = BigInt('100000000000000000000000000000000000000'); // 1e38 + + // Anti-overflow limits: Params and DerivedParams (static, only needs to be checked on pool creation) + static readonly _ROTATION_VECTOR_NORM_ACCURACY = BigInt('1000'); // 1e3 (1e-15 in normal precision) + static readonly _MAX_STRETCH_FACTOR = BigInt('100000000000000000000000000'); // 1e26 (1e8 in normal precision) + static readonly _DERIVED_TAU_NORM_ACCURACY_XP = BigInt( + '100000000000000000000000', + ); // 1e23 (1e-15 in extra precision) + static readonly _MAX_INV_INVARIANT_DENOMINATOR_XP = BigInt( + '10000000000000000000000000000000000000000000', + ); // 1e43 (1e5 in extra precision) + static readonly _DERIVED_DSQ_NORM_ACCURACY_XP = BigInt( + '100000000000000000000000', + ); // 1e23 (1e-15 in extra precision) + + // Anti-overflow limits: Dynamic values (checked before operations that use them) + static readonly _MAX_BALANCES = BigInt( + '100000000000000000000000000000000000', + ); // 1e34 (1e16 in normal precision) + static readonly _MAX_INVARIANT = BigInt( + '3000000000000000000000000000000000000', + ); // 3e37 (3e19 in normal precision) + + // Invariant growth limit: non-proportional add cannot cause the invariant to increase by more than this ratio + static readonly MIN_INVARIANT_RATIO = BigInt('600000000000000000'); // 60e16 (60%) + // Invariant shrink limit: non-proportional remove cannot cause the invariant to decrease by less than this ratio + static readonly MAX_INVARIANT_RATIO = BigInt('5000000000000000000'); // 500e16 (500%) + + static scalarProd(t1: Vector2, t2: Vector2): bigint { + const xProd = SignedFixedPoint.mulDownMag(t1.x, t2.x); + const yProd = SignedFixedPoint.mulDownMag(t1.y, t2.y); + return xProd + yProd; + } + + static scalarProdXp(t1: Vector2, t2: Vector2): bigint { + return ( + SignedFixedPoint.mulXp(t1.x, t2.x) + + SignedFixedPoint.mulXp(t1.y, t2.y) + ); + } + + static mulA(params: EclpParams, tp: Vector2): Vector2 { + return { + x: SignedFixedPoint.divDownMagU( + SignedFixedPoint.mulDownMagU(params.c, tp.x) - + SignedFixedPoint.mulDownMagU(params.s, tp.y), + params.lambda, + ), + y: + SignedFixedPoint.mulDownMagU(params.s, tp.x) + + SignedFixedPoint.mulDownMagU(params.c, tp.y), + }; + } + + static virtualOffset0( + p: EclpParams, + d: DerivedEclpParams, + r: Vector2, + ): bigint { + const termXp = SignedFixedPoint.divXpU(d.tauBeta.x, d.dSq); + let a: bigint; + + if (d.tauBeta.x > 0n) { + a = SignedFixedPoint.mulUpXpToNpU( + SignedFixedPoint.mulUpMagU( + SignedFixedPoint.mulUpMagU(r.x, p.lambda), + p.c, + ), + termXp, + ); + } else { + a = SignedFixedPoint.mulUpXpToNpU( + SignedFixedPoint.mulDownMagU( + SignedFixedPoint.mulDownMagU(r.y, p.lambda), + p.c, + ), + termXp, + ); + } + + return ( + a + + SignedFixedPoint.mulUpXpToNpU( + SignedFixedPoint.mulUpMagU(r.x, p.s), + SignedFixedPoint.divXpU(d.tauBeta.y, d.dSq), + ) + ); + } + + static virtualOffset1( + p: EclpParams, + d: DerivedEclpParams, + r: Vector2, + ): bigint { + const termXp = SignedFixedPoint.divXpU(d.tauAlpha.x, d.dSq); + let b: bigint; + + if (d.tauAlpha.x < 0n) { + b = SignedFixedPoint.mulUpXpToNpU( + SignedFixedPoint.mulUpMagU( + SignedFixedPoint.mulUpMagU(r.x, p.lambda), + p.s, + ), + -termXp, + ); + } else { + b = SignedFixedPoint.mulUpXpToNpU( + SignedFixedPoint.mulDownMagU( + SignedFixedPoint.mulDownMagU(-r.y, p.lambda), + p.s, + ), + termXp, + ); + } + + return ( + b + + SignedFixedPoint.mulUpXpToNpU( + SignedFixedPoint.mulUpMagU(r.x, p.c), + SignedFixedPoint.divXpU(d.tauAlpha.y, d.dSq), + ) + ); + } + + static maxBalances0( + p: EclpParams, + d: DerivedEclpParams, + r: Vector2, + ): bigint { + const termXp1 = SignedFixedPoint.divXpU( + d.tauBeta.x - d.tauAlpha.x, + d.dSq, + ); + const termXp2 = SignedFixedPoint.divXpU( + d.tauBeta.y - d.tauAlpha.y, + d.dSq, + ); + + const xp = SignedFixedPoint.mulDownXpToNpU( + SignedFixedPoint.mulDownMagU( + SignedFixedPoint.mulDownMagU(r.y, p.lambda), + p.c, + ), + termXp1, + ); + + const term2 = + termXp2 > 0n + ? SignedFixedPoint.mulDownMagU(r.y, p.s) + : SignedFixedPoint.mulUpMagU(r.x, p.s); + + return xp + SignedFixedPoint.mulDownXpToNpU(term2, termXp2); + } + + static maxBalances1( + p: EclpParams, + d: DerivedEclpParams, + r: Vector2, + ): bigint { + const termXp1 = SignedFixedPoint.divXpU( + d.tauBeta.x - d.tauAlpha.x, + d.dSq, + ); + const termXp2 = SignedFixedPoint.divXpU( + d.tauAlpha.y - d.tauBeta.y, + d.dSq, + ); + + const yp = SignedFixedPoint.mulDownXpToNpU( + SignedFixedPoint.mulDownMagU( + SignedFixedPoint.mulDownMagU(r.y, p.lambda), + p.s, + ), + termXp1, + ); + + const term2 = + termXp2 > 0n + ? SignedFixedPoint.mulDownMagU(r.y, p.c) + : SignedFixedPoint.mulUpMagU(r.x, p.c); + + return yp + SignedFixedPoint.mulDownXpToNpU(term2, termXp2); + } + + static calcAtAChi( + x: bigint, + y: bigint, + p: EclpParams, + d: DerivedEclpParams, + ): bigint { + const dSq2 = SignedFixedPoint.mulXpU(d.dSq, d.dSq); + + const termXp = SignedFixedPoint.divXpU( + SignedFixedPoint.divDownMagU( + SignedFixedPoint.divDownMagU(d.w, p.lambda) + d.z, + p.lambda, + ), + dSq2, + ); + + let val = SignedFixedPoint.mulDownXpToNpU( + SignedFixedPoint.mulDownMagU(x, p.c) - + SignedFixedPoint.mulDownMagU(y, p.s), + termXp, + ); + + const termNp1 = SignedFixedPoint.mulDownMagU(x, p.lambda); + const termNp2 = SignedFixedPoint.mulDownMagU(y, p.lambda); + + val += SignedFixedPoint.mulDownXpToNpU( + SignedFixedPoint.mulDownMagU(termNp1, p.s) + + SignedFixedPoint.mulDownMagU(termNp2, p.c), + SignedFixedPoint.divXpU(d.u, dSq2), + ); + + val += SignedFixedPoint.mulDownXpToNpU( + SignedFixedPoint.mulDownMagU(x, p.s) + + SignedFixedPoint.mulDownMagU(y, p.c), + SignedFixedPoint.divXpU(d.v, dSq2), + ); + + return val; + } + + static calcAChiAChiInXp(p: EclpParams, d: DerivedEclpParams): bigint { + const dSq3 = SignedFixedPoint.mulXpU( + SignedFixedPoint.mulXpU(d.dSq, d.dSq), + d.dSq, + ); + + let val = SignedFixedPoint.mulUpMagU( + p.lambda, + SignedFixedPoint.divXpU( + SignedFixedPoint.mulXpU(2n * d.u, d.v), + dSq3, + ), + ); + + val += SignedFixedPoint.mulUpMagU( + SignedFixedPoint.mulUpMagU( + SignedFixedPoint.divXpU( + SignedFixedPoint.mulXpU(d.u + 1n, d.u + 1n), + dSq3, + ), + p.lambda, + ), + p.lambda, + ); + + val += SignedFixedPoint.divXpU(SignedFixedPoint.mulXpU(d.v, d.v), dSq3); + + const termXp = SignedFixedPoint.divUpMagU(d.w, p.lambda) + d.z; + val += SignedFixedPoint.divXpU( + SignedFixedPoint.mulXpU(termXp, termXp), + dSq3, + ); + + return val; + } + + static calculateInvariantWithError( + balances: bigint[], + params: EclpParams, + derived: DerivedEclpParams, + ): [bigint, bigint] { + const x = balances[0]; + const y = balances[1]; + + if (x + y > this._MAX_BALANCES) { + throw new MaxBalancesExceededError(); + } + + const atAChi = this.calcAtAChi(x, y, params, derived); + const achiachi = this.calcAChiAChiInXp(params, derived); + + // Calculate error (simplified for this example) + const err = + (SignedFixedPoint.mulUpMagU(params.lambda, x + y) / this._ONE_XP + + 1n) * + 20n; + + const mulDenominator = SignedFixedPoint.divXpU( + this._ONE_XP, + achiachi - this._ONE_XP, + ); + + const invariant = SignedFixedPoint.mulDownXpToNpU( + atAChi - err, + mulDenominator, + ); + + // Error calculation (simplified) + const scaledErr = SignedFixedPoint.mulUpXpToNpU(err, mulDenominator); + const totalErr = + scaledErr + + (invariant * + ((params.lambda * params.lambda) / + BigInt('10000000000000000000000000000000000000')) * + 40n) / + this._ONE_XP + + 1n; + + if (invariant + totalErr > this._MAX_INVARIANT) { + throw new MaxInvariantExceededError(); + } + + return [invariant, totalErr]; + } + + static calcMinAtxAChiySqPlusAtxSq( + x: bigint, + y: bigint, + p: EclpParams, + d: DerivedEclpParams, + ): bigint { + let termNp = + SignedFixedPoint.mulUpMagU( + SignedFixedPoint.mulUpMagU( + SignedFixedPoint.mulUpMagU(x, x), + p.c, + ), + p.c, + ) + + SignedFixedPoint.mulUpMagU( + SignedFixedPoint.mulUpMagU( + SignedFixedPoint.mulUpMagU(y, y), + p.s, + ), + p.s, + ); + + termNp = + termNp - + SignedFixedPoint.mulDownMagU( + SignedFixedPoint.mulDownMagU( + SignedFixedPoint.mulDownMagU(x, y), + p.c * 2n, + ), + p.s, + ); + + let termXp = + SignedFixedPoint.mulXpU(d.u, d.u) + + SignedFixedPoint.divDownMagU( + SignedFixedPoint.mulXpU(d.u * 2n, d.v), + p.lambda, + ) + + SignedFixedPoint.divDownMagU( + SignedFixedPoint.divDownMagU( + SignedFixedPoint.mulXpU(d.v, d.v), + p.lambda, + ), + p.lambda, + ); + + termXp = SignedFixedPoint.divXpU( + termXp, + SignedFixedPoint.mulXpU( + SignedFixedPoint.mulXpU( + SignedFixedPoint.mulXpU(d.dSq, d.dSq), + d.dSq, + ), + d.dSq, + ), + ); + + let val = SignedFixedPoint.mulDownXpToNpU(-termNp, termXp); + + val = + val + + SignedFixedPoint.mulDownXpToNpU( + SignedFixedPoint.divDownMagU( + SignedFixedPoint.divDownMagU(termNp - 9n, p.lambda), + p.lambda, + ), + SignedFixedPoint.divXpU(SignedFixedPoint.ONE_XP, d.dSq), + ); + + return val; + } + + static calc2AtxAtyAChixAChiy( + x: bigint, + y: bigint, + p: EclpParams, + d: DerivedEclpParams, + ): bigint { + let termNp = SignedFixedPoint.mulDownMagU( + SignedFixedPoint.mulDownMagU( + SignedFixedPoint.mulDownMagU(x, x) - + SignedFixedPoint.mulUpMagU(y, y), + 2n * p.c, + ), + p.s, + ); + + const xy = SignedFixedPoint.mulDownMagU(y, 2n * x); + + termNp = + termNp + + SignedFixedPoint.mulDownMagU( + SignedFixedPoint.mulDownMagU(xy, p.c), + p.c, + ) - + SignedFixedPoint.mulDownMagU( + SignedFixedPoint.mulDownMagU(xy, p.s), + p.s, + ); + + let termXp = + SignedFixedPoint.mulXpU(d.z, d.u) + + SignedFixedPoint.divDownMagU( + SignedFixedPoint.divDownMagU( + SignedFixedPoint.mulXpU(d.w, d.v), + p.lambda, + ), + p.lambda, + ); + + termXp = + termXp + + SignedFixedPoint.divDownMagU( + SignedFixedPoint.mulXpU(d.w, d.u) + + SignedFixedPoint.mulXpU(d.z, d.v), + p.lambda, + ); + + termXp = SignedFixedPoint.divXpU( + termXp, + SignedFixedPoint.mulXpU( + SignedFixedPoint.mulXpU( + SignedFixedPoint.mulXpU(d.dSq, d.dSq), + d.dSq, + ), + d.dSq, + ), + ); + + return SignedFixedPoint.mulDownXpToNpU(termNp, termXp); + } + + static calcMinAtyAChixSqPlusAtySq( + x: bigint, + y: bigint, + p: EclpParams, + d: DerivedEclpParams, + ): bigint { + let termNp = + SignedFixedPoint.mulUpMagU( + SignedFixedPoint.mulUpMagU( + SignedFixedPoint.mulUpMagU(x, x), + p.s, + ), + p.s, + ) + + SignedFixedPoint.mulUpMagU( + SignedFixedPoint.mulUpMagU( + SignedFixedPoint.mulUpMagU(y, y), + p.c, + ), + p.c, + ); + + termNp = + termNp + + SignedFixedPoint.mulUpMagU( + SignedFixedPoint.mulUpMagU( + SignedFixedPoint.mulUpMagU(x, y), + p.s * 2n, + ), + p.c, + ); + + let termXp = + SignedFixedPoint.mulXpU(d.z, d.z) + + SignedFixedPoint.divDownMagU( + SignedFixedPoint.divDownMagU( + SignedFixedPoint.mulXpU(d.w, d.w), + p.lambda, + ), + p.lambda, + ); + + termXp = + termXp + + SignedFixedPoint.divDownMagU( + SignedFixedPoint.mulXpU(2n * d.z, d.w), + p.lambda, + ); + + termXp = SignedFixedPoint.divXpU( + termXp, + SignedFixedPoint.mulXpU( + SignedFixedPoint.mulXpU( + SignedFixedPoint.mulXpU(d.dSq, d.dSq), + d.dSq, + ), + d.dSq, + ), + ); + + let val = SignedFixedPoint.mulDownXpToNpU(-termNp, termXp); + + val = + val + + SignedFixedPoint.mulDownXpToNpU( + termNp - 9n, + SignedFixedPoint.divXpU(SignedFixedPoint.ONE_XP, d.dSq), + ); + + return val; + } + + static calcInvariantSqrt( + x: bigint, + y: bigint, + p: EclpParams, + d: DerivedEclpParams, + ): [bigint, bigint] { + let val = + this.calcMinAtxAChiySqPlusAtxSq(x, y, p, d) + + this.calc2AtxAtyAChixAChiy(x, y, p, d) + + this.calcMinAtyAChixSqPlusAtySq(x, y, p, d); + + const err = + (SignedFixedPoint.mulUpMagU(x, x) + + SignedFixedPoint.mulUpMagU(y, y)) / + BigInt('1000000000000000000000000000000000000000'); // 1e38 + + val = val > 0n ? GyroPoolMath.sqrt(val > 0n ? val : 0n, 5n) : 0n; + + return [val, err]; + } + + static calcSpotPrice0in1( + balances: bigint[], + params: EclpParams, + derived: DerivedEclpParams, + invariant: bigint, + ): bigint { + const r: Vector2 = { x: invariant, y: invariant }; + const ab: Vector2 = { + x: this.virtualOffset0(params, derived, r), + y: this.virtualOffset1(params, derived, r), + }; + const vec: Vector2 = { + x: balances[0] - ab.x, + y: balances[1] - ab.y, + }; + + const transformedVec = this.mulA(params, vec); + const pc: Vector2 = { + x: SignedFixedPoint.divDownMagU(transformedVec.x, transformedVec.y), + y: this._ONE, + }; + + const pgx = this.scalarProd( + pc, + this.mulA(params, { x: this._ONE, y: 0n }), + ); + return SignedFixedPoint.divDownMag( + pgx, + this.scalarProd(pc, this.mulA(params, { x: 0n, y: this._ONE })), + ); + } + + static checkAssetBounds( + params: EclpParams, + derived: DerivedEclpParams, + invariant: Vector2, + newBal: bigint, + assetIndex: number, + ): void { + if (assetIndex === 0) { + const xPlus = this.maxBalances0(params, derived, invariant); + if (newBal > this._MAX_BALANCES || newBal > xPlus) { + throw new Error('Asset bounds exceeded'); + } + } else { + const yPlus = this.maxBalances1(params, derived, invariant); + if (newBal > this._MAX_BALANCES || newBal > yPlus) { + throw new Error('Asset bounds exceeded'); + } + } + } + + static calcOutGivenIn( + balances: bigint[], + amountIn: bigint, + tokenInIsToken0: boolean, + params: EclpParams, + derived: DerivedEclpParams, + invariant: Vector2, + ): bigint { + const [ixIn, ixOut, calcGiven] = tokenInIsToken0 + ? [0, 1, this.calcYGivenX] + : [1, 0, this.calcXGivenY]; + + const balInNew = balances[ixIn] + amountIn; + this.checkAssetBounds(params, derived, invariant, balInNew, ixIn); + const balOutNew = calcGiven.call( + this, + balInNew, + params, + derived, + invariant, + ); + return balances[ixOut] - balOutNew; + } + + static calcInGivenOut( + balances: bigint[], + amountOut: bigint, + tokenInIsToken0: boolean, + params: EclpParams, + derived: DerivedEclpParams, + invariant: Vector2, + ): bigint { + const [ixIn, ixOut, calcGiven] = tokenInIsToken0 + ? [0, 1, this.calcXGivenY] // Note: reversed compared to calcOutGivenIn + : [1, 0, this.calcYGivenX]; // Note: reversed compared to calcOutGivenIn + + if (amountOut > balances[ixOut]) { + throw new Error('Asset bounds exceeded'); + } + const balOutNew = balances[ixOut] - amountOut; + const balInNew = calcGiven.call( + this, + balOutNew, + params, + derived, + invariant, + ); + this.checkAssetBounds(params, derived, invariant, balInNew, ixIn); + return balInNew - balances[ixIn]; + } + + static solveQuadraticSwap( + lambda: bigint, + x: bigint, + s: bigint, + c: bigint, + r: Vector2, + ab: Vector2, + tauBeta: Vector2, + dSq: bigint, + ): bigint { + const lamBar: Vector2 = { + x: + SignedFixedPoint.ONE_XP - + SignedFixedPoint.divDownMagU( + SignedFixedPoint.divDownMagU( + SignedFixedPoint.ONE_XP, + lambda, + ), + lambda, + ), + y: + SignedFixedPoint.ONE_XP - + SignedFixedPoint.divUpMagU( + SignedFixedPoint.divUpMagU(SignedFixedPoint.ONE_XP, lambda), + lambda, + ), + }; + + const q: QParams = { a: 0n, b: 0n, c: 0n }; + const xp = x - ab.x; + + if (xp > 0n) { + q.b = SignedFixedPoint.mulUpXpToNpU( + SignedFixedPoint.mulDownMagU( + SignedFixedPoint.mulDownMagU(-xp, s), + c, + ), + SignedFixedPoint.divXpU(lamBar.y, dSq), + ); + } else { + q.b = SignedFixedPoint.mulUpXpToNpU( + SignedFixedPoint.mulUpMagU( + SignedFixedPoint.mulUpMagU(-xp, s), + c, + ), + SignedFixedPoint.divXpU(lamBar.x, dSq) + 1n, + ); + } + + const sTerm: Vector2 = { + x: SignedFixedPoint.divXpU( + SignedFixedPoint.mulDownMagU( + SignedFixedPoint.mulDownMagU(lamBar.y, s), + s, + ), + dSq, + ), + y: + SignedFixedPoint.divXpU( + SignedFixedPoint.mulUpMagU( + SignedFixedPoint.mulUpMagU(lamBar.x, s), + s, + ), + dSq + 1n, + ) + 1n, + }; + + sTerm.x = SignedFixedPoint.ONE_XP - sTerm.x; + sTerm.y = SignedFixedPoint.ONE_XP - sTerm.y; + + q.c = -this.calcXpXpDivLambdaLambda(x, r, lambda, s, c, tauBeta, dSq); + q.c = + q.c + + SignedFixedPoint.mulDownXpToNpU( + SignedFixedPoint.mulDownMagU(r.y, r.y), + sTerm.y, + ); + + q.c = q.c > 0n ? GyroPoolMath.sqrt(q.c, 5n) : 0n; + + if (q.b - q.c > 0n) { + q.a = SignedFixedPoint.mulUpXpToNpU( + q.b - q.c, + SignedFixedPoint.divXpU(SignedFixedPoint.ONE_XP, sTerm.y) + 1n, + ); + } else { + q.a = SignedFixedPoint.mulUpXpToNpU( + q.b - q.c, + SignedFixedPoint.divXpU(SignedFixedPoint.ONE_XP, sTerm.x), + ); + } + + return q.a + ab.y; + } + + static calcXpXpDivLambdaLambda( + x: bigint, + r: Vector2, + lambda: bigint, + s: bigint, + c: bigint, + tauBeta: Vector2, + dSq: bigint, + ): bigint { + const sqVars: Vector2 = { + x: SignedFixedPoint.mulXpU(dSq, dSq), + y: SignedFixedPoint.mulUpMagU(r.x, r.x), + }; + + const q: QParams = { a: 0n, b: 0n, c: 0n }; + const termXp = SignedFixedPoint.divXpU( + SignedFixedPoint.mulXpU(tauBeta.x, tauBeta.y), + sqVars.x, + ); + + if (termXp > 0n) { + q.a = SignedFixedPoint.mulUpMagU(sqVars.y, 2n * s); + q.a = SignedFixedPoint.mulUpXpToNpU( + SignedFixedPoint.mulUpMagU(q.a, c), + termXp + 7n, + ); + } else { + q.a = SignedFixedPoint.mulDownMagU(r.y, r.y); + q.a = SignedFixedPoint.mulDownMagU(q.a, 2n * s); + q.a = SignedFixedPoint.mulUpXpToNpU( + SignedFixedPoint.mulDownMagU(q.a, c), + termXp, + ); + } + + if (tauBeta.x < 0n) { + q.b = SignedFixedPoint.mulUpXpToNpU( + SignedFixedPoint.mulUpMagU( + SignedFixedPoint.mulUpMagU(r.x, x), + 2n * c, + ), + -SignedFixedPoint.divXpU(tauBeta.x, dSq) + 3n, + ); + } else { + q.b = SignedFixedPoint.mulUpXpToNpU( + SignedFixedPoint.mulDownMagU( + SignedFixedPoint.mulDownMagU(-r.y, x), + 2n * c, + ), + SignedFixedPoint.divXpU(tauBeta.x, dSq), + ); + } + q.a = q.a + q.b; + + const termXp2 = + SignedFixedPoint.divXpU( + SignedFixedPoint.mulXpU(tauBeta.y, tauBeta.y), + sqVars.x, + ) + 7n; + + q.b = SignedFixedPoint.mulUpMagU(sqVars.y, s); + q.b = SignedFixedPoint.mulUpXpToNpU( + SignedFixedPoint.mulUpMagU(q.b, s), + termXp2, + ); + + q.c = SignedFixedPoint.mulUpXpToNpU( + SignedFixedPoint.mulDownMagU( + SignedFixedPoint.mulDownMagU(-r.y, x), + 2n * s, + ), + SignedFixedPoint.divXpU(tauBeta.y, dSq), + ); + + q.b = q.b + q.c + SignedFixedPoint.mulUpMagU(x, x); + q.b = + q.b > 0n + ? SignedFixedPoint.divUpMagU(q.b, lambda) + : SignedFixedPoint.divDownMagU(q.b, lambda); + + q.a = q.a + q.b; + q.a = + q.a > 0n + ? SignedFixedPoint.divUpMagU(q.a, lambda) + : SignedFixedPoint.divDownMagU(q.a, lambda); + + const val = SignedFixedPoint.mulUpMagU( + SignedFixedPoint.mulUpMagU(sqVars.y, c), + c, + ); + return SignedFixedPoint.mulUpXpToNpU(val, termXp2) + q.a; + } + + static calcYGivenX( + x: bigint, + params: EclpParams, + d: DerivedEclpParams, + r: Vector2, + ): bigint { + const ab: Vector2 = { + x: this.virtualOffset0(params, d, r), + y: this.virtualOffset1(params, d, r), + }; + return this.solveQuadraticSwap( + params.lambda, + x, + params.s, + params.c, + r, + ab, + d.tauBeta, + d.dSq, + ); + } + + static calcXGivenY( + y: bigint, + params: EclpParams, + d: DerivedEclpParams, + r: Vector2, + ): bigint { + const ba: Vector2 = { + x: this.virtualOffset1(params, d, r), + y: this.virtualOffset0(params, d, r), + }; + return this.solveQuadraticSwap( + params.lambda, + y, + params.c, + params.s, + r, + ba, + { x: -d.tauAlpha.x, y: d.tauAlpha.y }, + d.dSq, + ); + } +} diff --git a/typescript/src/gyro/gyroECLPPool.ts b/typescript/src/gyro/gyroECLPPool.ts new file mode 100644 index 0000000..e0a952b --- /dev/null +++ b/typescript/src/gyro/gyroECLPPool.ts @@ -0,0 +1,236 @@ +import { MAX_UINT256, MAX_BALANCE } from '../constants'; +import { + MaxSingleTokenRemoveParams, + MaxSwapParams, + type PoolBase, + Rounding, + SwapKind, + type SwapParams, +} from '../vault/types'; +import { toRawUndoRateRoundDown } from '../vault/utils'; +import { MathSol } from '../utils/math'; +import { + DerivedEclpParams, + EclpParams, + GyroECLPMath, + Vector2, +} from './gyroECLPMath'; +import { GyroECLPImmutable } from './gyroECLPData'; + +type PoolParams = { + eclpParams: EclpParams; + derivedECLPParams: DerivedEclpParams; +}; + +export class GyroECLP implements PoolBase { + public poolParams: PoolParams; + + constructor(poolState: GyroECLPImmutable) { + this.poolParams = { + eclpParams: { + alpha: poolState.paramsAlpha, + beta: poolState.paramsBeta, + c: poolState.paramsC, + s: poolState.paramsS, + lambda: poolState.paramsLambda, + }, + derivedECLPParams: { + tauAlpha: { + x: poolState.tauAlphaX, + y: poolState.tauAlphaY, + }, + tauBeta: { + x: poolState.tauBetaX, + y: poolState.tauBetaY, + }, + u: poolState.u, + v: poolState.v, + w: poolState.w, + z: poolState.z, + dSq: poolState.dSq, + }, + }; + } + + getMaximumInvariantRatio(): bigint { + return GyroECLPMath.MAX_INVARIANT_RATIO; + } + + getMinimumInvariantRatio(): bigint { + return GyroECLPMath.MIN_INVARIANT_RATIO; + } + + /** + * Returns the max amount that can be swapped in relation to the swapKind. + * @param maxSwapParams + * @returns GivenIn: Returns the max amount in. GivenOut: Returns the max amount out. + */ + getMaxSwapAmount(maxSwapParams: MaxSwapParams): bigint { + const { + balancesLiveScaled18, + indexIn, + indexOut, + tokenRates, + scalingFactors, + swapKind, + } = maxSwapParams; + if (swapKind === SwapKind.GivenIn) { + // MAX_BALANCE comes from SC limit and is max pool can hold + const diff = MAX_BALANCE - balancesLiveScaled18[indexIn]; + // Scale to token in (and remove rate) + return toRawUndoRateRoundDown( + diff, + scalingFactors[indexIn], + tokenRates[indexIn], + ); + } + // 99% of token out balance + const max = MathSol.mulDownFixed( + 990000000000000000n, + balancesLiveScaled18[indexOut], + ); + // Scale to token out + return toRawUndoRateRoundDown( + max, + scalingFactors[indexOut], + tokenRates[indexOut], + ); + } + + getMaxSingleTokenAddAmount(): bigint { + return MAX_UINT256; + } + + getMaxSingleTokenRemoveAmount( + maxRemoveParams: MaxSingleTokenRemoveParams, + ): bigint { + const { + isExactIn, + totalSupply, + tokenOutBalance, + tokenOutScalingFactor, + tokenOutRate, + } = maxRemoveParams; + return this.getMaxSwapAmount({ + swapKind: isExactIn ? SwapKind.GivenIn : SwapKind.GivenOut, + balancesLiveScaled18: [totalSupply, tokenOutBalance], + tokenRates: [1000000000000000000n, tokenOutRate], + scalingFactors: [1000000000000000000n, tokenOutScalingFactor], + indexIn: 0, + indexOut: 1, + }); + } + + onSwap(swapParams: SwapParams): bigint { + const { + swapKind, + balancesLiveScaled18: balancesScaled18, + indexIn, + amountGivenScaled18, + } = swapParams; + + const tokenInIsToken0 = indexIn === 0; + + const { eclpParams, derivedECLPParams } = this.poolParams; + + const [currentInvariant, invErr] = + GyroECLPMath.calculateInvariantWithError( + balancesScaled18, + eclpParams, + derivedECLPParams, + ); + // invariant = overestimate in x-component, underestimate in y-component + // No overflow in `+` due to constraints to the different values enforced in GyroECLPMath. + const invariant: Vector2 = { + x: currentInvariant + 2n * invErr, + y: currentInvariant, + }; + + if (swapKind === SwapKind.GivenIn) { + const amountOutScaled18 = GyroECLPMath.calcOutGivenIn( + balancesScaled18, + amountGivenScaled18, + tokenInIsToken0, + eclpParams, + derivedECLPParams, + invariant, + ); + + return amountOutScaled18; + } + + const amountInScaled18 = GyroECLPMath.calcInGivenOut( + balancesScaled18, + amountGivenScaled18, + tokenInIsToken0, + eclpParams, + derivedECLPParams, + invariant, + ); + + return amountInScaled18; + } + + computeInvariant( + balancesLiveScaled18: bigint[], + rounding: Rounding, + ): bigint { + const { eclpParams, derivedECLPParams } = this.poolParams; + const [currentInvariant, invErr] = + GyroECLPMath.calculateInvariantWithError( + balancesLiveScaled18, + eclpParams, + derivedECLPParams, + ); + + if (rounding == Rounding.ROUND_DOWN) { + return currentInvariant - invErr; + } else { + return currentInvariant + invErr; + } + } + + computeBalance( + balancesLiveScaled18: bigint[], + tokenInIndex: number, + invariantRatio: bigint, + ): bigint { + const { eclpParams, derivedECLPParams } = this.poolParams; + + const [currentInvariant, invErr] = + GyroECLPMath.calculateInvariantWithError( + balancesLiveScaled18, + eclpParams, + derivedECLPParams, + ); + + // The invariant vector contains the rounded up and rounded down invariant. Both are needed when computing + // the virtual offsets. Depending on tauAlpha and tauBeta values, we want to use the invariant rounded up + // or rounded down to make sure we're conservative in the output. + const invariant: Vector2 = { + x: MathSol.mulUpFixed(currentInvariant + invErr, invariantRatio), + y: MathSol.mulUpFixed(currentInvariant - invErr, invariantRatio), + }; + + // Edge case check. Should never happen except for insane tokens. If this is hit, actually adding the + // tokens would lead to a revert or (if it went through) a deadlock downstream, so we catch it here. + if (invariant.x > GyroECLPMath._MAX_INVARIANT) + throw Error(`GyroECLPMath.MaxInvariantExceeded`); + + if (tokenInIndex === 0) { + return GyroECLPMath.calcXGivenY( + balancesLiveScaled18[1], + eclpParams, + derivedECLPParams, + invariant, + ); + } else { + return GyroECLPMath.calcYGivenX( + balancesLiveScaled18[0], + eclpParams, + derivedECLPParams, + invariant, + ); + } + } +} diff --git a/typescript/src/gyro/index.ts b/typescript/src/gyro/index.ts index 0e964c5..af3f8cd 100644 --- a/typescript/src/gyro/index.ts +++ b/typescript/src/gyro/index.ts @@ -1,3 +1,6 @@ export * from './gyro2CLPData'; export * from './gyro2CLPMath'; export * from './gyro2CLPPool'; +export * from './gyroECLPData'; +export * from './gyroECLPMath'; +export * from './gyroECLPPool'; diff --git a/typescript/src/gyro/signedFixedPoint.ts b/typescript/src/gyro/signedFixedPoint.ts new file mode 100644 index 0000000..8797eeb --- /dev/null +++ b/typescript/src/gyro/signedFixedPoint.ts @@ -0,0 +1,237 @@ +class FixedPointError extends Error { + constructor(message: string) { + super(message); + this.name = 'FixedPointError'; + } +} + +export class SignedFixedPoint { + public static readonly ONE = BigInt('1000000000000000000'); // 1e18 + public static readonly ONE_XP = BigInt( + '100000000000000000000000000000000000000', + ); // 1e38 + + static add(a: bigint, b: bigint): bigint { + const c = a + b; + if (!(b >= 0n ? c >= a : c < a)) { + throw new FixedPointError('AddOverflow'); + } + return c; + } + + static addMag(a: bigint, b: bigint): bigint { + return a > 0n ? this.add(a, b) : this.sub(a, b); + } + + static sub(a: bigint, b: bigint): bigint { + const c = a - b; + if (!(b <= 0n ? c >= a : c < a)) { + throw new FixedPointError('SubOverflow'); + } + return c; + } + + static mulDownMag(a: bigint, b: bigint): bigint { + const product = a * b; + if (!(a === 0n || product / a === b)) { + throw new FixedPointError('MulOverflow'); + } + return product / this.ONE; + } + + static mulDownMagU(a: bigint, b: bigint): bigint { + return (a * b) / this.ONE; + } + + static mulUpMag(a: bigint, b: bigint): bigint { + const product = a * b; + if (!(a === 0n || product / a === b)) { + throw new FixedPointError('MulOverflow'); + } + + if (product > 0n) { + return (product - 1n) / this.ONE + 1n; + } else if (product < 0n) { + return (product + 1n) / this.ONE - 1n; + } + return 0n; + } + + static mulUpMagU(a: bigint, b: bigint): bigint { + const product = a * b; + if (product > 0n) { + return (product - 1n) / this.ONE + 1n; + } else if (product < 0n) { + return (product + 1n) / this.ONE - 1n; + } + return 0n; + } + + static divDownMag(a: bigint, b: bigint): bigint { + if (b === 0n) { + throw new FixedPointError('ZeroDivision'); + } + if (a === 0n) { + return 0n; + } + + const aInflated = a * this.ONE; + if (aInflated / a !== this.ONE) { + throw new FixedPointError('DivInterval'); + } + + return aInflated / b; + } + + static divDownMagU(a: bigint, b: bigint): bigint { + if (b === 0n) { + throw new FixedPointError('ZeroDivision'); + } + return (a * this.ONE) / b; + } + + static divUpMag(a: bigint, b: bigint): bigint { + if (b === 0n) { + throw new FixedPointError('ZeroDivision'); + } + if (a === 0n) { + return 0n; + } + + let localA = a; + let localB = b; + if (b < 0n) { + localB = -b; + localA = -a; + } + + const aInflated = localA * this.ONE; + if (aInflated / localA !== this.ONE) { + throw new FixedPointError('DivInterval'); + } + + if (aInflated > 0n) { + return (aInflated - 1n) / localB + 1n; + } + return (aInflated + 1n) / localB - 1n; + } + + static divUpMagU(a: bigint, b: bigint): bigint { + if (b === 0n) { + throw new FixedPointError('ZeroDivision'); + } + if (a === 0n) { + return 0n; + } + + let localA = a; + let localB = b; + if (b < 0n) { + localB = -b; + localA = -a; + } + + if (localA > 0n) { + return (localA * this.ONE - 1n) / localB + 1n; + } + return (localA * this.ONE + 1n) / localB - 1n; + } + + static mulXp(a: bigint, b: bigint): bigint { + const product = a * b; + if (!(a === 0n || product / a === b)) { + throw new FixedPointError('MulOverflow'); + } + return product / this.ONE_XP; + } + + static mulXpU(a: bigint, b: bigint): bigint { + return (a * b) / this.ONE_XP; + } + + static divXp(a: bigint, b: bigint): bigint { + if (b === 0n) { + throw new FixedPointError('ZeroDivision'); + } + if (a === 0n) { + return 0n; + } + + const aInflated = a * this.ONE_XP; + if (aInflated / a !== this.ONE_XP) { + throw new FixedPointError('DivInterval'); + } + + return aInflated / b; + } + + static divXpU(a: bigint, b: bigint): bigint { + if (b === 0n) { + throw new FixedPointError('ZeroDivision'); + } + return (a * this.ONE_XP) / b; + } + + static mulDownXpToNp(a: bigint, b: bigint): bigint { + const E19 = BigInt('10000000000000000000'); + const b1 = b / E19; + const prod1 = a * b1; + if (!(a === 0n || prod1 / a === b1)) { + throw new FixedPointError('MulOverflow'); + } + const b2 = b % E19; + const prod2 = a * b2; + if (!(a === 0n || prod2 / a === b2)) { + throw new FixedPointError('MulOverflow'); + } + return prod1 >= 0n && prod2 >= 0n + ? (prod1 + prod2 / E19) / E19 + : (prod1 + prod2 / E19 + 1n) / E19 - 1n; + } + + static mulDownXpToNpU(a: bigint, b: bigint): bigint { + const E19 = BigInt('10000000000000000000'); + const b1 = b / E19; + const b2 = b % E19; + const prod1 = a * b1; + const prod2 = a * b2; + return prod1 >= 0n && prod2 >= 0n + ? (prod1 + prod2 / E19) / E19 + : (prod1 + prod2 / E19 + 1n) / E19 - 1n; + } + + static mulUpXpToNp(a: bigint, b: bigint): bigint { + const E19 = BigInt('10000000000000000000'); + const b1 = b / E19; + const prod1 = a * b1; + if (!(a === 0n || prod1 / a === b1)) { + throw new FixedPointError('MulOverflow'); + } + const b2 = b % E19; + const prod2 = a * b2; + if (!(a === 0n || prod2 / a === b2)) { + throw new FixedPointError('MulOverflow'); + } + return prod1 <= 0n && prod2 <= 0n + ? (prod1 + prod2 / E19) / E19 + : (prod1 + prod2 / E19 - 1n) / E19 + 1n; + } + + static mulUpXpToNpU(a: bigint, b: bigint): bigint { + const E19 = BigInt('10000000000000000000'); + const b1 = b / E19; + const b2 = b % E19; + const prod1 = a * b1; + const prod2 = a * b2; + return prod1 <= 0n && prod2 <= 0n + ? (prod1 + prod2 / E19) / E19 + : (prod1 + prod2 / E19 - 1n) / E19 + 1n; + } + + static complement(x: bigint): bigint { + if (x >= this.ONE || x <= 0n) { + return 0n; + } + return this.ONE - x; + } +} From c9b571ef5e572091a95287742ae88751620caf8f Mon Sep 17 00:00:00 2001 From: johngrantuk Date: Fri, 10 Jan 2025 11:10:11 +0000 Subject: [PATCH 05/16] feat(Python): Add Gyro2CLP support (untested). --- python/src/pools/gyro/__init__.py | 0 python/src/pools/gyro/gyro2CLP.py | 198 +++++++++++++++++ python/src/pools/gyro/gyro2CLP_math.py | 284 ++++++++++++++++++++++++ python/src/pools/gyro/gyro_pool_math.py | 108 +++++++++ python/src/utils.py | 2 + python/src/vault.py | 6 +- 6 files changed, 596 insertions(+), 2 deletions(-) create mode 100644 python/src/pools/gyro/__init__.py create mode 100644 python/src/pools/gyro/gyro2CLP.py create mode 100644 python/src/pools/gyro/gyro2CLP_math.py create mode 100644 python/src/pools/gyro/gyro_pool_math.py diff --git a/python/src/pools/gyro/__init__.py b/python/src/pools/gyro/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/python/src/pools/gyro/gyro2CLP.py b/python/src/pools/gyro/gyro2CLP.py new file mode 100644 index 0000000..d45c1f4 --- /dev/null +++ b/python/src/pools/gyro/gyro2CLP.py @@ -0,0 +1,198 @@ +from src.maths import ( + Rounding, + mul_down_fixed, + div_down_fixed, + mul_up_fixed, + div_up, +) +from src.pools.gyro.gyro2CLP_math import ( + calculate_invariant, + calculate_virtual_parameter0, + calculate_virtual_parameter1, + calc_out_given_in, + calc_in_given_out, +) +from src.utils import MAX_UINT256 +from src.swap import SwapKind + + +class Gyro2CLP: + def __init__(self, pool_state): + self.normalized_weights = pool_state["weights"] + if pool_state["sqrtAlpha"] >= pool_state["sqrtBeta"]: + raise ValueError("SqrtParamsWrong") + self.sqrt_alpha = pool_state["sqrtAlpha"] + self.sqrt_beta = pool_state["sqrtBeta"] + + def get_maximum_invariant_ratio(self) -> int: + return MAX_UINT256 + + def get_minimum_invariant_ratio(self) -> int: + return 0 + + def on_swap(self, swap_params): + token_in_is_token_0 = swap_params["index_in"] == 0 + balance_token_in_scaled18 = swap_params["balances_live_scaled18"][ + swap_params["index_in"] + ] + balance_token_out_scaled18 = swap_params["balances_live_scaled18"][ + swap_params["index_out"] + ] + + virtual_balance_in, virtual_balance_out = self.get_virtual_offsets( + balance_token_in_scaled18, + balance_token_out_scaled18, + token_in_is_token_0, + self.sqrt_alpha, + self.sqrt_beta, + ) + + if swap_params["swap_kind"] == SwapKind.GIVENIN.value: + return calc_out_given_in( + balance_token_in_scaled18, + balance_token_out_scaled18, + swap_params["amount_given_scaled18"], + virtual_balance_in, + virtual_balance_out, + ) + + return calc_in_given_out( + balance_token_in_scaled18, + balance_token_out_scaled18, + swap_params["amount_given_scaled18"], + virtual_balance_in, + virtual_balance_out, + ) + + def compute_invariant(self, balances_live_scaled18, rounding): + return calculate_invariant( + balances_live_scaled18, + self.sqrt_alpha, + self.sqrt_beta, + rounding, + ) + + def compute_balance( + self, + balances_live_scaled18, + token_in_index, + invariant_ratio, + ): + """ + /********************************************************************************************** + // Gyro invariant formula is: + // Lˆ2 = (x + a)(y + b) + // where: + // a = L / _sqrtBeta + // b = L * _sqrtAlpha + // + // In computeBalance, we want to know the new balance of a token, given that the invariant + // changed and the other token balance didn't change. To calculate that for "x", we use: + // + // (L*Lratio)ˆ2 = (newX + (L*Lratio) / _sqrtBeta)(y + (L*Lratio) * _sqrtAlpha) + // + // To simplify, let's rename a few terms: + // + // squareNewInv = (newX + a)(y + b) + // + // Isolating newX: newX = (squareNewInv/(y + b)) - a + // For newY: newY = (squareNewInv/(x + a)) - b + **********************************************************************************************/ + """ + + # // `computeBalance` is used to calculate unbalanced adds and removes, when the BPT value is specified. + # // A bigger invariant in `computeAddLiquiditySingleTokenExactOut` means that more tokens are required to + # // fulfill the trade, and a bigger invariant in `computeRemoveLiquiditySingleTokenExactIn` means that the + # // amount out is lower. So, the invariant should always be rounded up. + invariant = calculate_invariant( + balances_live_scaled18, + self.sqrt_alpha, + self.sqrt_beta, + Rounding.ROUND_UP, + ) + + # // New invariant + invariant = mul_up_fixed(invariant, invariant_ratio) + square_new_inv = invariant * invariant + # // L / sqrt(beta) + a = div_down_fixed(invariant, self.sqrt_beta) + # // L * sqrt(alpha) + b = mul_down_fixed(invariant, self.sqrt_alpha) + + new_balance = 0 + if token_in_index == 0: + # // if newBalance = newX + new_balance = div_up(square_new_inv, balances_live_scaled18[1] + b) - a + else: + # // if newBalance = newY + new_balance = div_up(square_new_inv, balances_live_scaled18[0] + a) - b + + return new_balance + + def get_virtual_offsets( + self, + balance_token_in_scaled18: int, + balance_token_out_scaled18: int, + token_in_is_token0: bool, + _sqrt_alpha: int, + _sqrt_beta: int, + ) -> dict[str, int]: + """ + Calculate virtual offsets for token balances. + + virtualBalanceIn is always rounded up, because: + * If swap is EXACT_IN: a bigger virtualBalanceIn leads to a lower amount out + * If swap is EXACT_OUT: a bigger virtualBalanceIn leads to a bigger amount in + + virtualBalanceOut is always rounded down, because: + * If swap is EXACT_IN: a lower virtualBalanceOut leads to a lower amount out + * If swap is EXACT_OUT: a lower virtualBalanceOut leads to a bigger amount in + + Args: + balance_token_in_scaled18: Input token balance scaled to 18 decimals + balance_token_out_scaled18: Output token balance scaled to 18 decimals + token_in_is_token0: Whether input token is token0 + _sqrt_alpha: Square root of alpha parameter + _sqrt_beta: Square root of beta parameter + + Returns: + Dictionary containing virtualBalanceIn and virtualBalanceOut + """ + # Initialize balances array + balances = [0, 0] + balances[0] = ( + balance_token_in_scaled18 + if token_in_is_token0 + else balance_token_out_scaled18 + ) + balances[1] = ( + balance_token_out_scaled18 + if token_in_is_token0 + else balance_token_in_scaled18 + ) + + # Calculate current invariant + current_invariant = calculate_invariant( + balances, _sqrt_alpha, _sqrt_beta, "ROUND_DOWN" + ) + + # Calculate virtual balances based on token position + if token_in_is_token0: + virtual_balance_in = calculate_virtual_parameter0( + current_invariant, _sqrt_beta, "ROUND_UP" + ) + virtual_balance_out = calculate_virtual_parameter1( + current_invariant, _sqrt_alpha, "ROUND_DOWN" + ) + else: + virtual_balance_in = calculate_virtual_parameter1( + current_invariant, _sqrt_alpha, "ROUND_UP" + ) + virtual_balance_out = calculate_virtual_parameter0( + current_invariant, _sqrt_beta, "ROUND_DOWN" + ) + + return { + "virtual_balance_in": virtual_balance_in, + "virtual_balance_out": virtual_balance_out, + } diff --git a/python/src/pools/gyro/gyro2CLP_math.py b/python/src/pools/gyro/gyro2CLP_math.py new file mode 100644 index 0000000..a88a5fe --- /dev/null +++ b/python/src/pools/gyro/gyro2CLP_math.py @@ -0,0 +1,284 @@ +from src.maths import ( + WAD, + mul_down_fixed, + div_up_fixed, + div_down_fixed, + mul_up_fixed, +) +from src.pools.gyro.gyro_pool_math import sqrt + + +def calculate_invariant( + balances: list[int], sqrt_alpha: int, sqrt_beta: int, rounding: str +) -> int: + """ + Calculate invariant using quadratic formula. + + The formula solves: 0 = (1-sqrt(alpha/beta)*L^2 - (y/sqrt(beta)+x*sqrt(alpha))*L - x*y) + Using quadratic formula: 0 = a*L^2 + b*L + c + Where a > 0, b < 0, and c < 0 + + For mb = -b and mc = -c: + L = (mb + (mb^2 + 4 * a * mc)^(1/2)) / (2 * a) + + Args: + balances: List of pool balances as integers + sqrt_alpha: Square root of alpha parameter + sqrt_beta: Square root of beta parameter + rounding: Rounding direction + + Returns: + Calculated invariant as integer + """ + # Get quadratic terms from helper function + a, mb, b_square, mc = calculate_quadratic_terms( + balances, sqrt_alpha, sqrt_beta, rounding + ) + + # Calculate final result using quadratic formula + return calculate_quadratic(a, mb, b_square, mc) + + +def calculate_quadratic_terms( + balances: list[int], sqrt_alpha: int, sqrt_beta: int, rounding: str +) -> dict[str, int]: + """ + Calculate the terms needed for quadratic formula solution. + + Args: + balances: List of pool balances as integers + sqrt_alpha: Square root of alpha parameter + sqrt_beta: Square root of beta parameter + rounding: Rounding direction ("ROUND_DOWN" or "ROUND_UP") + + Returns: + Dictionary containing a, mb, b_square, and mc terms + """ + # Define rounding functions based on rounding direction + div_up_or_down = div_down_fixed if rounding == "ROUND_DOWN" else div_up_fixed + + mul_up_or_down = mul_down_fixed if rounding == "ROUND_DOWN" else mul_up_fixed + + mul_down_or_up = mul_up_fixed if rounding == "ROUND_DOWN" else mul_down_fixed + + # Calculate 'a' term + # Note: 'a' follows opposite rounding than 'b' and 'c' since it's in denominator + a = WAD - div_up_or_down(sqrt_alpha, sqrt_beta) + + # Calculate 'b' terms (in numerator) + b_term0 = div_up_or_down(balances[1], sqrt_beta) + b_term1 = mul_up_or_down(balances[0], sqrt_alpha) + mb = b_term0 + b_term1 + + # Calculate 'c' term (in numerator) + mc = mul_up_or_down(balances[0], balances[1]) + + # Calculate b² for better fixed point precision + # b² = x² * alpha + x*y*2*sqrt(alpha/beta) + y²/beta + b_square = mul_up_or_down( + mul_up_or_down(mul_up_or_down(balances[0], balances[0]), sqrt_alpha), sqrt_alpha + ) + + b_sq2 = div_up_or_down( + 2 * mul_up_or_down(mul_up_or_down(balances[0], balances[1]), sqrt_alpha), + sqrt_beta, + ) + + b_sq3 = div_up_or_down( + mul_up_or_down(balances[1], balances[1]), mul_down_or_up(sqrt_beta, sqrt_beta) + ) + + b_square = b_square + b_sq2 + b_sq3 + + return {"a": a, "mb": mb, "b_square": b_square, "mc": mc} + + +def calculate_quadratic( + a: int, + mb: int, + b_square: int, # b² can be calculated separately with more precision + mc: int, +) -> int: + """ + Calculate quadratic formula solution using provided terms. + + Args: + a: 'a' term from quadratic equation + mb: -b term from quadratic equation + b_square: b² term (calculated separately for precision) + mc: -c term from quadratic equation + + Returns: + Calculated invariant + """ + # Calculate denominator + denominator = mul_up_fixed(a, 2 * WAD) + + # Order multiplications for fixed point precision + add_term = mul_down_fixed(mul_down_fixed(mc, 4 * WAD), a) + + # The minus sign in the radicand cancels out in this special case + radicand = b_square + add_term + + # Calculate square root + sqr_result = sqrt(radicand, 5) + + # The minus sign in the numerator cancels out in this special case + numerator = mb + sqr_result + + # Calculate final result + invariant = div_down_fixed(numerator, denominator) + + return invariant + + +def calc_out_given_in( + balance_in: int, + balance_out: int, + amount_in: int, + virtual_offset_in: int, + virtual_offset_out: int, +) -> int: + # """ + # Calculate the output amount given an input amount for a trade. + + # Described for X = 'in' asset and Y = 'out' asset, but equivalent for the other case: + # dX = incrX = amountIn > 0 + # dY = incrY = amountOut < 0 + # x = balanceIn x' = x + virtualParamX + # y = balanceOut y' = y + virtualParamY + # L = inv.Liq / x' * y' \ y' * dX + # |dy| = y' - | -------------------------- | = -------------- - + # x' = virtIn \ ( x' + dX) / x' + dX + # y' = virtOut + + # Note that -dy > 0 is what the trader receives. + # We exploit the fact that this formula is symmetric up to virtualOffset{X,Y}. + # We do not use L^2, but rather x' * y', to prevent potential accumulation of errors. + # We add a very small safety margin to compensate for potential errors in the invariant. + + # Args: + # balance_in: Current balance of input token + # balance_out: Current balance of output token + # amount_in: Amount of input token being traded + # virtual_offset_in: Virtual offset parameter for input token + # virtual_offset_out: Virtual offset parameter for output token + + # Returns: + # Amount of output token to be received + + # Raises: + # ValueError: If calculated output amount exceeds available balance + # """ + # The factors lead to a multiplicative "safety margin" between virtual offsets + # that is very slightly larger than 3e-18 + virt_in_over = balance_in + mul_up_fixed(virtual_offset_in, WAD + 2) + + virt_out_under = balance_out + mul_down_fixed(virtual_offset_out, WAD - 1) + + # Calculate output amount + amount_out = div_down_fixed( + mul_down_fixed(virt_out_under, amount_in), virt_in_over + amount_in + ) + + # Ensure amountOut < balanceOut + if not amount_out <= balance_out: + raise ValueError("AssetBoundsExceeded") + + return amount_out + + +def calc_in_given_out( + balance_in: int, + balance_out: int, + amount_out: int, + virtual_offset_in: int, + virtual_offset_out: int, +) -> int: + # """ + # Calculate the input amount required given a desired output amount for a trade. + + # dX = incrX = amountIn > 0 + # dY = incrY = amountOut < 0 + # x = balanceIn x' = x + virtualParamX + # y = balanceOut y' = y + virtualParamY + # x = balanceIn + # L = inv.Liq / x' * y' \ x' * dy + # dx = | -------------------------- | - x' = - ----------- + # x' = virtIn \ y' + dy / y' + dy + # y' = virtOut + + # Note that dy < 0 < dx. + # We exploit the fact that this formula is symmetric up to virtualOffset{X,Y}. + # We do not use L^2, but rather x' * y', to prevent potential accumulation of errors. + # We add a very small safety margin to compensate for potential errors in the invariant. + + # Args: + # balance_in: Current balance of input token + # balance_out: Current balance of output token + # amount_out: Desired amount of output token + # virtual_offset_in: Virtual offset parameter for input token + # virtual_offset_out: Virtual offset parameter for output token + + # Returns: + # Required amount of input token + + # Raises: + # ValueError: If requested output amount exceeds available balance + # """ + # Check if output amount exceeds balance + if not amount_out <= balance_out: + raise ValueError("AssetBoundsExceeded") + + # The factors lead to a multiplicative "safety margin" between virtual offsets + # that is very slightly larger than 3e-18 + virt_in_over = balance_in + mul_up_fixed(virtual_offset_in, WAD + 2) + + virt_out_under = balance_out + mul_down_fixed(virtual_offset_out, WAD - 1) + + # Calculate input amount + amount_in = div_up_fixed( + mul_up_fixed(virt_in_over, amount_out), virt_out_under - amount_out + ) + + return amount_in + + +def calculate_virtual_parameter0(invariant: int, _sqrt_beta: int, rounding: str) -> int: + """ + Calculate the virtual offset 'a' for reserves 'x', as in (x+a)*(y+b)=L^2. + + Args: + invariant: Pool invariant value + _sqrt_beta: Square root of beta parameter + rounding: Rounding direction ("ROUND_DOWN" or "ROUND_UP") + + Returns: + Virtual parameter 'a' + """ + return ( + div_down_fixed(invariant, _sqrt_beta) + if rounding == "ROUND_DOWN" + else div_up_fixed(invariant, _sqrt_beta) + ) + + +def calculate_virtual_parameter1( + invariant: int, _sqrt_alpha: int, rounding: str +) -> int: + """ + Calculate the virtual offset 'b' for reserves 'y', as in (x+a)*(y+b)=L^2. + + Args: + invariant: Pool invariant value + _sqrt_alpha: Square root of alpha parameter + rounding: Rounding direction ("ROUND_DOWN" or "ROUND_UP") + + Returns: + Virtual parameter 'b' + """ + return ( + mul_down_fixed(invariant, _sqrt_alpha) + if rounding == "ROUND_DOWN" + else mul_up_fixed(invariant, _sqrt_alpha) + ) diff --git a/python/src/pools/gyro/gyro_pool_math.py b/python/src/pools/gyro/gyro_pool_math.py new file mode 100644 index 0000000..9e40632 --- /dev/null +++ b/python/src/pools/gyro/gyro_pool_math.py @@ -0,0 +1,108 @@ +from src.maths import ( + WAD, + mul_down_fixed, + mul_up_fixed, +) + + +_SQRT_1E_NEG_1 = 316227766016837933 +_SQRT_1E_NEG_3 = 31622776601683793 +_SQRT_1E_NEG_5 = 3162277660168379 +_SQRT_1E_NEG_7 = 316227766016837 +_SQRT_1E_NEG_9 = 31622776601683 +_SQRT_1E_NEG_11 = 3162277660168 +_SQRT_1E_NEG_13 = 316227766016 +_SQRT_1E_NEG_15 = 31622776601 +_SQRT_1E_NEG_17 = 3162277660 + + +@staticmethod +def sqrt(x: int, tolerance: int) -> int: + if x == 0: + return 0 + + guess = _make_initial_guess(x) + + # Perform Newton's method iterations + for _ in range(7): + guess = (guess + (x * WAD) // guess) // 2 + + guess_squared = mul_down_fixed(guess, guess) + if not ( + guess_squared <= x + mul_up_fixed(guess, tolerance) + and guess_squared >= x - mul_up_fixed(guess, tolerance) + ): + raise ValueError("_sqrt FAILED") + + return guess + + +@staticmethod +def _make_initial_guess(x: int) -> int: + if x >= WAD: + return (1 << _int_log2_halved(x // WAD)) * WAD + else: + if x <= 10: + return _SQRT_1E_NEG_17 + if x <= 100: + return 10**10 + if x <= 1000: + return _SQRT_1E_NEG_15 + if x <= 10000: + return 10**11 + if x <= 100000: + return _SQRT_1E_NEG_13 + if x <= 1000000: + return 10**12 + if x <= 10000000: + return _SQRT_1E_NEG_11 + if x <= 100000000: + return 10**13 + if x <= 1000000000: + return _SQRT_1E_NEG_9 + if x <= 10000000000: + return 10**14 + if x <= 100000000000: + return _SQRT_1E_NEG_7 + if x <= 1000000000000: + return 10**15 + if x <= 10000000000000: + return _SQRT_1E_NEG_5 + if x <= 100000000000000: + return 10**16 + if x <= 1000000000000000: + return _SQRT_1E_NEG_3 + if x <= 10000000000000000: + return 10**17 + if x <= 100000000000000000: + return _SQRT_1E_NEG_1 + return x + + +@staticmethod +def _int_log2_halved(x: int) -> int: + n = 0 + + if x >= 1 << 128: + x >>= 128 + n += 64 + if x >= 1 << 64: + x >>= 64 + n += 32 + if x >= 1 << 32: + x >>= 32 + n += 16 + if x >= 1 << 16: + x >>= 16 + n += 8 + if x >= 1 << 8: + x >>= 8 + n += 4 + if x >= 1 << 4: + x >>= 4 + n += 2 + if x >= 1 << 2: + x >>= 2 + n += 1 + + return n diff --git a/python/src/utils.py b/python/src/utils.py index 45fef8f..42b5043 100644 --- a/python/src/utils.py +++ b/python/src/utils.py @@ -1,5 +1,7 @@ from src.maths import div_down_fixed, mul_up_fixed, mul_down_fixed, div_up_fixed +MAX_UINT256 = 2**256 - 1 + def find_case_insensitive_index_in_list(strings, target): # Convert the target to lowercase diff --git a/python/src/vault.py b/python/src/vault.py index a9ca70e..0dea9b4 100644 --- a/python/src/vault.py +++ b/python/src/vault.py @@ -1,11 +1,12 @@ from src.swap import swap from src.add_liquidity import add_liquidity from src.remove_liquidity import remove_liquidity -from src.pools.weighted import Weighted from src.pools.buffer.erc4626_buffer_wrap_or_unwrap import erc4626_buffer_wrap_or_unwrap -from src.pools.stable import Stable from src.hooks.default_hook import DefaultHook from src.hooks.exit_fee_hook import ExitFeeHook +from src.pools.weighted import Weighted +from src.pools.stable import Stable +from src.pools.gyro.gyro2CLP import Gyro2CLP class Vault: @@ -13,6 +14,7 @@ def __init__(self, *, custom_pool_classes=None, custom_hook_classes=None): self.pool_classes = { "WEIGHTED": Weighted, "STABLE": Stable, + "GYRO": Gyro2CLP, } self.hook_classes = {"ExitFee": ExitFeeHook} if custom_pool_classes is not None: From ab087f94cdfd21ff5da3610d2e45a9bfade04f79 Mon Sep 17 00:00:00 2001 From: johngrantuk Date: Thu, 16 Jan 2025 11:19:38 +0000 Subject: [PATCH 06/16] feat(Python): Add GyroECLP support (untested). --- python/src/pools/gyro/gyroECLP.py | 125 +++++ python/src/pools/gyro/gyroECLP_math.py | 591 ++++++++++++++++++++ python/src/pools/gyro/signed_fixed_point.py | 211 +++++++ python/src/vault.py | 2 + 4 files changed, 929 insertions(+) create mode 100644 python/src/pools/gyro/gyroECLP.py create mode 100644 python/src/pools/gyro/gyroECLP_math.py create mode 100644 python/src/pools/gyro/signed_fixed_point.py diff --git a/python/src/pools/gyro/gyroECLP.py b/python/src/pools/gyro/gyroECLP.py new file mode 100644 index 0000000..39efcdb --- /dev/null +++ b/python/src/pools/gyro/gyroECLP.py @@ -0,0 +1,125 @@ +from dataclasses import dataclass +from src.maths import ( + Rounding, + mul_up_fixed, +) +from src.swap import SwapKind +from src.pools.gyro.gyroECLP_math import ( + EclpParams, + DerivedEclpParams, + Vector2, + GyroECLPMath, +) + + +@dataclass +class PoolParams: + eclp_params: EclpParams + derived_eclp_params: DerivedEclpParams + + +class GyroECLP: + def __init__(self, pool_state): + self.pool_params = PoolParams( + eclp_params=EclpParams( + alpha=pool_state.params_alpha, + beta=pool_state.params_beta, + c=pool_state.params_c, + s=pool_state.params_s, + lambda_=pool_state.params_lambda, + ), + derived_eclp_params=DerivedEclpParams( + tauAlpha=Vector2(x=pool_state.tau_alpha_x, y=pool_state.tau_alpha_y), + tauBeta=Vector2(x=pool_state.tau_beta_x, y=pool_state.tau_beta_y), + u=pool_state.u, + v=pool_state.v, + w=pool_state.w, + z=pool_state.z, + dSq=pool_state.d_sq, + ), + ) + + def get_maximum_invariant_ratio(self) -> int: + return GyroECLPMath.MAX_INVARIANT_RATIO + + def get_minimum_invariant_ratio(self) -> int: + return GyroECLPMath.MIN_INVARIANT_RATIO + + def on_swap(self, swap_params): + token_in_is_token_0 = swap_params["index_in"] == 0 + + eclp_params = self.pool_params.eclp_params + derived_eclp_params = self.pool_params.derived_eclp_params + + # Tuple unpacking for the returned values from calculateInvariantWithError + current_invariant, inv_err = GyroECLPMath.calculate_invariant_with_error( + swap_params["balances_live_scaled18"], eclp_params, derived_eclp_params + ) + + invariant = Vector2(x=current_invariant + 2 * inv_err, y=current_invariant) + + if swap_params["swap_kind"] == SwapKind.GIVENIN.value: + return GyroECLPMath.calc_out_given_in( + swap_params["balances_live_scaled18"], + swap_params["amount_given_scaled18"], + token_in_is_token_0, + eclp_params, + derived_eclp_params, + invariant, + ) + + return GyroECLPMath.calc_out_given_in( + swap_params["balances_live_scaled18"], + swap_params["amount_given_scaled18"], + token_in_is_token_0, + eclp_params, + derived_eclp_params, + invariant, + ) + + def compute_invariant(self, balances_live_scaled18, rounding): + eclp_params = self.pool_params.eclp_params + derived_eclp_params = self.pool_params.derived_eclp_params + + current_invariant, inv_err = GyroECLPMath.calculate_invariant_with_error( + balances_live_scaled18, eclp_params, derived_eclp_params + ) + + if rounding == Rounding.ROUND_DOWN: + return current_invariant - inv_err + return current_invariant + inv_err + + def compute_balance( + self, + balances_live_scaled18, + token_in_index, + invariant_ratio, + ): + eclp_params = self.pool_params.eclp_params + derived_eclp_params = self.pool_params.derived_eclp_params + + current_invariant, inv_err = GyroECLPMath.calculate_invariant_with_error( + balances_live_scaled18, eclp_params, derived_eclp_params + ) + + # // The invariant vector contains the rounded up and rounded down invariant. Both are needed when computing + # // the virtual offsets. Depending on tauAlpha and tauBeta values, we want to use the invariant rounded up + # // or rounded down to make sure we're conservative in the output. + invariant = Vector2( + x=mul_up_fixed(current_invariant + inv_err, invariant_ratio), + y=mul_up_fixed(current_invariant - inv_err, invariant_ratio), + ) + + # // Edge case check. Should never happen except for insane tokens. If this is hit, actually adding the + # // tokens would lead to a revert or (if it went through) a deadlock downstream, so we catch it here. + if invariant.x > GyroECLPMath.MAX_INVARIANT: + raise ValueError("GyroECLPMath.MaxInvariantExceeded") + + if token_in_index == 0: + return GyroECLPMath.calc_x_given_y( + balances_live_scaled18[1], eclp_params, derived_eclp_params, invariant + ) + + return GyroECLPMath.calc_y_given_x( + balances_live_scaled18[0], eclp_params, derived_eclp_params, invariant + ) diff --git a/python/src/pools/gyro/gyroECLP_math.py b/python/src/pools/gyro/gyroECLP_math.py new file mode 100644 index 0000000..92baa42 --- /dev/null +++ b/python/src/pools/gyro/gyroECLP_math.py @@ -0,0 +1,591 @@ +from dataclasses import dataclass +from typing import List, Tuple +from src.pools.gyro.signed_fixed_point import SignedFixedPoint +from src.pools.gyro.gyro_pool_math import sqrt + + +@dataclass +class Vector2: + x: int + y: int + + +@dataclass +class QParams: + a: int + b: int + c: int + + +@dataclass +class EclpParams: + alpha: int + beta: int + c: int + s: int + lambda_: int # Using lambda_ since lambda is a keyword in Python + + +@dataclass +class DerivedEclpParams: + tauAlpha: Vector2 + tauBeta: Vector2 + u: int + v: int + w: int + z: int + dSq: int + + +class MaxBalancesExceededError(Exception): + def __init__(self): + super().__init__("Max assets exceeded") + + +class MaxInvariantExceededError(Exception): + def __init__(self): + super().__init__("Max invariant exceeded") + + +class GyroECLPMath: + # Constants + _ONEHALF = int("500000000000000000") # 0.5e18 + _ONE = int("1000000000000000000") # 1e18 + _ONE_XP = int("100000000000000000000000000000000000000") # 1e38 + + # Anti-overflow limits: Params and DerivedParams + _ROTATION_VECTOR_NORM_ACCURACY = int("1000") # 1e3 (1e-15 in normal precision) + _MAX_STRETCH_FACTOR = int( + "100000000000000000000000000" + ) # 1e26 (1e8 in normal precision) + _DERIVED_TAU_NORM_ACCURACY_XP = int("100000000000000000000000") # 1e23 + _MAX_INV_INVARIANT_DENOMINATOR_XP = int( + "10000000000000000000000000000000000000000000" + ) # 1e43 + _DERIVED_DSQ_NORM_ACCURACY_XP = int("100000000000000000000000") # 1e23 + + # Anti-overflow limits: Dynamic values + _MAX_BALANCES = int("100000000000000000000000000000000000") # 1e34 + MAX_INVARIANT = int("3000000000000000000000000000000000000") # 3e37 + + # Invariant ratio limits + MIN_INVARIANT_RATIO = int("600000000000000000") # 60e16 (60%) + MAX_INVARIANT_RATIO = int("5000000000000000000") # 500e16 (500%) + + @staticmethod + def scalar_prod(t1: Vector2, t2: Vector2) -> int: + x_prod = SignedFixedPoint.mul_down_mag(t1.x, t2.x) + y_prod = SignedFixedPoint.mul_down_mag(t1.y, t2.y) + return x_prod + y_prod + + @staticmethod + def scalar_prod_xp(t1: Vector2, t2: Vector2) -> int: + return SignedFixedPoint.mul_xp(t1.x, t2.x) + SignedFixedPoint.mul_xp(t1.y, t2.y) + + @staticmethod + def mul_a(params: EclpParams, tp: Vector2) -> Vector2: + return Vector2( + x=SignedFixedPoint.div_down_mag_u( + SignedFixedPoint.mul_down_mag_u(params.c, tp.x) + - SignedFixedPoint.mul_down_mag_u(params.s, tp.y), + params.lambda_, + ), + y=( + SignedFixedPoint.mul_down_mag_u(params.s, tp.x) + + SignedFixedPoint.mul_down_mag_u(params.c, tp.y) + ), + ) + + @classmethod + def virtual_offset0(cls, p: EclpParams, d: DerivedEclpParams, r: Vector2) -> int: + term_xp = SignedFixedPoint.div_xp_u(d.tauBeta.x, d.dSq) + + if d.tauBeta.x > 0: + a = SignedFixedPoint.mul_up_xp_to_np_u( + SignedFixedPoint.mul_up_mag_u( + SignedFixedPoint.mul_up_mag_u(r.x, p.lambda_), p.c + ), + term_xp, + ) + else: + a = SignedFixedPoint.mul_up_xp_to_np_u( + SignedFixedPoint.mul_down_mag_u( + SignedFixedPoint.mul_down_mag_u(r.y, p.lambda_), p.c + ), + term_xp, + ) + + return a + SignedFixedPoint.mul_up_xp_to_np_u( + SignedFixedPoint.mul_up_mag_u(r.x, p.s), + SignedFixedPoint.div_xp_u(d.tauBeta.y, d.dSq), + ) + + @classmethod + def virtual_offset1(cls, p: EclpParams, d: DerivedEclpParams, r: Vector2) -> int: + term_xp = SignedFixedPoint.div_xp_u(d.tauAlpha.x, d.dSq) + + if d.tauAlpha.x < 0: + b = SignedFixedPoint.mul_up_xp_to_np_u( + SignedFixedPoint.mul_up_mag_u( + SignedFixedPoint.mul_up_mag_u(r.x, p.lambda_), p.s + ), + -term_xp, + ) + else: + b = SignedFixedPoint.mul_up_xp_to_np_u( + SignedFixedPoint.mul_down_mag_u( + SignedFixedPoint.mul_down_mag_u(-r.y, p.lambda_), p.s + ), + term_xp, + ) + + return b + SignedFixedPoint.mul_up_xp_to_np_u( + SignedFixedPoint.mul_up_mag_u(r.x, p.c), + SignedFixedPoint.div_xp_u(d.tauAlpha.y, d.dSq), + ) + + @classmethod + def max_balances0(cls, p: EclpParams, d: DerivedEclpParams, r: Vector2) -> int: + term_xp1 = SignedFixedPoint.div_xp_u(d.tauBeta.x - d.tauAlpha.x, d.dSq) + term_xp2 = SignedFixedPoint.div_xp_u(d.tauBeta.y - d.tauAlpha.y, d.dSq) + + xp = SignedFixedPoint.mul_down_xp_to_np_u( + SignedFixedPoint.mul_down_mag_u( + SignedFixedPoint.mul_down_mag_u(r.y, p.lambda_), p.c + ), + term_xp1, + ) + + term2 = ( + SignedFixedPoint.mul_down_mag_u(r.y, p.s) + if term_xp2 > 0 + else SignedFixedPoint.mul_up_mag_u(r.x, p.s) + ) + + return xp + SignedFixedPoint.mul_down_xp_to_np_u(term2, term_xp2) + + @classmethod + def max_balances1(cls, p: EclpParams, d: DerivedEclpParams, r: Vector2) -> int: + term_xp1 = SignedFixedPoint.div_xp_u(d.tau_beta.x - d.tau_alpha.x, d.d_sq) + + term_xp2 = SignedFixedPoint.div_xp_u(d.tau_alpha.y - d.tau_beta.y, d.d_sq) + + yp = SignedFixedPoint.mul_down_xp_to_np_u( + SignedFixedPoint.mul_down_mag_u( + SignedFixedPoint.mul_down_mag_u(r.y, p.lambda_), + p.s, + ), + term_xp1, + ) + + term2 = ( + SignedFixedPoint.mul_down_mag_u(r.y, p.c) + if term_xp2 > 0 + else SignedFixedPoint.mul_up_mag_u(r.x, p.c) + ) + + return yp + SignedFixedPoint.mul_down_xp_to_np_u(term2, term_xp2) + + @classmethod + def calc_at_a_chi(cls, x: int, y: int, p: EclpParams, d: DerivedEclpParams) -> int: + d_sq2 = SignedFixedPoint.mul_xp_u(d.dSq, d.dSq) + + term_xp = SignedFixedPoint.div_xp_u( + SignedFixedPoint.div_down_mag_u( + SignedFixedPoint.div_down_mag_u(d.w, p.lambda_) + d.z, + p.lambda_, + ), + d_sq2, + ) + + val = SignedFixedPoint.mul_down_xp_to_np_u( + SignedFixedPoint.mul_down_mag_u(x, p.c) + - SignedFixedPoint.mul_down_mag_u(y, p.s), + term_xp, + ) + + term_np1 = SignedFixedPoint.mul_down_mag_u(x, p.lambda_) + term_np2 = SignedFixedPoint.mul_down_mag_u(y, p.lambda_) + + val += SignedFixedPoint.mul_down_xp_to_np_u( + SignedFixedPoint.mul_down_mag_u(term_np1, p.s) + + SignedFixedPoint.mul_down_mag_u(term_np2, p.c), + SignedFixedPoint.div_xp_u(d.u, d_sq2), + ) + + val += SignedFixedPoint.mul_down_xp_to_np_u( + SignedFixedPoint.mul_down_mag_u(x, p.s) + + SignedFixedPoint.mul_down_mag_u(y, p.c), + SignedFixedPoint.div_xp_u(d.v, d_sq2), + ) + return val + + @classmethod + def calc_a_chi_a_chi_in_xp(cls, p: EclpParams, d: DerivedEclpParams) -> int: + d_sq3 = SignedFixedPoint.mul_xp_u( + SignedFixedPoint.mul_xp_u(d.dSq, d.dSq), + d.dSq, + ) + + val = SignedFixedPoint.mul_up_mag_u( + p.lambda_, + SignedFixedPoint.div_xp_u( + SignedFixedPoint.mul_xp_u(2 * d.u, d.v), + d_sq3, + ), + ) + + val += SignedFixedPoint.mul_up_mag_u( + SignedFixedPoint.mul_up_mag_u( + SignedFixedPoint.div_xp_u( + SignedFixedPoint.mul_xp_u(d.u + 1, d.u + 1), + d_sq3, + ), + p.lambda_, + ), + p.lambda_, + ) + + val += SignedFixedPoint.div_xp_u(SignedFixedPoint.mul_xp_u(d.v, d.v), d_sq3) + + term_xp = SignedFixedPoint.div_up_mag_u(d.w, p.lambda_) + d.z + val += SignedFixedPoint.div_xp_u( + SignedFixedPoint.mul_xp_u(term_xp, term_xp), + d_sq3, + ) + + return val + + @classmethod + def calculate_invariant_with_error( + cls, balances: List[int], params: EclpParams, derived: DerivedEclpParams + ) -> Tuple[int, int]: + x, y = balances[0], balances[1] + + if x + y > cls._MAX_BALANCES: + raise MaxBalancesExceededError() + + at_a_chi = cls.calc_at_a_chi(x, y, params, derived) + achiachi = cls.calc_a_chi_a_chi_in_xp(params, derived) + + # Calculate error (simplified) + err = ( + SignedFixedPoint.mul_up_mag_u(params.lambda_, x + y) // cls._ONE_XP + 1 + ) * 20 + + mul_denominator = SignedFixedPoint.div_xp_u(cls._ONE_XP, achiachi - cls._ONE_XP) + + invariant = SignedFixedPoint.mul_down_xp_to_np_u( + at_a_chi - err, mul_denominator + ) + + # Error calculation (simplified) + scaled_err = SignedFixedPoint.mul_up_xp_to_np_u(err, mul_denominator) + total_err = ( + scaled_err + + ( + invariant + * ( + (params.lambda_ * params.lambda_) + // int("10000000000000000000000000000000000000") + ) + * 40 + ) + // cls._ONE_XP + + 1 + ) + + if invariant + total_err > cls.MAX_INVARIANT: + raise MaxInvariantExceededError() + + return invariant, total_err + + @classmethod + def calc_spot_price0in1( + cls, + balances: List[int], + params: EclpParams, + derived: DerivedEclpParams, + invariant: int, + ) -> int: + r = Vector2(x=invariant, y=invariant) + ab = Vector2( + x=cls.virtual_offset0(params, derived, r), + y=cls.virtual_offset1(params, derived, r), + ) + vec = Vector2(x=balances[0] - ab.x, y=balances[1] - ab.y) + + transformed_vec = cls.mul_a(params, vec) + pc = Vector2( + x=SignedFixedPoint.div_down_mag_u(transformed_vec.x, transformed_vec.y), + y=cls._ONE, + ) + + pgx = cls.scalar_prod(pc, cls.mul_a(params, Vector2(x=cls._ONE, y=0))) + return SignedFixedPoint.div_down_mag( + pgx, cls.scalar_prod(pc, cls.mul_a(params, Vector2(x=0, y=cls._ONE))) + ) + + @classmethod + def check_asset_bounds( + cls, + params: EclpParams, + derived: DerivedEclpParams, + invariant: Vector2, + new_bal: int, + asset_index: int, + ) -> None: + if asset_index == 0: + x_plus = cls.max_balances0(params, derived, invariant) + if new_bal > cls._MAX_BALANCES or new_bal > x_plus: + raise ValueError("Asset bounds exceeded") + else: + y_plus = cls.max_balances1(params, derived, invariant) + if new_bal > cls._MAX_BALANCES or new_bal > y_plus: + raise ValueError("Asset bounds exceeded") + + @classmethod + def calc_out_given_in( + cls, + balances: List[int], + amount_in: int, + token_in_is_token0: bool, + params: EclpParams, + derived: DerivedEclpParams, + invariant: Vector2, + ) -> int: + if token_in_is_token0: + ix_in, ix_out, calc_given = 0, 1, cls.calc_y_given_x + else: + ix_in, ix_out, calc_given = 1, 0, cls.calc_x_given_y + + bal_in_new = balances[ix_in] + amount_in + cls.check_asset_bounds(params, derived, invariant, bal_in_new, ix_in) + bal_out_new = calc_given(bal_in_new, params, derived, invariant) + return balances[ix_out] - bal_out_new + + @classmethod + def calc_in_given_out( + cls, + balances: List[int], + amount_out: int, + token_in_is_token0: bool, + params: EclpParams, + derived: DerivedEclpParams, + invariant: Vector2, + ) -> int: + if token_in_is_token0: + ix_in, ix_out, calc_given = ( + 0, + 1, + cls.calc_x_given_y, + ) # Note: reversed compared to calc_out_given_in + else: + ix_in, ix_out, calc_given = ( + 1, + 0, + cls.calc_y_given_x, + ) # Note: reversed compared to calc_out_given_in + + if amount_out > balances[ix_out]: + raise ValueError("Asset bounds exceeded") + + bal_out_new = balances[ix_out] - amount_out + bal_in_new = calc_given(bal_out_new, params, derived, invariant) + cls.check_asset_bounds(params, derived, invariant, bal_in_new, ix_in) + return bal_in_new - balances[ix_in] + + @classmethod + def solve_quadratic_swap( + cls, + lambda_: int, + x: int, + s: int, + c: int, + r: Vector2, + ab: Vector2, + tau_beta: Vector2, + d_sq: int, + ) -> int: + lam_bar = Vector2( + x=SignedFixedPoint.ONE_XP + - SignedFixedPoint.div_down_mag_u( + SignedFixedPoint.div_down_mag_u(SignedFixedPoint.ONE_XP, lambda_), + lambda_, + ), + y=SignedFixedPoint.ONE_XP + - SignedFixedPoint.div_up_mag_u( + SignedFixedPoint.div_up_mag_u(SignedFixedPoint.ONE_XP, lambda_), lambda_ + ), + ) + + q = {"a": 0, "b": 0, "c": 0} + xp = x - ab.x + + if xp > 0: + q["b"] = SignedFixedPoint.mul_up_xp_to_np_u( + SignedFixedPoint.mul_down_mag_u( + SignedFixedPoint.mul_down_mag_u(-xp, s), c + ), + SignedFixedPoint.div_xp_u(lam_bar.y, d_sq), + ) + else: + q["b"] = SignedFixedPoint.mul_up_xp_to_np_u( + SignedFixedPoint.mul_up_mag_u(SignedFixedPoint.mul_up_mag_u(-xp, s), c), + SignedFixedPoint.div_xp_u(lam_bar.x, d_sq) + 1, + ) + + s_term = Vector2( + x=SignedFixedPoint.div_xp_u( + SignedFixedPoint.mul_down_mag_u( + SignedFixedPoint.mul_down_mag_u(lam_bar.y, s), s + ), + d_sq, + ), + y=SignedFixedPoint.div_xp_u( + SignedFixedPoint.mul_up_mag_u( + SignedFixedPoint.mul_up_mag_u(lam_bar.x, s), s + ), + d_sq + 1, + ) + + 1, + ) + + s_term.x = SignedFixedPoint.ONE_XP - s_term.x + s_term.y = SignedFixedPoint.ONE_XP - s_term.y + + q["c"] = -cls.calc_xp_xp_div_lambda_lambda(x, r, lambda_, s, c, tau_beta, d_sq) + q["c"] = q["c"] + SignedFixedPoint.mul_down_xp_to_np_u( + SignedFixedPoint.mul_down_mag_u(r.y, r.y), s_term.y + ) + + q["c"] = sqrt(q["c"], 5) if q["c"] > 0 else 0 + + if q["b"] - q["c"] > 0: + q["a"] = SignedFixedPoint.mul_up_xp_to_np_u( + q["b"] - q["c"], + SignedFixedPoint.div_xp_u(SignedFixedPoint.ONE_XP, s_term.y) + 1, + ) + else: + q["a"] = SignedFixedPoint.mul_up_xp_to_np_u( + q["b"] - q["c"], + SignedFixedPoint.div_xp_u(SignedFixedPoint.ONE_XP, s_term.x), + ) + + return q["a"] + ab.y + + @classmethod + def calc_xp_xp_div_lambda_lambda( + cls, + x: int, + r: Vector2, + lambda_: int, + s: int, + c: int, + tau_beta: Vector2, + d_sq: int, + ) -> int: + sq_vars = Vector2( + x=SignedFixedPoint.mul_xp_u(d_sq, d_sq), + y=SignedFixedPoint.mul_up_mag_u(r.x, r.x), + ) + + q = {"a": 0, "b": 0, "c": 0} + term_xp = SignedFixedPoint.div_xp_u( + SignedFixedPoint.mul_xp_u(tau_beta.x, tau_beta.y), sq_vars.x + ) + + if term_xp > 0: + q["a"] = SignedFixedPoint.mul_up_mag_u(sq_vars.y, 2 * s) + q["a"] = SignedFixedPoint.mul_up_xp_to_np_u( + SignedFixedPoint.mul_up_mag_u(q["a"], c), term_xp + 7 + ) + else: + q["a"] = SignedFixedPoint.mul_down_mag_u(r.y, r.y) + q["a"] = SignedFixedPoint.mul_down_mag_u(q["a"], 2 * s) + q["a"] = SignedFixedPoint.mul_up_xp_to_np_u( + SignedFixedPoint.mul_down_mag_u(q["a"], c), term_xp + ) + + if tau_beta.x < 0: + q["b"] = SignedFixedPoint.mul_up_xp_to_np_u( + SignedFixedPoint.mul_up_mag_u( + SignedFixedPoint.mul_up_mag_u(r.x, x), 2 * c + ), + -SignedFixedPoint.div_xp_u(tau_beta.x, d_sq) + 3, + ) + else: + q["b"] = SignedFixedPoint.mul_up_xp_to_np_u( + SignedFixedPoint.mul_down_mag_u( + SignedFixedPoint.mul_down_mag_u(-r.y, x), 2 * c + ), + SignedFixedPoint.div_xp_u(tau_beta.x, d_sq), + ) + q["a"] = q["a"] + q["b"] + + term_xp2 = ( + SignedFixedPoint.div_xp_u( + SignedFixedPoint.mul_xp_u(tau_beta.y, tau_beta.y), sq_vars.x + ) + + 7 + ) + + q["b"] = SignedFixedPoint.mul_up_mag_u(sq_vars.y, s) + q["b"] = SignedFixedPoint.mul_up_xp_to_np_u( + SignedFixedPoint.mul_up_mag_u(q["b"], s), term_xp2 + ) + + q["c"] = SignedFixedPoint.mul_up_xp_to_np_u( + SignedFixedPoint.mul_down_mag_u( + SignedFixedPoint.mul_down_mag_u(-r.y, x), 2 * s + ), + SignedFixedPoint.div_xp_u(tau_beta.y, d_sq), + ) + + q["b"] = q["b"] + q["c"] + SignedFixedPoint.mul_up_mag_u(x, x) + q["b"] = ( + SignedFixedPoint.div_up_mag_u(q["b"], lambda_) + if q["b"] > 0 + else SignedFixedPoint.div_down_mag_u(q["b"], lambda_) + ) + + q["a"] = q["a"] + q["b"] + q["a"] = ( + SignedFixedPoint.div_up_mag_u(q["a"], lambda_) + if q["a"] > 0 + else SignedFixedPoint.div_down_mag_u(q["a"], lambda_) + ) + + val = SignedFixedPoint.mul_up_mag_u( + SignedFixedPoint.mul_up_mag_u(sq_vars.y, c), c + ) + return SignedFixedPoint.mul_up_xp_to_np_u(val, term_xp2) + q["a"] + + @classmethod + def calc_y_given_x( + cls, x: int, params: EclpParams, d: DerivedEclpParams, r: Vector2 + ) -> int: + ab = Vector2( + x=cls.virtual_offset0(params, d, r), y=cls.virtual_offset1(params, d, r) + ) + return cls.solve_quadratic_swap( + params.lambda_, x, params.s, params.c, r, ab, d.tau_beta, d.d_sq + ) + + @classmethod + def calc_x_given_y( + cls, y: int, params: EclpParams, d: DerivedEclpParams, r: Vector2 + ) -> int: + ba = Vector2( + x=cls.virtual_offset1(params, d, r), y=cls.virtual_offset0(params, d, r) + ) + return cls.solve_quadratic_swap( + params.lambda_, + y, + params.c, + params.s, + r, + ba, + Vector2(x=-d.tau_alpha.x, y=d.tau_alpha.y), + d.d_sq, + ) diff --git a/python/src/pools/gyro/signed_fixed_point.py b/python/src/pools/gyro/signed_fixed_point.py new file mode 100644 index 0000000..2499830 --- /dev/null +++ b/python/src/pools/gyro/signed_fixed_point.py @@ -0,0 +1,211 @@ +class FixedPointError(Exception): + pass + + +class SignedFixedPoint: + # Constants + ONE = int("1000000000000000000") # 1e18 + ONE_XP = int("100000000000000000000000000000000000000") # 1e38 + + @staticmethod + def add(a: int, b: int) -> int: + c = a + b + if not (b >= 0 and c >= a or b < 0 and c < a): + raise FixedPointError("AddOverflow") + return c + + @classmethod + def add_mag(cls, a: int, b: int) -> int: + return cls.add(a, b) if a > 0 else cls.sub(a, b) + + @staticmethod + def sub(a: int, b: int) -> int: + c = a - b + if not (b <= 0 and c >= a or b > 0 and c < a): + raise FixedPointError("SubOverflow") + return c + + @classmethod + def mul_down_mag(cls, a: int, b: int) -> int: + product = a * b + if not (a == 0 or product // a == b): + raise FixedPointError("MulOverflow") + return product // cls.ONE + + @classmethod + def mul_down_mag_u(cls, a: int, b: int) -> int: + return (a * b) // cls.ONE + + @classmethod + def mul_up_mag(cls, a: int, b: int) -> int: + product = a * b + if not (a == 0 or product // a == b): + raise FixedPointError("MulOverflow") + + if product > 0: + return (product - 1) // cls.ONE + 1 + if product < 0: + return (product + 1) // cls.ONE - 1 + return 0 + + @classmethod + def mul_up_mag_u(cls, a: int, b: int) -> int: + product = a * b + if product > 0: + return (product - 1) // cls.ONE + 1 + if product < 0: + return (product + 1) // cls.ONE - 1 + return 0 + + @classmethod + def div_down_mag(cls, a: int, b: int) -> int: + if b == 0: + raise FixedPointError("ZeroDivision") + if a == 0: + return 0 + + a_inflated = a * cls.ONE + if a_inflated // a != cls.ONE: + raise FixedPointError("DivInterval") + + return a_inflated // b + + @classmethod + def div_down_mag_u(cls, a: int, b: int) -> int: + if b == 0: + raise FixedPointError("ZeroDivision") + return (a * cls.ONE) // b + + @classmethod + def div_up_mag(cls, a: int, b: int) -> int: + if b == 0: + raise FixedPointError("ZeroDivision") + if a == 0: + return 0 + + local_a = a + local_b = b + if b < 0: + local_b = -b + local_a = -a + + a_inflated = local_a * cls.ONE + if a_inflated // local_a != cls.ONE: + raise FixedPointError("DivInterval") + + if a_inflated > 0: + return (a_inflated - 1) // local_b + 1 + return (a_inflated + 1) // local_b - 1 + + @classmethod + def div_up_mag_u(cls, a: int, b: int) -> int: + if b == 0: + raise FixedPointError("ZeroDivision") + if a == 0: + return 0 + + local_a = a + local_b = b + if b < 0: + local_b = -b + local_a = -a + + if local_a > 0: + return (local_a * cls.ONE - 1) // local_b + 1 + return (local_a * cls.ONE + 1) // local_b - 1 + + @classmethod + def mul_xp(cls, a: int, b: int) -> int: + product = a * b + if not (a == 0 or product // a == b): + raise FixedPointError("MulOverflow") + return product // cls.ONE_XP + + @classmethod + def mul_xp_u(cls, a: int, b: int) -> int: + return (a * b) // cls.ONE_XP + + @classmethod + def div_xp(cls, a: int, b: int) -> int: + if b == 0: + raise FixedPointError("ZeroDivision") + if a == 0: + return 0 + + a_inflated = a * cls.ONE_XP + if a_inflated // a != cls.ONE_XP: + raise FixedPointError("DivInterval") + + return a_inflated // b + + @classmethod + def div_xp_u(cls, a: int, b: int) -> int: + if b == 0: + raise FixedPointError("ZeroDivision") + return (a * cls.ONE_XP) // b + + @classmethod + def mul_down_xp_to_np(cls, a: int, b: int) -> int: + e_19 = int("10000000000000000000") + b1 = b // e_19 + prod1 = a * b1 + if not (a == 0 or prod1 // a == b1): + raise FixedPointError("MulOverflow") + b2 = b % e_19 + prod2 = a * b2 + if not (a == 0 or prod2 // a == b2): + raise FixedPointError("MulOverflow") + return ( + (prod1 + prod2 // e_19) // e_19 + if prod1 >= 0 and prod2 >= 0 + else (prod1 + prod2 // e_19 + 1) // e_19 - 1 + ) + + @classmethod + def mul_down_xp_to_np_u(cls, a: int, b: int) -> int: + e_19 = int("10000000000000000000") + b1 = b // e_19 + b2 = b % e_19 + prod1 = a * b1 + prod2 = a * b2 + return ( + (prod1 + prod2 // e_19) // e_19 + if prod1 >= 0 and prod2 >= 0 + else (prod1 + prod2 // e_19 + 1) // e_19 - 1 + ) + + @classmethod + def mul_up_xp_to_np(cls, a: int, b: int) -> int: + e_19 = int("10000000000000000000") + b1 = b // e_19 + prod1 = a * b1 + if not (a == 0 or prod1 // a == b1): + raise FixedPointError("MulOverflow") + b2 = b % e_19 + prod2 = a * b2 + if not (a == 0 or prod2 // a == b2): + raise FixedPointError("MulOverflow") + return ( + (prod1 + prod2 // e_19) // e_19 + if prod1 <= 0 and prod2 <= 0 + else (prod1 + prod2 // e_19 - 1) // e_19 + 1 + ) + + @classmethod + def mul_up_xp_to_np_u(cls, a: int, b: int) -> int: + e_19 = int("10000000000000000000") + b1 = b // e_19 + b2 = b % e_19 + prod1 = a * b1 + prod2 = a * b2 + return ( + (prod1 + prod2 // e_19) // e_19 + if prod1 <= 0 and prod2 <= 0 + else (prod1 + prod2 // e_19 - 1) // e_19 + 1 + ) + + @classmethod + def complement(cls, x: int) -> int: + if x >= cls.ONE or x <= 0: + return 0 + return cls.ONE - x diff --git a/python/src/vault.py b/python/src/vault.py index 0dea9b4..7fd0701 100644 --- a/python/src/vault.py +++ b/python/src/vault.py @@ -7,6 +7,7 @@ from src.pools.weighted import Weighted from src.pools.stable import Stable from src.pools.gyro.gyro2CLP import Gyro2CLP +from src.pools.gyro.gyroECLP import GyroECLP class Vault: @@ -15,6 +16,7 @@ def __init__(self, *, custom_pool_classes=None, custom_hook_classes=None): "WEIGHTED": Weighted, "STABLE": Stable, "GYRO": Gyro2CLP, + "GYROE": GyroECLP, } self.hook_classes = {"ExitFee": ExitFeeHook} if custom_pool_classes is not None: From 409f7c2a6ef986d2d33b58fa2e96b5ffa3f43732 Mon Sep 17 00:00:00 2001 From: johngrantuk Date: Wed, 26 Feb 2025 11:46:18 +0000 Subject: [PATCH 07/16] chore: Bump SDK package. --- testData/bun.lockb | Bin 66406 -> 66406 bytes testData/package.json | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/testData/bun.lockb b/testData/bun.lockb index a73389ee7e4eb40edccf2650c079d6c3e1f5695a..889aabd381445dc8514567b2491cee4df73a6736 100755 GIT binary patch delta 162 zcmV;T0A2s)h6Lt@1duKu1A&)F4;}s!wvy%nQNJ?X!uIm=k61{u}(q{ z0Roc|95=Jb4<$E1z*6Z7Q3!&g@U{FnMK{&{aBSOC2;we_3)&sN0as!2t4XtnOimz4 zJ1+0j>PEr?te4tUUby!zH()U4csZegvn@x^Yy~ndF)lNckhd7K8nliB1Tro&0JE&O QZ65_PE-@}MvmVz-ENc};0RR91 delta 160 zcmV;R0AK&+h6Lt@1duKuBgs2Hh+f)#7N^u~07#u)M~5POLP|Nv{KKwt4Y$Jou}(q{ z0RfW{7&o)X4<$E1)CFtI{a5Ruzo_ngbG>voEFDosp`O2Tl10}djaARVv$%ms){bTy zK*-E?a2Eo?V_MO@WMalcKwhM`Jta=Avn@x^Yy&YaF_VtB9 Date: Wed, 26 Feb 2025 11:47:38 +0000 Subject: [PATCH 08/16] test: Handle GyroECLP test generation. --- testData/src/abi/gyroECLP.ts | 863 ++++++++++++++++++++++++++ testData/src/getPool.ts | 7 +- testData/src/gyroECLP.ts | 160 +++++ typescript/test/utils/readTestData.ts | 36 +- 4 files changed, 1064 insertions(+), 2 deletions(-) create mode 100644 testData/src/abi/gyroECLP.ts create mode 100644 testData/src/gyroECLP.ts diff --git a/testData/src/abi/gyroECLP.ts b/testData/src/abi/gyroECLP.ts new file mode 100644 index 0000000..29b77a8 --- /dev/null +++ b/testData/src/abi/gyroECLP.ts @@ -0,0 +1,863 @@ +export const gyroECLPAbi = [ + { + inputs: [ + { + components: [ + { internalType: 'string', name: 'name', type: 'string' }, + { internalType: 'string', name: 'symbol', type: 'string' }, + { + components: [ + { + internalType: 'int256', + name: 'alpha', + type: 'int256', + }, + { + internalType: 'int256', + name: 'beta', + type: 'int256', + }, + { + internalType: 'int256', + name: 'c', + type: 'int256', + }, + { + internalType: 'int256', + name: 's', + type: 'int256', + }, + { + internalType: 'int256', + name: 'lambda', + type: 'int256', + }, + ], + internalType: 'struct IGyroECLPPool.EclpParams', + name: 'eclpParams', + type: 'tuple', + }, + { + components: [ + { + components: [ + { + internalType: 'int256', + name: 'x', + type: 'int256', + }, + { + internalType: 'int256', + name: 'y', + type: 'int256', + }, + ], + internalType: 'struct IGyroECLPPool.Vector2', + name: 'tauAlpha', + type: 'tuple', + }, + { + components: [ + { + internalType: 'int256', + name: 'x', + type: 'int256', + }, + { + internalType: 'int256', + name: 'y', + type: 'int256', + }, + ], + internalType: 'struct IGyroECLPPool.Vector2', + name: 'tauBeta', + type: 'tuple', + }, + { + internalType: 'int256', + name: 'u', + type: 'int256', + }, + { + internalType: 'int256', + name: 'v', + type: 'int256', + }, + { + internalType: 'int256', + name: 'w', + type: 'int256', + }, + { + internalType: 'int256', + name: 'z', + type: 'int256', + }, + { + internalType: 'int256', + name: 'dSq', + type: 'int256', + }, + ], + internalType: 'struct IGyroECLPPool.DerivedEclpParams', + name: 'derivedEclpParams', + type: 'tuple', + }, + { internalType: 'string', name: 'version', type: 'string' }, + ], + internalType: 'struct IGyroECLPPool.GyroECLPPoolParams', + name: 'params', + type: 'tuple', + }, + { internalType: 'contract IVault', name: 'vault', type: 'address' }, + ], + stateMutability: 'nonpayable', + type: 'constructor', + }, + { inputs: [], name: 'AssetBoundsExceeded', type: 'error' }, + { inputs: [], name: 'DerivedDsqWrong', type: 'error' }, + { inputs: [], name: 'DerivedTauAlphaNotNormalized', type: 'error' }, + { inputs: [], name: 'DerivedTauAlphaYWrong', type: 'error' }, + { inputs: [], name: 'DerivedTauBetaNotNormalized', type: 'error' }, + { inputs: [], name: 'DerivedTauBetaYWrong', type: 'error' }, + { inputs: [], name: 'DerivedTauXWrong', type: 'error' }, + { inputs: [], name: 'DerivedUWrong', type: 'error' }, + { inputs: [], name: 'DerivedVWrong', type: 'error' }, + { inputs: [], name: 'DerivedWWrong', type: 'error' }, + { inputs: [], name: 'DerivedZWrong', type: 'error' }, + { inputs: [], name: 'ECDSAInvalidSignature', type: 'error' }, + { + inputs: [{ internalType: 'uint256', name: 'length', type: 'uint256' }], + name: 'ECDSAInvalidSignatureLength', + type: 'error', + }, + { + inputs: [{ internalType: 'bytes32', name: 's', type: 'bytes32' }], + name: 'ECDSAInvalidSignatureS', + type: 'error', + }, + { + inputs: [ + { internalType: 'uint256', name: 'deadline', type: 'uint256' }, + ], + name: 'ERC2612ExpiredSignature', + type: 'error', + }, + { + inputs: [ + { internalType: 'address', name: 'signer', type: 'address' }, + { internalType: 'address', name: 'owner', type: 'address' }, + ], + name: 'ERC2612InvalidSigner', + type: 'error', + }, + { + inputs: [ + { internalType: 'address', name: 'account', type: 'address' }, + { internalType: 'uint256', name: 'currentNonce', type: 'uint256' }, + ], + name: 'InvalidAccountNonce', + type: 'error', + }, + { inputs: [], name: 'InvalidShortString', type: 'error' }, + { inputs: [], name: 'InvariantDenominatorWrong', type: 'error' }, + { inputs: [], name: 'MaxAssetsExceeded', type: 'error' }, + { inputs: [], name: 'MaxInvariantExceeded', type: 'error' }, + { inputs: [], name: 'MulOverflow', type: 'error' }, + { inputs: [], name: 'RotationVectorCWrong', type: 'error' }, + { inputs: [], name: 'RotationVectorNotNormalized', type: 'error' }, + { inputs: [], name: 'RotationVectorSWrong', type: 'error' }, + { + inputs: [{ internalType: 'int256', name: 'value', type: 'int256' }], + name: 'SafeCastOverflowedIntToUint', + type: 'error', + }, + { + inputs: [{ internalType: 'uint256', name: 'value', type: 'uint256' }], + name: 'SafeCastOverflowedUintToInt', + type: 'error', + }, + { + inputs: [{ internalType: 'address', name: 'sender', type: 'address' }], + name: 'SenderIsNotVault', + type: 'error', + }, + { inputs: [], name: 'StretchingFactorWrong', type: 'error' }, + { + inputs: [{ internalType: 'string', name: 'str', type: 'string' }], + name: 'StringTooLong', + type: 'error', + }, + { inputs: [], name: 'ZeroDivision', type: 'error' }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'address', + name: 'owner', + type: 'address', + }, + { + indexed: true, + internalType: 'address', + name: 'spender', + type: 'address', + }, + { + indexed: false, + internalType: 'uint256', + name: 'value', + type: 'uint256', + }, + ], + name: 'Approval', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: 'bool', + name: 'derivedParamsValidated', + type: 'bool', + }, + ], + name: 'ECLPDerivedParamsValidated', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: 'bool', + name: 'paramsValidated', + type: 'bool', + }, + ], + name: 'ECLPParamsValidated', + type: 'event', + }, + { + anonymous: false, + inputs: [], + name: 'EIP712DomainChanged', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'address', + name: 'from', + type: 'address', + }, + { + indexed: true, + internalType: 'address', + name: 'to', + type: 'address', + }, + { + indexed: false, + internalType: 'uint256', + name: 'value', + type: 'uint256', + }, + ], + name: 'Transfer', + type: 'event', + }, + { + inputs: [], + name: 'DOMAIN_SEPARATOR', + outputs: [{ internalType: 'bytes32', name: '', type: 'bytes32' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'PERMIT_TYPEHASH', + outputs: [{ internalType: 'bytes32', name: '', type: 'bytes32' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { internalType: 'address', name: 'owner', type: 'address' }, + { internalType: 'address', name: 'spender', type: 'address' }, + ], + name: 'allowance', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { internalType: 'address', name: 'spender', type: 'address' }, + { internalType: 'uint256', name: 'amount', type: 'uint256' }, + ], + name: 'approve', + outputs: [{ internalType: 'bool', name: '', type: 'bool' }], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [{ internalType: 'address', name: 'account', type: 'address' }], + name: 'balanceOf', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { + internalType: 'uint256[]', + name: 'balancesLiveScaled18', + type: 'uint256[]', + }, + { internalType: 'uint256', name: 'tokenInIndex', type: 'uint256' }, + { + internalType: 'uint256', + name: 'invariantRatio', + type: 'uint256', + }, + ], + name: 'computeBalance', + outputs: [ + { internalType: 'uint256', name: 'newBalance', type: 'uint256' }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { + internalType: 'uint256[]', + name: 'balancesLiveScaled18', + type: 'uint256[]', + }, + { internalType: 'enum Rounding', name: 'rounding', type: 'uint8' }, + ], + name: 'computeInvariant', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'decimals', + outputs: [{ internalType: 'uint8', name: '', type: 'uint8' }], + stateMutability: 'pure', + type: 'function', + }, + { + inputs: [], + name: 'eip712Domain', + outputs: [ + { internalType: 'bytes1', name: 'fields', type: 'bytes1' }, + { internalType: 'string', name: 'name', type: 'string' }, + { internalType: 'string', name: 'version', type: 'string' }, + { internalType: 'uint256', name: 'chainId', type: 'uint256' }, + { + internalType: 'address', + name: 'verifyingContract', + type: 'address', + }, + { internalType: 'bytes32', name: 'salt', type: 'bytes32' }, + { + internalType: 'uint256[]', + name: 'extensions', + type: 'uint256[]', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { internalType: 'address', name: 'owner', type: 'address' }, + { internalType: 'address', name: 'spender', type: 'address' }, + { internalType: 'uint256', name: 'amount', type: 'uint256' }, + ], + name: 'emitApproval', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { internalType: 'address', name: 'from', type: 'address' }, + { internalType: 'address', name: 'to', type: 'address' }, + { internalType: 'uint256', name: 'amount', type: 'uint256' }, + ], + name: 'emitTransfer', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [], + name: 'getAggregateFeePercentages', + outputs: [ + { + internalType: 'uint256', + name: 'aggregateSwapFeePercentage', + type: 'uint256', + }, + { + internalType: 'uint256', + name: 'aggregateYieldFeePercentage', + type: 'uint256', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'getCurrentLiveBalances', + outputs: [ + { + internalType: 'uint256[]', + name: 'balancesLiveScaled18', + type: 'uint256[]', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'getECLPParams', + outputs: [ + { + components: [ + { internalType: 'int256', name: 'alpha', type: 'int256' }, + { internalType: 'int256', name: 'beta', type: 'int256' }, + { internalType: 'int256', name: 'c', type: 'int256' }, + { internalType: 'int256', name: 's', type: 'int256' }, + { internalType: 'int256', name: 'lambda', type: 'int256' }, + ], + internalType: 'struct IGyroECLPPool.EclpParams', + name: 'params', + type: 'tuple', + }, + { + components: [ + { + components: [ + { + internalType: 'int256', + name: 'x', + type: 'int256', + }, + { + internalType: 'int256', + name: 'y', + type: 'int256', + }, + ], + internalType: 'struct IGyroECLPPool.Vector2', + name: 'tauAlpha', + type: 'tuple', + }, + { + components: [ + { + internalType: 'int256', + name: 'x', + type: 'int256', + }, + { + internalType: 'int256', + name: 'y', + type: 'int256', + }, + ], + internalType: 'struct IGyroECLPPool.Vector2', + name: 'tauBeta', + type: 'tuple', + }, + { internalType: 'int256', name: 'u', type: 'int256' }, + { internalType: 'int256', name: 'v', type: 'int256' }, + { internalType: 'int256', name: 'w', type: 'int256' }, + { internalType: 'int256', name: 'z', type: 'int256' }, + { internalType: 'int256', name: 'dSq', type: 'int256' }, + ], + internalType: 'struct IGyroECLPPool.DerivedEclpParams', + name: 'd', + type: 'tuple', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'getGyroECLPPoolDynamicData', + outputs: [ + { + components: [ + { + internalType: 'uint256[]', + name: 'balancesLiveScaled18', + type: 'uint256[]', + }, + { + internalType: 'uint256[]', + name: 'tokenRates', + type: 'uint256[]', + }, + { + internalType: 'uint256', + name: 'staticSwapFeePercentage', + type: 'uint256', + }, + { + internalType: 'uint256', + name: 'totalSupply', + type: 'uint256', + }, + { + internalType: 'uint256', + name: 'bptRate', + type: 'uint256', + }, + { + internalType: 'bool', + name: 'isPoolInitialized', + type: 'bool', + }, + { + internalType: 'bool', + name: 'isPoolPaused', + type: 'bool', + }, + { + internalType: 'bool', + name: 'isPoolInRecoveryMode', + type: 'bool', + }, + ], + internalType: 'struct GyroECLPPoolDynamicData', + name: 'data', + type: 'tuple', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'getGyroECLPPoolImmutableData', + outputs: [ + { + components: [ + { + internalType: 'contract IERC20[]', + name: 'tokens', + type: 'address[]', + }, + { + internalType: 'uint256[]', + name: 'decimalScalingFactors', + type: 'uint256[]', + }, + { + internalType: 'int256', + name: 'paramsAlpha', + type: 'int256', + }, + { + internalType: 'int256', + name: 'paramsBeta', + type: 'int256', + }, + { internalType: 'int256', name: 'paramsC', type: 'int256' }, + { internalType: 'int256', name: 'paramsS', type: 'int256' }, + { + internalType: 'int256', + name: 'paramsLambda', + type: 'int256', + }, + { + internalType: 'int256', + name: 'tauAlphaX', + type: 'int256', + }, + { + internalType: 'int256', + name: 'tauAlphaY', + type: 'int256', + }, + { + internalType: 'int256', + name: 'tauBetaX', + type: 'int256', + }, + { + internalType: 'int256', + name: 'tauBetaY', + type: 'int256', + }, + { internalType: 'int256', name: 'u', type: 'int256' }, + { internalType: 'int256', name: 'v', type: 'int256' }, + { internalType: 'int256', name: 'w', type: 'int256' }, + { internalType: 'int256', name: 'z', type: 'int256' }, + { internalType: 'int256', name: 'dSq', type: 'int256' }, + ], + internalType: 'struct GyroECLPPoolImmutableData', + name: 'data', + type: 'tuple', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'getMaximumInvariantRatio', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'pure', + type: 'function', + }, + { + inputs: [], + name: 'getMaximumSwapFeePercentage', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'pure', + type: 'function', + }, + { + inputs: [], + name: 'getMinimumInvariantRatio', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'pure', + type: 'function', + }, + { + inputs: [], + name: 'getMinimumSwapFeePercentage', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'pure', + type: 'function', + }, + { + inputs: [], + name: 'getRate', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'getStaticSwapFeePercentage', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'getTokenInfo', + outputs: [ + { + internalType: 'contract IERC20[]', + name: 'tokens', + type: 'address[]', + }, + { + components: [ + { + internalType: 'enum TokenType', + name: 'tokenType', + type: 'uint8', + }, + { + internalType: 'contract IRateProvider', + name: 'rateProvider', + type: 'address', + }, + { + internalType: 'bool', + name: 'paysYieldFees', + type: 'bool', + }, + ], + internalType: 'struct TokenInfo[]', + name: 'tokenInfo', + type: 'tuple[]', + }, + { + internalType: 'uint256[]', + name: 'balancesRaw', + type: 'uint256[]', + }, + { + internalType: 'uint256[]', + name: 'lastBalancesLiveScaled18', + type: 'uint256[]', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'getTokens', + outputs: [ + { + internalType: 'contract IERC20[]', + name: 'tokens', + type: 'address[]', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'getVault', + outputs: [ + { internalType: 'contract IVault', name: '', type: 'address' }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'incrementNonce', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [], + name: 'name', + outputs: [{ internalType: 'string', name: '', type: 'string' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [{ internalType: 'address', name: 'owner', type: 'address' }], + name: 'nonces', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { + components: [ + { + internalType: 'enum SwapKind', + name: 'kind', + type: 'uint8', + }, + { + internalType: 'uint256', + name: 'amountGivenScaled18', + type: 'uint256', + }, + { + internalType: 'uint256[]', + name: 'balancesScaled18', + type: 'uint256[]', + }, + { + internalType: 'uint256', + name: 'indexIn', + type: 'uint256', + }, + { + internalType: 'uint256', + name: 'indexOut', + type: 'uint256', + }, + { + internalType: 'address', + name: 'router', + type: 'address', + }, + { internalType: 'bytes', name: 'userData', type: 'bytes' }, + ], + internalType: 'struct PoolSwapParams', + name: 'request', + type: 'tuple', + }, + ], + name: 'onSwap', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { internalType: 'address', name: 'owner', type: 'address' }, + { internalType: 'address', name: 'spender', type: 'address' }, + { internalType: 'uint256', name: 'amount', type: 'uint256' }, + { internalType: 'uint256', name: 'deadline', type: 'uint256' }, + { internalType: 'uint8', name: 'v', type: 'uint8' }, + { internalType: 'bytes32', name: 'r', type: 'bytes32' }, + { internalType: 'bytes32', name: 's', type: 'bytes32' }, + ], + name: 'permit', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { internalType: 'bytes4', name: 'interfaceId', type: 'bytes4' }, + ], + name: 'supportsInterface', + outputs: [{ internalType: 'bool', name: '', type: 'bool' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'symbol', + outputs: [{ internalType: 'string', name: '', type: 'string' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'totalSupply', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { internalType: 'address', name: 'to', type: 'address' }, + { internalType: 'uint256', name: 'amount', type: 'uint256' }, + ], + name: 'transfer', + outputs: [{ internalType: 'bool', name: '', type: 'bool' }], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { internalType: 'address', name: 'from', type: 'address' }, + { internalType: 'address', name: 'to', type: 'address' }, + { internalType: 'uint256', name: 'amount', type: 'uint256' }, + ], + name: 'transferFrom', + outputs: [{ internalType: 'bool', name: '', type: 'bool' }], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [], + name: 'version', + outputs: [{ internalType: 'string', name: '', type: 'string' }], + stateMutability: 'view', + type: 'function', + }, +] as const; diff --git a/testData/src/getPool.ts b/testData/src/getPool.ts index 0ade414..12c0643 100644 --- a/testData/src/getPool.ts +++ b/testData/src/getPool.ts @@ -3,6 +3,7 @@ import { WeightedPool } from './weightedPool'; import { StablePool } from './stablePool'; import type { PoolBase } from './types'; import { BufferPool } from './buffer'; +import { GyroECLPPool } from './gyroECLP'; export async function getPool( rpcUrl: string, @@ -12,10 +13,14 @@ export async function getPool( poolAddress: Address, ): Promise { // Find onchain data fetching via pool type - const poolData: Record = { + const poolData: Record< + string, + WeightedPool | StablePool | BufferPool | GyroECLPPool + > = { WEIGHTED: new WeightedPool(rpcUrl, chainId), STABLE: new StablePool(rpcUrl, chainId), Buffer: new BufferPool(rpcUrl, chainId), + GYROE: new GyroECLPPool(rpcUrl, chainId), }; if (!poolData[poolType]) throw new Error(`getPool: Unsupported pool type: ${poolType}`); diff --git a/testData/src/gyroECLP.ts b/testData/src/gyroECLP.ts new file mode 100644 index 0000000..97497c0 --- /dev/null +++ b/testData/src/gyroECLP.ts @@ -0,0 +1,160 @@ +import { + type PublicClient, + createPublicClient, + http, + type Address, + parseAbi, + type Chain, +} from 'viem'; +import { CHAINS, VAULT_V3, vaultExtensionAbi_V3 } from '@balancer/sdk'; +import { vaultExplorerAbi } from './abi/vaultExplorer'; +import { gyroECLPAbi } from './abi/gyroECLP'; + +type GyroECLPMutable = { + swapFee: bigint; + totalSupply: bigint; + balancesLiveScaled18: bigint[]; + tokenRates: bigint[]; + aggregateSwapFee: bigint; +}; + +type GyroECLPImmutable = { + tokens: bigint[]; + scalingFactors: bigint[]; + paramsAlpha: bigint; + paramsBeta: bigint; + paramsC: bigint; + paramsS: bigint; + paramsLambda: bigint; + tauAlphaX: bigint; + tauAlphaY: bigint; + tauBetaX: bigint; + tauBetaY: bigint; + u: bigint; + v: bigint; + w: bigint; + z: bigint; + dSq: bigint; +}; + +type TransformBigintToString = { + [K in keyof T]: T[K] extends bigint + ? string + : T[K] extends bigint[] + ? string[] + : T[K]; +}; + +export class GyroECLPPool { + client: PublicClient; + vault: Address; + + constructor( + public rpcUrl: string, + public chainId: number, + ) { + this.client = createPublicClient({ + transport: http(this.rpcUrl), + chain: CHAINS[this.chainId] as Chain, + }); + this.vault = VAULT_V3[this.chainId]; + } + + async fetchImmutableData( + address: Address, + blockNumber: bigint, + ): Promise> { + const poolTokensCall = { + address: this.vault, + abi: vaultExtensionAbi_V3, + functionName: 'getPoolTokenInfo', + args: [address], + } as const; + const tokenRatesCall = { + address: this.vault, + abi: vaultExtensionAbi_V3, + functionName: 'getPoolTokenRates', + args: [address], + } as const; + const immutableDataCall = { + address, + abi: gyroECLPAbi, + functionName: 'getGyroECLPPoolImmutableData', + } as const; + const multicallResult = await this.client.multicall({ + contracts: [poolTokensCall, tokenRatesCall, immutableDataCall], + allowFailure: false, + blockNumber, + }); + + return { + tokens: multicallResult[0][0].map((token) => token), + scalingFactors: multicallResult[1][0].map((sf) => sf.toString()), + paramsAlpha: multicallResult[2].paramsAlpha.toString(), + paramsBeta: multicallResult[2].paramsBeta.toString(), + paramsC: multicallResult[2].paramsC.toString(), + paramsS: multicallResult[2].paramsS.toString(), + paramsLambda: multicallResult[2].paramsLambda.toString(), + tauAlphaX: multicallResult[2].tauAlphaX.toString(), + tauAlphaY: multicallResult[2].tauAlphaY.toString(), + tauBetaX: multicallResult[2].tauBetaX.toString(), + tauBetaY: multicallResult[2].tauBetaY.toString(), + u: multicallResult[2].u.toString(), + v: multicallResult[2].v.toString(), + w: multicallResult[2].w.toString(), + z: multicallResult[2].z.toString(), + dSq: multicallResult[2].dSq.toString(), + }; + } + + async fetchMutableData( + address: Address, + blockNumber: bigint, + ): Promise> { + const totalSupplyCall = { + address: this.vault, + abi: parseAbi([ + 'function totalSupply(address token) external view returns (uint256)', + ]), + functionName: 'totalSupply', + args: [address], + } as const; + const liveBalancesCall = { + address: this.vault, + abi: vaultExtensionAbi_V3, + functionName: 'getCurrentLiveBalances', + args: [address], + } as const; + const tokenRatesCall = { + address: this.vault, + abi: vaultExtensionAbi_V3, + functionName: 'getPoolTokenRates', + args: [address], + } as const; + const poolConfigCall = { + address: this.vault, + abi: vaultExplorerAbi, + functionName: 'getPoolConfig', + args: [address], + } as const; + + const multicallResult = await this.client.multicall({ + contracts: [ + totalSupplyCall, + liveBalancesCall, + tokenRatesCall, + poolConfigCall, + ], + allowFailure: false, + blockNumber, + }); + return { + swapFee: multicallResult[3].staticSwapFeePercentage.toString(), + totalSupply: multicallResult[0].toString(), + balancesLiveScaled18: multicallResult[1].map((b) => b.toString()), + tokenRates: multicallResult[2][1].map((b) => b.toString()), + aggregateSwapFee: + multicallResult[3].aggregateSwapFeePercentage.toString(), + }; + } +} diff --git a/typescript/test/utils/readTestData.ts b/typescript/test/utils/readTestData.ts index 675374e..fb85723 100644 --- a/typescript/test/utils/readTestData.ts +++ b/typescript/test/utils/readTestData.ts @@ -1,4 +1,5 @@ import { BufferState } from '@/buffer/data'; +import { GyroECLPState } from '@/gyro'; import type { StableState } from '@/stable/data'; import type { WeightedState } from '@/weighted/data'; import * as fs from 'node:fs'; @@ -16,7 +17,9 @@ type StablePool = PoolBase & StableState; type BufferPool = PoolBase & BufferState; -type SupportedPools = WeightedPool | StablePool | BufferPool; +type GyroEPool = PoolBase & GyroECLPState; + +type SupportedPools = WeightedPool | StablePool | BufferPool | GyroEPool; type PoolsMap = Map; @@ -182,6 +185,37 @@ function mapPool( rate: BigInt(pool.rate), }; } + if (pool.poolType === 'GYROE') { + return { + ...pool, + scalingFactors: pool.scalingFactors.map((sf) => BigInt(sf)), + swapFee: BigInt(pool.swapFee), + balancesLiveScaled18: pool.balancesLiveScaled18.map((b) => + BigInt(b), + ), + tokenRates: pool.tokenRates.map((r) => BigInt(r)), + totalSupply: BigInt(pool.totalSupply), + aggregateSwapFee: BigInt(pool.aggregateSwapFee ?? '0'), + supportsUnbalancedLiquidity: + pool.supportsUnbalancedLiquidity === undefined + ? true + : pool.supportsUnbalancedLiquidity, + paramsAlpha: BigInt(pool.paramsAlpha), + paramsBeta: BigInt(pool.paramsBeta), + paramsC: BigInt(pool.paramsC), + paramsS: BigInt(pool.paramsS), + paramsLambda: BigInt(pool.paramsLambda), + tauAlphaX: BigInt(pool.tauAlphaX), + tauAlphaY: BigInt(pool.tauAlphaY), + tauBetaX: BigInt(pool.tauBetaX), + tauBetaY: BigInt(pool.tauBetaY), + u: BigInt(pool.u), + v: BigInt(pool.v), + w: BigInt(pool.w), + z: BigInt(pool.z), + dSq: BigInt(pool.dSq), + }; + } console.log(pool); throw new Error('mapPool: Unsupported Pool Type'); } From 72a12e93ff14c9764d7531a870e7669b470ced8d Mon Sep 17 00:00:00 2001 From: johngrantuk Date: Wed, 26 Feb 2025 11:48:07 +0000 Subject: [PATCH 09/16] test: Add GyroECLP test. --- testData/config.json | 33 +++++++++ .../testData/11155111-7748718-GyroECLP.json | 71 +++++++++++++++++++ 2 files changed, 104 insertions(+) create mode 100644 testData/testData/11155111-7748718-GyroECLP.json diff --git a/testData/config.json b/testData/config.json index 022a57c..6118454 100644 --- a/testData/config.json +++ b/testData/config.json @@ -224,6 +224,39 @@ "tokenOut": "0x0FE906e030a44eF24CA8c7dC7B7c53A6C4F00ce9" } ] + }, + { + "testName": "GyroECLP", + "chainId": "11155111", + "blockNumber": "7748718", + "poolAddress": "0x80fd5bc9d4fA6C22132f8bb2d9d30B01c3336FB3", + "poolType": "GYROE", + "swaps": [ + { + "swapKind": 0, + "amountRaw": "1000000000000000000", + "tokenIn": "0xB77EB1A70A96fDAAeB31DB1b42F2b8b5846b2613", + "tokenOut": "0xb19382073c7A0aDdbb56Ac6AF1808Fa49e377B75" + }, + { + "swapKind": 1, + "amountRaw": "10000000000000", + "tokenIn": "0xB77EB1A70A96fDAAeB31DB1b42F2b8b5846b2613", + "tokenOut": "0xb19382073c7A0aDdbb56Ac6AF1808Fa49e377B75" + }, + { + "swapKind": 0, + "amountRaw": "1000000000000000000", + "tokenIn": "0xb19382073c7A0aDdbb56Ac6AF1808Fa49e377B75", + "tokenOut": "0xB77EB1A70A96fDAAeB31DB1b42F2b8b5846b2613" + }, + { + "swapKind": 1, + "amountRaw": "10000000000000", + "tokenIn": "0xb19382073c7A0aDdbb56Ac6AF1808Fa49e377B75", + "tokenOut": "0xB77EB1A70A96fDAAeB31DB1b42F2b8b5846b2613" + } + ] } ] } \ No newline at end of file diff --git a/testData/testData/11155111-7748718-GyroECLP.json b/testData/testData/11155111-7748718-GyroECLP.json new file mode 100644 index 0000000..33ea269 --- /dev/null +++ b/testData/testData/11155111-7748718-GyroECLP.json @@ -0,0 +1,71 @@ +{ + "swaps": [ + { + "swapKind": 0, + "amountRaw": "1000000000000000000", + "tokenIn": "0xB77EB1A70A96fDAAeB31DB1b42F2b8b5846b2613", + "tokenOut": "0xb19382073c7A0aDdbb56Ac6AF1808Fa49e377B75", + "outputRaw": "989980003877180195" + }, + { + "swapKind": 1, + "amountRaw": "10000000000000", + "tokenIn": "0xB77EB1A70A96fDAAeB31DB1b42F2b8b5846b2613", + "tokenOut": "0xb19382073c7A0aDdbb56Ac6AF1808Fa49e377B75", + "outputRaw": "10099488370678" + }, + { + "swapKind": 0, + "amountRaw": "1000000000000000000", + "tokenIn": "0xb19382073c7A0aDdbb56Ac6AF1808Fa49e377B75", + "tokenOut": "0xB77EB1A70A96fDAAeB31DB1b42F2b8b5846b2613", + "outputRaw": "989529488258373725" + }, + { + "swapKind": 1, + "amountRaw": "10000000000000", + "tokenIn": "0xb19382073c7A0aDdbb56Ac6AF1808Fa49e377B75", + "tokenOut": "0xB77EB1A70A96fDAAeB31DB1b42F2b8b5846b2613", + "outputRaw": "10102532135967" + } + ], + "pool": { + "chainId": "11155111", + "blockNumber": "7748718", + "poolType": "GYROE", + "poolAddress": "0x80fd5bc9d4fA6C22132f8bb2d9d30B01c3336FB3", + "tokens": [ + "0xb19382073c7A0aDdbb56Ac6AF1808Fa49e377B75", + "0xB77EB1A70A96fDAAeB31DB1b42F2b8b5846b2613" + ], + "scalingFactors": [ + "1", + "1" + ], + "paramsAlpha": "998502246630054917", + "paramsBeta": "1000200040008001600", + "paramsC": "707106781186547524", + "paramsS": "707106781186547524", + "paramsLambda": "4000000000000000000000", + "tauAlphaX": "-94861212813096057289512505574275160547", + "tauAlphaY": "31644119574235279926451292677567331630", + "tauBetaX": "37142269533113549537591131345643981951", + "tauBetaY": "92846388265400743995957747409218517601", + "u": "66001741173104803338721745994955553010", + "v": "62245253919818011890633399060291020887", + "w": "30601134345582732000058913853921008022", + "z": "-28859471639991253843240999485797747790", + "dSq": "99999999999999999886624093342106115200", + "swapFee": "10000000000000000", + "totalSupply": "535740808545474", + "balancesLiveScaled18": [ + "1000000000000000000", + "1000000000000000000" + ], + "tokenRates": [ + "1000000000000000000", + "1000000000000000000" + ], + "aggregateSwapFee": "0" + } +} \ No newline at end of file From 56faea73a603ee133b90afe08995fac2a0c1c9c7 Mon Sep 17 00:00:00 2001 From: johngrantuk Date: Wed, 26 Feb 2025 11:48:43 +0000 Subject: [PATCH 10/16] feat(TS): Add GyroECLP to Vault. --- typescript/src/vault/types.ts | 7 ++++++- typescript/src/vault/vault.ts | 2 ++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/typescript/src/vault/types.ts b/typescript/src/vault/types.ts index 7d9b1f9..9584dc5 100644 --- a/typescript/src/vault/types.ts +++ b/typescript/src/vault/types.ts @@ -1,3 +1,4 @@ +import { GyroECLPState } from '@/gyro'; import { StableState } from '@/stable'; import { WeightedState } from '@/weighted'; @@ -18,7 +19,11 @@ export type BasePoolState = { hookType?: string; }; -export type PoolState = BasePoolState | WeightedState | StableState; +export type PoolState = + | BasePoolState + | WeightedState + | StableState + | GyroECLPState; export enum SwapKind { GivenIn = 0, diff --git a/typescript/src/vault/vault.ts b/typescript/src/vault/vault.ts index c97f4f6..f86d02b 100644 --- a/typescript/src/vault/vault.ts +++ b/typescript/src/vault/vault.ts @@ -8,6 +8,7 @@ import { } from './basePoolMath'; import { Weighted } from '../weighted'; import { Stable } from '../stable'; +import { GyroECLP } from '../gyro'; import { BufferState, erc4626BufferWrapOrUnwrap } from '../buffer'; import { isSameAddress, @@ -56,6 +57,7 @@ export class Vault { this.poolClasses = { WEIGHTED: Weighted, STABLE: Stable, + GYROE: GyroECLP, // custom add liquidity types take precedence over base types ...customPoolClasses, }; From 7f4e3a01cf0674e3c54d7a8661deb3416d33c9e9 Mon Sep 17 00:00:00 2001 From: johngrantuk Date: Wed, 26 Feb 2025 11:49:10 +0000 Subject: [PATCH 11/16] fix(TS): Fix GyroECLP maths. --- typescript/src/gyro/gyroECLPMath.ts | 103 ++++++++++++++++++++-------- 1 file changed, 73 insertions(+), 30 deletions(-) diff --git a/typescript/src/gyro/gyroECLPMath.ts b/typescript/src/gyro/gyroECLPMath.ts index 3ebfb2d..3c37198 100644 --- a/typescript/src/gyro/gyroECLPMath.ts +++ b/typescript/src/gyro/gyroECLPMath.ts @@ -240,6 +240,8 @@ export class GyroECLPMath { ): bigint { const dSq2 = SignedFixedPoint.mulXpU(d.dSq, d.dSq); + // (cx - sy) * (w/lambda + z) / lambda + // account for 2 factors of dSq (4 s,c factors) const termXp = SignedFixedPoint.divXpU( SignedFixedPoint.divDownMagU( SignedFixedPoint.divDownMagU(d.w, p.lambda) + d.z, @@ -254,21 +256,33 @@ export class GyroECLPMath { termXp, ); - const termNp1 = SignedFixedPoint.mulDownMagU(x, p.lambda); - const termNp2 = SignedFixedPoint.mulDownMagU(y, p.lambda); - - val += SignedFixedPoint.mulDownXpToNpU( - SignedFixedPoint.mulDownMagU(termNp1, p.s) + - SignedFixedPoint.mulDownMagU(termNp2, p.c), - SignedFixedPoint.divXpU(d.u, dSq2), - ); + // (x lambda s + y lambda c) * u, note u > 0 + let termNp = + SignedFixedPoint.mulDownMagU( + SignedFixedPoint.mulDownMagU(x, p.lambda), + p.s, + ) + + SignedFixedPoint.mulDownMagU( + SignedFixedPoint.mulDownMagU(y, p.lambda), + p.c, + ); + val = + val + + SignedFixedPoint.mulDownXpToNpU( + termNp, + SignedFixedPoint.divXpU(d.u, dSq2), + ); - val += SignedFixedPoint.mulDownXpToNpU( + // (sx+cy) * v, note v > 0 + termNp = SignedFixedPoint.mulDownMagU(x, p.s) + - SignedFixedPoint.mulDownMagU(y, p.c), - SignedFixedPoint.divXpU(d.v, dSq2), - ); - + SignedFixedPoint.mulDownMagU(y, p.c); + val = + val + + SignedFixedPoint.mulDownXpToNpU( + termNp, + SignedFixedPoint.divXpU(d.v, dSq2), + ); return val; } @@ -321,40 +335,64 @@ export class GyroECLPMath { } const atAChi = this.calcAtAChi(x, y, params, derived); - const achiachi = this.calcAChiAChiInXp(params, derived); - - // Calculate error (simplified for this example) - const err = + const invariantResult = this.calcInvariantSqrt(x, y, params, derived); + const sqrt = invariantResult[0]; + let err = invariantResult[1]; + + // Note: the minimum non-zero value of sqrt is 1e-9 since the minimum argument is 1e-18 + if (sqrt > 0) { + // err + 1 to account for O(eps_np) term ignored before + err = SignedFixedPoint.divUpMagU(err + 1n, 2n * sqrt); + } else { + // In the false case here, the extra precision error does not magnify, and so the error inside the sqrt is + // O(1e-18) + // somedayTODO: The true case will almost surely never happen (can it be removed) + err = err > 0 ? GyroPoolMath.sqrt(err, 5n) : BigInt(1e9); + } + // Calculate the error in the numerator, scale the error by 20 to be sure all possible terms accounted for + err = (SignedFixedPoint.mulUpMagU(params.lambda, x + y) / this._ONE_XP + + err + 1n) * 20n; + const achiachi = this.calcAChiAChiInXp(params, derived); + // A chi \cdot A chi > 1, so round it up to round denominator up. + // Denominator uses extra precision, so we do * 1/denominator so we are sure the calculation doesn't overflow. const mulDenominator = SignedFixedPoint.divXpU( this._ONE_XP, achiachi - this._ONE_XP, ); + // As alternative, could do, but could overflow: invariant = (AtAChi.add(sqrt) - err).divXp(denominator); const invariant = SignedFixedPoint.mulDownXpToNpU( - atAChi - err, + atAChi + sqrt - err, mulDenominator, ); - - // Error calculation (simplified) - const scaledErr = SignedFixedPoint.mulUpXpToNpU(err, mulDenominator); - const totalErr = - scaledErr + - (invariant * - ((params.lambda * params.lambda) / - BigInt('10000000000000000000000000000000000000')) * + // Error scales if denominator is small. + // NB: This error calculation computes the error in the expression "numerator / denominator", but in this code + // We actually use the formula "numerator * (1 / denominator)" to compute the invariant. This affects this line + // and the one below. + err = SignedFixedPoint.mulUpXpToNpU(err, mulDenominator); + // Account for relative error due to error in the denominator. + // Error in denominator is O(epsilon) if lambda<1e11, scale up by 10 to be sure we catch it, and add O(eps). + // Error in denominator is lambda^2 * 2e-37 and scales relative to the result / denominator. + // Scale by a constant to account for errors in the scaling factor itself and limited compounding. + // Calculating lambda^2 without decimals so that the calculation will never overflow, the lost precision isn't + // important. + err = + err + + (SignedFixedPoint.mulUpXpToNpU(invariant, mulDenominator) * + ((params.lambda * params.lambda) / BigInt(1e36)) * 40n) / this._ONE_XP + 1n; - if (invariant + totalErr > this._MAX_INVARIANT) { + if (invariant + err > this._MAX_INVARIANT) { throw new MaxInvariantExceededError(); } - return [invariant, totalErr]; + return [invariant, err]; } static calcMinAtxAChiySqPlusAtxSq( @@ -577,7 +615,7 @@ export class GyroECLPMath { SignedFixedPoint.mulUpMagU(y, y)) / BigInt('1000000000000000000000000000000000000000'); // 1e38 - val = val > 0n ? GyroPoolMath.sqrt(val > 0n ? val : 0n, 5n) : 0n; + val = val > 0n ? GyroPoolMath.sqrt(val, 5n) : 0n; return [val, err]; } @@ -834,7 +872,7 @@ export class GyroECLPMath { } q.a = q.a + q.b; - const termXp2 = + let termXp2 = SignedFixedPoint.divXpU( SignedFixedPoint.mulXpU(tauBeta.y, tauBeta.y), sqVars.x, @@ -866,6 +904,11 @@ export class GyroECLPMath { ? SignedFixedPoint.divUpMagU(q.a, lambda) : SignedFixedPoint.divDownMagU(q.a, lambda); + termXp2 = + SignedFixedPoint.divXpU( + SignedFixedPoint.mulXpU(tauBeta.x, tauBeta.x), + sqVars.x, + ) + 7n; const val = SignedFixedPoint.mulUpMagU( SignedFixedPoint.mulUpMagU(sqVars.y, c), c, From 0b68c7e41a85e4b44ad60974b870f7768449d1be Mon Sep 17 00:00:00 2001 From: johngrantuk Date: Wed, 26 Feb 2025 13:29:01 +0000 Subject: [PATCH 12/16] fix(Python): WIP fixes for GyroECLP. --- python/src/pools/gyro/gyroECLP.py | 25 +++++------ python/src/pools/gyro/gyroECLP_math.py | 60 +++++++++++++------------- 2 files changed, 43 insertions(+), 42 deletions(-) diff --git a/python/src/pools/gyro/gyroECLP.py b/python/src/pools/gyro/gyroECLP.py index 39efcdb..9aec784 100644 --- a/python/src/pools/gyro/gyroECLP.py +++ b/python/src/pools/gyro/gyroECLP.py @@ -20,22 +20,23 @@ class PoolParams: class GyroECLP: def __init__(self, pool_state): + print(pool_state) self.pool_params = PoolParams( eclp_params=EclpParams( - alpha=pool_state.params_alpha, - beta=pool_state.params_beta, - c=pool_state.params_c, - s=pool_state.params_s, - lambda_=pool_state.params_lambda, + alpha=pool_state.get("paramsAlpha"), + beta=pool_state.get("paramsBeta"), + c=pool_state.get("paramsC"), + s=pool_state.get("paramsS"), + lambda_=pool_state.get("paramsLambda"), ), derived_eclp_params=DerivedEclpParams( - tauAlpha=Vector2(x=pool_state.tau_alpha_x, y=pool_state.tau_alpha_y), - tauBeta=Vector2(x=pool_state.tau_beta_x, y=pool_state.tau_beta_y), - u=pool_state.u, - v=pool_state.v, - w=pool_state.w, - z=pool_state.z, - dSq=pool_state.d_sq, + tauAlpha=Vector2(x=pool_state.get("tauAlphaX"), y=pool_state.get("tauAlphaY")), + tauBeta=Vector2(x=pool_state.get("tauBetaY"), y=pool_state.get("tauBetaY")), + u=pool_state.get("u"), + v=pool_state.get("v"), + w=pool_state.get("w"), + z=pool_state.get("z"), + dSq=pool_state.get("dSq"), ), ) diff --git a/python/src/pools/gyro/gyroECLP_math.py b/python/src/pools/gyro/gyroECLP_math.py index 92baa42..e6fc44f 100644 --- a/python/src/pools/gyro/gyroECLP_math.py +++ b/python/src/pools/gyro/gyroECLP_math.py @@ -166,9 +166,9 @@ def max_balances0(cls, p: EclpParams, d: DerivedEclpParams, r: Vector2) -> int: @classmethod def max_balances1(cls, p: EclpParams, d: DerivedEclpParams, r: Vector2) -> int: - term_xp1 = SignedFixedPoint.div_xp_u(d.tau_beta.x - d.tau_alpha.x, d.d_sq) + term_xp1 = SignedFixedPoint.div_xp_u(d.tauBeta.x - d.tauAlpha.x, d.dSq) - term_xp2 = SignedFixedPoint.div_xp_u(d.tau_alpha.y - d.tau_beta.y, d.d_sq) + term_xp2 = SignedFixedPoint.div_xp_u(d.tauAlpha.y - d.tauBeta.y, d.dSq) yp = SignedFixedPoint.mul_down_xp_to_np_u( SignedFixedPoint.mul_down_mag_u( @@ -188,14 +188,14 @@ def max_balances1(cls, p: EclpParams, d: DerivedEclpParams, r: Vector2) -> int: @classmethod def calc_at_a_chi(cls, x: int, y: int, p: EclpParams, d: DerivedEclpParams) -> int: - d_sq2 = SignedFixedPoint.mul_xp_u(d.dSq, d.dSq) + dSq2 = SignedFixedPoint.mul_xp_u(d.dSq, d.dSq) term_xp = SignedFixedPoint.div_xp_u( SignedFixedPoint.div_down_mag_u( SignedFixedPoint.div_down_mag_u(d.w, p.lambda_) + d.z, p.lambda_, ), - d_sq2, + dSq2, ) val = SignedFixedPoint.mul_down_xp_to_np_u( @@ -210,19 +210,19 @@ def calc_at_a_chi(cls, x: int, y: int, p: EclpParams, d: DerivedEclpParams) -> i val += SignedFixedPoint.mul_down_xp_to_np_u( SignedFixedPoint.mul_down_mag_u(term_np1, p.s) + SignedFixedPoint.mul_down_mag_u(term_np2, p.c), - SignedFixedPoint.div_xp_u(d.u, d_sq2), + SignedFixedPoint.div_xp_u(d.u, dSq2), ) val += SignedFixedPoint.mul_down_xp_to_np_u( SignedFixedPoint.mul_down_mag_u(x, p.s) + SignedFixedPoint.mul_down_mag_u(y, p.c), - SignedFixedPoint.div_xp_u(d.v, d_sq2), + SignedFixedPoint.div_xp_u(d.v, dSq2), ) return val @classmethod def calc_a_chi_a_chi_in_xp(cls, p: EclpParams, d: DerivedEclpParams) -> int: - d_sq3 = SignedFixedPoint.mul_xp_u( + dSq3 = SignedFixedPoint.mul_xp_u( SignedFixedPoint.mul_xp_u(d.dSq, d.dSq), d.dSq, ) @@ -231,7 +231,7 @@ def calc_a_chi_a_chi_in_xp(cls, p: EclpParams, d: DerivedEclpParams) -> int: p.lambda_, SignedFixedPoint.div_xp_u( SignedFixedPoint.mul_xp_u(2 * d.u, d.v), - d_sq3, + dSq3, ), ) @@ -239,19 +239,19 @@ def calc_a_chi_a_chi_in_xp(cls, p: EclpParams, d: DerivedEclpParams) -> int: SignedFixedPoint.mul_up_mag_u( SignedFixedPoint.div_xp_u( SignedFixedPoint.mul_xp_u(d.u + 1, d.u + 1), - d_sq3, + dSq3, ), p.lambda_, ), p.lambda_, ) - val += SignedFixedPoint.div_xp_u(SignedFixedPoint.mul_xp_u(d.v, d.v), d_sq3) + val += SignedFixedPoint.div_xp_u(SignedFixedPoint.mul_xp_u(d.v, d.v), dSq3) term_xp = SignedFixedPoint.div_up_mag_u(d.w, p.lambda_) + d.z val += SignedFixedPoint.div_xp_u( SignedFixedPoint.mul_xp_u(term_xp, term_xp), - d_sq3, + dSq3, ) return val @@ -404,8 +404,8 @@ def solve_quadratic_swap( c: int, r: Vector2, ab: Vector2, - tau_beta: Vector2, - d_sq: int, + tauBeta: Vector2, + dSq: int, ) -> int: lam_bar = Vector2( x=SignedFixedPoint.ONE_XP @@ -427,12 +427,12 @@ def solve_quadratic_swap( SignedFixedPoint.mul_down_mag_u( SignedFixedPoint.mul_down_mag_u(-xp, s), c ), - SignedFixedPoint.div_xp_u(lam_bar.y, d_sq), + SignedFixedPoint.div_xp_u(lam_bar.y, dSq), ) else: q["b"] = SignedFixedPoint.mul_up_xp_to_np_u( SignedFixedPoint.mul_up_mag_u(SignedFixedPoint.mul_up_mag_u(-xp, s), c), - SignedFixedPoint.div_xp_u(lam_bar.x, d_sq) + 1, + SignedFixedPoint.div_xp_u(lam_bar.x, dSq) + 1, ) s_term = Vector2( @@ -440,13 +440,13 @@ def solve_quadratic_swap( SignedFixedPoint.mul_down_mag_u( SignedFixedPoint.mul_down_mag_u(lam_bar.y, s), s ), - d_sq, + dSq, ), y=SignedFixedPoint.div_xp_u( SignedFixedPoint.mul_up_mag_u( SignedFixedPoint.mul_up_mag_u(lam_bar.x, s), s ), - d_sq + 1, + dSq + 1, ) + 1, ) @@ -454,7 +454,7 @@ def solve_quadratic_swap( s_term.x = SignedFixedPoint.ONE_XP - s_term.x s_term.y = SignedFixedPoint.ONE_XP - s_term.y - q["c"] = -cls.calc_xp_xp_div_lambda_lambda(x, r, lambda_, s, c, tau_beta, d_sq) + q["c"] = -cls.calc_xp_xp_div_lambda_lambda(x, r, lambda_, s, c, tauBeta, dSq) q["c"] = q["c"] + SignedFixedPoint.mul_down_xp_to_np_u( SignedFixedPoint.mul_down_mag_u(r.y, r.y), s_term.y ) @@ -482,17 +482,17 @@ def calc_xp_xp_div_lambda_lambda( lambda_: int, s: int, c: int, - tau_beta: Vector2, - d_sq: int, + tauBeta: Vector2, + dSq: int, ) -> int: sq_vars = Vector2( - x=SignedFixedPoint.mul_xp_u(d_sq, d_sq), + x=SignedFixedPoint.mul_xp_u(dSq, dSq), y=SignedFixedPoint.mul_up_mag_u(r.x, r.x), ) q = {"a": 0, "b": 0, "c": 0} term_xp = SignedFixedPoint.div_xp_u( - SignedFixedPoint.mul_xp_u(tau_beta.x, tau_beta.y), sq_vars.x + SignedFixedPoint.mul_xp_u(tauBeta.x, tauBeta.y), sq_vars.x ) if term_xp > 0: @@ -507,25 +507,25 @@ def calc_xp_xp_div_lambda_lambda( SignedFixedPoint.mul_down_mag_u(q["a"], c), term_xp ) - if tau_beta.x < 0: + if tauBeta.x < 0: q["b"] = SignedFixedPoint.mul_up_xp_to_np_u( SignedFixedPoint.mul_up_mag_u( SignedFixedPoint.mul_up_mag_u(r.x, x), 2 * c ), - -SignedFixedPoint.div_xp_u(tau_beta.x, d_sq) + 3, + -SignedFixedPoint.div_xp_u(tauBeta.x, dSq) + 3, ) else: q["b"] = SignedFixedPoint.mul_up_xp_to_np_u( SignedFixedPoint.mul_down_mag_u( SignedFixedPoint.mul_down_mag_u(-r.y, x), 2 * c ), - SignedFixedPoint.div_xp_u(tau_beta.x, d_sq), + SignedFixedPoint.div_xp_u(tauBeta.x, dSq), ) q["a"] = q["a"] + q["b"] term_xp2 = ( SignedFixedPoint.div_xp_u( - SignedFixedPoint.mul_xp_u(tau_beta.y, tau_beta.y), sq_vars.x + SignedFixedPoint.mul_xp_u(tauBeta.y, tauBeta.y), sq_vars.x ) + 7 ) @@ -539,7 +539,7 @@ def calc_xp_xp_div_lambda_lambda( SignedFixedPoint.mul_down_mag_u( SignedFixedPoint.mul_down_mag_u(-r.y, x), 2 * s ), - SignedFixedPoint.div_xp_u(tau_beta.y, d_sq), + SignedFixedPoint.div_xp_u(tauBeta.y, dSq), ) q["b"] = q["b"] + q["c"] + SignedFixedPoint.mul_up_mag_u(x, x) @@ -569,7 +569,7 @@ def calc_y_given_x( x=cls.virtual_offset0(params, d, r), y=cls.virtual_offset1(params, d, r) ) return cls.solve_quadratic_swap( - params.lambda_, x, params.s, params.c, r, ab, d.tau_beta, d.d_sq + params.lambda_, x, params.s, params.c, r, ab, d.tauBeta, d.dSq ) @classmethod @@ -586,6 +586,6 @@ def calc_x_given_y( params.s, r, ba, - Vector2(x=-d.tau_alpha.x, y=d.tau_alpha.y), - d.d_sq, + Vector2(x=-d.tauAlpha.x, y=d.tauAlpha.y), + d.dSq, ) From 4fc26a8450874e0d8adaeb6222c9c423da8217e8 Mon Sep 17 00:00:00 2001 From: johngrantuk Date: Wed, 26 Feb 2025 13:34:22 +0000 Subject: [PATCH 13/16] test(TS): GyroECLP add tests. --- testData/config.json | 33 +++++++++++++++++++ .../testData/11155111-7748718-GyroECLP.json | 26 +++++++++++++++ 2 files changed, 59 insertions(+) diff --git a/testData/config.json b/testData/config.json index 6118454..6313586 100644 --- a/testData/config.json +++ b/testData/config.json @@ -256,6 +256,39 @@ "tokenIn": "0xb19382073c7A0aDdbb56Ac6AF1808Fa49e377B75", "tokenOut": "0xB77EB1A70A96fDAAeB31DB1b42F2b8b5846b2613" } + ], + "adds": [ + { + "kind": "Unbalanced", + "inputAmountsRaw": [ + "1000000000000000000", + "1000000000000000000" + ], + "tokens": [ + "0xB77EB1A70A96fDAAeB31DB1b42F2b8b5846b2613", + "0xb19382073c7A0aDdbb56Ac6AF1808Fa49e377B75" + ], + "decimals": [ + 18, + 18 + ] + }, + { + "kind": "SingleToken", + "bptOutRaw": [ + "1000000000000000" + ], + "tokenIn": "0xB77EB1A70A96fDAAeB31DB1b42F2b8b5846b2613", + "decimals": 18 + }, + { + "kind": "SingleToken", + "bptOutRaw": [ + "1000000000000000" + ], + "tokenIn": "0xb19382073c7A0aDdbb56Ac6AF1808Fa49e377B75", + "decimals": 18 + } ] } ] diff --git a/testData/testData/11155111-7748718-GyroECLP.json b/testData/testData/11155111-7748718-GyroECLP.json index 33ea269..86f4f0b 100644 --- a/testData/testData/11155111-7748718-GyroECLP.json +++ b/testData/testData/11155111-7748718-GyroECLP.json @@ -29,6 +29,32 @@ "outputRaw": "10102532135967" } ], + "adds": [ + { + "kind": "Unbalanced", + "inputAmountsRaw": [ + "1000000000000000000", + "1000000000000000000" + ], + "bptOutRaw": "535740808545469" + }, + { + "kind": "SingleToken", + "inputAmountsRaw": [ + "0", + "3751931476957855248" + ], + "bptOutRaw": "1000000000000000" + }, + { + "kind": "SingleToken", + "inputAmountsRaw": [ + "3752577002386175353", + "0" + ], + "bptOutRaw": "1000000000000000" + } + ], "pool": { "chainId": "11155111", "blockNumber": "7748718", From d6fcaa8afb2e1e638700c57cf3b5699fa8f3e6aa Mon Sep 17 00:00:00 2001 From: johngrantuk Date: Wed, 26 Feb 2025 13:38:08 +0000 Subject: [PATCH 14/16] test(TS): GyroECLP remove tests. --- testData/config.json | 31 ++++++++++++++ .../testData/11155111-7748718-GyroECLP.json | 42 +++++++++++++++++++ 2 files changed, 73 insertions(+) diff --git a/testData/config.json b/testData/config.json index 6313586..12bd68a 100644 --- a/testData/config.json +++ b/testData/config.json @@ -289,6 +289,37 @@ "tokenIn": "0xb19382073c7A0aDdbb56Ac6AF1808Fa49e377B75", "decimals": 18 } + ], + "removes": [ + { + "kind": "Proportional", + "bpt": "0x80fd5bc9d4fA6C22132f8bb2d9d30B01c3336FB3", + "bptInRaw": "1000000000" + }, + { + "kind": "SingleTokenExactIn", + "bpt": "0x80fd5bc9d4fA6C22132f8bb2d9d30B01c3336FB3", + "bptInRaw": "1000000000000", + "token": "0xB77EB1A70A96fDAAeB31DB1b42F2b8b5846b2613" + }, + { + "kind": "SingleTokenExactIn", + "bpt": "0x80fd5bc9d4fA6C22132f8bb2d9d30B01c3336FB3", + "bptInRaw": "10000000000000", + "token": "0xb19382073c7A0aDdbb56Ac6AF1808Fa49e377B75" + }, + { + "kind": "SingleTokenExactOut", + "token": "0xB77EB1A70A96fDAAeB31DB1b42F2b8b5846b2613", + "amountOutRaw": "100000000000000000", + "decimals": 18 + }, + { + "kind": "SingleTokenExactOut", + "token": "0xb19382073c7A0aDdbb56Ac6AF1808Fa49e377B75", + "amountOutRaw": "100000000000000000", + "decimals": 18 + } ] } ] diff --git a/testData/testData/11155111-7748718-GyroECLP.json b/testData/testData/11155111-7748718-GyroECLP.json index 86f4f0b..1b06b4b 100644 --- a/testData/testData/11155111-7748718-GyroECLP.json +++ b/testData/testData/11155111-7748718-GyroECLP.json @@ -55,6 +55,48 @@ "bptOutRaw": "1000000000000000" } ], + "removes": [ + { + "kind": "Proportional", + "amountsOutRaw": [ + "1866574253910", + "1866574253910" + ], + "bptInRaw": "1000000000" + }, + { + "kind": "SingleTokenExactIn", + "amountsOutRaw": [ + "0", + "3714203719112322" + ], + "bptInRaw": "1000000000000" + }, + { + "kind": "SingleTokenExactIn", + "amountsOutRaw": [ + "37147546215432546", + "0" + ], + "bptInRaw": "10000000000000" + }, + { + "kind": "SingleTokenExactOut", + "amountsOutRaw": [ + "0", + "100000000000000000" + ], + "bptInRaw": "26924481195102" + }, + { + "kind": "SingleTokenExactOut", + "amountsOutRaw": [ + "100000000000000000", + "0" + ], + "bptInRaw": "26920442359386" + } + ], "pool": { "chainId": "11155111", "blockNumber": "7748718", From f1f42ad68ff3e9b472b526fd0206811300531560 Mon Sep 17 00:00:00 2001 From: johngrantuk Date: Thu, 27 Feb 2025 10:56:32 +0000 Subject: [PATCH 15/16] fix(Python): Fix maths for ECLP. --- python/src/pools/gyro/gyroECLP.py | 11 +- python/src/pools/gyro/gyroECLP_math.py | 272 ++++++++++++++++++-- python/src/pools/gyro/signed_fixed_point.py | 28 +- 3 files changed, 273 insertions(+), 38 deletions(-) diff --git a/python/src/pools/gyro/gyroECLP.py b/python/src/pools/gyro/gyroECLP.py index 9aec784..cb33d5b 100644 --- a/python/src/pools/gyro/gyroECLP.py +++ b/python/src/pools/gyro/gyroECLP.py @@ -20,7 +20,6 @@ class PoolParams: class GyroECLP: def __init__(self, pool_state): - print(pool_state) self.pool_params = PoolParams( eclp_params=EclpParams( alpha=pool_state.get("paramsAlpha"), @@ -30,8 +29,12 @@ def __init__(self, pool_state): lambda_=pool_state.get("paramsLambda"), ), derived_eclp_params=DerivedEclpParams( - tauAlpha=Vector2(x=pool_state.get("tauAlphaX"), y=pool_state.get("tauAlphaY")), - tauBeta=Vector2(x=pool_state.get("tauBetaY"), y=pool_state.get("tauBetaY")), + tauAlpha=Vector2( + x=pool_state.get("tauAlphaX"), y=pool_state.get("tauAlphaY") + ), + tauBeta=Vector2( + x=pool_state.get("tauBetaX"), y=pool_state.get("tauBetaY") + ), u=pool_state.get("u"), v=pool_state.get("v"), w=pool_state.get("w"), @@ -69,7 +72,7 @@ def on_swap(self, swap_params): invariant, ) - return GyroECLPMath.calc_out_given_in( + return GyroECLPMath.calc_in_given_out( swap_params["balances_live_scaled18"], swap_params["amount_given_scaled18"], token_in_is_token_0, diff --git a/python/src/pools/gyro/gyroECLP_math.py b/python/src/pools/gyro/gyroECLP_math.py index e6fc44f..cd2a26d 100644 --- a/python/src/pools/gyro/gyroECLP_math.py +++ b/python/src/pools/gyro/gyroECLP_math.py @@ -204,20 +204,25 @@ def calc_at_a_chi(cls, x: int, y: int, p: EclpParams, d: DerivedEclpParams) -> i term_xp, ) - term_np1 = SignedFixedPoint.mul_down_mag_u(x, p.lambda_) - term_np2 = SignedFixedPoint.mul_down_mag_u(y, p.lambda_) + ## (x lambda s + y lambda c) * u, note u > 0 + term_np = SignedFixedPoint.mul_down_mag_u( + SignedFixedPoint.mul_down_mag_u(x, p.lambda_), p.s + ) + SignedFixedPoint.mul_down_mag_u( + SignedFixedPoint.mul_down_mag_u(y, p.lambda_), p.c + ) - val += SignedFixedPoint.mul_down_xp_to_np_u( - SignedFixedPoint.mul_down_mag_u(term_np1, p.s) - + SignedFixedPoint.mul_down_mag_u(term_np2, p.c), - SignedFixedPoint.div_xp_u(d.u, dSq2), + val = val + SignedFixedPoint.mul_down_xp_to_np_u( + term_np, SignedFixedPoint.div_xp_u(d.u, dSq2) ) - val += SignedFixedPoint.mul_down_xp_to_np_u( - SignedFixedPoint.mul_down_mag_u(x, p.s) - + SignedFixedPoint.mul_down_mag_u(y, p.c), - SignedFixedPoint.div_xp_u(d.v, dSq2), + # (sx+cy) * v, note v > 0 + term_np = SignedFixedPoint.mul_down_mag_u( + x, p.s + ) + SignedFixedPoint.mul_down_mag_u(y, p.c) + val = val + SignedFixedPoint.mul_down_xp_to_np_u( + term_np, SignedFixedPoint.div_xp_u(d.v, dSq2) ) + return val @classmethod @@ -266,39 +271,247 @@ def calculate_invariant_with_error( raise MaxBalancesExceededError() at_a_chi = cls.calc_at_a_chi(x, y, params, derived) - achiachi = cls.calc_a_chi_a_chi_in_xp(params, derived) + invariant_result = cls.calc_invariant_sqrt(x, y, params, derived) + sqrt = invariant_result[0] + err = invariant_result[1] + + # Note: the minimum non-zero value of sqrt is 1e-9 since the minimum argument is 1e-18 + if sqrt > 0: + # err + 1 to account for O(eps_np) term ignored before + err = SignedFixedPoint.div_up_mag_u(err + 1, 2 * sqrt) + else: + # In the false case here, the extra precision error does not magnify, and so the error inside the sqrt is + # O(1e-18) + # somedayTODO: The true case will almost surely never happen (can it be removed) + err = sqrt(err, 5) if err > 0 else int(1e9) - # Calculate error (simplified) + # Calculate the error in the numerator, scale the error by 20 to be sure all possible terms accounted for err = ( - SignedFixedPoint.mul_up_mag_u(params.lambda_, x + y) // cls._ONE_XP + 1 + SignedFixedPoint.mul_up_mag_u(params.lambda_, x + y) // cls._ONE_XP + + err + + 1 ) * 20 - mul_denominator = SignedFixedPoint.div_xp_u(cls._ONE_XP, achiachi - cls._ONE_XP) + achiachi = cls.calc_a_chi_a_chi_in_xp(params, derived) + # A chi \cdot A chi > 1, so round it up to round denominator up. + # Denominator uses extra precision, so we do * 1/denominator so we are sure the calculation doesn't overflow. + mul_denominator = SignedFixedPoint.div_xp_u( + cls._ONE_XP, + achiachi - cls._ONE_XP, + ) + # As an alternative, could do, but could overflow: + # invariant = (AtAChi.add(sqrt) - err).divXp(denominator) invariant = SignedFixedPoint.mul_down_xp_to_np_u( - at_a_chi - err, mul_denominator + at_a_chi + sqrt - err, + mul_denominator, ) - # Error calculation (simplified) - scaled_err = SignedFixedPoint.mul_up_xp_to_np_u(err, mul_denominator) - total_err = ( - scaled_err + # Error scales if denominator is small. + # NB: This error calculation computes the error in the expression "numerator / denominator", + # but in this code, we actually use the formula "numerator * (1 / denominator)" to compute the invariant. + # This affects this line and the one below. + err = SignedFixedPoint.mul_up_xp_to_np_u(err, mul_denominator) + + # Account for relative error due to error in the denominator. + # Error in denominator is O(epsilon) if lambda<1e11, scale up by 10 to be sure we catch it, and add O(eps). + # Error in denominator is lambda^2 * 2e-37 and scales relative to the result / denominator. + # Scale by a constant to account for errors in the scaling factor itself and limited compounding. + # Calculating lambda^2 without decimals so that the calculation will never overflow, the lost precision isn't important. + err = ( + err + ( - invariant - * ( - (params.lambda_ * params.lambda_) - // int("10000000000000000000000000000000000000") - ) + SignedFixedPoint.mul_up_xp_to_np_u(invariant, mul_denominator) + * ((params.lambda_ * params.lambda_) // int(1e36)) * 40 ) // cls._ONE_XP + 1 ) - if invariant + total_err > cls.MAX_INVARIANT: + if invariant + err > cls.MAX_INVARIANT: raise MaxInvariantExceededError() - return invariant, total_err + return invariant, err + + @classmethod + def calc_invariant_sqrt( + cls, + x: int, + y: int, + p: EclpParams, + d: DerivedEclpParams, + ) -> tuple[int, int]: + val = ( + cls.calc_min_atx_a_chiy_sq_plus_atx_sq(x, y, p, d) + + cls.calc_2_atx_aty_a_chix_a_chiy(x, y, p, d) + + cls.calc_min_aty_a_chix_sq_plus_aty_sq(x, y, p, d) + ) + + err = ( + SignedFixedPoint.mul_up_mag_u(x, x) + SignedFixedPoint.mul_up_mag_u(y, y) + ) // int(1e38) + + val = sqrt(val, 5) if val > 0 else 0 + + return val, err + + @classmethod + def calc_min_atx_a_chiy_sq_plus_atx_sq( + cls, + x: int, + y: int, + p: EclpParams, + d: DerivedEclpParams, + ) -> int: + term_np = SignedFixedPoint.mul_up_mag_u( + SignedFixedPoint.mul_up_mag_u(SignedFixedPoint.mul_up_mag_u(x, x), p.c), + p.c, + ) + SignedFixedPoint.mul_up_mag_u( + SignedFixedPoint.mul_up_mag_u(SignedFixedPoint.mul_up_mag_u(y, y), p.s), + p.s, + ) + + term_np -= SignedFixedPoint.mul_down_mag_u( + SignedFixedPoint.mul_down_mag_u( + SignedFixedPoint.mul_down_mag_u(x, y), p.c * 2 + ), + p.s, + ) + + term_xp = ( + SignedFixedPoint.mul_xp_u(d.u, d.u) + + SignedFixedPoint.div_down_mag_u( + SignedFixedPoint.mul_xp_u(d.u * 2, d.v), p.lambda_ + ) + + SignedFixedPoint.div_down_mag_u( + SignedFixedPoint.div_down_mag_u( + SignedFixedPoint.mul_xp_u(d.v, d.v), p.lambda_ + ), + p.lambda_, + ) + ) + + term_xp = SignedFixedPoint.div_xp_u( + term_xp, + SignedFixedPoint.mul_xp_u( + SignedFixedPoint.mul_xp_u( + SignedFixedPoint.mul_xp_u(d.dSq, d.dSq), d.dSq + ), + d.dSq, + ), + ) + + val = SignedFixedPoint.mul_down_xp_to_np_u(-term_np, term_xp) + + val += SignedFixedPoint.mul_down_xp_to_np_u( + SignedFixedPoint.div_down_mag_u( + SignedFixedPoint.div_down_mag_u(term_np - 9, p.lambda_), p.lambda_ + ), + SignedFixedPoint.div_xp_u(SignedFixedPoint.ONE_XP, d.dSq), + ) + + return val + + @classmethod + def calc_2_atx_aty_a_chix_a_chiy( + cls, + x: int, + y: int, + p: EclpParams, + d: DerivedEclpParams, + ) -> int: + term_np = SignedFixedPoint.mul_down_mag_u( + SignedFixedPoint.mul_down_mag_u( + SignedFixedPoint.mul_down_mag_u(x, x) + - SignedFixedPoint.mul_up_mag_u(y, y), + 2 * p.c, + ), + p.s, + ) + + xy = SignedFixedPoint.mul_down_mag_u(y, 2 * x) + + term_np += SignedFixedPoint.mul_down_mag_u( + SignedFixedPoint.mul_down_mag_u(xy, p.c), p.c + ) - SignedFixedPoint.mul_down_mag_u( + SignedFixedPoint.mul_down_mag_u(xy, p.s), p.s + ) + + term_xp = SignedFixedPoint.mul_xp_u(d.z, d.u) + SignedFixedPoint.div_down_mag_u( + SignedFixedPoint.div_down_mag_u( + SignedFixedPoint.mul_xp_u(d.w, d.v), p.lambda_ + ), + p.lambda_, + ) + + term_xp += SignedFixedPoint.div_down_mag_u( + SignedFixedPoint.mul_xp_u(d.w, d.u) + SignedFixedPoint.mul_xp_u(d.z, d.v), + p.lambda_, + ) + + term_xp = SignedFixedPoint.div_xp_u( + term_xp, + SignedFixedPoint.mul_xp_u( + SignedFixedPoint.mul_xp_u( + SignedFixedPoint.mul_xp_u(d.dSq, d.dSq), d.dSq + ), + d.dSq, + ), + ) + + return SignedFixedPoint.mul_down_xp_to_np_u(term_np, term_xp) + + @classmethod + def calc_min_aty_a_chix_sq_plus_aty_sq( + cls, + x: int, + y: int, + p: EclpParams, + d: DerivedEclpParams, + ) -> int: + term_np = SignedFixedPoint.mul_up_mag_u( + SignedFixedPoint.mul_up_mag_u(SignedFixedPoint.mul_up_mag_u(x, x), p.s), + p.s, + ) + SignedFixedPoint.mul_up_mag_u( + SignedFixedPoint.mul_up_mag_u(SignedFixedPoint.mul_up_mag_u(y, y), p.c), + p.c, + ) + + term_np += SignedFixedPoint.mul_up_mag_u( + SignedFixedPoint.mul_up_mag_u(SignedFixedPoint.mul_up_mag_u(x, y), p.s * 2), + p.c, + ) + + term_xp = SignedFixedPoint.mul_xp_u(d.z, d.z) + SignedFixedPoint.div_down_mag_u( + SignedFixedPoint.div_down_mag_u( + SignedFixedPoint.mul_xp_u(d.w, d.w), p.lambda_ + ), + p.lambda_, + ) + + term_xp += SignedFixedPoint.div_down_mag_u( + SignedFixedPoint.mul_xp_u(2 * d.z, d.w), p.lambda_ + ) + + term_xp = SignedFixedPoint.div_xp_u( + term_xp, + SignedFixedPoint.mul_xp_u( + SignedFixedPoint.mul_xp_u( + SignedFixedPoint.mul_xp_u(d.dSq, d.dSq), d.dSq + ), + d.dSq, + ), + ) + + val = SignedFixedPoint.mul_down_xp_to_np_u(-term_np, term_xp) + + val += SignedFixedPoint.mul_down_xp_to_np_u( + term_np - 9, + SignedFixedPoint.div_xp_u(SignedFixedPoint.ONE_XP, d.dSq), + ) + + return val @classmethod def calc_spot_price0in1( @@ -556,6 +769,13 @@ def calc_xp_xp_div_lambda_lambda( else SignedFixedPoint.div_down_mag_u(q["a"], lambda_) ) + term_xp2 = ( + SignedFixedPoint.div_xp_u( + SignedFixedPoint.mul_xp_u(tauBeta.x, tauBeta.x), sq_vars.x + ) + + 7 + ) + val = SignedFixedPoint.mul_up_mag_u( SignedFixedPoint.mul_up_mag_u(sq_vars.y, c), c ) diff --git a/python/src/pools/gyro/signed_fixed_point.py b/python/src/pools/gyro/signed_fixed_point.py index 2499830..2ce7771 100644 --- a/python/src/pools/gyro/signed_fixed_point.py +++ b/python/src/pools/gyro/signed_fixed_point.py @@ -34,7 +34,9 @@ def mul_down_mag(cls, a: int, b: int) -> int: @classmethod def mul_down_mag_u(cls, a: int, b: int) -> int: - return (a * b) // cls.ONE + product = a * b + result = abs(product) // cls.ONE + return -result if product < 0 else result @classmethod def mul_up_mag(cls, a: int, b: int) -> int: @@ -74,7 +76,12 @@ def div_down_mag(cls, a: int, b: int) -> int: def div_down_mag_u(cls, a: int, b: int) -> int: if b == 0: raise FixedPointError("ZeroDivision") - return (a * cls.ONE) // b + + # Implement truncating division (division toward zero) + product = a * cls.ONE + abs_result = abs(product) // abs(b) + # Apply the correct sign + return -abs_result if (product < 0) != (b < 0) else abs_result @classmethod def div_up_mag(cls, a: int, b: int) -> int: @@ -193,16 +200,21 @@ def mul_up_xp_to_np(cls, a: int, b: int) -> int: @classmethod def mul_up_xp_to_np_u(cls, a: int, b: int) -> int: - e_19 = int("10000000000000000000") + e_19 = 10**19 b1 = b // e_19 b2 = b % e_19 prod1 = a * b1 prod2 = a * b2 - return ( - (prod1 + prod2 // e_19) // e_19 - if prod1 <= 0 and prod2 <= 0 - else (prod1 + prod2 // e_19 - 1) // e_19 + 1 - ) + + # For division, implement truncation toward zero (like Solidity) + def trunc_div(x, y): + result = abs(x) // abs(y) + return -result if (x < 0) != (y < 0) else result + + if prod1 <= 0 and prod2 <= 0: + return trunc_div(prod1 + trunc_div(prod2, e_19), e_19) + else: + return trunc_div(prod1 + trunc_div(prod2, e_19) - 1, e_19) + 1 @classmethod def complement(cls, x: int) -> int: From 53e7c0688623409879c820e68c10e3c39f8a9294 Mon Sep 17 00:00:00 2001 From: johngrantuk Date: Thu, 27 Feb 2025 10:58:30 +0000 Subject: [PATCH 16/16] chore: Bump TS Package version. --- typescript/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/typescript/package.json b/typescript/package.json index 4bbf5cf..4837d13 100644 --- a/typescript/package.json +++ b/typescript/package.json @@ -9,7 +9,7 @@ "publishConfig": { "access": "public" }, - "version": "0.0.22", + "version": "0.0.23", "main": "dist/index.js", "module": "dist/index.mjs", "types": "dist/index.d.ts",