From e03b44455126963a54ade972bf016ade7c8739ac Mon Sep 17 00:00:00 2001 From: johngrantuk Date: Thu, 5 Dec 2024 20:50:30 +0000 Subject: [PATCH 1/3] refactor: Move scaling into utils. --- typescript/src/vault/utils.ts | 54 ++++++++++++++++++++++ typescript/src/vault/vault.ts | 86 +++++++---------------------------- 2 files changed, 70 insertions(+), 70 deletions(-) diff --git a/typescript/src/vault/utils.ts b/typescript/src/vault/utils.ts index ced29dc..a2374f5 100644 --- a/typescript/src/vault/utils.ts +++ b/typescript/src/vault/utils.ts @@ -1,3 +1,57 @@ +import { MathSol } from '../utils/math'; + export function isSameAddress(addressOne: string, addressTwo: string) { return addressOne.toLowerCase() === addressTwo.toLowerCase(); } + +/** + * @dev Reverses the `scalingFactor` and `tokenRate` applied to `amount`, resulting in a smaller or equal value + * depending on whether it needed scaling/rate adjustment or not. The result is rounded down. + */ +export function toRawUndoRateRoundDown( + amount: bigint, + scalingFactor: bigint, + tokenRate: bigint, +): bigint { + // Do division last. Scaling factor is not a FP18, but a FP18 normalized by FP(1). + // `scalingFactor * tokenRate` is a precise FP18, so there is no rounding direction here. + return MathSol.divDownFixed(amount, scalingFactor * tokenRate); +} + +/** + * @dev Reverses the `scalingFactor` and `tokenRate` applied to `amount`, resulting in a smaller or equal value + * depending on whether it needed scaling/rate adjustment or not. The result is rounded up. + */ +export function toRawUndoRateRoundUp( + amount: bigint, + scalingFactor: bigint, + tokenRate: bigint, +): bigint { + // Do division last. Scaling factor is not a FP18, but a FP18 normalized by FP(1). + // `scalingFactor * tokenRate` is a precise FP18, so there is no rounding direction here. + return MathSol.divUpFixed(amount, scalingFactor * tokenRate); +} + +/** + * @dev Applies `scalingFactor` and `tokenRate` to `amount`, resulting in a larger or equal value depending on + * whether it needed scaling/rate adjustment or not. The result is rounded down. + */ +export function toScaled18ApplyRateRoundDown( + amount: bigint, + scalingFactor: bigint, + tokenRate: bigint, +): bigint { + return MathSol.mulDownFixed(amount * scalingFactor, tokenRate); +} + +/** + * @dev Applies `scalingFactor` and `tokenRate` to `amount`, resulting in a larger or equal value depending on + * whether it needed scaling/rate adjustment or not. The result is rounded up. + */ +export function toScaled18ApplyRateRoundUp( + amount: bigint, + scalingFactor: bigint, + tokenRate: bigint, +): bigint { + return MathSol.mulUpFixed(amount * scalingFactor, tokenRate); +} diff --git a/typescript/src/vault/vault.ts b/typescript/src/vault/vault.ts index 1e99237..87d75c4 100644 --- a/typescript/src/vault/vault.ts +++ b/typescript/src/vault/vault.ts @@ -9,7 +9,13 @@ import { import { Weighted } from '../weighted'; import { Stable } from '../stable'; import { BufferState, erc4626BufferWrapOrUnwrap } from '../buffer'; -import { isSameAddress } from './utils'; +import { + isSameAddress, + toRawUndoRateRoundDown, + toRawUndoRateRoundUp, + toScaled18ApplyRateRoundDown, + toScaled18ApplyRateRoundUp, +} from './utils'; import { AddKind, AddLiquidityInput, @@ -221,7 +227,7 @@ export class Vault { let amountCalculatedRaw = 0n; if (swapInput.swapKind === SwapKind.GivenIn) { // For `ExactIn` the amount calculated is leaving the Vault, so we round down. - amountCalculatedRaw = this._toRawUndoRateRoundDown( + amountCalculatedRaw = toRawUndoRateRoundDown( amountCalculatedScaled18, poolState.scalingFactors[outputIndex], // If the swap is ExactIn, the amountCalculated is the amount of tokenOut. So, we want to use the rate @@ -243,7 +249,7 @@ export class Vault { amountCalculatedScaled18 += totalSwapFeeAmountScaled18; // For `ExactOut` the amount calculated is entering the Vault, so we round up. - amountCalculatedRaw = this._toRawUndoRateRoundUp( + amountCalculatedRaw = toRawUndoRateRoundUp( amountCalculatedScaled18, poolState.scalingFactors[inputIndex], poolState.tokenRates[inputIndex], @@ -415,7 +421,7 @@ export class Vault { const amountsInRaw: bigint[] = new Array(poolState.tokens.length); for (let i = 0; i < poolState.tokens.length; i++) { // amountsInRaw are amounts actually entering the Pool, so we round up. - amountsInRaw[i] = this._toRawUndoRateRoundUp( + amountsInRaw[i] = toRawUndoRateRoundUp( amountsInScaled18[i], poolState.scalingFactors[i], poolState.tokenRates[i], @@ -593,7 +599,7 @@ export class Vault { for (let i = 0; i < poolState.tokens.length; ++i) { // amountsOut are amounts exiting the Pool, so we round down. - amountsOutRaw[i] = this._toRawUndoRateRoundDown( + amountsOutRaw[i] = toRawUndoRateRoundDown( amountsOutScaled18[i], poolState.scalingFactors[i], poolState.tokenRates[i], @@ -660,7 +666,7 @@ export class Vault { // The total swap fee does not go into the pool; amountIn does, and the raw fee at this point does not // modify it. Given that all of the fee may belong to the pool creator (i.e. outside pool balances), // we round down to protect the invariant. - const totalSwapFeeAmountRaw = this._toRawUndoRateRoundDown( + const totalSwapFeeAmountRaw = toRawUndoRateRoundDown( swapFeeAmountScaled18, decimalScalingFactors[index], tokenRates[index], @@ -705,11 +711,7 @@ export class Vault { tokenRates: bigint[], ): bigint[] { return amounts.map((a, i) => - this._toScaled18ApplyRateRoundDown( - a, - scalingFactors[i], - tokenRates[i], - ), + toScaled18ApplyRateRoundDown(a, scalingFactors[i], tokenRates[i]), ); } @@ -722,11 +724,7 @@ export class Vault { tokenRates: bigint[], ): bigint[] { return amounts.map((a, i) => - this._toScaled18ApplyRateRoundUp( - a, - scalingFactors[i], - tokenRates[i], - ), + toScaled18ApplyRateRoundUp(a, scalingFactors[i], tokenRates[i]), ); } @@ -742,12 +740,12 @@ export class Vault { // to a lower calculated amountOut, favoring the pool. const amountGivenScaled18 = swapKind === SwapKind.GivenIn - ? this._toScaled18ApplyRateRoundDown( + ? toScaled18ApplyRateRoundDown( amountGivenRaw, scalingFactors[indexIn], tokenRates[indexIn], ) - : this._toScaled18ApplyRateRoundUp( + : toScaled18ApplyRateRoundUp( amountGivenRaw, scalingFactors[indexOut], this._computeRateRoundUp(tokenRates[indexOut]), @@ -755,58 +753,6 @@ export class Vault { return amountGivenScaled18; } - /** - * @dev Reverses the `scalingFactor` and `tokenRate` applied to `amount`, resulting in a smaller or equal value - * depending on whether it needed scaling/rate adjustment or not. The result is rounded down. - */ - private _toRawUndoRateRoundDown( - amount: bigint, - scalingFactor: bigint, - tokenRate: bigint, - ): bigint { - // Do division last. Scaling factor is not a FP18, but a FP18 normalized by FP(1). - // `scalingFactor * tokenRate` is a precise FP18, so there is no rounding direction here. - return MathSol.divDownFixed(amount, scalingFactor * tokenRate); - } - - /** - * @dev Reverses the `scalingFactor` and `tokenRate` applied to `amount`, resulting in a smaller or equal value - * depending on whether it needed scaling/rate adjustment or not. The result is rounded up. - */ - private _toRawUndoRateRoundUp( - amount: bigint, - scalingFactor: bigint, - tokenRate: bigint, - ): bigint { - // Do division last. Scaling factor is not a FP18, but a FP18 normalized by FP(1). - // `scalingFactor * tokenRate` is a precise FP18, so there is no rounding direction here. - return MathSol.divUpFixed(amount, scalingFactor * tokenRate); - } - - /** - * @dev Applies `scalingFactor` and `tokenRate` to `amount`, resulting in a larger or equal value depending on - * whether it needed scaling/rate adjustment or not. The result is rounded down. - */ - private _toScaled18ApplyRateRoundDown( - amount: bigint, - scalingFactor: bigint, - tokenRate: bigint, - ): bigint { - return MathSol.mulDownFixed(amount * scalingFactor, tokenRate); - } - - /** - * @dev Applies `scalingFactor` and `tokenRate` to `amount`, resulting in a larger or equal value depending on - * whether it needed scaling/rate adjustment or not. The result is rounded up. - */ - private _toScaled18ApplyRateRoundUp( - amount: bigint, - scalingFactor: bigint, - tokenRate: bigint, - ): bigint { - return MathSol.mulUpFixed(amount * scalingFactor, tokenRate); - } - /** * @notice Rounds up a rate informed by a rate provider. * @dev Rates calculated by an external rate provider have rounding errors. Intuitively, a rate provider From 27612179d9e365ef91c76eaefcb95268b085ea79 Mon Sep 17 00:00:00 2001 From: johngrantuk Date: Thu, 5 Dec 2024 20:58:01 +0000 Subject: [PATCH 2/3] fix: Stable pool max swap amount, exactOut. --- typescript/src/stable/stablePool.ts | 40 ++++++++++++++----- typescript/src/weighted/weightedPool.ts | 51 ++++++++++++++++--------- typescript/test/stablePool.test.ts | 44 ++++++++++++++++++--- typescript/test/weightedPool.test.ts | 18 ++++++++- 4 files changed, 119 insertions(+), 34 deletions(-) diff --git a/typescript/src/stable/stablePool.ts b/typescript/src/stable/stablePool.ts index 7d77028..86134ba 100644 --- a/typescript/src/stable/stablePool.ts +++ b/typescript/src/stable/stablePool.ts @@ -1,5 +1,6 @@ import { MAX_UINT256, MAX_BALANCE } from '../constants'; import { MathSol } from '../utils/math'; +import { toRawUndoRateRoundDown } from '../vault/utils'; import { MaxSingleTokenRemoveParams, MaxSwapParams, @@ -24,18 +25,39 @@ export class Stable implements PoolBase { } /** - * Returns the max amount that can be swapped (in relation to the amount specified by user). + * Returns the max amount that can be swapped in relation to the swapKind. * @param maxSwapParams - * @returns Returned amount/scaling is respective to the tokenOut because that’s what we’re taking out of the pool and what limits the swap size. + * @returns GivenIn: Returns the max amount in. GivenOut: Returns the max amount out. */ getMaxSwapAmount(maxSwapParams: MaxSwapParams): bigint { - const { balancesLiveScaled18, indexIn, tokenRates, scalingFactors } = - maxSwapParams; - - const diff = MAX_BALANCE - balancesLiveScaled18[indexIn]; - return MathSol.divDownFixed( - diff, - scalingFactors[indexIn] * tokenRates[indexIn], + 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], ); } diff --git a/typescript/src/weighted/weightedPool.ts b/typescript/src/weighted/weightedPool.ts index 3a93643..c285526 100644 --- a/typescript/src/weighted/weightedPool.ts +++ b/typescript/src/weighted/weightedPool.ts @@ -1,5 +1,6 @@ import { MAX_UINT256 } from '../constants'; import { MathSol } from '../utils/math'; +import { toRawUndoRateRoundDown } from '../vault/utils'; import { MaxSingleTokenRemoveParams, MaxSwapParams, @@ -26,27 +27,41 @@ export class Weighted implements PoolBase { } /** - * Returns the max amount that can be swapped (in relation to the amount specified by user). + * Returns the max amount that can be swapped in relation to the swapKind. * @param maxSwapParams - * @returns Returned amount/scaling is respective to the tokenOut because that’s what we’re taking out of the pool and what limits the swap size. + * @returns GivenIn: Returns the max amount in. GivenOut: Returns the max amount out. */ - getMaxSwapAmount(swapParams: MaxSwapParams): bigint { - if (swapParams.swapKind === SwapKind.GivenIn) - return MathSol.divDownFixed( - MathSol.mulDownFixed( - swapParams.balancesLiveScaled18[swapParams.indexIn], - _MAX_IN_RATIO, - ), - swapParams.scalingFactors[swapParams.indexIn] * - swapParams.tokenRates[swapParams.indexIn], + getMaxSwapAmount(maxSwapParams: MaxSwapParams): bigint { + const { + balancesLiveScaled18, + indexIn, + indexOut, + tokenRates, + scalingFactors, + swapKind, + } = maxSwapParams; + if (swapKind === SwapKind.GivenIn) { + const max18 = MathSol.mulDownFixed( + balancesLiveScaled18[indexIn], + _MAX_IN_RATIO, ); - return MathSol.divDownFixed( - MathSol.mulDownFixed( - swapParams.balancesLiveScaled18[swapParams.indexOut], - _MAX_OUT_RATIO, - ), - swapParams.scalingFactors[swapParams.indexOut] * - swapParams.tokenRates[swapParams.indexOut], + // Scale to token in (and remove rate) + return toRawUndoRateRoundDown( + max18, + scalingFactors[indexIn], + tokenRates[indexIn], + ); + } + + const max18 = MathSol.mulDownFixed( + balancesLiveScaled18[indexOut], + _MAX_OUT_RATIO, + ); + // Scale to token out + return toRawUndoRateRoundDown( + max18, + scalingFactors[indexOut], + tokenRates[indexOut], ); } diff --git a/typescript/test/stablePool.test.ts b/typescript/test/stablePool.test.ts index 48445bc..f686c57 100644 --- a/typescript/test/stablePool.test.ts +++ b/typescript/test/stablePool.test.ts @@ -9,7 +9,25 @@ describe('stable pool', () => { }); describe('getMaxSwapAmount', () => { describe('no rate', () => { - test('exact in', () => { + test('exact in, 18 decimals', () => { + const swapParams = { + swapKind: SwapKind.GivenIn, + amountGivenScaled18: 0n, + balancesLiveScaled18: [ + 60000000000000000000n, + 40000000000000000000n, + ], + tokenRates: [1000000000000000000n, 1000000000000000000n], + scalingFactors: [1n, 1000000000000n], + indexIn: 0, + indexOut: 1, + }; + const maxSwapAmount = pool.getMaxSwapAmount(swapParams); + expect(maxSwapAmount).to.eq( + 340282366920938463403374607431768211456n, + ); + }); + test('exact in, 6 decimals', () => { const swapParams = { swapKind: SwapKind.GivenIn, amountGivenScaled18: 0n, @@ -25,7 +43,7 @@ describe('stable pool', () => { const maxSwapAmount = pool.getMaxSwapAmount(swapParams); expect(maxSwapAmount).to.eq(340282366920938463403374607n); }); - test('exact out', () => { + test('exact out, 18decimals', () => { const swapParams = { swapKind: SwapKind.GivenOut, amountGivenScaled18: 0n, @@ -39,7 +57,23 @@ describe('stable pool', () => { indexOut: 1, }; const maxSwapAmount = pool.getMaxSwapAmount(swapParams); - expect(maxSwapAmount).to.eq(340282366920938463403374607n); + expect(maxSwapAmount).to.eq(39600000000000000000n); + }); + test('exact out, 6decimals', () => { + const swapParams = { + swapKind: SwapKind.GivenOut, + amountGivenScaled18: 0n, + balancesLiveScaled18: [ + 60000000000000000000n, + 40000000000000000000n, + ], + tokenRates: [1000000000000000000n, 1000000000000000000n], + scalingFactors: [1n, 1000000000000n], + indexIn: 0, + indexOut: 1, + }; + const maxSwapAmount = pool.getMaxSwapAmount(swapParams); + expect(maxSwapAmount).to.eq(39600000n); }); }); describe('with rate', () => { @@ -75,9 +109,7 @@ describe('stable pool', () => { indexOut: 1, }; const maxSwapAmount = pool.getMaxSwapAmount(swapParams); - expect(maxSwapAmount).to.eq( - 170141183460469231701687303715884105728n, - ); + expect(maxSwapAmount).to.eq(9900000n); }); }); }); diff --git a/typescript/test/weightedPool.test.ts b/typescript/test/weightedPool.test.ts index d10d527..fa13896 100644 --- a/typescript/test/weightedPool.test.ts +++ b/typescript/test/weightedPool.test.ts @@ -8,7 +8,7 @@ describe('weighted pool', () => { weights: [60000000000000000000n, 40000000000000000000n], }); describe('getMaxSwapAmount', () => { - test('exact in', () => { + test('exact in, 18 decimals', () => { const swapParams = { swapKind: SwapKind.GivenIn, amountGivenScaled18: 0n, @@ -24,6 +24,22 @@ describe('weighted pool', () => { const maxSwapAmount = pool.getMaxSwapAmount(swapParams); expect(maxSwapAmount).to.eq(18000000000000000000n); }); + test('exact in, 6 decimals', () => { + const swapParams = { + swapKind: SwapKind.GivenIn, + amountGivenScaled18: 0n, + balancesLiveScaled18: [ + 60000000000000000000n, + 40000000000000000000n, + ], + tokenRates: [1000000000000000000n, 1000000000000000000n], + scalingFactors: [1000000000000n, 1n], + indexIn: 0, + indexOut: 1, + }; + const maxSwapAmount = pool.getMaxSwapAmount(swapParams); + expect(maxSwapAmount).to.eq(18000000n); + }); test('exact out', () => { const swapParams = { swapKind: SwapKind.GivenOut, From 34f6b9283ab01ad053794e18a3b8fc6a4c424eaf Mon Sep 17 00:00:00 2001 From: johngrantuk Date: Thu, 5 Dec 2024 20:58:20 +0000 Subject: [PATCH 3/3] chore: Package. --- typescript/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/typescript/package.json b/typescript/package.json index 891c825..94a5e8e 100644 --- a/typescript/package.json +++ b/typescript/package.json @@ -9,7 +9,7 @@ "publishConfig": { "access": "public" }, - "version": "0.0.18", + "version": "0.0.19", "main": "dist/index.js", "module": "dist/index.mjs", "types": "dist/index.d.ts",