From cebb5dcbbc1226a900563b48a5af36b362504b1d Mon Sep 17 00:00:00 2001 From: Austrian <114922365+0xAustrian@users.noreply.github.com> Date: Tue, 14 May 2024 12:57:21 -0300 Subject: [PATCH 1/3] test: swapExactAmountIn happyPath (#14) * test: happyPath for exitPool * test: remove unnecessary mock * test: wip swapExactAmountIn * test: improve assumes * test: more progress on happyPath * test: wip * test: working version with commented require * test: improve assumes * test: bound denorms * test: bound swapFee * fix: remove unused function --- foundry.toml | 2 +- test/unit/BPool.t.sol | 111 +++++++++++++++++++++++++++++++++++++++++- 2 files changed, 111 insertions(+), 2 deletions(-) diff --git a/foundry.toml b/foundry.toml index bd346497..015deb5e 100644 --- a/foundry.toml +++ b/foundry.toml @@ -26,7 +26,7 @@ src = 'src/interfaces/' [fuzz] runs = 1000 -max_test_rejects = 1000000 +max_test_rejects = 1_000_000 [rpc_endpoints] mainnet = "${MAINNET_RPC}" diff --git a/test/unit/BPool.t.sol b/test/unit/BPool.t.sol index b221e367..cd5e28dd 100644 --- a/test/unit/BPool.t.sol +++ b/test/unit/BPool.t.sol @@ -5,6 +5,7 @@ import {BPool} from 'contracts/BPool.sol'; import {MockBPool} from 'test/smock/MockBPool.sol'; import {BConst} from 'contracts/BConst.sol'; +import {BMath} from 'contracts/BMath.sol'; import {IERC20} from 'contracts/BToken.sol'; import {Test} from 'forge-std/Test.sol'; import {LibString} from 'solmate/utils/LibString.sol'; @@ -65,6 +66,10 @@ abstract contract BasePoolTest is Test, BConst, Utils { bPool.set__publicSwap(_isPublicSwap); } + function _setSwapFee(uint256 _swapFee) internal { + bPool.set__swapFee(_swapFee); + } + function _setFinalize(bool _isFinalized) internal { bPool.set__finalized(_isFinalized); } @@ -500,7 +505,111 @@ contract BPool_Unit_ExitPool is BasePoolTest { function test_Emit_LogCall() private view {} } -contract BPool_Unit_SwapExactAmountIn is BasePoolTest { +contract BPool_Unit_SwapExactAmountIn is BasePoolTest, BMath { + address tokenIn; + address tokenOut; + + struct SwapExactAmountIn_FuzzScenario { + uint256 tokenAmountIn; + uint256 tokenInBalance; + uint256 tokenInDenorm; + uint256 tokenOutBalance; + uint256 tokenOutDenorm; + uint256 swapFee; + } + + function _setValues(SwapExactAmountIn_FuzzScenario memory _fuzz) internal { + tokenIn = tokens[0]; + tokenOut = tokens[1]; + + // Create mocks for tokenIn and tokenOut (only use the first 2 tokens) + _mockTransferFrom(tokenIn); + _mockTransfer(tokenOut); + + // Set balances + _setRecord( + tokenIn, + BPool.Record({ + bound: true, + index: 0, // NOTE: irrelevant for this method + denorm: _fuzz.tokenInDenorm, + balance: _fuzz.tokenInBalance + }) + ); + _setRecord( + tokenOut, + BPool.Record({ + bound: true, + index: 0, // NOTE: irrelevant for this method + denorm: _fuzz.tokenOutDenorm, + balance: _fuzz.tokenOutBalance + }) + ); + + // Set swapFee + _setSwapFee(_fuzz.swapFee); + // Set public swap + _setPublicSwap(true); + // Set finalize + _setFinalize(true); + } + + function _assumeHappyPath(SwapExactAmountIn_FuzzScenario memory _fuzz) internal view { + // safe bound assumptions + _fuzz.tokenInDenorm = bound(_fuzz.tokenInDenorm, MIN_WEIGHT, MAX_WEIGHT); + _fuzz.tokenOutDenorm = bound(_fuzz.tokenOutDenorm, MIN_WEIGHT, MAX_WEIGHT); + _fuzz.swapFee = bound(_fuzz.swapFee, MIN_FEE, MAX_FEE); + + // min + vm.assume(_fuzz.tokenInBalance >= MIN_BALANCE); + vm.assume(_fuzz.tokenOutBalance >= MIN_BALANCE); + + // max - calcSpotPrice (spotPriceBefore) + vm.assume(_fuzz.tokenInBalance < type(uint256).max / _fuzz.tokenInDenorm); + vm.assume(_fuzz.tokenOutBalance < type(uint256).max / _fuzz.tokenOutDenorm); + + // max - calcSpotPrice (spotPriceAfter) + vm.assume(_fuzz.tokenAmountIn < type(uint256).max - _fuzz.tokenInBalance); + vm.assume(_fuzz.tokenInBalance + _fuzz.tokenAmountIn < type(uint256).max / _fuzz.tokenInDenorm); + + // internal calculation for calcSpotPrice + uint256 _numer = bdiv(_fuzz.tokenInBalance, _fuzz.tokenInDenorm); + uint256 _denom = bdiv(_fuzz.tokenOutBalance, _fuzz.tokenOutDenorm); + uint256 _ratio = bdiv(_numer, _denom); + uint256 _scale = bdiv(BONE, bsub(BONE, _fuzz.swapFee)); + vm.assume(_ratio < type(uint256).max / _scale); + + // MAX_IN_RATIO + vm.assume(_fuzz.tokenAmountIn <= bmul(_fuzz.tokenInBalance, MAX_IN_RATIO)); + + // L338 BPool.sol + uint256 _spotPriceBefore = calcSpotPrice( + _fuzz.tokenInBalance, _fuzz.tokenInDenorm, _fuzz.tokenOutBalance, _fuzz.tokenOutDenorm, _fuzz.swapFee + ); + uint256 _tokenAmountOut = calcOutGivenIn( + _fuzz.tokenInBalance, + _fuzz.tokenInDenorm, + _fuzz.tokenOutBalance, + _fuzz.tokenOutDenorm, + _fuzz.tokenAmountIn, + _fuzz.swapFee + ); + vm.assume(_tokenAmountOut > BONE); + vm.assume(bmul(_spotPriceBefore, _tokenAmountOut) <= _fuzz.tokenAmountIn); + } + + modifier happyPath(SwapExactAmountIn_FuzzScenario memory _fuzz) { + _assumeHappyPath(_fuzz); + _setValues(_fuzz); + _; + } + + function test_HappyPath(SwapExactAmountIn_FuzzScenario memory _fuzz) public happyPath(_fuzz) { + uint256 _maxPrice = type(uint256).max; + uint256 _minAmountOut = 0; + bPool.swapExactAmountIn(tokenIn, _fuzz.tokenAmountIn, tokenOut, _minAmountOut, _maxPrice); + } + function test_Revert_NotBoundTokenIn() private view {} function test_Revert_NotBoundTokenOut() private view {} From e24a0d33959265e3829921d1bacce307ea9a696b Mon Sep 17 00:00:00 2001 From: Austrian <114922365+0xAustrian@users.noreply.github.com> Date: Tue, 14 May 2024 13:00:12 -0300 Subject: [PATCH 2/3] feat: add unchecked to BNum functions (#18) --- src/contracts/BNum.sol | 154 +++++++++++++++++++++++------------------ 1 file changed, 87 insertions(+), 67 deletions(-) diff --git a/src/contracts/BNum.sol b/src/contracts/BNum.sol index 10d2d9af..f142d8c3 100644 --- a/src/contracts/BNum.sol +++ b/src/contracts/BNum.sol @@ -17,114 +17,134 @@ import './BConst.sol'; contract BNum is BConst { function btoi(uint256 a) internal pure returns (uint256) { - return a / BONE; + unchecked { + return a / BONE; + } } function bfloor(uint256 a) internal pure returns (uint256) { - return btoi(a) * BONE; + unchecked { + return btoi(a) * BONE; + } } function badd(uint256 a, uint256 b) internal pure returns (uint256) { - uint256 c = a + b; - require(c >= a, 'ERR_ADD_OVERFLOW'); - return c; + unchecked { + uint256 c = a + b; + require(c >= a, 'ERR_ADD_OVERFLOW'); + return c; + } } function bsub(uint256 a, uint256 b) internal pure returns (uint256) { - (uint256 c, bool flag) = bsubSign(a, b); - require(!flag, 'ERR_SUB_UNDERFLOW'); - return c; + unchecked { + (uint256 c, bool flag) = bsubSign(a, b); + require(!flag, 'ERR_SUB_UNDERFLOW'); + return c; + } } function bsubSign(uint256 a, uint256 b) internal pure returns (uint256, bool) { - if (a >= b) { - return (a - b, false); - } else { - return (b - a, true); + unchecked { + if (a >= b) { + return (a - b, false); + } else { + return (b - a, true); + } } } function bmul(uint256 a, uint256 b) internal pure returns (uint256) { - uint256 c0 = a * b; - require(a == 0 || c0 / a == b, 'ERR_MUL_OVERFLOW'); - uint256 c1 = c0 + (BONE / 2); - require(c1 >= c0, 'ERR_MUL_OVERFLOW'); - uint256 c2 = c1 / BONE; - return c2; + unchecked { + uint256 c0 = a * b; + require(a == 0 || c0 / a == b, 'ERR_MUL_OVERFLOW'); + uint256 c1 = c0 + (BONE / 2); + require(c1 >= c0, 'ERR_MUL_OVERFLOW'); + uint256 c2 = c1 / BONE; + return c2; + } } function bdiv(uint256 a, uint256 b) internal pure returns (uint256) { - require(b != 0, 'ERR_DIV_ZERO'); - uint256 c0 = a * BONE; - require(a == 0 || c0 / a == BONE, 'ERR_DIV_INTERNAL'); // bmul overflow - uint256 c1 = c0 + (b / 2); - require(c1 >= c0, 'ERR_DIV_INTERNAL'); // badd require - uint256 c2 = c1 / b; - return c2; + unchecked { + require(b != 0, 'ERR_DIV_ZERO'); + uint256 c0 = a * BONE; + require(a == 0 || c0 / a == BONE, 'ERR_DIV_INTERNAL'); // bmul overflow + uint256 c1 = c0 + (b / 2); + require(c1 >= c0, 'ERR_DIV_INTERNAL'); // badd require + uint256 c2 = c1 / b; + return c2; + } } // DSMath.wpow function bpowi(uint256 a, uint256 n) internal pure returns (uint256) { - uint256 z = n % 2 != 0 ? a : BONE; + unchecked { + uint256 z = n % 2 != 0 ? a : BONE; - for (n /= 2; n != 0; n /= 2) { - a = bmul(a, a); + for (n /= 2; n != 0; n /= 2) { + a = bmul(a, a); - if (n % 2 != 0) { - z = bmul(z, a); + if (n % 2 != 0) { + z = bmul(z, a); + } } + return z; } - return z; } // Compute b^(e.w) by splitting it into (b^e)*(b^0.w). // Use `bpowi` for `b^e` and `bpowK` for k iterations // of approximation of b^0.w function bpow(uint256 base, uint256 exp) internal pure returns (uint256) { - require(base >= MIN_BPOW_BASE, 'ERR_BPOW_BASE_TOO_LOW'); - require(base <= MAX_BPOW_BASE, 'ERR_BPOW_BASE_TOO_HIGH'); + unchecked { + require(base >= MIN_BPOW_BASE, 'ERR_BPOW_BASE_TOO_LOW'); + require(base <= MAX_BPOW_BASE, 'ERR_BPOW_BASE_TOO_HIGH'); - uint256 whole = bfloor(exp); - uint256 remain = bsub(exp, whole); + uint256 whole = bfloor(exp); + uint256 remain = bsub(exp, whole); - uint256 wholePow = bpowi(base, btoi(whole)); + uint256 wholePow = bpowi(base, btoi(whole)); - if (remain == 0) { - return wholePow; - } + if (remain == 0) { + return wholePow; + } - uint256 partialResult = bpowApprox(base, remain, BPOW_PRECISION); - return bmul(wholePow, partialResult); + uint256 partialResult = bpowApprox(base, remain, BPOW_PRECISION); + return bmul(wholePow, partialResult); + } } function bpowApprox(uint256 base, uint256 exp, uint256 precision) internal pure returns (uint256) { - // term 0: - uint256 a = exp; - (uint256 x, bool xneg) = bsubSign(base, BONE); - uint256 term = BONE; - uint256 sum = term; - bool negative = false; - - // term(k) = numer / denom - // = (product(a - i - 1, i=1-->k) * x^k) / (k!) - // each iteration, multiply previous term by (a-(k-1)) * x / k - // continue until term is less than precision - for (uint256 i = 1; term >= precision; i++) { - uint256 bigK = i * BONE; - (uint256 c, bool cneg) = bsubSign(a, bsub(bigK, BONE)); - term = bmul(term, bmul(c, x)); - term = bdiv(term, bigK); - if (term == 0) break; - - if (xneg) negative = !negative; - if (cneg) negative = !negative; - if (negative) { - sum = bsub(sum, term); - } else { - sum = badd(sum, term); + unchecked { + // term 0: + uint256 a = exp; + (uint256 x, bool xneg) = bsubSign(base, BONE); + uint256 term = BONE; + uint256 sum = term; + bool negative = false; + + // term(k) = numer / denom + // = (product(a - i - 1, i=1-->k) * x^k) / (k!) + // each iteration, multiply previous term by (a-(k-1)) * x / k + // continue until term is less than precision + for (uint256 i = 1; term >= precision; i++) { + uint256 bigK = i * BONE; + (uint256 c, bool cneg) = bsubSign(a, bsub(bigK, BONE)); + term = bmul(term, bmul(c, x)); + term = bdiv(term, bigK); + if (term == 0) break; + + if (xneg) negative = !negative; + if (cneg) negative = !negative; + if (negative) { + sum = bsub(sum, term); + } else { + sum = badd(sum, term); + } } - } - return sum; + return sum; + } } } From 8c76c6c0449a650d94fdd65f7c9929a3224b8177 Mon Sep 17 00:00:00 2001 From: Austrian <114922365+0xAustrian@users.noreply.github.com> Date: Wed, 15 May 2024 09:57:57 -0300 Subject: [PATCH 3/3] test: swapExactAmountOut happyPath (#19) * test: add happyPath * fix: lint * test: suite improvement --- test/unit/BPool.t.sol | 164 +++++++++++++++++++++++++++++++++++++++--- 1 file changed, 156 insertions(+), 8 deletions(-) diff --git a/test/unit/BPool.t.sol b/test/unit/BPool.t.sol index cd5e28dd..d7658306 100644 --- a/test/unit/BPool.t.sol +++ b/test/unit/BPool.t.sol @@ -14,7 +14,7 @@ import {Utils} from 'test/unit/Utils.sol'; // TODO: remove once `private` keyword is removed in all test cases /* solhint-disable */ -abstract contract BasePoolTest is Test, BConst, Utils { +abstract contract BasePoolTest is Test, BConst, Utils, BMath { using LibString for *; uint256 public constant TOKENS_AMOUNT = 3; @@ -81,6 +81,34 @@ abstract contract BasePoolTest is Test, BConst, Utils { function _setTotalSupply(uint256 _totalSupply) internal { _setPoolBalance(address(0), _totalSupply); } + + function _assumeCalcSpotPrice( + uint256 _tokenInBalance, + uint256 _tokenInDenorm, + uint256 _tokenOutBalance, + uint256 _tokenOutDenorm, + uint256 _swapFee + ) internal pure { + uint256 _numer = bdiv(_tokenInBalance, _tokenInDenorm); + uint256 _denom = bdiv(_tokenOutBalance, _tokenOutDenorm); + uint256 _ratio = bdiv(_numer, _denom); + uint256 _scale = bdiv(BONE, bsub(BONE, _swapFee)); + vm.assume(_ratio < type(uint256).max / _scale); + } + + function _assumeCalcInGivenOut( + uint256 _tokenOutDenorm, + uint256 _tokenInDenorm, + uint256 _tokenOutBalance, + uint256 _tokenAmountOut, + uint256 _tokenInBalance + ) internal pure { + uint256 _weightRatio = bdiv(_tokenOutDenorm, _tokenInDenorm); + uint256 _diff = bsub(_tokenOutBalance, _tokenAmountOut); + uint256 _y = bdiv(_tokenOutBalance, _diff); + uint256 _foo = bpow(_y, _weightRatio); + vm.assume(bsub(_foo, BONE) < type(uint256).max / _tokenInBalance); + } } contract BPool_Unit_Constructor is BasePoolTest { @@ -505,7 +533,7 @@ contract BPool_Unit_ExitPool is BasePoolTest { function test_Emit_LogCall() private view {} } -contract BPool_Unit_SwapExactAmountIn is BasePoolTest, BMath { +contract BPool_Unit_SwapExactAmountIn is BasePoolTest { address tokenIn; address tokenOut; @@ -554,7 +582,7 @@ contract BPool_Unit_SwapExactAmountIn is BasePoolTest, BMath { _setFinalize(true); } - function _assumeHappyPath(SwapExactAmountIn_FuzzScenario memory _fuzz) internal view { + function _assumeHappyPath(SwapExactAmountIn_FuzzScenario memory _fuzz) internal pure { // safe bound assumptions _fuzz.tokenInDenorm = bound(_fuzz.tokenInDenorm, MIN_WEIGHT, MAX_WEIGHT); _fuzz.tokenOutDenorm = bound(_fuzz.tokenOutDenorm, MIN_WEIGHT, MAX_WEIGHT); @@ -573,11 +601,9 @@ contract BPool_Unit_SwapExactAmountIn is BasePoolTest, BMath { vm.assume(_fuzz.tokenInBalance + _fuzz.tokenAmountIn < type(uint256).max / _fuzz.tokenInDenorm); // internal calculation for calcSpotPrice - uint256 _numer = bdiv(_fuzz.tokenInBalance, _fuzz.tokenInDenorm); - uint256 _denom = bdiv(_fuzz.tokenOutBalance, _fuzz.tokenOutDenorm); - uint256 _ratio = bdiv(_numer, _denom); - uint256 _scale = bdiv(BONE, bsub(BONE, _fuzz.swapFee)); - vm.assume(_ratio < type(uint256).max / _scale); + _assumeCalcSpotPrice( + _fuzz.tokenInBalance, _fuzz.tokenInDenorm, _fuzz.tokenOutBalance, _fuzz.tokenOutDenorm, _fuzz.swapFee + ); // MAX_IN_RATIO vm.assume(_fuzz.tokenAmountIn <= bmul(_fuzz.tokenInBalance, MAX_IN_RATIO)); @@ -646,6 +672,128 @@ contract BPool_Unit_SwapExactAmountIn is BasePoolTest, BMath { } contract BPool_Unit_SwapExactAmountOut is BasePoolTest { + address tokenIn; + address tokenOut; + + struct SwapExactAmountOut_FuzzScenario { + uint256 tokenAmountOut; + uint256 tokenInBalance; + uint256 tokenInDenorm; + uint256 tokenOutBalance; + uint256 tokenOutDenorm; + uint256 swapFee; + } + + function _setValues(SwapExactAmountOut_FuzzScenario memory _fuzz) internal { + tokenIn = tokens[0]; + tokenOut = tokens[1]; + + // Create mocks for tokenIn and tokenOut (only use the first 2 tokens) + _mockTransferFrom(tokenIn); + _mockTransfer(tokenOut); + + // Set balances + _setRecord( + tokenIn, + BPool.Record({ + bound: true, + index: 0, // NOTE: irrelevant for this method + denorm: _fuzz.tokenInDenorm, + balance: _fuzz.tokenInBalance + }) + ); + _setRecord( + tokenOut, + BPool.Record({ + bound: true, + index: 0, // NOTE: irrelevant for this method + denorm: _fuzz.tokenOutDenorm, + balance: _fuzz.tokenOutBalance + }) + ); + + // Set swapFee + _setSwapFee(_fuzz.swapFee); + // Set public swap + _setPublicSwap(true); + // Set finalize + _setFinalize(true); + } + + function _assumeHappyPath(SwapExactAmountOut_FuzzScenario memory _fuzz) internal view { + // safe bound assumptions + _fuzz.tokenInDenorm = bound(_fuzz.tokenInDenorm, MIN_WEIGHT, MAX_WEIGHT); + _fuzz.tokenOutDenorm = bound(_fuzz.tokenOutDenorm, MIN_WEIGHT, MAX_WEIGHT); + _fuzz.swapFee = bound(_fuzz.swapFee, MIN_FEE, MAX_FEE); + + // min + vm.assume(_fuzz.tokenInBalance >= MIN_BALANCE); + vm.assume(_fuzz.tokenOutBalance >= MIN_BALANCE); + + // max - calcSpotPrice (spotPriceBefore) + vm.assume(_fuzz.tokenInBalance < type(uint256).max / _fuzz.tokenInDenorm); + vm.assume(_fuzz.tokenOutBalance < type(uint256).max / _fuzz.tokenOutDenorm); + + // max - calcSpotPrice (spotPriceAfter) + vm.assume(_fuzz.tokenAmountOut < type(uint256).max - _fuzz.tokenOutBalance); + vm.assume(_fuzz.tokenOutBalance + _fuzz.tokenAmountOut < type(uint256).max / _fuzz.tokenOutDenorm); + + // internal calculation for calcSpotPrice (spotPriceBefore) + _assumeCalcSpotPrice( + _fuzz.tokenInBalance, _fuzz.tokenInDenorm, _fuzz.tokenOutBalance, _fuzz.tokenOutDenorm, _fuzz.swapFee + ); + + // MAX_OUT_RATIO + vm.assume(_fuzz.tokenAmountOut <= bmul(_fuzz.tokenOutBalance, MAX_OUT_RATIO)); + + // L364 BPool.sol + uint256 _spotPriceBefore = calcSpotPrice( + _fuzz.tokenInBalance, _fuzz.tokenInDenorm, _fuzz.tokenOutBalance, _fuzz.tokenOutDenorm, _fuzz.swapFee + ); + + // internal calculation for calcInGivenOut + _assumeCalcInGivenOut( + _fuzz.tokenOutDenorm, _fuzz.tokenInDenorm, _fuzz.tokenOutBalance, _fuzz.tokenAmountOut, _fuzz.tokenInBalance + ); + + uint256 _tokenAmountIn = calcInGivenOut( + _fuzz.tokenInBalance, + _fuzz.tokenInDenorm, + _fuzz.tokenOutBalance, + _fuzz.tokenOutDenorm, + _fuzz.tokenAmountOut, + _fuzz.swapFee + ); + + vm.assume(_tokenAmountIn > BONE); + vm.assume(bmul(_spotPriceBefore, _fuzz.tokenAmountOut) <= _tokenAmountIn); + + // max - calcSpotPrice (spotPriceAfter) + vm.assume(_tokenAmountIn < type(uint256).max - _fuzz.tokenInBalance); + vm.assume(_fuzz.tokenInBalance + _tokenAmountIn < type(uint256).max / _fuzz.tokenInDenorm); + + // internal calculation for calcSpotPrice (spotPriceAfter) + _assumeCalcSpotPrice( + _fuzz.tokenInBalance + _tokenAmountIn, + _fuzz.tokenInDenorm, + _fuzz.tokenOutBalance - _fuzz.tokenAmountOut, + _fuzz.tokenOutDenorm, + _fuzz.swapFee + ); + } + + modifier happyPath(SwapExactAmountOut_FuzzScenario memory _fuzz) { + _assumeHappyPath(_fuzz); + _setValues(_fuzz); + _; + } + + function test_HappyPath(SwapExactAmountOut_FuzzScenario memory _fuzz) public happyPath(_fuzz) { + uint256 _maxPrice = type(uint256).max; + uint256 _maxAmountIn = type(uint256).max; + bPool.swapExactAmountOut(tokenIn, _maxAmountIn, tokenOut, _fuzz.tokenAmountOut, _maxPrice); + } + function test_Revert_NotBoundTokenIn() private view {} function test_Revert_NotBoundTokenOut() private view {}