Skip to content

Commit

Permalink
Merge pull request #78 from balancer/fix-limits-exactOut
Browse files Browse the repository at this point in the history
Fix limits exact out
  • Loading branch information
johngrantuk authored Dec 5, 2024
2 parents 698cb5c + 34f6b92 commit 65ae653
Show file tree
Hide file tree
Showing 7 changed files with 190 additions and 105 deletions.
2 changes: 1 addition & 1 deletion typescript/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
40 changes: 31 additions & 9 deletions typescript/src/stable/stablePool.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { MAX_UINT256, MAX_BALANCE } from '../constants';
import { MathSol } from '../utils/math';
import { toRawUndoRateRoundDown } from '../vault/utils';
import {
MaxSingleTokenRemoveParams,
MaxSwapParams,
Expand All @@ -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],
);
}

Expand Down
54 changes: 54 additions & 0 deletions typescript/src/vault/utils.ts
Original file line number Diff line number Diff line change
@@ -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);
}
86 changes: 16 additions & 70 deletions typescript/src/vault/vault.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand All @@ -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],
Expand Down Expand Up @@ -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],
Expand Down Expand Up @@ -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],
Expand Down Expand Up @@ -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],
Expand Down Expand Up @@ -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]),
);
}

Expand All @@ -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]),
);
}

Expand All @@ -742,71 +740,19 @@ 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]),
);
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
Expand Down
51 changes: 33 additions & 18 deletions typescript/src/weighted/weightedPool.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { MAX_UINT256 } from '../constants';
import { MathSol } from '../utils/math';
import { toRawUndoRateRoundDown } from '../vault/utils';
import {
MaxSingleTokenRemoveParams,
MaxSwapParams,
Expand All @@ -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],
);
}

Expand Down
Loading

0 comments on commit 65ae653

Please sign in to comment.