diff --git a/pkg/solidity-utils/contracts/math/StableMath.sol b/pkg/solidity-utils/contracts/math/StableMath.sol index 8164fee6a..bcd0c61eb 100644 --- a/pkg/solidity-utils/contracts/math/StableMath.sol +++ b/pkg/solidity-utils/contracts/math/StableMath.sol @@ -7,6 +7,8 @@ import "./FixedPoint.sol"; // Some variables have non mixed case names (e.g. P_D) that relate to the mathematical derivations. // solhint-disable private-vars-leading-underscore, var-name-mixedcase +// Any new logic changes to this library need to be propagated to the `StableMathMock` for tests to pass. + library StableMath { using FixedPoint for uint256; diff --git a/pkg/solidity-utils/contracts/test/RoundingMock.sol b/pkg/solidity-utils/contracts/test/RoundingMock.sol index 983227f08..56566ee98 100644 --- a/pkg/solidity-utils/contracts/test/RoundingMock.sol +++ b/pkg/solidity-utils/contracts/test/RoundingMock.sol @@ -5,77 +5,38 @@ pragma solidity ^0.8.24; import { FixedPoint } from "../math/FixedPoint.sol"; abstract contract RoundingMock { - // `Disabled` basically preserves the existing direction, while the other two either change or preserve the direction - // according to the operation. I.e. `RoundDown` changes the direction for operations that round up, and preserve the - // direction for operations that round down, and vice versa. - enum MockRounding { - Disabled, - RoundDown, - RoundUp - } - - MockRounding public mockRounding; - - function setMockRounding(MockRounding _mockRounding) external { - mockRounding = _mockRounding; - } - - function mulDown(uint256 a, uint256 b) internal view returns (uint256) { - if (mockRounding == MockRounding.RoundUp) { + 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 mulUp(uint256 a, uint256 b) internal view returns (uint256) { - if (mockRounding == MockRounding.RoundDown) { - return FixedPoint.mulDown(a, b); - } else { - return FixedPoint.mulUp(a, b); - } - } - - function divDown(uint256 a, uint256 b) internal view returns (uint256) { - if (mockRounding == MockRounding.RoundUp) { + 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 divUp(uint256 a, uint256 b) internal view returns (uint256) { - if (mockRounding == MockRounding.RoundDown) { - 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 FixedPoint.divUp(a, b); - } - } - - function divUpRaw(uint256 a, uint256 b) internal view returns (uint256) { - if (mockRounding == MockRounding.RoundDown) { return divDownRaw(a, b); - } else { - return FixedPoint.divUpRaw(a, b); } } - function powDown(uint256 x, uint256 y) internal view returns (uint256) { - if (mockRounding == MockRounding.RoundUp) { + 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 powUp(uint256 x, uint256 y) internal view returns (uint256) { - if (mockRounding == MockRounding.RoundDown) { - return FixedPoint.powDown(x, y); - } else { - return FixedPoint.powUp(x, y); - } - } - function divDownRaw(uint256 a, uint256 b) private pure returns (uint256) { return a / b; } diff --git a/pkg/solidity-utils/contracts/test/StableMathMock.sol b/pkg/solidity-utils/contracts/test/StableMathMock.sol index 22ca36f5c..535a6f141 100644 --- a/pkg/solidity-utils/contracts/test/StableMathMock.sol +++ b/pkg/solidity-utils/contracts/test/StableMathMock.sol @@ -8,7 +8,7 @@ import { FixedPoint } from "../math/FixedPoint.sol"; import { RoundingMock } from "./RoundingMock.sol"; // The `StableMathMock` contract mocks the `StableMath` library for testing purposes. Its mock functions are meant to be -// logically equivalent to the base ones, but with the ability to control the rounding direction using the `RoundingMock`. +// logically equivalent to the base ones, but with the ability to control the rounding permutation using the `RoundingMock`. contract StableMathMock is RoundingMock { using FixedPoint for uint256; @@ -150,7 +150,301 @@ contract StableMathMock is RoundingMock { function mockComputeInvariant( uint256 amplificationParameter, uint256[] memory balances - ) public pure returns (uint256) { + ) external pure returns (uint256) { + return _mockComputeInvariant(amplificationParameter, balances); + } + + function mockComputeOutGivenExactIn( + uint256 amplificationParameter, + uint256[] memory balances, + uint256 tokenIndexIn, + uint256 tokenIndexOut, + uint256 tokenAmountIn, + uint256 invariant + ) external pure returns (uint256) { + bool[3] memory roundingPermutationBase = [true, true, true]; + + return + _mockComputeOutGivenExactIn( + amplificationParameter, + balances, + tokenIndexIn, + tokenIndexOut, + tokenAmountIn, + invariant, + roundingPermutationBase + ); + } + + function mockComputeOutGivenExactIn( + uint256 amplificationParameter, + uint256[] memory balances, + uint256 tokenIndexIn, + uint256 tokenIndexOut, + uint256 tokenAmountIn, + uint256 invariant, + bool[3] memory roundingPermutation + ) external pure returns (uint256) { + return + _mockComputeOutGivenExactIn( + amplificationParameter, + balances, + tokenIndexIn, + tokenIndexOut, + tokenAmountIn, + invariant, + roundingPermutation + ); + } + + function mockComputeInGivenExactOut( + uint256 amplificationParameter, + uint256[] memory balances, + uint256 tokenIndexIn, + uint256 tokenIndexOut, + uint256 tokenAmountOut, + uint256 invariant + ) external pure returns (uint256) { + bool[3] memory roundingPermutationBase = [true, true, true]; + + return + _mockComputeInGivenExactOut( + amplificationParameter, + balances, + tokenIndexIn, + tokenIndexOut, + tokenAmountOut, + invariant, + roundingPermutationBase + ); + } + + function mockComputeInGivenExactOut( + uint256 amplificationParameter, + uint256[] memory balances, + uint256 tokenIndexIn, + uint256 tokenIndexOut, + uint256 tokenAmountOut, + uint256 invariant, + bool[3] memory roundingPermutation + ) external pure returns (uint256) { + return + _mockComputeInGivenExactOut( + amplificationParameter, + balances, + tokenIndexIn, + tokenIndexOut, + tokenAmountOut, + invariant, + roundingPermutation + ); + } + + function mockComputeBptOutGivenExactTokensIn( + uint256 amp, + uint256[] memory balances, + uint256[] memory amountsIn, + uint256 bptTotalSupply, + uint256 currentInvariant, + uint256 swapFeePercentage + ) external pure returns (uint256) { + bool[7] memory roundingPermutationBase = [false, false, false, false, false, false, false]; + + return + _mockComputeBptOutGivenExactTokensIn( + amp, + balances, + amountsIn, + bptTotalSupply, + currentInvariant, + swapFeePercentage, + roundingPermutationBase + ); + } + + function mockComputeBptOutGivenExactTokensIn( + uint256 amp, + uint256[] memory balances, + uint256[] memory amountsIn, + uint256 bptTotalSupply, + uint256 currentInvariant, + uint256 swapFeePercentage, + bool[7] memory roundingPermutation + ) external pure returns (uint256) { + return + _mockComputeBptOutGivenExactTokensIn( + amp, + balances, + amountsIn, + bptTotalSupply, + currentInvariant, + swapFeePercentage, + roundingPermutation + ); + } + + function mockComputeTokenInGivenExactBptOut( + uint256 amp, + uint256[] memory balances, + uint256 tokenIndex, + uint256 bptAmountOut, + uint256 bptTotalSupply, + uint256 currentInvariant, + uint256 swapFeePercentage + ) external pure returns (uint256) { + bool[8] memory roundingPermutationBase = [true, true, true, true, true, true, false, true]; + + return + _mockComputeTokenInGivenExactBptOut( + amp, + balances, + tokenIndex, + bptAmountOut, + bptTotalSupply, + currentInvariant, + swapFeePercentage, + roundingPermutationBase + ); + } + + function mockComputeTokenInGivenExactBptOut( + uint256 amp, + uint256[] memory balances, + uint256 tokenIndex, + uint256 bptAmountOut, + uint256 bptTotalSupply, + uint256 currentInvariant, + uint256 swapFeePercentage, + bool[8] memory roundingPermutation + ) external pure returns (uint256) { + return + _mockComputeTokenInGivenExactBptOut( + amp, + balances, + tokenIndex, + bptAmountOut, + bptTotalSupply, + currentInvariant, + swapFeePercentage, + roundingPermutation + ); + } + + function mockComputeBptInGivenExactTokensOut( + uint256 amp, + uint256[] memory balances, + uint256[] memory amountsOut, + uint256 bptTotalSupply, + uint256 currentInvariant, + uint256 swapFeePercentage + ) external pure returns (uint256) { + bool[7] memory roundingPermutationBase = [false, false, false, true, true, false, true]; + + return + _mockComputeBptInGivenExactTokensOut( + amp, + balances, + amountsOut, + bptTotalSupply, + currentInvariant, + swapFeePercentage, + roundingPermutationBase + ); + } + + function mockComputeBptInGivenExactTokensOut( + uint256 amp, + uint256[] memory balances, + uint256[] memory amountsOut, + uint256 bptTotalSupply, + uint256 currentInvariant, + uint256 swapFeePercentage, + bool[7] memory roundingPermutation + ) external pure returns (uint256) { + return + _mockComputeBptInGivenExactTokensOut( + amp, + balances, + amountsOut, + bptTotalSupply, + currentInvariant, + swapFeePercentage, + roundingPermutation + ); + } + + function mockComputeTokenOutGivenExactBptIn( + uint256 amp, + uint256[] memory balances, + uint256 tokenIndex, + uint256 bptAmountIn, + uint256 bptTotalSupply, + uint256 currentInvariant, + uint256 swapFeePercentage + ) external pure returns (uint256) { + bool[8] memory roundingPermutationBase = [true, true, true, true, true, false, true, false]; + + return + _mockComputeTokenOutGivenExactBptIn( + amp, + balances, + tokenIndex, + bptAmountIn, + bptTotalSupply, + currentInvariant, + swapFeePercentage, + roundingPermutationBase + ); + } + + function mockComputeTokenOutGivenExactBptIn( + uint256 amp, + uint256[] memory balances, + uint256 tokenIndex, + uint256 bptAmountIn, + uint256 bptTotalSupply, + uint256 currentInvariant, + uint256 swapFeePercentage, + bool[8] memory roundingPermutation + ) external pure returns (uint256) { + return + _mockComputeTokenOutGivenExactBptIn( + amp, + balances, + tokenIndex, + bptAmountIn, + bptTotalSupply, + currentInvariant, + swapFeePercentage, + roundingPermutation + ); + } + + function mockComputeBalance( + uint256 amplificationParameter, + uint256[] memory balances, + uint256 invariant, + uint256 tokenIndex + ) external pure returns (uint256) { + bool[3] memory roundingPermutationBase = [true, true, true]; + + return _mockComputeBalance(amplificationParameter, balances, invariant, tokenIndex, roundingPermutationBase); + } + + function mockComputeBalance( + uint256 amplificationParameter, + uint256[] memory balances, + uint256 invariant, + uint256 tokenIndex, + bool[3] memory roundingPermutation + ) external pure returns (uint256) { + return _mockComputeBalance(amplificationParameter, balances, invariant, tokenIndex, roundingPermutation); + } + + function _mockComputeInvariant( + uint256 amplificationParameter, + uint256[] memory balances + ) internal pure returns (uint256) { uint256 sum = 0; uint256 numTokens = balances.length; for (uint256 i = 0; i < numTokens; ++i) { @@ -189,48 +483,63 @@ contract StableMathMock is RoundingMock { revert StableMath.StableInvariantDidntConverge(); } - function mockComputeOutGivenExactIn( + function _mockComputeOutGivenExactIn( uint256 amplificationParameter, uint256[] memory balances, uint256 tokenIndexIn, uint256 tokenIndexOut, uint256 tokenAmountIn, - uint256 invariant - ) external view returns (uint256) { + uint256 invariant, + bool[3] memory roundingPermutation + ) internal pure returns (uint256) { balances[tokenIndexIn] += tokenAmountIn; - uint256 finalBalanceOut = mockComputeBalance(amplificationParameter, balances, invariant, tokenIndexOut); + uint256 finalBalanceOut = _mockComputeBalance( + amplificationParameter, + balances, + invariant, + tokenIndexOut, + roundingPermutation + ); balances[tokenIndexIn] -= tokenAmountIn; return balances[tokenIndexOut] - finalBalanceOut - 1; } - function mockComputeInGivenExactOut( + function _mockComputeInGivenExactOut( uint256 amplificationParameter, uint256[] memory balances, uint256 tokenIndexIn, uint256 tokenIndexOut, uint256 tokenAmountOut, - uint256 invariant - ) external view returns (uint256) { + uint256 invariant, + bool[3] memory roundingPermutation + ) internal pure returns (uint256) { balances[tokenIndexOut] -= tokenAmountOut; - uint256 finalBalanceIn = mockComputeBalance(amplificationParameter, balances, invariant, tokenIndexIn); + uint256 finalBalanceIn = _mockComputeBalance( + amplificationParameter, + balances, + invariant, + tokenIndexIn, + roundingPermutation + ); balances[tokenIndexOut] += tokenAmountOut; return finalBalanceIn - balances[tokenIndexIn] + 1; } - function mockComputeBptOutGivenExactTokensIn( + function _mockComputeBptOutGivenExactTokensIn( uint256 amp, uint256[] memory balances, uint256[] memory amountsIn, uint256 bptTotalSupply, uint256 currentInvariant, - uint256 swapFeePercentage - ) external view returns (uint256) { + uint256 swapFeePercentage, + bool[7] memory roundingPermutation + ) internal pure returns (uint256) { uint256 sumBalances = 0; uint256 numTokens = balances.length; for (uint256 i = 0; i < numTokens; ++i) { @@ -240,9 +549,9 @@ contract StableMathMock is RoundingMock { uint256[] memory balanceRatiosWithFee = new uint256[](numTokens); uint256 invariantRatioWithFees = 0; for (uint256 i = 0; i < numTokens; ++i) { - uint256 currentWeight = divDown(balances[i], sumBalances); - balanceRatiosWithFee[i] = divDown(balances[i] + amountsIn[i], balances[i]); - invariantRatioWithFees += mulDown(balanceRatiosWithFee[i], currentWeight); + uint256 currentWeight = mockDiv(balances[i], sumBalances, roundingPermutation[0]); + balanceRatiosWithFee[i] = mockDiv(balances[i] + amountsIn[i], balances[i], roundingPermutation[1]); + invariantRatioWithFees += mockMul(balanceRatiosWithFee[i], currentWeight, roundingPermutation[2]); } uint256[] memory newBalances = new uint256[](numTokens); @@ -250,9 +559,15 @@ contract StableMathMock is RoundingMock { uint256 amountInWithoutFee; if (balanceRatiosWithFee[i] > invariantRatioWithFees) { - uint256 nonTaxableAmount = mulDown(balances[i], invariantRatioWithFees - FixedPoint.ONE); + uint256 nonTaxableAmount = mockMul( + balances[i], + invariantRatioWithFees - FixedPoint.ONE, + roundingPermutation[3] + ); uint256 taxableAmount = amountsIn[i] - nonTaxableAmount; - amountInWithoutFee = nonTaxableAmount + mulDown(taxableAmount, FixedPoint.ONE - swapFeePercentage); + amountInWithoutFee = + nonTaxableAmount + + mockMul(taxableAmount, FixedPoint.ONE - swapFeePercentage, roundingPermutation[4]); } else { amountInWithoutFee = amountsIn[i]; } @@ -260,28 +575,44 @@ contract StableMathMock is RoundingMock { newBalances[i] = balances[i] + amountInWithoutFee; } - uint256 newInvariant = mockComputeInvariant(amp, newBalances); - uint256 invariantRatio = divDown(newInvariant, currentInvariant); + uint256 newInvariant = _mockComputeInvariant(amp, newBalances); + uint256 invariantRatio = mockDiv(newInvariant, currentInvariant, roundingPermutation[5]); if (invariantRatio > FixedPoint.ONE) { - return mulDown(bptTotalSupply, invariantRatio - FixedPoint.ONE); + return mockMul(bptTotalSupply, invariantRatio - FixedPoint.ONE, roundingPermutation[6]); } else { return 0; } } - function mockComputeTokenInGivenExactBptOut( + function _mockComputeTokenInGivenExactBptOut( uint256 amp, uint256[] memory balances, uint256 tokenIndex, uint256 bptAmountOut, uint256 bptTotalSupply, uint256 currentInvariant, - uint256 swapFeePercentage - ) external view returns (uint256) { - uint256 newInvariant = mulUp(divUp(bptTotalSupply + bptAmountOut, bptTotalSupply), currentInvariant); - - uint256 newBalanceTokenIndex = mockComputeBalance(amp, balances, newInvariant, tokenIndex); + uint256 swapFeePercentage, + bool[8] memory roundingPermutation + ) internal pure returns (uint256) { + uint256 newInvariant = mockMul( + mockDiv(bptTotalSupply + bptAmountOut, bptTotalSupply, roundingPermutation[0]), + currentInvariant, + roundingPermutation[1] + ); + + bool[3] memory subRoundingPermutation = [ + roundingPermutation[2], + roundingPermutation[3], + roundingPermutation[4] + ]; + uint256 newBalanceTokenIndex = _mockComputeBalance( + amp, + balances, + newInvariant, + tokenIndex, + subRoundingPermutation + ); uint256 amountInWithoutFee = newBalanceTokenIndex - balances[tokenIndex]; uint256 sumBalances = 0; @@ -289,22 +620,23 @@ contract StableMathMock is RoundingMock { sumBalances += balances[i]; } - uint256 currentWeight = divUp(balances[tokenIndex], sumBalances); + uint256 currentWeight = mockDiv(balances[tokenIndex], sumBalances, roundingPermutation[5]); uint256 taxablePercentage = currentWeight.complement(); - uint256 taxableAmount = mulDown(amountInWithoutFee, taxablePercentage); + uint256 taxableAmount = mockMul(amountInWithoutFee, taxablePercentage, roundingPermutation[6]); uint256 nonTaxableAmount = amountInWithoutFee - taxableAmount; - return nonTaxableAmount + divUp(taxableAmount, FixedPoint.ONE - swapFeePercentage); + return nonTaxableAmount + mockDiv(taxableAmount, FixedPoint.ONE - swapFeePercentage, roundingPermutation[7]); } - function mockComputeBptInGivenExactTokensOut( + function _mockComputeBptInGivenExactTokensOut( uint256 amp, uint256[] memory balances, uint256[] memory amountsOut, uint256 bptTotalSupply, uint256 currentInvariant, - uint256 swapFeePercentage - ) external view returns (uint256) { + uint256 swapFeePercentage, + bool[7] memory roundingPermutation + ) internal pure returns (uint256) { uint256 sumBalances = 0; uint256 numTokens = balances.length; for (uint256 i = 0; i < numTokens; ++i) { @@ -314,18 +646,24 @@ contract StableMathMock is RoundingMock { uint256[] memory balanceRatiosWithoutFee = new uint256[](numTokens); uint256 invariantRatioWithoutFees = 0; for (uint256 i = 0; i < numTokens; ++i) { - uint256 currentWeight = divDown(balances[i], sumBalances); - balanceRatiosWithoutFee[i] = divDown(balances[i] - amountsOut[i], balances[i]); - invariantRatioWithoutFees += mulDown(balanceRatiosWithoutFee[i], currentWeight); + uint256 currentWeight = mockDiv(balances[i], sumBalances, roundingPermutation[0]); + balanceRatiosWithoutFee[i] = mockDiv(balances[i] - amountsOut[i], balances[i], roundingPermutation[1]); + invariantRatioWithoutFees += mockMul(balanceRatiosWithoutFee[i], currentWeight, roundingPermutation[2]); } uint256[] memory newBalances = new uint256[](numTokens); for (uint256 i = 0; i < numTokens; ++i) { uint256 amountOutWithFee; if (invariantRatioWithoutFees > balanceRatiosWithoutFee[i]) { - uint256 nonTaxableAmount = mulUp(balances[i], invariantRatioWithoutFees.complement()); + uint256 nonTaxableAmount = mockMul( + balances[i], + invariantRatioWithoutFees.complement(), + roundingPermutation[3] + ); uint256 taxableAmount = amountsOut[i] - nonTaxableAmount; - amountOutWithFee = nonTaxableAmount + divUp(taxableAmount, FixedPoint.ONE - swapFeePercentage); + amountOutWithFee = + nonTaxableAmount + + mockDiv(taxableAmount, FixedPoint.ONE - swapFeePercentage, roundingPermutation[4]); } else { amountOutWithFee = amountsOut[i]; } @@ -333,24 +671,40 @@ contract StableMathMock is RoundingMock { newBalances[i] = balances[i] - amountOutWithFee; } - uint256 newInvariant = mockComputeInvariant(amp, newBalances); - uint256 invariantRatio = divDown(newInvariant, currentInvariant); + uint256 newInvariant = _mockComputeInvariant(amp, newBalances); + uint256 invariantRatio = mockDiv(newInvariant, currentInvariant, roundingPermutation[5]); - return mulUp(bptTotalSupply, invariantRatio.complement()); + return mockMul(bptTotalSupply, invariantRatio.complement(), roundingPermutation[6]); } - function mockComputeTokenOutGivenExactBptIn( + function _mockComputeTokenOutGivenExactBptIn( uint256 amp, uint256[] memory balances, uint256 tokenIndex, uint256 bptAmountIn, uint256 bptTotalSupply, uint256 currentInvariant, - uint256 swapFeePercentage - ) external view returns (uint256) { - uint256 newInvariant = mulUp(divUp(bptTotalSupply - bptAmountIn, bptTotalSupply), currentInvariant); - - uint256 newBalanceTokenIndex = mockComputeBalance(amp, balances, newInvariant, tokenIndex); + uint256 swapFeePercentage, + bool[8] memory roundingPermutation + ) internal pure returns (uint256) { + uint256 newInvariant = mockMul( + mockDiv(bptTotalSupply - bptAmountIn, bptTotalSupply, roundingPermutation[0]), + currentInvariant, + roundingPermutation[1] + ); + + bool[3] memory subRoundingPermutation = [ + roundingPermutation[2], + roundingPermutation[3], + roundingPermutation[4] + ]; + uint256 newBalanceTokenIndex = _mockComputeBalance( + amp, + balances, + newInvariant, + tokenIndex, + subRoundingPermutation + ); uint256 amountOutWithoutFee = balances[tokenIndex] - newBalanceTokenIndex; uint256 sumBalances = 0; @@ -358,20 +712,21 @@ contract StableMathMock is RoundingMock { sumBalances += balances[i]; } - uint256 currentWeight = divDown(balances[tokenIndex], sumBalances); + uint256 currentWeight = mockDiv(balances[tokenIndex], sumBalances, roundingPermutation[5]); uint256 taxablePercentage = currentWeight.complement(); - uint256 taxableAmount = mulUp(amountOutWithoutFee, taxablePercentage); + uint256 taxableAmount = mockMul(amountOutWithoutFee, taxablePercentage, roundingPermutation[6]); uint256 nonTaxableAmount = amountOutWithoutFee - taxableAmount; - return nonTaxableAmount + mulDown(taxableAmount, FixedPoint.ONE - swapFeePercentage); + return nonTaxableAmount + mockMul(taxableAmount, FixedPoint.ONE - swapFeePercentage, roundingPermutation[7]); } - function mockComputeBalance( + function _mockComputeBalance( uint256 amplificationParameter, uint256[] memory balances, uint256 invariant, - uint256 tokenIndex - ) public view returns (uint256) { + uint256 tokenIndex, + bool[3] memory roundingPermutation + ) internal pure returns (uint256) { uint256 numTokens = balances.length; uint256 ampTimesTotal = amplificationParameter * numTokens; uint256 sum = balances[0]; @@ -383,15 +738,20 @@ contract StableMathMock is RoundingMock { sum = sum - balances[tokenIndex]; uint256 inv2 = invariant * invariant; - uint256 c = (divUpRaw(inv2, ampTimesTotal * P_D) * StableMath.AMP_PRECISION) * balances[tokenIndex]; + uint256 c = (mockDivRaw(inv2, ampTimesTotal * P_D, roundingPermutation[0]) * StableMath.AMP_PRECISION) * + balances[tokenIndex]; uint256 b = sum + ((invariant / ampTimesTotal) * StableMath.AMP_PRECISION); uint256 prevTokenBalance = 0; - uint256 tokenBalance = divUpRaw(inv2 + c, invariant + b); + uint256 tokenBalance = mockDivRaw(inv2 + c, invariant + b, roundingPermutation[1]); for (uint256 i = 0; i < 255; ++i) { prevTokenBalance = tokenBalance; - tokenBalance = divUpRaw((tokenBalance * tokenBalance) + c, (tokenBalance * 2) + b - invariant); + tokenBalance = mockDivRaw( + (tokenBalance * tokenBalance) + c, + (tokenBalance * 2) + b - invariant, + roundingPermutation[2] + ); if (tokenBalance > prevTokenBalance) { if (tokenBalance - prevTokenBalance <= 1) { diff --git a/pkg/solidity-utils/package.json b/pkg/solidity-utils/package.json index b72c76931..f4f70b1a2 100644 --- a/pkg/solidity-utils/package.json +++ b/pkg/solidity-utils/package.json @@ -26,8 +26,8 @@ "prettier": "npx prettier --write --plugin=prettier-plugin-solidity 'contracts/**/*.sol' 'test/**/*.sol'", "test": "yarn test:hardhat && yarn test:forge", "test:hardhat": "hardhat test", - "test:forge": "forge test --no-match-test \"FFI\" -vvv", - "test:stress": "FOUNDRY_PROFILE=intense forge test --ffi -vvv", + "test:forge": "forge test --no-match-test \"FFI\" -vvv --via-ir", + "test:stress": "FOUNDRY_PROFILE=intense forge test --ffi -vvv --via-ir", "coverage": "./coverage.sh", "coverage:hardhat": "hardhat coverage", "coverage:forge": "forge coverage", diff --git a/pkg/solidity-utils/test/foundry/StableMath.t.sol b/pkg/solidity-utils/test/foundry/StableMath.t.sol index 8ef0cf160..5554988ec 100644 --- a/pkg/solidity-utils/test/foundry/StableMath.t.sol +++ b/pkg/solidity-utils/test/foundry/StableMath.t.sol @@ -7,12 +7,11 @@ import "forge-std/Test.sol"; import { FixedPoint } from "../../contracts/math/FixedPoint.sol"; import { StableMathMock } from "../../contracts/test/StableMathMock.sol"; -import { RoundingMock } from "../../contracts/test/RoundingMock.sol"; // In the `StableMath` functions, the protocol aims for computing a value either as small as possible or as large as possible // by means of rounding in its favor; in order to achieve this, it may use arbitrary rounding directions during the calculations. -// The objective of `StableMathTest` is to verify that the implemented rounding combinations favor the protocol more than other -// solutions (e.g., always rounding down or always rounding up, which are the combinations that `RoundingMock` offers). +// The objective of `StableMathTest` is to verify that the implemented rounding permutations favor the protocol more than other +// solutions (e.g., always rounding down or always rounding up). contract StableMathTest is Test { uint256 constant NUM_TOKENS = 2; @@ -118,28 +117,18 @@ contract StableMathTest is Test { swapFeePercentage = bound(rawSwapFeePercentage, MIN_SWAP_FEE, MAX_SWAP_FEE); } - function testComputeInvariantRounding__Fuzz(uint256 rawAmp, uint256[NUM_TOKENS] calldata rawBalances) external { + function testComputeInvariantRounding__Fuzz( + uint256 rawAmp, + uint256[NUM_TOKENS] calldata rawBalances + ) external view { uint256 amp = boundAmp(rawAmp); uint256[] memory balances = boundBalances(rawBalances); uint256 invariant = stableMathMock.computeInvariant(amp, balances); - stableMathMock.setMockRounding(RoundingMock.MockRounding.Disabled); - uint256 invariantUnrounded = stableMathMock.mockComputeInvariant(amp, balances); - - stableMathMock.setMockRounding(RoundingMock.MockRounding.RoundDown); - uint256 invariantRoundedDown = stableMathMock.mockComputeInvariant(amp, balances); - - stableMathMock.setMockRounding(RoundingMock.MockRounding.RoundUp); - uint256 invariantRoundedUp = stableMathMock.mockComputeInvariant(amp, balances); + uint256 invariantUnpermuted = stableMathMock.mockComputeInvariant(amp, balances); - assertEq(invariant, invariantUnrounded, "Mock function and base one should be equivalent."); - assertLe( - invariant, - invariantRoundedDown, - "Output should be less than or equal to the rounded down mock value." - ); - assertLe(invariant, invariantRoundedUp, "Output should be less than or equal to the rounded up mock value."); + assertEq(invariant, invariantUnpermuted, "Mock function and base one should be equivalent."); } function testComputeOutGivenExactInRounding__Fuzz( @@ -148,8 +137,9 @@ contract StableMathTest is Test { uint256 rawTokenIndexIn, uint256 rawTokenIndexOut, uint256 rawTokenAmountIn, - uint256 rawInvariant - ) external { + uint256 rawInvariant, + bool[3] calldata roundingPermutation + ) external view { uint256 amp = boundAmp(rawAmp); uint256[] memory balances = boundBalances(rawBalances); (uint256 tokenIndexIn, uint256 tokenIndexOut) = boundTokenIndexes(rawTokenIndexIn, rawTokenIndexOut); @@ -165,8 +155,7 @@ contract StableMathTest is Test { invariant ); - stableMathMock.setMockRounding(RoundingMock.MockRounding.Disabled); - uint256 outGivenExactInUnrounded = stableMathMock.mockComputeOutGivenExactIn( + uint256 outGivenExactInUnpermuted = stableMathMock.mockComputeOutGivenExactIn( amp, balances, tokenIndexIn, @@ -174,37 +163,21 @@ contract StableMathTest is Test { tokenAmountIn, invariant ); - - stableMathMock.setMockRounding(RoundingMock.MockRounding.RoundDown); - uint256 outGivenExactInRoundedDown = stableMathMock.mockComputeOutGivenExactIn( + uint256 outGivenExactInPermuted = stableMathMock.mockComputeOutGivenExactIn( amp, balances, tokenIndexIn, tokenIndexOut, tokenAmountIn, - invariant - ); - - stableMathMock.setMockRounding(RoundingMock.MockRounding.RoundUp); - uint256 outGivenExactInRoundedUp = stableMathMock.mockComputeOutGivenExactIn( - amp, - balances, - tokenIndexIn, - tokenIndexOut, - tokenAmountIn, - invariant + invariant, + roundingPermutation ); - assertEq(outGivenExactIn, outGivenExactInUnrounded, "Mock function and base one should be equivalent."); + assertEq(outGivenExactIn, outGivenExactInUnpermuted, "Mock function and base one should be equivalent."); assertLe( outGivenExactIn, - outGivenExactInRoundedDown, - "Output should be less than or equal to the rounded down mock value." - ); - assertLe( - outGivenExactIn, - outGivenExactInRoundedUp, - "Output should be less than or equal to the rounded up mock value." + outGivenExactInPermuted, + "Output should be less than or equal to the permuted mock value." ); } @@ -214,8 +187,9 @@ contract StableMathTest is Test { uint256 rawTokenIndexIn, uint256 rawTokenIndexOut, uint256 rawTokenAmountOut, - uint256 rawInvariant - ) external { + uint256 rawInvariant, + bool[3] calldata roundingPermutation + ) external view { uint256 amp = boundAmp(rawAmp); uint256[] memory balances = boundBalances(rawBalances); (uint256 tokenIndexIn, uint256 tokenIndexOut) = boundTokenIndexes(rawTokenIndexIn, rawTokenIndexOut); @@ -231,18 +205,7 @@ contract StableMathTest is Test { invariant ); - stableMathMock.setMockRounding(RoundingMock.MockRounding.Disabled); - uint256 inGivenExactOutUnrounded = stableMathMock.mockComputeInGivenExactOut( - amp, - balances, - tokenIndexIn, - tokenIndexOut, - tokenAmountOut, - invariant - ); - - stableMathMock.setMockRounding(RoundingMock.MockRounding.RoundDown); - uint256 inGivenExactOutRoundedDown = stableMathMock.mockComputeInGivenExactOut( + uint256 inGivenExactOutUnpermuted = stableMathMock.mockComputeInGivenExactOut( amp, balances, tokenIndexIn, @@ -250,27 +213,21 @@ contract StableMathTest is Test { tokenAmountOut, invariant ); - - stableMathMock.setMockRounding(RoundingMock.MockRounding.RoundUp); - uint256 inGivenExactOutRoundedUp = stableMathMock.mockComputeInGivenExactOut( + uint256 inGivenExactOutPermuted = stableMathMock.mockComputeInGivenExactOut( amp, balances, tokenIndexIn, tokenIndexOut, tokenAmountOut, - invariant + invariant, + roundingPermutation ); - assertEq(inGivenExactOut, inGivenExactOutUnrounded, "Mock function and base one should be equivalent."); + assertEq(inGivenExactOut, inGivenExactOutUnpermuted, "Mock function and base one should be equivalent."); assertGe( inGivenExactOut, - inGivenExactOutRoundedDown, - "Output should be greater than or equal to the rounded down mock value." - ); - assertGe( - inGivenExactOut, - inGivenExactOutRoundedUp, - "Output should be greater than or equal to the rounded up mock value." + inGivenExactOutPermuted, + "Output should be greater than or equal to the permuted mock value." ); } @@ -280,8 +237,9 @@ contract StableMathTest is Test { uint256[NUM_TOKENS] calldata rawAmountsIn, uint256 rawBptTotalSupply, uint256 rawCurrentInvariant, - uint256 rawSwapFeePercentage - ) external { + uint256 rawSwapFeePercentage, + bool[7] calldata roundingPermutation + ) external view { uint256 amp = boundAmp(rawAmp); uint256[] memory balances = boundBalances(rawBalances); uint256[] memory amountsIn = boundAmountsIn(rawAmountsIn, balances); @@ -298,8 +256,7 @@ contract StableMathTest is Test { swapFeePercentage ); - stableMathMock.setMockRounding(RoundingMock.MockRounding.Disabled); - uint256 bptOutGivenExactTokensInUnrounded = stableMathMock.mockComputeBptOutGivenExactTokensIn( + uint256 bptOutGivenExactTokensInUnpermuted = stableMathMock.mockComputeBptOutGivenExactTokensIn( amp, balances, amountsIn, @@ -307,39 +264,27 @@ contract StableMathTest is Test { currentInvariant, swapFeePercentage ); - - stableMathMock.setMockRounding(RoundingMock.MockRounding.RoundDown); - uint256 bptOutGivenExactTokensInRoundedDown = stableMathMock.mockComputeBptOutGivenExactTokensIn( + uint256 bptOutGivenExactTokensInPermuted = stableMathMock.mockComputeBptOutGivenExactTokensIn( amp, balances, amountsIn, bptTotalSupply, currentInvariant, - swapFeePercentage - ); - - stableMathMock.setMockRounding(RoundingMock.MockRounding.RoundUp); - uint256 bptOutGivenExactTokensInRoundedUp = stableMathMock.mockComputeBptOutGivenExactTokensIn( - amp, - balances, - amountsIn, - bptTotalSupply, - currentInvariant, - swapFeePercentage + swapFeePercentage, + roundingPermutation ); assertEq( bptOutGivenExactTokensIn, - bptOutGivenExactTokensInUnrounded, + bptOutGivenExactTokensInUnpermuted, "Mock function and base one should be equivalent." ); - assertLe( - bptOutGivenExactTokensIn, - bptOutGivenExactTokensInRoundedDown, - "Output should be less than or equal to the rounded down mock value." - ); // BUG: Revise rounding in `computeBptOutGivenExactTokensIn()` - // assertLe(bptOutGivenExactTokensIn, bptOutGivenExactTokensInRoundedUp, "Output should be less than or equal to the rounded up mock value."); + // assertLe( + // bptOutGivenExactTokensIn, + // bptOutGivenExactTokensInPermuted, + // "Output should be less than or equal to the permuted mock value." + // ); } function testComputeTokenInGivenExactBptOutRounding__Fuzz( @@ -349,8 +294,9 @@ contract StableMathTest is Test { uint256 rawBptAmountOut, uint256 rawBptTotalSupply, uint256 rawCurrentInvariant, - uint256 rawSwapFeePercentage - ) external { + uint256 rawSwapFeePercentage, + bool[8] calldata roundingPermutation + ) external view { uint256 amp = boundAmp(rawAmp); uint256[] memory balances = boundBalances(rawBalances); uint256 tokenIndex = boundTokenIndex(rawTokenIndex); @@ -369,8 +315,7 @@ contract StableMathTest is Test { swapFeePercentage ); - stableMathMock.setMockRounding(RoundingMock.MockRounding.Disabled); - uint256 tokenInGivenExactBptOutUnrounded = stableMathMock.mockComputeTokenInGivenExactBptOut( + uint256 tokenInGivenExactBptOutUnpermuted = stableMathMock.mockComputeTokenInGivenExactBptOut( amp, balances, tokenIndex, @@ -379,37 +324,28 @@ contract StableMathTest is Test { currentInvariant, swapFeePercentage ); - - stableMathMock.setMockRounding(RoundingMock.MockRounding.RoundDown); - uint256 tokenInGivenExactBptOutRoundedDown = stableMathMock.mockComputeTokenInGivenExactBptOut( + uint256 tokenInGivenExactBptOutPermuted = stableMathMock.mockComputeTokenInGivenExactBptOut( amp, balances, tokenIndex, bptAmountOut, bptTotalSupply, currentInvariant, - swapFeePercentage - ); - - stableMathMock.setMockRounding(RoundingMock.MockRounding.RoundUp); - uint256 tokenInGivenExactBptOutRoundedUp = stableMathMock.mockComputeTokenInGivenExactBptOut( - amp, - balances, - tokenIndex, - bptAmountOut, - bptTotalSupply, - currentInvariant, - swapFeePercentage + swapFeePercentage, + roundingPermutation ); assertEq( tokenInGivenExactBptOut, - tokenInGivenExactBptOutUnrounded, + tokenInGivenExactBptOutUnpermuted, "Mock function and base one should be equivalent." ); // BUG: Revise rounding in `computeTokenInGivenExactBptOut()` - // assertGe(tokenInGivenExactBptOut, tokenInGivenExactBptOutRoundedDown, "Output should be greater than or equal to the rounded down mock value."); - // assertGe(tokenInGivenExactBptOut, tokenInGivenExactBptOutRoundedUp, "Output should be greater than or equal to the rounded up mock value."); + // assertGe( + // tokenInGivenExactBptOut, + // tokenInGivenExactBptOutPermuted, + // "Output should be greater than or equal to the permuted mock value." + // ); } function testComputeBptInGivenExactTokensOutRounding__Fuzz( @@ -418,8 +354,9 @@ contract StableMathTest is Test { uint256[NUM_TOKENS] calldata rawAmountsOut, uint256 rawBptTotalSupply, uint256 rawCurrentInvariant, - uint256 rawSwapFeePercentage - ) external { + uint256 rawSwapFeePercentage, + bool[7] calldata roundingPermutation + ) external view { uint256 amp = boundAmp(rawAmp); uint256[] memory balances = boundBalances(rawBalances); uint256[] memory amountsOut = boundAmountsOut(rawAmountsOut, balances); @@ -436,8 +373,7 @@ contract StableMathTest is Test { swapFeePercentage ); - stableMathMock.setMockRounding(RoundingMock.MockRounding.Disabled); - uint256 bptInGivenExactTokensOutUnrounded = stableMathMock.mockComputeBptInGivenExactTokensOut( + uint256 bptInGivenExactTokensOutUnpermuted = stableMathMock.mockComputeBptInGivenExactTokensOut( amp, balances, amountsOut, @@ -445,35 +381,27 @@ contract StableMathTest is Test { currentInvariant, swapFeePercentage ); - - stableMathMock.setMockRounding(RoundingMock.MockRounding.RoundDown); - uint256 bptInGivenExactTokensOutRoundedDown = stableMathMock.mockComputeBptInGivenExactTokensOut( - amp, - balances, - amountsOut, - bptTotalSupply, - currentInvariant, - swapFeePercentage - ); - - stableMathMock.setMockRounding(RoundingMock.MockRounding.RoundUp); - uint256 bptInGivenExactTokensOutRoundedUp = stableMathMock.mockComputeBptInGivenExactTokensOut( + uint256 bptInGivenExactTokensOutPermuted = stableMathMock.mockComputeBptInGivenExactTokensOut( amp, balances, amountsOut, bptTotalSupply, currentInvariant, - swapFeePercentage + swapFeePercentage, + roundingPermutation ); assertEq( bptInGivenExactTokensOut, - bptInGivenExactTokensOutUnrounded, + bptInGivenExactTokensOutUnpermuted, "Mock function and base one should be equivalent." ); // BUG: Revise rounding in `computeBptInGivenExactTokensOut()` - // assertGe(bptInGivenExactTokensOut, bptInGivenExactTokensOutRoundedDown, "Output should be greater than or equal to the rounded down mock value."); - // assertGe(bptInGivenExactTokensOut, bptInGivenExactTokensOutRoundedUp, "Output should be greater than or equal to the rounded up mock value."); + // assertGe( + // bptInGivenExactTokensOut, + // bptInGivenExactTokensOutPermuted, + // "Output should be greater than or equal to the permuted mock value." + // ); } function testComputeTokenOutGivenExactBptInRounding__Fuzz( @@ -483,8 +411,9 @@ contract StableMathTest is Test { uint256 rawBptAmountIn, uint256 rawBptTotalSupply, uint256 rawCurrentInvariant, - uint256 rawSwapFeePercentage - ) external { + uint256 rawSwapFeePercentage, + bool[8] calldata roundingPermutation + ) external view { uint256 amp = boundAmp(rawAmp); uint256[] memory balances = boundBalances(rawBalances); uint256 tokenIndex = boundTokenIndex(rawTokenIndex); @@ -503,19 +432,7 @@ contract StableMathTest is Test { swapFeePercentage ); - stableMathMock.setMockRounding(RoundingMock.MockRounding.Disabled); - uint256 tokenOutGivenExactBptInUnrounded = stableMathMock.mockComputeTokenOutGivenExactBptIn( - amp, - balances, - tokenIndex, - bptAmountIn, - bptTotalSupply, - currentInvariant, - swapFeePercentage - ); - - stableMathMock.setMockRounding(RoundingMock.MockRounding.RoundDown); - uint256 tokenOutGivenExactBptInRoundedDown = stableMathMock.mockComputeTokenOutGivenExactBptIn( + uint256 tokenOutGivenExactBptInUnpermuted = stableMathMock.mockComputeTokenOutGivenExactBptIn( amp, balances, tokenIndex, @@ -524,32 +441,26 @@ contract StableMathTest is Test { currentInvariant, swapFeePercentage ); - - stableMathMock.setMockRounding(RoundingMock.MockRounding.RoundUp); - uint256 tokenOutGivenExactBptInRoundedUp = stableMathMock.mockComputeTokenOutGivenExactBptIn( + uint256 tokenOutGivenExactBptInPermuted = stableMathMock.mockComputeTokenOutGivenExactBptIn( amp, balances, tokenIndex, bptAmountIn, bptTotalSupply, currentInvariant, - swapFeePercentage + swapFeePercentage, + roundingPermutation ); assertEq( tokenOutGivenExactBptIn, - tokenOutGivenExactBptInUnrounded, + tokenOutGivenExactBptInUnpermuted, "Mock function and base one should be equivalent." ); assertLe( tokenOutGivenExactBptIn, - tokenOutGivenExactBptInRoundedDown, - "Output should be less than or equal to the rounded down mock value." - ); - assertLe( - tokenOutGivenExactBptIn, - tokenOutGivenExactBptInRoundedUp, - "Output should be less than or equal to the rounded up mock value." + tokenOutGivenExactBptInPermuted, + "Output should be less than or equal to the permuted mock value." ); } @@ -557,8 +468,9 @@ contract StableMathTest is Test { uint256 rawAmp, uint256[NUM_TOKENS] calldata rawBalances, uint256 rawInvariant, - uint256 rawTokenIndex - ) external { + uint256 rawTokenIndex, + bool[3] calldata roundingPermutation + ) external view { uint256 amp = boundAmp(rawAmp); uint256[] memory balances = boundBalances(rawBalances); uint256 invariant = boundInvariantOut(rawInvariant); @@ -566,17 +478,16 @@ contract StableMathTest is Test { uint256 balance = stableMathMock.computeBalance(amp, balances, invariant, tokenIndex); - stableMathMock.setMockRounding(RoundingMock.MockRounding.Disabled); - uint256 balanceUnrounded = stableMathMock.mockComputeBalance(amp, balances, invariant, tokenIndex); - - stableMathMock.setMockRounding(RoundingMock.MockRounding.RoundDown); - uint256 balanceRoundedDown = stableMathMock.mockComputeBalance(amp, balances, invariant, tokenIndex); - - stableMathMock.setMockRounding(RoundingMock.MockRounding.RoundUp); - uint256 balanceRoundedUp = stableMathMock.mockComputeBalance(amp, balances, invariant, tokenIndex); + uint256 balanceUnpermuted = stableMathMock.mockComputeBalance(amp, balances, invariant, tokenIndex); + uint256 balancePermuted = stableMathMock.mockComputeBalance( + amp, + balances, + invariant, + tokenIndex, + roundingPermutation + ); - assertEq(balance, balanceUnrounded, "Mock function and base one should be equivalent."); - assertGe(balance, balanceRoundedDown, "Output should be greater than or equal to the rounded down mock value."); - assertGe(balance, balanceRoundedUp, "Output should be greater than or equal to the rounded up mock value."); + assertEq(balance, balanceUnpermuted, "Mock function and base one should be equivalent."); + assertGe(balance, balancePermuted, "Output should be greater than or equal to the permuted mock value."); } }