Skip to content

Commit

Permalink
feat: add btt tests for exitswap pool amount in (#169)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
0xteddybear authored Jul 22, 2024
1 parent dfbed26 commit cb92f9d
Show file tree
Hide file tree
Showing 3 changed files with 132 additions and 249 deletions.
249 changes: 0 additions & 249 deletions test/unit/BPool.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
108 changes: 108 additions & 0 deletions test/unit/BPool/BPool_ExitswapPoolAmountIn.t.sol
Original file line number Diff line number Diff line change
@@ -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);
}
}
24 changes: 24 additions & 0 deletions test/unit/BPool/BPool_ExitswapPoolAmountIn.tree
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit cb92f9d

Please sign in to comment.