From 28fbf44e2ab44fe67c232871edb3ee2ac19de027 Mon Sep 17 00:00:00 2001 From: teddy Date: Tue, 16 Jul 2024 10:13:50 -0300 Subject: [PATCH 1/2] test: btt bpool swap exact amount out (#153) * test: btt tests for bpool.swapExactAmountIn * chore: delete preexisting unit tests * test: small renames from feedback * test: be explicit about untestable code * test: adding skipped test for unreachable condition * test: code wasnt so unreachable after all * test: tree and scaffolding * test: btt tests for swapExactAmountOut * chore: delete preexisting unit tests --- test/unit/BPool.t.sol | 350 ------------------ .../unit/BPool/BPool_SwapExactAmountOut.t.sol | 134 +++++++ test/unit/BPool/BPool_SwapExactAmountOut.tree | 28 ++ 3 files changed, 162 insertions(+), 350 deletions(-) create mode 100644 test/unit/BPool/BPool_SwapExactAmountOut.t.sol create mode 100644 test/unit/BPool/BPool_SwapExactAmountOut.tree diff --git a/test/unit/BPool.t.sol b/test/unit/BPool.t.sol index 16868e32..39f5fedd 100644 --- a/test/unit/BPool.t.sol +++ b/test/unit/BPool.t.sol @@ -818,356 +818,6 @@ contract BPool_Unit_GetSpotPriceSansFee is BasePoolTest { } } -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, - IBPool.Record({ - bound: true, - index: 0, // NOTE: irrelevant for this method - denorm: _fuzz.tokenInDenorm - }) - ); - _mockPoolBalance(tokenIn, _fuzz.tokenInBalance); - - _setRecord( - tokenOut, - IBPool.Record({ - bound: true, - index: 0, // NOTE: irrelevant for this method - denorm: _fuzz.tokenOutDenorm - }) - ); - _mockPoolBalance(tokenOut, _fuzz.tokenOutBalance); - - // Set swapFee - _setSwapFee(_fuzz.swapFee); - // Set finalize - _setFinalize(true); - } - - function _assumeHappyPath(SwapExactAmountOut_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); - _fuzz.swapFee = bound(_fuzz.swapFee, MIN_FEE, MAX_FEE); - - _fuzz.tokenInBalance = bound(_fuzz.tokenInBalance, MIN_BALANCE, type(uint256).max); - _fuzz.tokenOutBalance = bound(_fuzz.tokenOutBalance, MIN_BALANCE, type(uint256).max); - - // 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(_tokenAmountIn < type(uint256).max / BONE); - vm.assume(_spotPriceBefore <= bdiv(_tokenAmountIn, _fuzz.tokenAmountOut)); - - // 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_Revert_NotBoundTokenIn( - SwapExactAmountOut_FuzzScenario memory _fuzz, - address _tokenIn - ) public happyPath(_fuzz) { - assumeNotForgeAddress(_tokenIn); - vm.assume(_tokenIn != tokenIn); - vm.assume(_tokenIn != tokenOut); - - vm.expectRevert(IBPool.BPool_TokenNotBound.selector); - bPool.swapExactAmountOut(_tokenIn, type(uint256).max, tokenOut, _fuzz.tokenAmountOut, type(uint256).max); - } - - function test_Revert_NotBoundTokenOut( - SwapExactAmountOut_FuzzScenario memory _fuzz, - address _tokenOut - ) public happyPath(_fuzz) { - assumeNotForgeAddress(_tokenOut); - vm.assume(_tokenOut != tokenIn); - vm.assume(_tokenOut != tokenOut); - - vm.expectRevert(IBPool.BPool_TokenNotBound.selector); - bPool.swapExactAmountOut(tokenIn, type(uint256).max, _tokenOut, _fuzz.tokenAmountOut, type(uint256).max); - } - - function test_Revert_NotFinalized(SwapExactAmountOut_FuzzScenario memory _fuzz) public happyPath(_fuzz) { - _setFinalize(false); - - vm.expectRevert(IBPool.BPool_PoolNotFinalized.selector); - bPool.swapExactAmountOut(tokenIn, type(uint256).max, tokenOut, _fuzz.tokenAmountOut, type(uint256).max); - } - - function test_Revert_TokenAmountOutAboveMaxOut(SwapExactAmountOut_FuzzScenario memory _fuzz) public happyPath(_fuzz) { - uint256 _tokenAmountOut = bmul(_fuzz.tokenOutBalance, MAX_OUT_RATIO) + 1; - - vm.expectRevert(IBPool.BPool_TokenAmountOutAboveMaxOut.selector); - bPool.swapExactAmountOut(tokenIn, type(uint256).max, tokenOut, _tokenAmountOut, type(uint256).max); - } - - function test_Revert_SpotPriceAboveMaxPrice( - SwapExactAmountOut_FuzzScenario memory _fuzz, - uint256 _maxPrice - ) public happyPath(_fuzz) { - uint256 _spotPriceBefore = calcSpotPrice( - _fuzz.tokenInBalance, _fuzz.tokenInDenorm, _fuzz.tokenOutBalance, _fuzz.tokenOutDenorm, _fuzz.swapFee - ); - vm.assume(_spotPriceBefore > 0); - _maxPrice = bound(_maxPrice, 0, _spotPriceBefore - 1); - - vm.expectRevert(IBPool.BPool_SpotPriceAboveMaxPrice.selector); - bPool.swapExactAmountOut(tokenIn, type(uint256).max, tokenOut, _fuzz.tokenAmountOut, _maxPrice); - } - - function test_Revert_TokenAmountInAboveMaxAmountIn( - SwapExactAmountOut_FuzzScenario memory _fuzz, - uint256 _maxAmountIn - ) public happyPath(_fuzz) { - uint256 _tokenAmountIn = calcInGivenOut( - _fuzz.tokenInBalance, - _fuzz.tokenInDenorm, - _fuzz.tokenOutBalance, - _fuzz.tokenOutDenorm, - _fuzz.tokenAmountOut, - _fuzz.swapFee - ); - _maxAmountIn = bound(_maxAmountIn, 0, _tokenAmountIn - 1); - - vm.expectRevert(IBPool.BPool_TokenAmountInAboveMaxAmountIn.selector); - bPool.swapExactAmountOut(tokenIn, _maxAmountIn, tokenOut, _fuzz.tokenAmountOut, type(uint256).max); - } - - function test_Revert_Reentrancy(SwapExactAmountOut_FuzzScenario memory _fuzz) public happyPath(_fuzz) { - _expectRevertByReentrancy(); - bPool.swapExactAmountOut(tokenIn, type(uint256).max, tokenOut, _fuzz.tokenAmountOut, type(uint256).max); - } - - function test_Revert_MathApprox() public { - vm.skip(true); - // TODO: this revert might be unreachable. Find a way to test it or remove the revert in the code. - } - - function test_Revert_SpotPriceAfterAboveMaxPrice(SwapExactAmountOut_FuzzScenario memory _fuzz) - public - happyPath(_fuzz) - { - uint256 _tokenAmountIn = calcInGivenOut( - _fuzz.tokenInBalance, - _fuzz.tokenInDenorm, - _fuzz.tokenOutBalance, - _fuzz.tokenOutDenorm, - _fuzz.tokenAmountOut, - _fuzz.swapFee - ); - uint256 _spotPriceBefore = calcSpotPrice( - _fuzz.tokenInBalance, _fuzz.tokenInDenorm, _fuzz.tokenOutBalance, _fuzz.tokenOutDenorm, _fuzz.swapFee - ); - uint256 _spotPriceAfter = calcSpotPrice( - _fuzz.tokenInBalance + _tokenAmountIn, - _fuzz.tokenInDenorm, - _fuzz.tokenOutBalance - _fuzz.tokenAmountOut, - _fuzz.tokenOutDenorm, - _fuzz.swapFee - ); - vm.assume(_spotPriceAfter > _spotPriceBefore); - - vm.expectRevert(IBPool.BPool_SpotPriceAboveMaxPrice.selector); - bPool.swapExactAmountOut(tokenIn, type(uint256).max, tokenOut, _fuzz.tokenAmountOut, _spotPriceBefore); - } - - function test_Revert_SpotPriceBeforeAboveTokenRatio(SwapExactAmountOut_FuzzScenario memory _fuzz) public { - // Replicating _assumeHappyPath, but removing irrelevant assumptions and conditioning the revert - _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); - _fuzz.tokenInBalance = bound(_fuzz.tokenInBalance, MIN_BALANCE, type(uint256).max); - _fuzz.tokenOutBalance = bound(_fuzz.tokenOutBalance, MIN_BALANCE, type(uint256).max); - vm.assume(_fuzz.tokenInBalance < type(uint256).max / _fuzz.tokenInDenorm); - vm.assume(_fuzz.tokenOutBalance < type(uint256).max / _fuzz.tokenOutDenorm); - vm.assume(_fuzz.tokenAmountOut < type(uint256).max - _fuzz.tokenOutBalance); - vm.assume(_fuzz.tokenOutBalance + _fuzz.tokenAmountOut < type(uint256).max / _fuzz.tokenOutDenorm); - vm.assume(_fuzz.tokenAmountOut <= bmul(_fuzz.tokenOutBalance, MAX_OUT_RATIO)); - _assumeCalcSpotPrice( - _fuzz.tokenInBalance, _fuzz.tokenInDenorm, _fuzz.tokenOutBalance, _fuzz.tokenOutDenorm, _fuzz.swapFee - ); - uint256 _spotPriceBefore = calcSpotPrice( - _fuzz.tokenInBalance, _fuzz.tokenInDenorm, _fuzz.tokenOutBalance, _fuzz.tokenOutDenorm, _fuzz.swapFee - ); - _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(_tokenAmountIn < type(uint256).max - _fuzz.tokenInBalance); - vm.assume(_fuzz.tokenInBalance + _tokenAmountIn < type(uint256).max / _fuzz.tokenInDenorm); - _assumeCalcSpotPrice( - _fuzz.tokenInBalance + _tokenAmountIn, - _fuzz.tokenInDenorm, - _fuzz.tokenOutBalance - _fuzz.tokenAmountOut, - _fuzz.tokenOutDenorm, - _fuzz.swapFee - ); - vm.assume(_spotPriceBefore > bdiv(_tokenAmountIn, _fuzz.tokenAmountOut)); - - _setValues(_fuzz); - - vm.expectRevert(IBPool.BPool_SpotPriceBeforeAboveTokenRatio.selector); - bPool.swapExactAmountOut(tokenIn, type(uint256).max, tokenOut, _fuzz.tokenAmountOut, type(uint256).max); - } - - function test_Emit_LogSwap(SwapExactAmountOut_FuzzScenario memory _fuzz) public happyPath(_fuzz) { - uint256 _tokenAmountIn = calcInGivenOut( - _fuzz.tokenInBalance, - _fuzz.tokenInDenorm, - _fuzz.tokenOutBalance, - _fuzz.tokenOutDenorm, - _fuzz.tokenAmountOut, - _fuzz.swapFee - ); - - vm.expectEmit(); - emit IBPool.LOG_SWAP(address(this), tokenIn, tokenOut, _tokenAmountIn, _fuzz.tokenAmountOut); - bPool.swapExactAmountOut(tokenIn, type(uint256).max, tokenOut, _fuzz.tokenAmountOut, type(uint256).max); - } - - function test_Set_ReentrancyLock(SwapExactAmountOut_FuzzScenario memory _fuzz) public happyPath(_fuzz) { - _expectSetReentrancyLock(); - bPool.swapExactAmountOut(tokenIn, type(uint256).max, tokenOut, _fuzz.tokenAmountOut, type(uint256).max); - } - - function test_Pull_TokenAmountIn(SwapExactAmountOut_FuzzScenario memory _fuzz) public happyPath(_fuzz) { - uint256 _tokenAmountIn = calcInGivenOut( - _fuzz.tokenInBalance, - _fuzz.tokenInDenorm, - _fuzz.tokenOutBalance, - _fuzz.tokenOutDenorm, - _fuzz.tokenAmountOut, - _fuzz.swapFee - ); - - vm.expectCall( - address(tokenIn), - abi.encodeWithSelector(IERC20.transferFrom.selector, address(this), address(bPool), _tokenAmountIn) - ); - bPool.swapExactAmountOut(tokenIn, type(uint256).max, tokenOut, _fuzz.tokenAmountOut, type(uint256).max); - } - - function test_Push_TokenAmountOut(SwapExactAmountOut_FuzzScenario memory _fuzz) public happyPath(_fuzz) { - vm.expectCall( - address(tokenOut), abi.encodeWithSelector(IERC20.transfer.selector, address(this), _fuzz.tokenAmountOut) - ); - bPool.swapExactAmountOut(tokenIn, type(uint256).max, tokenOut, _fuzz.tokenAmountOut, type(uint256).max); - } - - function test_Returns_AmountAndPrice(SwapExactAmountOut_FuzzScenario memory _fuzz) public happyPath(_fuzz) { - uint256 _expectedTokenAmountIn = calcInGivenOut( - _fuzz.tokenInBalance, - _fuzz.tokenInDenorm, - _fuzz.tokenOutBalance, - _fuzz.tokenOutDenorm, - _fuzz.tokenAmountOut, - _fuzz.swapFee - ); - uint256 _expectedSpotPriceAfter = calcSpotPrice( - _fuzz.tokenInBalance + _expectedTokenAmountIn, - _fuzz.tokenInDenorm, - _fuzz.tokenOutBalance - _fuzz.tokenAmountOut, - _fuzz.tokenOutDenorm, - _fuzz.swapFee - ); - - (uint256 _tokenAmountIn, uint256 _spotPriceAfter) = - bPool.swapExactAmountOut(tokenIn, type(uint256).max, tokenOut, _fuzz.tokenAmountOut, type(uint256).max); - - assertEq(_expectedTokenAmountIn, _tokenAmountIn); - assertEq(_expectedSpotPriceAfter, _spotPriceAfter); - } - - function test_Emit_LogCall(SwapExactAmountOut_FuzzScenario memory _fuzz) public happyPath(_fuzz) { - vm.expectEmit(); - bytes memory _data = abi.encodeWithSelector( - BPool.swapExactAmountOut.selector, tokenIn, type(uint256).max, tokenOut, _fuzz.tokenAmountOut, type(uint256).max - ); - emit IBPool.LOG_CALL(BPool.swapExactAmountOut.selector, address(this), _data); - - bPool.swapExactAmountOut(tokenIn, type(uint256).max, tokenOut, _fuzz.tokenAmountOut, type(uint256).max); - } -} - contract BPool_Unit_JoinswapExternAmountIn is BasePoolTest { address tokenIn; diff --git a/test/unit/BPool/BPool_SwapExactAmountOut.t.sol b/test/unit/BPool/BPool_SwapExactAmountOut.t.sol new file mode 100644 index 00000000..f648b9a9 --- /dev/null +++ b/test/unit/BPool/BPool_SwapExactAmountOut.t.sol @@ -0,0 +1,134 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.25; + +import {BPoolBase} from './BPoolBase.sol'; +import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; + +import {BNum} from 'contracts/BNum.sol'; +import {IBPool} from 'interfaces/IBPool.sol'; + +contract BPoolSwapExactAmountOut is BPoolBase, BNum { + // Valid scenario + address public tokenIn; + uint256 public tokenAmountOut = 1e18; + + uint256 public tokenInBalance = 50e18; + uint256 public tokenOutBalance = 20e18; + // pool is expected to keep 3X the value of tokenOut than tokenIn + uint256 public tokenInWeight = 1e18; + uint256 public tokenOutWeight = 3e18; + + address public tokenOut; + // (tokenInBalance / tokenInWeight) / (tokenOutBalance/ tokenOutWeight) + uint256 public spotPriceBeforeSwapWithoutFee = 7.5e18; + uint256 public spotPriceBeforeSwap = bmul(spotPriceBeforeSwapWithoutFee, bdiv(BONE, bsub(BONE, MIN_FEE))); + // from bmath: bi*((bo/(bo-ao))^(wo/wi) - 1)/(1-f) + // (50*((20/(20-1))^(3) - 1))/(1-10^-6) + uint256 public expectedAmountIn = 8.317547317401523552e18; + // (tokenInBalance / tokenInWeight) / (tokenOutBalance/ tokenOutWeight) + // (50+8.317547317401523553 / 1) / (19/ 3) + uint256 public spotPriceAfterSwapWithoutFee = 9.208033786958135298e18; + uint256 public spotPriceAfterSwap = bmul(spotPriceAfterSwapWithoutFee, bdiv(BONE, bsub(BONE, MIN_FEE))); + + function setUp() public virtual override { + super.setUp(); + tokenIn = tokens[0]; + tokenOut = tokens[1]; + bPool.set__finalized(true); + bPool.set__tokens(tokens); + _setRecord(tokenIn, IBPool.Record({bound: true, index: 0, denorm: tokenInWeight})); + _setRecord(tokenOut, IBPool.Record({bound: true, index: 1, denorm: tokenOutWeight})); + + vm.mockCall(tokenIn, abi.encodePacked(IERC20.balanceOf.selector), abi.encode(uint256(tokenInBalance))); + vm.mockCall(tokenOut, abi.encodePacked(IERC20.balanceOf.selector), abi.encode(uint256(tokenOutBalance))); + } + + function test_RevertWhen_ReentrancyLockIsSet() external { + bPool.call__setLock(_MUTEX_TAKEN); + // it should revert + vm.expectRevert(IBPool.BPool_Reentrancy.selector); + bPool.swapExactAmountOut(tokenIn, expectedAmountIn, tokenOut, tokenAmountOut, spotPriceAfterSwap); + } + + function test_RevertWhen_PoolIsNotFinalized() external { + bPool.set__finalized(false); + // it should revert + vm.expectRevert(IBPool.BPool_PoolNotFinalized.selector); + bPool.swapExactAmountOut(tokenIn, expectedAmountIn, tokenOut, tokenAmountOut, spotPriceAfterSwap); + } + + function test_RevertWhen_TokenInIsNotBound() external { + // it should revert + vm.expectRevert(IBPool.BPool_TokenNotBound.selector); + bPool.swapExactAmountOut(makeAddr('unkonwn token'), expectedAmountIn, tokenOut, tokenAmountOut, spotPriceAfterSwap); + } + + function test_RevertWhen_TokenOutIsNotBound() external { + // it should revert + vm.expectRevert(IBPool.BPool_TokenNotBound.selector); + bPool.swapExactAmountOut(tokenIn, expectedAmountIn, makeAddr('unkonwn token'), tokenAmountOut, spotPriceAfterSwap); + } + + function test_RevertWhen_TokenOutExceedsMaxAllowedRatio(uint256 tokenAmountOut_) external { + tokenAmountOut_ = bound(tokenAmountOut_, bmul(tokenOutBalance, MAX_OUT_RATIO + 1), type(uint256).max); + // it should revert + vm.expectRevert(IBPool.BPool_TokenAmountOutAboveMaxOut.selector); + bPool.swapExactAmountOut(tokenIn, expectedAmountIn, tokenOut, tokenAmountOut_, spotPriceAfterSwap); + } + + function test_RevertWhen_SpotPriceBeforeSwapExceedsMaxPrice() external { + vm.expectRevert(IBPool.BPool_SpotPriceAboveMaxPrice.selector); + // it should revert + bPool.swapExactAmountOut(tokenIn, expectedAmountIn, tokenOut, tokenAmountOut, spotPriceBeforeSwap - 1); + } + + function test_RevertWhen_SpotPriceAfterSwapExceedsMaxPrice() external { + vm.expectRevert(IBPool.BPool_SpotPriceAboveMaxPrice.selector); + // it should revert + bPool.swapExactAmountOut(tokenIn, expectedAmountIn, tokenOut, tokenAmountOut, spotPriceAfterSwap - 1); + } + + function test_RevertWhen_RequiredTokenInIsMoreThanMaxAmountIn() external { + vm.expectRevert(IBPool.BPool_TokenAmountInAboveMaxAmountIn.selector); + // it should revert + bPool.swapExactAmountOut(tokenIn, expectedAmountIn - 1, tokenOut, tokenAmountOut, spotPriceAfterSwap); + } + + function test_RevertWhen_TokenRatioAfterSwapExceedsSpotPriceBeforeSwap() external { + // it should revert + // skipping since the code for this is unreachable without manually + // overriding `calcSpotPrice` in a mock: + // P_{sb} = \frac{\frac{b_i}{w_i}}{\frac{b_o}{w_o}} + // P_{sa} = \frac{\frac{b_i + a_i}{w_i}}{\frac{b_o - a_o}{w_o}} + // ...and both a_i (amount in) and a_o (amount out) are uints + vm.skip(true); + } + + function test_WhenPreconditionsAreMet() external { + // it sets reentrancy lock + bPool.expectCall__setLock(_MUTEX_TAKEN); + // it calls _pullUnderlying for tokenIn + bPool.mock_call__pullUnderlying(tokenIn, address(this), expectedAmountIn); + bPool.expectCall__pullUnderlying(tokenIn, address(this), expectedAmountIn); + // it calls _pushUnderlying for tokenOut + bPool.mock_call__pushUnderlying(tokenOut, address(this), tokenAmountOut); + bPool.expectCall__pushUnderlying(tokenOut, address(this), tokenAmountOut); + bytes memory _data = abi.encodeCall( + IBPool.swapExactAmountOut, (tokenIn, expectedAmountIn, tokenOut, tokenAmountOut, spotPriceAfterSwap) + ); + // it emits a LOG_CALL event + vm.expectEmit(); + emit IBPool.LOG_CALL(IBPool.swapExactAmountOut.selector, address(this), _data); + // it emits a LOG_SWAP event + vm.expectEmit(); + emit IBPool.LOG_SWAP(address(this), tokenIn, tokenOut, expectedAmountIn, tokenAmountOut); + // it returns the tokenIn amount swapped + // it returns the spot price after the swap + (uint256 in_, uint256 priceAfter) = + bPool.swapExactAmountOut(tokenIn, expectedAmountIn, tokenOut, tokenAmountOut, spotPriceAfterSwap); + assertEq(in_, expectedAmountIn); + assertEq(priceAfter, spotPriceAfterSwap); + // it clears the reeentrancy lock + assertEq(bPool.call__getLock(), _MUTEX_FREE); + } +} diff --git a/test/unit/BPool/BPool_SwapExactAmountOut.tree b/test/unit/BPool/BPool_SwapExactAmountOut.tree new file mode 100644 index 00000000..6535647a --- /dev/null +++ b/test/unit/BPool/BPool_SwapExactAmountOut.tree @@ -0,0 +1,28 @@ +BPool::SwapExactAmountOut +├── when reentrancy lock is set +│ └── it should revert +├── when pool is not finalized +│ └── it should revert +├── when token in is not bound +│ └── it should revert +├── when token out is not bound +│ └── it should revert +├── when token out exceeds max allowed ratio +│ └── it should revert +├── when spot price before swap exceeds maxPrice +│ └── it should revert +├── when spot price after swap exceeds maxPrice +│ └── it should revert +├── when required tokenIn is more than maxAmountIn +│ └── it should revert +├── when token ratio after swap exceeds spot price before swap +│ └── it should revert +└── when preconditions are met + ├── it emits a LOG_CALL event + ├── it sets the reentrancy lock + ├── it emits a LOG_SWAP event + ├── it calls _pullUnderlying for tokenIn + ├── it calls _pushUnderlying for tokenOut + ├── it returns the tokenIn amount swapped + ├── it returns the spot price after the swap + └── it clears the reeentrancy lock From 598b6ee3bba109d45c7a4b2ae0601bc510df9edf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wei=C3=9Fer=20Hase?= Date: Tue, 16 Jul 2024 18:52:49 +0200 Subject: [PATCH 2/2] feat: increasing max swap fee to 99.9999% (#158) * feat: increasing max swap fee to 99.999% * fix: updating gas snapshots * feat: improving and cleaning BMath tree * fix: messup in tree --- .forge-snapshots/newBCoWFactory.snap | 2 +- .forge-snapshots/newBCoWPool.snap | 2 +- .forge-snapshots/newBFactory.snap | 2 +- .forge-snapshots/newBPool.snap | 2 +- src/contracts/BConst.sol | 2 +- test/unit/BMath.t.sol | 28 ++++++++++++++++++---------- test/unit/BMath.tree | 20 +++++++++++--------- test/unit/BPool.t.sol | 2 ++ 8 files changed, 36 insertions(+), 24 deletions(-) diff --git a/.forge-snapshots/newBCoWFactory.snap b/.forge-snapshots/newBCoWFactory.snap index 5f4e5d08..2be4d980 100644 --- a/.forge-snapshots/newBCoWFactory.snap +++ b/.forge-snapshots/newBCoWFactory.snap @@ -1 +1 @@ -4890648 \ No newline at end of file +4899289 \ No newline at end of file diff --git a/.forge-snapshots/newBCoWPool.snap b/.forge-snapshots/newBCoWPool.snap index 5c3c9e3f..02c4c1d9 100644 --- a/.forge-snapshots/newBCoWPool.snap +++ b/.forge-snapshots/newBCoWPool.snap @@ -1 +1 @@ -4034907 \ No newline at end of file +4042925 \ No newline at end of file diff --git a/.forge-snapshots/newBFactory.snap b/.forge-snapshots/newBFactory.snap index d28ec6cf..2eed98b7 100644 --- a/.forge-snapshots/newBFactory.snap +++ b/.forge-snapshots/newBFactory.snap @@ -1 +1 @@ -4131877 \ No newline at end of file +4140477 \ No newline at end of file diff --git a/.forge-snapshots/newBPool.snap b/.forge-snapshots/newBPool.snap index e4fc526d..e3eb6576 100644 --- a/.forge-snapshots/newBPool.snap +++ b/.forge-snapshots/newBPool.snap @@ -1 +1 @@ -3478592 \ No newline at end of file +3486610 \ No newline at end of file diff --git a/src/contracts/BConst.sol b/src/contracts/BConst.sol index 3a184a01..5e42302c 100644 --- a/src/contracts/BConst.sol +++ b/src/contracts/BConst.sol @@ -17,7 +17,7 @@ contract BConst { /// @notice The minimum swap fee that can be set. uint256 public constant MIN_FEE = BONE / 10 ** 6; /// @notice The maximum swap fee that can be set. - uint256 public constant MAX_FEE = BONE / 10; + uint256 public constant MAX_FEE = BONE - MIN_FEE; /// @notice The immutable exit fee percentage uint256 public constant EXIT_FEE = 0; diff --git a/test/unit/BMath.t.sol b/test/unit/BMath.t.sol index 25158305..ad91456b 100644 --- a/test/unit/BMath.t.sol +++ b/test/unit/BMath.t.sol @@ -113,15 +113,6 @@ contract BMathTest is Test, BConst { bMath.calcOutGivenIn(balanceIn, weightIn, balanceOut, weightOut, amountIn, _swapFee); } - function test_CalcOutGivenInWhenSwapFeeEqualsBONE() external virtual { - uint256 _swapFee = BONE; - - // it should return zero - uint256 _amountOut = bMath.calcOutGivenIn(balanceIn, weightIn, balanceOut, weightOut, amountIn, _swapFee); - - assertEq(_amountOut, 0); - } - function test_CalcOutGivenInRevertWhen_TokenAmountInTooBig(uint256 _amountIn) external { _amountIn = bound(_amountIn, type(uint256).max / (BONE - swapFee) + 1, type(uint256).max); @@ -154,6 +145,15 @@ contract BMathTest is Test, BConst { bMath.calcOutGivenIn(_balanceIn, weightIn, balanceOut, weightOut, amountIn, _swapFee); } + function test_CalcOutGivenInWhenSwapFeeEqualsBONE() external virtual { + uint256 _swapFee = BONE; + + // it should return zero + uint256 _amountOut = bMath.calcOutGivenIn(balanceIn, weightIn, balanceOut, weightOut, amountIn, _swapFee); + + assertEq(_amountOut, 0); + } + function test_CalcOutGivenInWhenTokenWeightInIsZero() external virtual { uint256 _weightIn = 0; @@ -477,7 +477,7 @@ contract BMathTest is Test, BConst { bMath.calcPoolInGivenSingleOut(_balanceOut, weightOut, poolSupply, totalWeight, amountOut, swapFee); } - function test_CalcPoolInGivenSingleOutRevertWhen_SwapFeeIs1AndTokenWeightOutIsZero() external { + function test_CalcPoolInGivenSingleOutRevertWhen_SwapFeeEqualsBONEAndTokenWeightOutIsZero() external { uint256 _swapFee = BONE; uint256 _weightOut = 0; @@ -488,6 +488,14 @@ contract BMathTest is Test, BConst { bMath.calcPoolInGivenSingleOut(balanceOut, _weightOut, poolSupply, totalWeight, amountOut, _swapFee); } + function test_CalcPoolInGivenSingleOutRevertWhen_SwapFeeGreaterThanBONE() external { + // it should revert + // subtraction underflow + vm.expectRevert(BNum.BNum_SubUnderflow.selector); + + bMath.calcPoolInGivenSingleOut(balanceOut, weightOut, poolSupply, totalWeight, amountOut, BONE + 1); + } + function test_CalcPoolInGivenSingleOutWhenTokenAmountOutIsZero() external virtual { uint256 _amountOut = 0; diff --git a/test/unit/BMath.tree b/test/unit/BMath.tree index be0417b9..90f8a185 100644 --- a/test/unit/BMath.tree +++ b/test/unit/BMath.tree @@ -5,9 +5,9 @@ BMathTest::calcSpotPrice │ └── it should revert // division by zero ├── when weighted token balance out is zero │ └── it should revert // division by zero -├── when swapFee greater than BONE +├── when swap fee greater than BONE │ └── it should revert // subtraction underflow -├── when swapFee equals BONE +├── when swap fee equals BONE │ └── it should revert // division by zero ├── when swap fee is zero │ └── it should return correct value @@ -21,14 +21,14 @@ BMathTest::calcOutGivenIn │ └── it should revert // division by zero ├── when swap fee greater than BONE │ └── it should revert // subtraction underflow -├── when swap fee equals BONE -│ └── it should return zero ├── when token amount in too big │ └── it should revert // ai * (1 - sf) > uint256 max ├── when token balance in and amount in are zero -│ └── it should revert // bi + (ai * (1 - swapFee)) = 0 +│ └── it should revert // bi + (ai * (1 - sf)) = 0 ├── when token balance in is zero and swap fee equals BONE -│ └── it should revert // bi + (ai * (1 - swapFee)) = 0 +│ └── it should revert // bi + (ai * (1 - sf)) = 0 +├── when swap fee equals BONE +│ └── it should return zero ├── when token weight in is zero │ └── it should return zero ├── when token weights are equal @@ -53,9 +53,9 @@ BMathTest::calcInGivenOut │ └── it should revert // subtraction underflow ├── when token amount out equals token balance out │ └── it should revert // division by zero -├── when swapFee greater than BONE +├── when swap fee greater than BONE │ └── it should revert // subtraction underflow -├── when swapFee equals BONE +├── when swap fee equals BONE │ └── it should revert // division by zero ├── when token weight out is zero │ └── it should return zero @@ -121,8 +121,10 @@ BMathTest::calcSingleOutGivenPoolIn BMathTest::calcPoolInGivenSingleOut ├── when token balance out is zero │ └── it should revert // subtraction underflow -├── when swap fee is 1 and token weight out is zero +├── when swap fee equals BONE and token weight out is zero │ └── it should revert // division by zero +├── when swap fee greater than BONE +│ └── it should revert // subtraction underflow ├── when token amount out is zero │ └── it should return zero ├── when pool supply is zero diff --git a/test/unit/BPool.t.sol b/test/unit/BPool.t.sol index 39f5fedd..97ad7488 100644 --- a/test/unit/BPool.t.sol +++ b/test/unit/BPool.t.sol @@ -243,6 +243,7 @@ abstract contract BasePoolTest is Test, BConst, Utils, BMath { uint256 _zoo = bsub(BONE, _normalizedWeight); uint256 _zar = bmul(_zoo, _swapFee); uint256 _tokenAmountOutBeforeSwapFee = bdiv(_tokenAmountOut, bsub(BONE, _zar)); + vm.assume(_tokenOutBalance >= _tokenAmountOutBeforeSwapFee); uint256 _newTokenOutBalance = bsub(_tokenOutBalance, _tokenAmountOutBeforeSwapFee); vm.assume(_newTokenOutBalance < type(uint256).max / _tokenOutBalance); @@ -1379,6 +1380,7 @@ contract BPool_Unit_ExitswapPoolAmountIn is BasePoolTest { _fuzz.poolAmountIn, _fuzz.swapFee ); + vm.assume(_fuzz.tokenOutBalance < type(uint256).max / MAX_OUT_RATIO); vm.assume(_tokenAmountOut > bmul(_fuzz.tokenOutBalance, MAX_OUT_RATIO)); _setValues(_fuzz);