Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

test: StableMath rounding #526

Merged
merged 14 commits into from
Jun 5, 2024
Merged
198 changes: 0 additions & 198 deletions pkg/solidity-utils/contracts/math/StableMath.sol
Original file line number Diff line number Diff line change
Expand Up @@ -172,204 +172,6 @@ library StableMath {
return finalBalanceIn - balances[tokenIndexIn] + 1;
}

function computeBptOutGivenExactTokensIn(
uint256 amp,
uint256[] memory balances,
uint256[] memory amountsIn,
uint256 bptTotalSupply,
uint256 currentInvariant,
uint256 swapFeePercentage
) internal pure returns (uint256) {
// BPT out, so we round down overall.

// First loop calculates the sum of all token balances, which will be used to calculate
// the current weights of each token, relative to this sum.
uint256 sumBalances = 0;
uint256 numTokens = balances.length;
for (uint256 i = 0; i < numTokens; ++i) {
sumBalances += balances[i];
}

// Calculate the weighted balance ratio without considering fees.
uint256[] memory balanceRatiosWithFee = new uint256[](numTokens);
// The weighted sum of token balance ratios with fee.
uint256 invariantRatioWithFees = 0;
for (uint256 i = 0; i < numTokens; ++i) {
// Round down to ultimately reduce the ratios with fees,
// which will be used later when calculating the `nonTaxableAmount` for each token.
uint256 currentWeight = balances[i].divDown(sumBalances);
balanceRatiosWithFee[i] = (balances[i] + amountsIn[i]).divDown(balances[i]);
invariantRatioWithFees += balanceRatiosWithFee[i].mulDown(currentWeight);
}

// Second loop calculates new amounts in, taking into account the fee on the percentage excess.
uint256[] memory newBalances = new uint256[](numTokens);
for (uint256 i = 0; i < numTokens; ++i) {
uint256 amountInWithoutFee;

// Charge fees only when the balance ratio is greater than the ideal (proportional) ratio.
if (balanceRatiosWithFee[i] > invariantRatioWithFees) {
// Round down to ultimately lower the `amountInWithoutFee`, consequently reducing the `newBalances`.
uint256 nonTaxableAmount = balances[i].mulDown(invariantRatioWithFees - FixedPoint.ONE);
uint256 taxableAmount = amountsIn[i] - nonTaxableAmount;
amountInWithoutFee = nonTaxableAmount + (taxableAmount.mulDown(FixedPoint.ONE - swapFeePercentage));
} else {
amountInWithoutFee = amountsIn[i];
}

newBalances[i] = balances[i] + amountInWithoutFee;
}

// Round down the `invariantRatio` to reduce the BPT out.
uint256 newInvariant = computeInvariant(amp, newBalances);
uint256 invariantRatio = newInvariant.divDown(currentInvariant);

// If the invariant didn't increase for any reason, we simply don't mint BPT.
if (invariantRatio > FixedPoint.ONE) {
// Round down to reduce the amount of BPT out.
return bptTotalSupply.mulDown(invariantRatio - FixedPoint.ONE);
} else {
return 0;
}
}

function computeTokenInGivenExactBptOut(
uint256 amp,
uint256[] memory balances,
uint256 tokenIndex,
uint256 bptAmountOut,
uint256 bptTotalSupply,
uint256 currentInvariant,
uint256 swapFeePercentage
) internal pure returns (uint256) {
// Token in, so we round up overall.

// Round up to ultimately increase the `amountInWithoutFee`.
uint256 newInvariant = (bptTotalSupply + bptAmountOut).divUp(bptTotalSupply).mulUp(currentInvariant);

// Calculate amount in without fee.
uint256 newBalanceTokenIndex = computeBalance(amp, balances, newInvariant, tokenIndex);
uint256 amountInWithoutFee = newBalanceTokenIndex - balances[tokenIndex];

// First calculate the sum of all token balances, which will be used to calculate
// the current weight of each token.
uint256 sumBalances = 0;
for (uint256 i = 0; i < balances.length; ++i) {
sumBalances += balances[i];
}

// We can now compute how much extra balance is being deposited and used in virtual swaps, and charge swap fees
// accordingly. Regarding rounding, a conflict of interests arises – the less the `taxableAmount`,
// the larger the `nonTaxableAmount`; we prioritize maximizing the latter.
uint256 currentWeight = balances[tokenIndex].divUp(sumBalances);
uint256 taxablePercentage = currentWeight.complement();
uint256 taxableAmount = amountInWithoutFee.mulDown(taxablePercentage);
uint256 nonTaxableAmount = amountInWithoutFee - taxableAmount;

// Round up to increase the amount of token in.
return nonTaxableAmount + (taxableAmount.divUp(FixedPoint.ONE - swapFeePercentage));
}

/**
* @dev Flow of calculations:
* amountsTokenOut -> amountsOutProportional ->
* amountOutPercentageExcess -> amountOutBeforeFee -> newInvariant -> amountBPTIn
*/
function computeBptInGivenExactTokensOut(
uint256 amp,
uint256[] memory balances,
uint256[] memory amountsOut,
uint256 bptTotalSupply,
uint256 currentInvariant,
uint256 swapFeePercentage
) internal pure returns (uint256) {
// BPT in, so we round up overall.

// First loop calculates the sum of all token balances, which will be used to calculate
// the current weights of each token relative to this sum.
uint256 sumBalances = 0;
uint256 numTokens = balances.length;
for (uint256 i = 0; i < numTokens; ++i) {
sumBalances += balances[i];
}

// Calculate the weighted balance ratio without considering fees.
uint256[] memory balanceRatiosWithoutFee = new uint256[](numTokens);
uint256 invariantRatioWithoutFees = 0;
for (uint256 i = 0; i < numTokens; ++i) {
// Round down to ultimately reduce the ratios without fees,
// which will be used later when calculating the `nonTaxableAmount` for each token.
uint256 currentWeight = balances[i].divDown(sumBalances);
balanceRatiosWithoutFee[i] = (balances[i] - amountsOut[i]).divDown(balances[i]);
invariantRatioWithoutFees += balanceRatiosWithoutFee[i].mulDown(currentWeight);
}

// Second loop calculates new amounts in, taking into account the fee on the percentage excess.
uint256[] memory newBalances = new uint256[](numTokens);
for (uint256 i = 0; i < numTokens; ++i) {
// Swap fees are typically charged on 'token in', but there is no 'token in' here, so we apply it to
// 'token out'. This results in slightly larger price impact.

uint256 amountOutWithFee;
if (invariantRatioWithoutFees > balanceRatiosWithoutFee[i]) {
// Round up to ultimately enlarge the `amountOutWithFee`, consequently reducing the `newBalances`.
uint256 nonTaxableAmount = balances[i].mulUp(invariantRatioWithoutFees.complement());
uint256 taxableAmount = amountsOut[i] - nonTaxableAmount;
amountOutWithFee = nonTaxableAmount + (taxableAmount.divUp(FixedPoint.ONE - swapFeePercentage));
} else {
amountOutWithFee = amountsOut[i];
}

newBalances[i] = balances[i] - amountOutWithFee;
}

// Round down the `invariantRatio` so that multiplying by its complement increases the BPT in.
uint256 newInvariant = computeInvariant(amp, newBalances);
uint256 invariantRatio = newInvariant.divDown(currentInvariant);

// Round up to increase the amount of BPT in.
return bptTotalSupply.mulUp(invariantRatio.complement());
}

function computeTokenOutGivenExactBptIn(
uint256 amp,
uint256[] memory balances,
uint256 tokenIndex,
uint256 bptAmountIn,
uint256 bptTotalSupply,
uint256 currentInvariant,
uint256 swapFeePercentage
) internal pure returns (uint256) {
// Token out, so we round down overall.

// Round up to ultimately decrease the `amountOutWithoutFee`.
uint256 newInvariant = (bptTotalSupply - bptAmountIn).divUp(bptTotalSupply).mulUp(currentInvariant);

// Calculate amount out without fee.
uint256 newBalanceTokenIndex = computeBalance(amp, balances, newInvariant, tokenIndex);
uint256 amountOutWithoutFee = balances[tokenIndex] - newBalanceTokenIndex;

// First calculate the sum of all token balances, which will be used to calculate
// the current weight of each token.
uint256 sumBalances = 0;
for (uint256 i = 0; i < balances.length; ++i) {
sumBalances += balances[i];
}

// We can now compute how much excess balance is being withdrawn as a result of the virtual swaps, which result
// in swap fees. Swap fees are typically charged on 'token in', but there is no 'token in' here, so we apply it
// to 'token out'. This results in slightly larger price impact. Regarding rounding, a conflict of interests
// arises – the greater the `taxableAmount`, the smaller the `nonTaxableAmount`; we prioritize minimizing
// the latter.
uint256 currentWeight = balances[tokenIndex].divDown(sumBalances);
uint256 taxablePercentage = currentWeight.complement();
uint256 taxableAmount = amountOutWithoutFee.mulUp(taxablePercentage);
uint256 nonTaxableAmount = amountOutWithoutFee - taxableAmount;

// Round down to reduce the amount of token out.
return nonTaxableAmount + (taxableAmount.mulDown(FixedPoint.ONE - swapFeePercentage));
}

// This function calculates the balance of a given token (tokenIndex)
// given all the other balances and the invariant.
function computeBalance(
Expand Down
32 changes: 18 additions & 14 deletions pkg/solidity-utils/contracts/test/FixedPointMock.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,34 +2,38 @@

pragma solidity ^0.8.24;

import "../math/FixedPoint.sol";
import { FixedPoint } from "../math/FixedPoint.sol";

contract FixedPointMock {
function powDown(uint256 x, uint256 y) public pure returns (uint256) {
return FixedPoint.powDown(x, y);
}

function powUp(uint256 x, uint256 y) public pure returns (uint256) {
return FixedPoint.powUp(x, y);
}

function mulDown(uint256 a, uint256 b) public pure returns (uint256) {
function mulDown(uint256 a, uint256 b) external pure returns (uint256) {
return FixedPoint.mulDown(a, b);
}

function mulUp(uint256 a, uint256 b) public pure returns (uint256) {
function mulUp(uint256 a, uint256 b) external pure returns (uint256) {
return FixedPoint.mulUp(a, b);
}

function divDown(uint256 a, uint256 b) public pure returns (uint256) {
function divDown(uint256 a, uint256 b) external pure returns (uint256) {
return FixedPoint.divDown(a, b);
}

function divUp(uint256 a, uint256 b) public pure returns (uint256) {
function divUp(uint256 a, uint256 b) external pure returns (uint256) {
return FixedPoint.divUp(a, b);
}

function complement(uint256 x) public pure returns (uint256) {
function divUpRaw(uint256 a, uint256 b) external pure returns (uint256) {
return FixedPoint.divUpRaw(a, b);
}

function powDown(uint256 x, uint256 y) external pure returns (uint256) {
return FixedPoint.powDown(x, y);
}

function powUp(uint256 x, uint256 y) external pure returns (uint256) {
return FixedPoint.powUp(x, y);
}

function complement(uint256 x) external pure returns (uint256) {
return FixedPoint.complement(x);
}
}
43 changes: 43 additions & 0 deletions pkg/solidity-utils/contracts/test/RoundingMock.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// SPDX-License-Identifier: GPL-3.0-or-later

pragma solidity ^0.8.24;

import { FixedPoint } from "../math/FixedPoint.sol";

library RoundingMock {
function mockMul(uint256 a, uint256 b, bool roundUp) internal pure returns (uint256) {
if (roundUp) {
return FixedPoint.mulUp(a, b);
} else {
return FixedPoint.mulDown(a, b);
}
}

function mockDiv(uint256 a, uint256 b, bool roundUp) internal pure returns (uint256) {
if (roundUp) {
return FixedPoint.divUp(a, b);
} else {
return FixedPoint.divDown(a, b);
}
}

function mockDivRaw(uint256 a, uint256 b, bool roundUp) internal pure returns (uint256) {
if (roundUp) {
return FixedPoint.divUpRaw(a, b);
} else {
return divDownRaw(a, b);
}
}

function mockPow(uint256 x, uint256 y, bool roundUp) internal pure returns (uint256) {
if (roundUp) {
return FixedPoint.powUp(x, y);
} else {
return FixedPoint.powDown(x, y);
}
}

function divDownRaw(uint256 a, uint256 b) private pure returns (uint256) {
return a / b;
}
}
Loading
Loading