From cb92f9da93011faa65b98d6448adba82492a7479 Mon Sep 17 00:00:00 2001 From: teddy Date: Mon, 22 Jul 2024 13:27:56 -0300 Subject: [PATCH] feat: add btt tests for exitswap pool amount in (#169) * refactor: explicitly set weights in every scenario, remove it from base contract * test: btt tests for joinswapExternAmountIn * chore: remove preexisting unit tests replaced by ones in this pr * test: query tokenIn balance * test: btt tests for exitswapPoolAmountIn * chore: remove preexisting unit tests replaced by ones in this pr * fix: feedback from review --- test/unit/BPool.t.sol | 249 ------------------ .../BPool/BPool_ExitswapPoolAmountIn.t.sol | 108 ++++++++ .../BPool/BPool_ExitswapPoolAmountIn.tree | 24 ++ 3 files changed, 132 insertions(+), 249 deletions(-) create mode 100644 test/unit/BPool/BPool_ExitswapPoolAmountIn.t.sol create mode 100644 test/unit/BPool/BPool_ExitswapPoolAmountIn.tree diff --git a/test/unit/BPool.t.sol b/test/unit/BPool.t.sol index 096e2889..f9828571 100644 --- a/test/unit/BPool.t.sol +++ b/test/unit/BPool.t.sol @@ -1051,255 +1051,6 @@ contract BPool_Unit_JoinswapPoolAmountOut is BasePoolTest { } } -contract BPool_Unit_ExitswapPoolAmountIn is BasePoolTest { - address tokenOut; - - struct ExitswapPoolAmountIn_FuzzScenario { - uint256 poolAmountIn; - uint256 tokenOutBalance; - uint256 tokenOutDenorm; - uint256 totalSupply; - uint256 totalWeight; - uint256 swapFee; - } - - function _setValues(ExitswapPoolAmountIn_FuzzScenario memory _fuzz) internal { - tokenOut = tokens[0]; - - // Create mocks for tokenOut - _mockTransfer(tokenOut); - - // Set balances - _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); - // Set balance - _setPoolBalance(address(this), _fuzz.poolAmountIn); // give LP tokens to fn caller - // Set totalSupply - _setTotalSupply(_fuzz.totalSupply - _fuzz.poolAmountIn); - // Set totalWeight - _setTotalWeight(_fuzz.totalWeight); - } - - function _assumeHappyPath(ExitswapPoolAmountIn_FuzzScenario memory _fuzz) internal pure { - // safe bound assumptions - _fuzz.tokenOutDenorm = bound(_fuzz.tokenOutDenorm, MIN_WEIGHT, MAX_WEIGHT); - _fuzz.swapFee = bound(_fuzz.swapFee, MIN_FEE, MAX_FEE); - _fuzz.totalWeight = bound(_fuzz.totalWeight, MIN_WEIGHT * TOKENS_AMOUNT, MAX_TOTAL_WEIGHT); - _fuzz.totalSupply = bound(_fuzz.totalSupply, INIT_POOL_SUPPLY, type(uint256).max); - - // max - vm.assume(_fuzz.poolAmountIn < _fuzz.totalSupply); - vm.assume(_fuzz.totalSupply < type(uint256).max - _fuzz.poolAmountIn); - - // min - vm.assume(_fuzz.tokenOutBalance >= MIN_BALANCE); - - // internal calculation for calcSingleOutGivenPoolIn - _assumeCalcSingleOutGivenPoolIn( - _fuzz.tokenOutBalance, - _fuzz.tokenOutDenorm, - _fuzz.totalSupply, - _fuzz.totalWeight, - _fuzz.poolAmountIn, - _fuzz.swapFee - ); - - uint256 _tokenAmountOut = calcSingleOutGivenPoolIn( - _fuzz.tokenOutBalance, - _fuzz.tokenOutDenorm, - _fuzz.totalSupply, - _fuzz.totalWeight, - _fuzz.poolAmountIn, - _fuzz.swapFee - ); - - // max - vm.assume(_fuzz.tokenOutBalance < type(uint256).max - _tokenAmountOut); - - // MAX_OUT_RATIO - vm.assume(_fuzz.tokenOutBalance < type(uint256).max / MAX_OUT_RATIO); - vm.assume(_tokenAmountOut <= bmul(_fuzz.tokenOutBalance, MAX_OUT_RATIO)); - } - - modifier happyPath(ExitswapPoolAmountIn_FuzzScenario memory _fuzz) { - _assumeHappyPath(_fuzz); - _setValues(_fuzz); - _; - } - - function test_Revert_NotFinalized(ExitswapPoolAmountIn_FuzzScenario memory _fuzz) public happyPath(_fuzz) { - _setFinalize(false); - - vm.expectRevert(IBPool.BPool_PoolNotFinalized.selector); - bPool.exitswapPoolAmountIn(tokenOut, _fuzz.poolAmountIn, 0); - } - - function test_Revert_NotBound( - ExitswapPoolAmountIn_FuzzScenario memory _fuzz, - address _tokenOut - ) public happyPath(_fuzz) { - vm.assume(_tokenOut != tokenOut); - assumeNotForgeAddress(_tokenOut); - - vm.expectRevert(IBPool.BPool_TokenNotBound.selector); - bPool.exitswapPoolAmountIn(_tokenOut, _fuzz.poolAmountIn, 0); - } - - function test_Revert_TokenAmountOutBelowMinAmountOut( - ExitswapPoolAmountIn_FuzzScenario memory _fuzz, - uint256 _minAmountOut - ) public happyPath(_fuzz) { - uint256 _tokenAmountOut = calcSingleOutGivenPoolIn( - _fuzz.tokenOutBalance, - _fuzz.tokenOutDenorm, - _fuzz.totalSupply, - _fuzz.totalWeight, - _fuzz.poolAmountIn, - _fuzz.swapFee - ); - _minAmountOut = bound(_minAmountOut, _tokenAmountOut + 1, type(uint256).max); - - vm.expectRevert(IBPool.BPool_TokenAmountOutBelowMinAmountOut.selector); - bPool.exitswapPoolAmountIn(tokenOut, _fuzz.poolAmountIn, _minAmountOut); - } - - function test_Revert_TokenAmountOutAboveMaxOut(ExitswapPoolAmountIn_FuzzScenario memory _fuzz) public { - // Replicating _assumeHappyPath, but removing irrelevant assumptions and conditioning the revert - _fuzz.tokenOutDenorm = bound(_fuzz.tokenOutDenorm, MIN_WEIGHT, MAX_WEIGHT); - _fuzz.swapFee = bound(_fuzz.swapFee, MIN_FEE, MAX_FEE); - _fuzz.totalWeight = bound(_fuzz.totalWeight, MIN_WEIGHT * TOKENS_AMOUNT, MAX_TOTAL_WEIGHT); - _fuzz.tokenOutBalance = bound(_fuzz.tokenOutBalance, MIN_BALANCE, type(uint256).max / MAX_OUT_RATIO); - _fuzz.totalSupply = bound(_fuzz.totalSupply, INIT_POOL_SUPPLY, type(uint256).max); - vm.assume(_fuzz.totalSupply < type(uint256).max - _fuzz.poolAmountIn); - vm.assume(_fuzz.poolAmountIn < _fuzz.totalSupply); - _assumeCalcSingleOutGivenPoolIn( - _fuzz.tokenOutBalance, - _fuzz.tokenOutDenorm, - _fuzz.totalSupply, - _fuzz.totalWeight, - _fuzz.poolAmountIn, - _fuzz.swapFee - ); - uint256 _tokenAmountOut = calcSingleOutGivenPoolIn( - _fuzz.tokenOutBalance, - _fuzz.tokenOutDenorm, - _fuzz.totalSupply, - _fuzz.totalWeight, - _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); - - vm.expectRevert(IBPool.BPool_TokenAmountOutAboveMaxOut.selector); - bPool.exitswapPoolAmountIn(tokenOut, _fuzz.poolAmountIn, 0); - } - - function test_Revert_Reentrancy(ExitswapPoolAmountIn_FuzzScenario memory _fuzz) public happyPath(_fuzz) { - _expectRevertByReentrancy(); - bPool.exitswapPoolAmountIn(tokenOut, _fuzz.poolAmountIn, 0); - } - - function test_Emit_LogExit(ExitswapPoolAmountIn_FuzzScenario memory _fuzz) public happyPath(_fuzz) { - uint256 _tokenAmountOut = calcSingleOutGivenPoolIn( - _fuzz.tokenOutBalance, - _fuzz.tokenOutDenorm, - _fuzz.totalSupply, - _fuzz.totalWeight, - _fuzz.poolAmountIn, - _fuzz.swapFee - ); - - vm.expectEmit(); - emit IBPool.LOG_EXIT(address(this), tokenOut, _tokenAmountOut); - - bPool.exitswapPoolAmountIn(tokenOut, _fuzz.poolAmountIn, 0); - } - - function test_Pull_PoolShare(ExitswapPoolAmountIn_FuzzScenario memory _fuzz) public happyPath(_fuzz) { - uint256 _balanceBefore = bPool.balanceOf(address(this)); - - bPool.exitswapPoolAmountIn(tokenOut, _fuzz.poolAmountIn, 0); - - assertEq(bPool.balanceOf(address(this)), _balanceBefore - _fuzz.poolAmountIn); - } - - function test_Set_ReentrancyLock(ExitswapPoolAmountIn_FuzzScenario memory _fuzz) public happyPath(_fuzz) { - _expectSetReentrancyLock(); - bPool.exitswapPoolAmountIn(tokenOut, _fuzz.poolAmountIn, 0); - } - - function test_Burn_PoolShare(ExitswapPoolAmountIn_FuzzScenario memory _fuzz) public happyPath(_fuzz) { - uint256 _totalSupplyBefore = bPool.totalSupply(); - uint256 _exitFee = bmul(_fuzz.poolAmountIn, EXIT_FEE); - - bPool.exitswapPoolAmountIn(tokenOut, _fuzz.poolAmountIn, 0); - - assertEq(bPool.totalSupply(), _totalSupplyBefore - bsub(_fuzz.poolAmountIn, _exitFee)); - } - - function test_Push_PoolShare(ExitswapPoolAmountIn_FuzzScenario memory _fuzz) public happyPath(_fuzz) { - address _factoryAddress = bPool.FACTORY(); - uint256 _balanceBefore = bPool.balanceOf(_factoryAddress); - uint256 _exitFee = bmul(_fuzz.poolAmountIn, EXIT_FEE); - - bPool.exitswapPoolAmountIn(tokenOut, _fuzz.poolAmountIn, 0); - - assertEq(bPool.balanceOf(_factoryAddress), _balanceBefore - _fuzz.poolAmountIn + _exitFee); - } - - function test_Push_Underlying(ExitswapPoolAmountIn_FuzzScenario memory _fuzz) public happyPath(_fuzz) { - uint256 _tokenAmountOut = calcSingleOutGivenPoolIn( - _fuzz.tokenOutBalance, - _fuzz.tokenOutDenorm, - _fuzz.totalSupply, - _fuzz.totalWeight, - _fuzz.poolAmountIn, - _fuzz.swapFee - ); - - vm.expectCall(address(tokenOut), abi.encodeWithSelector(IERC20.transfer.selector, address(this), _tokenAmountOut)); - bPool.exitswapPoolAmountIn(tokenOut, _fuzz.poolAmountIn, 0); - } - - function test_Returns_TokenAmountOut(ExitswapPoolAmountIn_FuzzScenario memory _fuzz) public happyPath(_fuzz) { - uint256 _expectedTokenAmountOut = calcSingleOutGivenPoolIn( - _fuzz.tokenOutBalance, - _fuzz.tokenOutDenorm, - _fuzz.totalSupply, - _fuzz.totalWeight, - _fuzz.poolAmountIn, - _fuzz.swapFee - ); - - (uint256 _tokenAmountOut) = bPool.exitswapPoolAmountIn(tokenOut, _fuzz.poolAmountIn, 0); - - assertEq(_tokenAmountOut, _expectedTokenAmountOut); - } - - function test_Emit_LogCall(ExitswapPoolAmountIn_FuzzScenario memory _fuzz) public happyPath(_fuzz) { - vm.expectEmit(); - bytes memory _data = abi.encodeWithSelector(BPool.exitswapPoolAmountIn.selector, tokenOut, _fuzz.poolAmountIn, 0); - emit IBPool.LOG_CALL(BPool.exitswapPoolAmountIn.selector, address(this), _data); - - bPool.exitswapPoolAmountIn(tokenOut, _fuzz.poolAmountIn, 0); - } -} - contract BPool_Unit_ExitswapExternAmountOut is BasePoolTest { address tokenOut; diff --git a/test/unit/BPool/BPool_ExitswapPoolAmountIn.t.sol b/test/unit/BPool/BPool_ExitswapPoolAmountIn.t.sol new file mode 100644 index 00000000..2650fddf --- /dev/null +++ b/test/unit/BPool/BPool_ExitswapPoolAmountIn.t.sol @@ -0,0 +1,108 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.25; + +import {BPoolBase} from './BPoolBase.sol'; + +import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; + +import {BMath} from 'contracts/BMath.sol'; +import {BNum} from 'contracts/BNum.sol'; +import {IBPool} from 'interfaces/IBPool.sol'; + +contract BPoolExitSwapPoolAmountIn is BPoolBase, BMath { + // Valid scenario: + address public tokenOut; + uint256 public tokenOutWeight = 3e18; + uint256 public tokenOutBalance = 400e18; + uint256 public totalWeight = 9e18; + uint256 public poolAmountIn = 1e18; + // calcSingleOutGivenPoolIn(400, 3, 100, 9, 1, 10^(-6)) + uint256 public expectedAmountOut = 11.880392079733333329e18; + + function setUp() public virtual override { + super.setUp(); + tokenOut = tokens[1]; + _setRecord(tokenOut, IBPool.Record({bound: true, index: 0, denorm: tokenOutWeight})); + bPool.set__tokens(tokens); + bPool.set__totalWeight(totalWeight); + bPool.set__finalized(true); + bPool.call__mintPoolShare(INIT_POOL_SUPPLY); + + vm.mockCall(tokenOut, abi.encodePacked(IERC20.balanceOf.selector), abi.encode(uint256(tokenOutBalance))); + + bPool.mock_call__pullPoolShare(address(this), poolAmountIn); + bPool.mock_call__burnPoolShare(poolAmountIn); + bPool.mock_call__pushPoolShare(deployer, 0); + bPool.mock_call__pushUnderlying(tokenOut, address(this), expectedAmountOut); + } + + function test_RevertWhen_ReentrancyLockIsSet() external { + bPool.call__setLock(_MUTEX_TAKEN); + // it should revert + vm.expectRevert(IBPool.BPool_Reentrancy.selector); + bPool.exitswapPoolAmountIn(tokenOut, poolAmountIn, expectedAmountOut); + } + + function test_RevertWhen_PoolIsNotFinalized() external { + bPool.set__finalized(false); + // it should revert + vm.expectRevert(IBPool.BPool_PoolNotFinalized.selector); + bPool.exitswapPoolAmountIn(tokenOut, poolAmountIn, expectedAmountOut); + } + + function test_RevertWhen_TokenIsNotBound() external { + // it should revert + vm.expectRevert(IBPool.BPool_TokenNotBound.selector); + bPool.exitswapPoolAmountIn(makeAddr('unknown token'), poolAmountIn, expectedAmountOut); + } + + function test_RevertWhen_TotalSupplyIsZero() external { + bPool.call__burnPoolShare(INIT_POOL_SUPPLY); + // it should revert + vm.expectRevert(BNum.BNum_SubUnderflow.selector); + bPool.exitswapPoolAmountIn(tokenOut, poolAmountIn, expectedAmountOut); + } + + function test_RevertWhen_ComputedTokenAmountOutIsLessThanMinAmountOut() external { + // it should revert + vm.expectRevert(IBPool.BPool_TokenAmountOutBelowMinAmountOut.selector); + bPool.exitswapPoolAmountIn(tokenOut, poolAmountIn, expectedAmountOut + 1); + } + + function test_RevertWhen_ComputedTokenAmountOutExceedsMaxAllowedRatio() external { + // trying to burn ~20 pool tokens would result in half of the tokenOut + // under management being sent to the caller: + // calcPoolInGivenSingleOut(tokenOutBalance, tokenOutWeight, INIT_POOL_SUPPLY, totalWeight, tokenOutBalance / 2, MIN_FEE); + // and MAX_OUT_RATIO is ~0.3 + uint256 poolAmountIn_ = 20e18; + // it should revert + vm.expectRevert(IBPool.BPool_TokenAmountOutAboveMaxOut.selector); + bPool.exitswapPoolAmountIn(tokenOut, poolAmountIn_, expectedAmountOut); + } + + function test_WhenPreconditionsAreMet() external { + // it sets the reentrancy lock + bPool.expectCall__setLock(_MUTEX_TAKEN); + // it queries token out balance + vm.expectCall(tokenOut, abi.encodeCall(IERC20.balanceOf, (address(bPool)))); + // it pulls poolAmountIn shares + bPool.expectCall__pullPoolShare(address(this), poolAmountIn); + // it burns poolAmountIn - exitFee shares + bPool.expectCall__burnPoolShare(poolAmountIn); + // it sends exitFee to factory + bPool.expectCall__pushPoolShare(deployer, 0); + // it calls _pushUnderlying for token out + bPool.expectCall__pushUnderlying(tokenOut, address(this), expectedAmountOut); + // it emits LOG_CALL event + bytes memory _data = abi.encodeCall(IBPool.exitswapPoolAmountIn, (tokenOut, poolAmountIn, expectedAmountOut)); + vm.expectEmit(); + emit IBPool.LOG_CALL(IBPool.exitswapPoolAmountIn.selector, address(this), _data); + // it emits LOG_EXIT event for token out + emit IBPool.LOG_EXIT(address(this), tokenOut, expectedAmountOut); + // it returns token out amount + uint256 out = bPool.exitswapPoolAmountIn(tokenOut, poolAmountIn, expectedAmountOut); + assertEq(out, expectedAmountOut); + // it clears the reentrancy lock + assertEq(bPool.call__getLock(), _MUTEX_FREE); + } +} diff --git a/test/unit/BPool/BPool_ExitswapPoolAmountIn.tree b/test/unit/BPool/BPool_ExitswapPoolAmountIn.tree new file mode 100644 index 00000000..b4e9d64a --- /dev/null +++ b/test/unit/BPool/BPool_ExitswapPoolAmountIn.tree @@ -0,0 +1,24 @@ +BPool::ExitSwapPoolAmountIn +├── when reentrancy lock is set +│ └── it should revert +├── when pool is not finalized +│ └── it should revert +├── when token is not bound +│ └── it should revert +├── when total supply is zero +│ └── it should revert // subtraction underflow +├── when computed token amount out is less than minAmountOut +│ └── it should revert +├── when computed token amount out exceeds max allowed ratio +│ └── it should revert +└── when preconditions are met + ├── it emits LOG_CALL event + ├── it sets the reentrancy lock + ├── it queries token out balance + ├── it emits LOG_EXIT event for token out + ├── it pulls poolAmountIn shares + ├── it burns poolAmountIn - exitFee shares + ├── it sends exitFee to factory + ├── it calls _pushUnderlying for token out + ├── it returns token out amount + └── it clears the reentrancy lock