From a6d0e0e0dfe36731ce3979c6b85eb1daf1db881e Mon Sep 17 00:00:00 2001 From: teddy Date: Wed, 26 Jun 2024 06:05:26 -0300 Subject: [PATCH 01/48] chore: small improvements from `8a60ea2` (#115) * chore: smallest of fixes * feat: dont publicly expose the current commitment * feat: use _viewlock_ check for commitment * docs: improve readme * feat: use SafeERC20 to support tokens without transfer return value * test: assert the expected pool bytecode is deployed * feat: use SafeERC20 to support underlying tokens without transfer return value --- .forge-snapshots/newBFactory.snap | 2 +- .forge-snapshots/newBPool.snap | 2 +- .forge-snapshots/settlementCoWSwap.snap | 2 +- .../settlementCoWSwapInverse.snap | 2 +- .forge-snapshots/swapExactAmountIn.snap | 2 +- .../swapExactAmountInInverse.snap | 2 +- README.md | 10 +++-- src/contracts/BCoWConst.sol | 8 +--- src/contracts/BCoWFactory.sol | 1 - src/contracts/BCoWPool.sol | 12 +----- src/contracts/BFactory.sol | 6 +-- src/contracts/BPool.sol | 15 +++---- src/interfaces/IBCoWPool.sol | 19 --------- src/interfaces/IBFactory.sol | 5 --- src/interfaces/IBPool.sol | 5 --- test/integration/PoolSwap.t.sol | 2 +- test/manual-smock/MockBCoWFactory.sol | 2 +- test/smock/MockBCoWPool.sol | 39 ------------------- test/unit/BCoWFactory.t.sol | 19 ++++++--- test/unit/BCoWPool.t.sol | 9 +---- test/unit/BFactory.t.sol | 27 +++++++++++-- test/unit/BPool.t.sol | 23 +++++++++-- 22 files changed, 83 insertions(+), 131 deletions(-) delete mode 100644 test/smock/MockBCoWPool.sol diff --git a/.forge-snapshots/newBFactory.snap b/.forge-snapshots/newBFactory.snap index ac9cbd4a..7a494ba2 100644 --- a/.forge-snapshots/newBFactory.snap +++ b/.forge-snapshots/newBFactory.snap @@ -1 +1 @@ -3528278 \ No newline at end of file +3670893 \ No newline at end of file diff --git a/.forge-snapshots/newBPool.snap b/.forge-snapshots/newBPool.snap index 06165dc3..fa6c2442 100644 --- a/.forge-snapshots/newBPool.snap +++ b/.forge-snapshots/newBPool.snap @@ -1 +1 @@ -3315258 \ No newline at end of file +3383845 \ No newline at end of file diff --git a/.forge-snapshots/settlementCoWSwap.snap b/.forge-snapshots/settlementCoWSwap.snap index d095a34d..f2a54e25 100644 --- a/.forge-snapshots/settlementCoWSwap.snap +++ b/.forge-snapshots/settlementCoWSwap.snap @@ -1 +1 @@ -186763 \ No newline at end of file +186729 \ No newline at end of file diff --git a/.forge-snapshots/settlementCoWSwapInverse.snap b/.forge-snapshots/settlementCoWSwapInverse.snap index a1231ec6..2b414ad7 100644 --- a/.forge-snapshots/settlementCoWSwapInverse.snap +++ b/.forge-snapshots/settlementCoWSwapInverse.snap @@ -1 +1 @@ -196603 \ No newline at end of file +196569 \ No newline at end of file diff --git a/.forge-snapshots/swapExactAmountIn.snap b/.forge-snapshots/swapExactAmountIn.snap index c7145a32..89f23e83 100644 --- a/.forge-snapshots/swapExactAmountIn.snap +++ b/.forge-snapshots/swapExactAmountIn.snap @@ -1 +1 @@ -81507 \ No newline at end of file +82842 \ No newline at end of file diff --git a/.forge-snapshots/swapExactAmountInInverse.snap b/.forge-snapshots/swapExactAmountInInverse.snap index 9a5eb54a..c1b3dc0b 100644 --- a/.forge-snapshots/swapExactAmountInInverse.snap +++ b/.forge-snapshots/swapExactAmountInInverse.snap @@ -1 +1 @@ -91166 \ No newline at end of file +92501 \ No newline at end of file diff --git a/README.md b/README.md index b98fe3c8..5a1eac83 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ yarn build # build artifacts to `out/` yarn test # run the tests ``` -## Changes on BPool from (Balancer V1)[https://github.com/balancer/balancer-core] +## Changes on BPool from [Balancer V1](https://github.com/balancer/balancer-core) - Migrated to Foundry project structure - Implementation of interfaces with Natspec documentation - Replaced `require(cond, 'STRING')` for `if(!cond) revert CustomError()` @@ -37,7 +37,7 @@ yarn test # run the tests - Immutably stores CoW Protocol's `SolutionSettler` and `VaultRelayer` addresses at deployment - Immutably stores Cow Protocol's a Domain Separator at deployment (to avoid replay attacks) - Immutably stores Cow Protocol's `GPv2Order.appData` to be allowed to swap -- Gives infinite ERC20 approval to the CoW Protocol's `VaultRelayer` contract +- Gives infinite ERC20 approval to the CoW Protocol's `VaultRelayer` contract at finalization time. - Implements IERC1271 `isValidSignature` method to allow for validating intentions of swaps - Implements a `commit` method to avoid multiple swaps from conflicting with each other. - This is stored in the same transient storage slot as reentrancy locks in order to prevent calls to swap/join functions within a settlement execution or vice versa. @@ -45,10 +45,12 @@ yarn test # run the tests - Validates the `GPv2Order` requirements before allowing the swap ## Features on BCoWFactory -- Added a `logBCoWPool` to log the finalization of BCoWPool contracts, to be called by a child pool +- Added a `logBCoWPool` to log the finalization of BCoWPool contracts, to be called by a child pool. ## Creating a Pool -- Create a new pool by calling `IBFactory.newBPool()` +- Create a new pool by calling the corresponding pool factory: + - `IBFactory.newBPool()` for regular Balancer `BPool`s + - `IBCoWFactory.newBPool()` for Balancer `BCoWPool`s, compatible with CoW Protocol - Give ERC20 allowance to the pool by calling `IERC20.approve(pool, amount)` - Bind tokens one by one by calling `IBPool.bind(token, amount, weight)` - The amount represents the initial balance of the token in the pool (pulled from the caller's balance) diff --git a/src/contracts/BCoWConst.sol b/src/contracts/BCoWConst.sol index 79cff191..c16c8f25 100644 --- a/src/contracts/BCoWConst.sol +++ b/src/contracts/BCoWConst.sol @@ -6,16 +6,10 @@ pragma solidity 0.8.25; * @notice Constants used in the scope of the BCoWPool contract. */ contract BCoWConst { - /** - * @notice The value representing the absence of a commitment. - * @return _emptyCommitment The commitment value representing no commitment. - */ - bytes32 public constant EMPTY_COMMITMENT = bytes32(0); - /** * @notice The largest possible duration of any AMM order, starting from the * current block timestamp. * @return _maxOrderDuration The maximum order duration. */ - uint32 public constant MAX_ORDER_DURATION = 5 * 60; + uint32 public constant MAX_ORDER_DURATION = 5 minutes; } diff --git a/src/contracts/BCoWFactory.sol b/src/contracts/BCoWFactory.sol index c2147917..29576b9c 100644 --- a/src/contracts/BCoWFactory.sol +++ b/src/contracts/BCoWFactory.sol @@ -4,7 +4,6 @@ pragma solidity 0.8.25; import {BCoWPool} from './BCoWPool.sol'; import {BFactory} from './BFactory.sol'; import {IBCoWFactory} from 'interfaces/IBCoWFactory.sol'; -import {IBFactory} from 'interfaces/IBFactory.sol'; import {IBPool} from 'interfaces/IBPool.sol'; /** diff --git a/src/contracts/BCoWPool.sol b/src/contracts/BCoWPool.sol index 9fc3d09a..911ae1b9 100644 --- a/src/contracts/BCoWPool.sol +++ b/src/contracts/BCoWPool.sol @@ -54,13 +54,10 @@ contract BCoWPool is IERC1271, IBCoWPool, BPool, BCoWConst { } /// @inheritdoc IBCoWPool - function commit(bytes32 orderHash) external { + function commit(bytes32 orderHash) external _viewlock_ { if (msg.sender != address(SOLUTION_SETTLER)) { revert CommitOutsideOfSettlement(); } - if (_getLock() != _MUTEX_FREE) { - revert BCoWPool_CommitmentAlreadySet(); - } _setLock(orderHash); } @@ -80,7 +77,7 @@ contract BCoWPool is IERC1271, IBCoWPool, BPool, BCoWConst { revert OrderDoesNotMatchMessageHash(); } - if (orderHash != commitment()) { + if (orderHash != _getLock()) { revert OrderDoesNotMatchCommitmentHash(); } @@ -91,11 +88,6 @@ contract BCoWPool is IERC1271, IBCoWPool, BPool, BCoWConst { return this.isValidSignature.selector; } - /// @inheritdoc IBCoWPool - function commitment() public view returns (bytes32 value) { - value = _getLock(); - } - /// @inheritdoc IBCoWPool function verify(GPv2Order.Data memory order) public view virtual { Record memory inRecord = _records[address(order.buyToken)]; diff --git a/src/contracts/BFactory.sol b/src/contracts/BFactory.sol index 6e2d8436..67f57262 100644 --- a/src/contracts/BFactory.sol +++ b/src/contracts/BFactory.sol @@ -2,6 +2,7 @@ pragma solidity 0.8.25; import {BPool} from './BPool.sol'; +import {SafeERC20} from '@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol'; import {IBFactory} from 'interfaces/IBFactory.sol'; import {IBPool} from 'interfaces/IBPool.sol'; @@ -43,10 +44,7 @@ contract BFactory is IBFactory { revert BFactory_NotBLabs(); } uint256 collected = pool.balanceOf(address(this)); - bool xfer = pool.transfer(_blabs, collected); - if (!xfer) { - revert BFactory_ERC20TransferFailed(); - } + SafeERC20.safeTransfer(pool, _blabs, collected); } /// @inheritdoc IBFactory diff --git a/src/contracts/BPool.sol b/src/contracts/BPool.sol index 6ae83c92..ca45cc94 100644 --- a/src/contracts/BPool.sol +++ b/src/contracts/BPool.sol @@ -4,6 +4,7 @@ pragma solidity 0.8.25; import {BMath} from './BMath.sol'; import {BToken} from './BToken.sol'; import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; +import {SafeERC20} from '@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol'; import {IBPool} from 'interfaces/IBPool.sol'; /** @@ -11,7 +12,9 @@ import {IBPool} from 'interfaces/IBPool.sol'; * @notice Pool contract that holds tokens, allows to swap, add and remove liquidity. */ contract BPool is BToken, BMath, IBPool { + using SafeERC20 for IERC20; /// @dev BFactory address to push token exitFee to + address internal _factory; /// @dev Has CONTROL role address internal _controller; @@ -42,7 +45,7 @@ contract BPool is BToken, BMath, IBPool { _setLock(_MUTEX_FREE); } - /// @dev Prevents reentrancy in view functions + /// @dev Throws an error when the reentrancy mutex is taken. Doesn't modify it. modifier _viewlock_() { if (_getLock() != _MUTEX_FREE) { revert BPool_Reentrancy(); @@ -620,10 +623,7 @@ contract BPool is BToken, BMath, IBPool { * @param amount The amount of tokens to pull */ function _pullUnderlying(address erc20, address from, uint256 amount) internal virtual { - bool xfer = IERC20(erc20).transferFrom(from, address(this), amount); - if (!xfer) { - revert BPool_ERC20TransferFailed(); - } + IERC20(erc20).safeTransferFrom(from, address(this), amount); } /** @@ -633,10 +633,7 @@ contract BPool is BToken, BMath, IBPool { * @param amount The amount of tokens to push */ function _pushUnderlying(address erc20, address to, uint256 amount) internal virtual { - bool xfer = IERC20(erc20).transfer(to, amount); - if (!xfer) { - revert BPool_ERC20TransferFailed(); - } + IERC20(erc20).safeTransfer(to, amount); } /** diff --git a/src/interfaces/IBCoWPool.sol b/src/interfaces/IBCoWPool.sol index cbd7f34d..5d8d60d5 100644 --- a/src/interfaces/IBCoWPool.sol +++ b/src/interfaces/IBCoWPool.sol @@ -61,14 +61,6 @@ interface IBCoWPool is IERC1271, IBPool { */ error BCoWPool_ReceiverIsNotBCoWPool(); - /** - * @notice Thrown when trying to set a commitment but there's one already in effect - * @dev Commitments can only be cleared naturally by finishing a transaction - * @dev Since commitments and reentrancy locks share a storage slot, a - * malicious settler could set a zero commitment to clear a reentrancy lock - */ - error BCoWPool_CommitmentAlreadySet(); - /** * @notice Restricts a specific AMM to being able to trade only the order * with the specified hash. @@ -109,17 +101,6 @@ interface IBCoWPool is IERC1271, IBPool { // solhint-disable-next-line style-guide-casing function APP_DATA() external view returns (bytes32 _appData); - /** - * @notice This function returns the commitment hash that has been set by the - * `commit` function. If no commitment has been set, then the value will be - * `EMPTY_COMMITMENT`. - * @return _commitment The commitment hash. - * @dev since commitments share a transient storage slot with reentrancy - * locks, this will return an invalid value while there's a reentrancy lock - * active - */ - function commitment() external view returns (bytes32 _commitment); - /** * @notice This function checks that the input order is admissible for the * constant-product curve for the given trading parameters. diff --git a/src/interfaces/IBFactory.sol b/src/interfaces/IBFactory.sol index 3203d7a7..d2e64841 100644 --- a/src/interfaces/IBFactory.sol +++ b/src/interfaces/IBFactory.sol @@ -23,11 +23,6 @@ interface IBFactory { */ error BFactory_NotBLabs(); - /** - * @notice Thrown when the ERC20 transfer fails - */ - error BFactory_ERC20TransferFailed(); - /** * @notice Creates a new BPool, assigning the caller as the pool controller * @return _pool The new BPool diff --git a/src/interfaces/IBPool.sol b/src/interfaces/IBPool.sol index c2f0f04c..d8a05bbf 100644 --- a/src/interfaces/IBPool.sol +++ b/src/interfaces/IBPool.sol @@ -201,11 +201,6 @@ interface IBPool is IERC20 { */ error BPool_PoolAmountInAboveMaxPoolAmountIn(); - /** - * @notice Thrown when the ERC20 transfer fails - */ - error BPool_ERC20TransferFailed(); - /** * @notice Sets the new swap fee * @param swapFee The new swap fee diff --git a/test/integration/PoolSwap.t.sol b/test/integration/PoolSwap.t.sol index 224a4c6d..625069a5 100644 --- a/test/integration/PoolSwap.t.sol +++ b/test/integration/PoolSwap.t.sol @@ -114,7 +114,7 @@ contract DirectPoolSwapIntegrationTest is PoolSwapIntegrationTest { vm.startPrank(swapper.addr); dai.approve(address(pool), type(uint256).max); - // swap 0.5 dai for weth + // swap 100 dai for ~0.1 weth snapStart('swapExactAmountIn'); pool.swapExactAmountIn(address(dai), DAI_AMOUNT, address(weth), 0, type(uint256).max); snapEnd(); diff --git a/test/manual-smock/MockBCoWFactory.sol b/test/manual-smock/MockBCoWFactory.sol index 150da5bb..2f581c3a 100644 --- a/test/manual-smock/MockBCoWFactory.sol +++ b/test/manual-smock/MockBCoWFactory.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity ^0.8.0; -import {BCoWFactory, BCoWPool, BFactory, IBCoWFactory, IBFactory, IBPool} from '../../src/contracts/BCoWFactory.sol'; +import {BCoWFactory, IBPool} from '../../src/contracts/BCoWFactory.sol'; import {Test} from 'forge-std/Test.sol'; contract MockBCoWFactory is BCoWFactory, Test { diff --git a/test/smock/MockBCoWPool.sol b/test/smock/MockBCoWPool.sol deleted file mode 100644 index 4ba59f07..00000000 --- a/test/smock/MockBCoWPool.sol +++ /dev/null @@ -1,39 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity ^0.8.0; - -import { - BCoWConst, - BCoWPool, - BPool, - GPv2Order, - IBCoWFactory, - IBCoWPool, - IERC1271, - IERC20, - ISettlement -} from '../../src/contracts/BCoWPool.sol'; -import {Test} from 'forge-std/Test.sol'; - -contract MockBCoWPool is BCoWPool, Test { - constructor(address _cowSolutionSettler, bytes32 _appData) BCoWPool(_cowSolutionSettler, _appData) {} - - function mock_call_commit(bytes32 orderHash) public { - vm.mockCall(address(this), abi.encodeWithSignature('commit(bytes32)', orderHash), abi.encode()); - } - - function mock_call_isValidSignature(bytes32 _hash, bytes memory signature, bytes4 _returnParam0) public { - vm.mockCall( - address(this), - abi.encodeWithSignature('isValidSignature(bytes32,bytes)', _hash, signature), - abi.encode(_returnParam0) - ); - } - - function mock_call_commitment(bytes32 value) public { - vm.mockCall(address(this), abi.encodeWithSignature('commitment()'), abi.encode(value)); - } - - function mock_call_verify(GPv2Order.Data memory order) public { - vm.mockCall(address(this), abi.encodeWithSignature('verify(GPv2Order.Data)', order), abi.encode()); - } -} diff --git a/test/unit/BCoWFactory.t.sol b/test/unit/BCoWFactory.t.sol index c3ae9a51..3a1cfb43 100644 --- a/test/unit/BCoWFactory.t.sol +++ b/test/unit/BCoWFactory.t.sol @@ -1,8 +1,15 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.25; -import {Base, BaseBFactory_Unit_Constructor, BaseBFactory_Unit_NewBPool} from './BFactory.t.sol'; +import { + Base, + BaseBFactory_Internal_NewBPool, + BaseBFactory_Unit_Constructor, + BaseBFactory_Unit_NewBPool +} from './BFactory.t.sol'; import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; + +import {BCoWPool} from 'contracts/BCoWPool.sol'; import {IBCoWFactory} from 'interfaces/IBCoWFactory.sol'; import {IBCoWPool} from 'interfaces/IBCoWPool.sol'; import {IBFactory} from 'interfaces/IBFactory.sol'; @@ -23,11 +30,10 @@ abstract contract BCoWFactoryTest is Base { } function _bPoolBytecode() internal virtual override returns (bytes memory _bytecode) { - vm.skip(true); - // NOTE: "runtimeCode" is not available for contracts containing immutable variables. - // return type(BCoWPool).runtimeCode; - return _bytecode; + // so we the easiest way to know the bytecode is to deploy it with the same + // parameters the factory would + return address(new BCoWPool(solutionSettler, appData)).code; } } @@ -81,3 +87,6 @@ contract BCoWPoolFactory_Unit_LogBCoWPool is BCoWFactoryTest { IBCoWFactory(address(bFactory)).logBCoWPool(); } } + +// solhint-disable-next-line no-empty-blocks +contract BCoWFactory_Internal_NewBPool is BaseBFactory_Internal_NewBPool, BCoWFactoryTest {} diff --git a/test/unit/BCoWPool.t.sol b/test/unit/BCoWPool.t.sol index 972fe11d..80d6b954 100644 --- a/test/unit/BCoWPool.t.sol +++ b/test/unit/BCoWPool.t.sol @@ -115,7 +115,6 @@ contract BCoWPool_Unit_Finalize is BaseCoWPoolTest { } } -/// @notice this tests both commit and commitment contract BCoWPool_Unit_Commit is BaseCoWPoolTest { function test_Revert_NonSolutionSettler(address sender, bytes32 orderHash) public { vm.assume(sender != cowSolutionSettler); @@ -128,16 +127,10 @@ contract BCoWPool_Unit_Commit is BaseCoWPoolTest { vm.assume(_existingCommitment != bytes32(0)); bCoWPool.call__setLock(_existingCommitment); vm.prank(cowSolutionSettler); - vm.expectRevert(IBCoWPool.BCoWPool_CommitmentAlreadySet.selector); + vm.expectRevert(IBPool.BPool_Reentrancy.selector); bCoWPool.commit(_newCommitment); } - function test_Set_Commitment(bytes32 orderHash) public { - vm.prank(cowSolutionSettler); - bCoWPool.commit(orderHash); - assertEq(bCoWPool.commitment(), orderHash); - } - function test_Call_SetLock(bytes32 orderHash) public { bCoWPool.expectCall__setLock(orderHash); vm.prank(cowSolutionSettler); diff --git a/test/unit/BFactory.t.sol b/test/unit/BFactory.t.sol index be71a541..cbc3c4e4 100644 --- a/test/unit/BFactory.t.sol +++ b/test/unit/BFactory.t.sol @@ -2,6 +2,7 @@ pragma solidity 0.8.25; import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; +import {SafeERC20} from '@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol'; import {BFactory} from 'contracts/BFactory.sol'; import {BPool} from 'contracts/BPool.sol'; import {Test} from 'forge-std/Test.sol'; @@ -207,7 +208,24 @@ contract BFactory_Unit_Collect is BFactoryTest { } /** - * @notice Test that the function fail if the transfer failed + * @notice Do not couple the collect logic to the known BToken + * implementation. Some ERC20's do not return a `bool` with to indicate + * success and only rely on reverting if there's an error. This test ensures + * we support them. + */ + function test_Succeed_TransferNotReturningBoolean(address _lpToken, uint256 _toCollect) public { + assumeNotForgeAddress(_lpToken); + + vm.mockCall(_lpToken, abi.encodeWithSelector(IERC20.balanceOf.selector, address(bFactory)), abi.encode(_toCollect)); + vm.mockCall(_lpToken, abi.encodeWithSelector(IERC20.transfer.selector, owner, _toCollect), abi.encode()); + + vm.expectCall(_lpToken, abi.encodeWithSelector(IERC20.transfer.selector, owner, _toCollect)); + vm.prank(owner); + bFactory.collect(IBPool(_lpToken)); + } + + /** + * @notice Test the function fails if the transfer failed */ function test_Revert_TransferFailed(address _lpToken, uint256 _toCollect) public { assumeNotForgeAddress(_lpToken); @@ -215,16 +233,19 @@ contract BFactory_Unit_Collect is BFactoryTest { vm.mockCall(_lpToken, abi.encodeWithSelector(IERC20.balanceOf.selector, address(bFactory)), abi.encode(_toCollect)); vm.mockCall(_lpToken, abi.encodeWithSelector(IERC20.transfer.selector, owner, _toCollect), abi.encode(false)); - vm.expectRevert(IBFactory.BFactory_ERC20TransferFailed.selector); + vm.expectRevert(abi.encodeWithSelector(SafeERC20.SafeERC20FailedOperation.selector, _lpToken)); vm.prank(owner); bFactory.collect(IBPool(_lpToken)); } } -contract BFactory_Internal_NewBPool is BFactoryTest { +abstract contract BaseBFactory_Internal_NewBPool is Base { function test_Deploy_NewBPool() public { IBPool _pool = MockBFactory(address(bFactory)).call__newBPool(); assertEq(_bPoolBytecode(), address(_pool).code); } } + +// solhint-disable-next-line no-empty-blocks +contract BFactory_Internal_NewBPool is BFactoryTest, BaseBFactory_Internal_NewBPool {} diff --git a/test/unit/BPool.t.sol b/test/unit/BPool.t.sol index 9048da78..9864170f 100644 --- a/test/unit/BPool.t.sol +++ b/test/unit/BPool.t.sol @@ -2,6 +2,7 @@ pragma solidity 0.8.25; import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; +import {SafeERC20} from '@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol'; import {BPool} from 'contracts/BPool.sol'; import {IBPool} from 'interfaces/IBPool.sol'; @@ -2994,12 +2995,20 @@ contract BPool_Unit__PullUnderlying is BasePoolTest { function test_Revert_ERC20False(address _erc20, address _from, uint256 _amount) public { assumeNotForgeAddress(_erc20); - vm.mockCall( _erc20, abi.encodeWithSelector(IERC20.transferFrom.selector, _from, address(bPool), _amount), abi.encode(false) ); - vm.expectRevert(IBPool.BPool_ERC20TransferFailed.selector); + vm.expectRevert(abi.encodeWithSelector(SafeERC20.SafeERC20FailedOperation.selector, _erc20)); + bPool.call__pullUnderlying(_erc20, _from, _amount); + } + + function test_Success_NoReturnValueERC20(address _erc20, address _from, uint256 _amount) public { + assumeNotForgeAddress(_erc20); + vm.mockCall( + _erc20, abi.encodeWithSelector(IERC20.transferFrom.selector, _from, address(bPool), _amount), abi.encode() + ); + bPool.call__pullUnderlying(_erc20, _from, _amount); } } @@ -3016,10 +3025,16 @@ contract BPool_Unit__PushUnderlying is BasePoolTest { function test_Revert_ERC20False(address _erc20, address _to, uint256 _amount) public { assumeNotForgeAddress(_erc20); - vm.mockCall(_erc20, abi.encodeWithSelector(IERC20.transfer.selector, _to, _amount), abi.encode(false)); - vm.expectRevert(IBPool.BPool_ERC20TransferFailed.selector); + vm.expectRevert(abi.encodeWithSelector(SafeERC20.SafeERC20FailedOperation.selector, _erc20)); + bPool.call__pushUnderlying(_erc20, _to, _amount); + } + + function test_Success_NoReturnValueERC20(address _erc20, address _to, uint256 _amount) public { + assumeNotForgeAddress(_erc20); + vm.mockCall(_erc20, abi.encodeWithSelector(IERC20.transfer.selector, _to, _amount), abi.encode()); + bPool.call__pushUnderlying(_erc20, _to, _amount); } } From 0e4c537a3ffb452e5f21ea62dca4dfef78e82340 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wei=C3=9Fer=20Hase?= Date: Thu, 27 Jun 2024 01:32:58 +0200 Subject: [PATCH 02/48] fix: informational findings in IR (#119) * fix: renaming TokenAmountInAboveMaxIn for AboveMaxRatio * fix: swapExactAmountOut natspec --- src/contracts/BCoWPool.sol | 2 +- src/contracts/BPool.sol | 6 +++--- src/interfaces/IBPool.sol | 6 +++--- test/unit/BCoWPool.t.sol | 2 +- test/unit/BPool.t.sol | 6 +++--- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/contracts/BCoWPool.sol b/src/contracts/BCoWPool.sol index 911ae1b9..d5af3a28 100644 --- a/src/contracts/BCoWPool.sol +++ b/src/contracts/BCoWPool.sol @@ -114,7 +114,7 @@ contract BCoWPool is IERC1271, IBCoWPool, BPool, BCoWConst { uint256 buyTokenBalance = order.buyToken.balanceOf(address(this)); if (order.buyAmount > bmul(buyTokenBalance, MAX_IN_RATIO)) { - revert BPool_TokenAmountInAboveMaxIn(); + revert BPool_TokenAmountInAboveMaxRatio(); } uint256 tokenAmountOut = calcOutGivenIn({ diff --git a/src/contracts/BPool.sol b/src/contracts/BPool.sol index ca45cc94..560b09f7 100644 --- a/src/contracts/BPool.sol +++ b/src/contracts/BPool.sol @@ -249,7 +249,7 @@ contract BPool is BToken, BMath, IBPool { uint256 tokenOutBalance = IERC20(tokenOut).balanceOf(address(this)); if (tokenAmountIn > bmul(tokenInBalance, MAX_IN_RATIO)) { - revert BPool_TokenAmountInAboveMaxIn(); + revert BPool_TokenAmountInAboveMaxRatio(); } uint256 spotPriceBefore = @@ -364,7 +364,7 @@ contract BPool is BToken, BMath, IBPool { Record storage inRecord = _records[tokenIn]; uint256 tokenInBalance = IERC20(tokenIn).balanceOf(address(this)); if (tokenAmountIn > bmul(tokenInBalance, MAX_IN_RATIO)) { - revert BPool_TokenAmountInAboveMaxIn(); + revert BPool_TokenAmountInAboveMaxRatio(); } poolAmountOut = @@ -408,7 +408,7 @@ contract BPool is BToken, BMath, IBPool { revert BPool_TokenAmountInAboveMaxAmountIn(); } if (tokenAmountIn > bmul(tokenInBalance, MAX_IN_RATIO)) { - revert BPool_TokenAmountInAboveMaxIn(); + revert BPool_TokenAmountInAboveMaxRatio(); } emit LOG_JOIN(msg.sender, tokenIn, tokenAmountIn); diff --git a/src/interfaces/IBPool.sol b/src/interfaces/IBPool.sol index d8a05bbf..fe5e664d 100644 --- a/src/interfaces/IBPool.sol +++ b/src/interfaces/IBPool.sol @@ -152,9 +152,9 @@ interface IBPool is IERC20 { error BPool_PoolNotFinalized(); /** - * @notice Thrown when the token amount in surpasses the maximum in allowed by the pool + * @notice Thrown when the token amount in surpasses the maximum in ratio allowed by the pool */ - error BPool_TokenAmountInAboveMaxIn(); + error BPool_TokenAmountInAboveMaxRatio(); /** * @notice Thrown when the spot price before the swap is above the max allowed by the caller @@ -265,7 +265,7 @@ interface IBPool is IERC20 { ) external returns (uint256 tokenAmountOut, uint256 spotPriceAfter); /** - * @notice Swaps as many tokens in as possible for an exact amount of tokens out + * @notice Swaps as many tokens in as needed for an exact amount of tokens out * @param tokenIn The address of the token to swap in * @param maxAmountIn The maximum amount of token to swap in * @param tokenOut The address of the token to swap out diff --git a/test/unit/BCoWPool.t.sol b/test/unit/BCoWPool.t.sol index 80d6b954..37c13573 100644 --- a/test/unit/BCoWPool.t.sol +++ b/test/unit/BCoWPool.t.sol @@ -253,7 +253,7 @@ contract BCoWPool_Unit_Verify is BaseCoWPoolTest, SwapExactAmountInUtils { GPv2Order.Data memory order = correctOrder; order.buyAmount = _tokenAmountIn; - vm.expectRevert(IBPool.BPool_TokenAmountInAboveMaxIn.selector); + vm.expectRevert(IBPool.BPool_TokenAmountInAboveMaxRatio.selector); bCoWPool.verify(order); } diff --git a/test/unit/BPool.t.sol b/test/unit/BPool.t.sol index 9864170f..3ee5ff5e 100644 --- a/test/unit/BPool.t.sol +++ b/test/unit/BPool.t.sol @@ -1533,7 +1533,7 @@ contract BPool_Unit_SwapExactAmountIn is SwapExactAmountInUtils { function test_Revert_TokenAmountInAboveMaxIn(SwapExactAmountIn_FuzzScenario memory _fuzz) public happyPath(_fuzz) { uint256 _tokenAmountIn = bmul(_fuzz.tokenInBalance, MAX_IN_RATIO) + 1; - vm.expectRevert(IBPool.BPool_TokenAmountInAboveMaxIn.selector); + vm.expectRevert(IBPool.BPool_TokenAmountInAboveMaxRatio.selector); bPool.swapExactAmountIn(tokenIn, _tokenAmountIn, tokenOut, 0, type(uint256).max); } @@ -2167,7 +2167,7 @@ contract BPool_Unit_JoinswapExternAmountIn is BasePoolTest { { uint256 _tokenAmountIn = bmul(_fuzz.tokenInBalance, MAX_IN_RATIO); - vm.expectRevert(IBPool.BPool_TokenAmountInAboveMaxIn.selector); + vm.expectRevert(IBPool.BPool_TokenAmountInAboveMaxRatio.selector); bPool.joinswapExternAmountIn(tokenIn, _tokenAmountIn + 1, 0); } @@ -2399,7 +2399,7 @@ contract BPool_Unit_JoinswapPoolAmountOut is BasePoolTest { _setValues(_fuzz); - vm.expectRevert(IBPool.BPool_TokenAmountInAboveMaxIn.selector); + vm.expectRevert(IBPool.BPool_TokenAmountInAboveMaxRatio.selector); bPool.joinswapPoolAmountOut(tokenIn, _fuzz.poolAmountOut, type(uint256).max); } From 332e674013a775035b45137727fa27a828f427ef Mon Sep 17 00:00:00 2001 From: teddy Date: Thu, 27 Jun 2024 06:01:03 -0300 Subject: [PATCH 03/48] ci: ensure gas snapshots are updated (#116) * ci: ensure gas snapshots are updated * chore: break snapshots to see if it is caught * fix: fetch some git history so diff has something to compare against * fix: run diff after running tests * fix: rename gas snapshot test so its run in CI * fix: run diff after running _integration_ tests * chore: update forge snapshots to their current values --- .forge-snapshots/newBFactory.snap | 2 +- .forge-snapshots/newBPool.snap | 2 +- .forge-snapshots/settlementCoWSwap.snap | 2 +- .forge-snapshots/settlementCoWSwapInverse.snap | 2 +- .forge-snapshots/swapExactAmountIn.snap | 2 +- .forge-snapshots/swapExactAmountInInverse.snap | 2 +- .github/workflows/ci.yml | 5 +++-- test/integration/DeploymentGas.t.sol | 2 +- test/smock/MockBFactory.sol | 2 +- test/smock/MockBPool.sol | 2 +- 10 files changed, 12 insertions(+), 11 deletions(-) diff --git a/.forge-snapshots/newBFactory.snap b/.forge-snapshots/newBFactory.snap index 7a494ba2..ce6f1abf 100644 --- a/.forge-snapshots/newBFactory.snap +++ b/.forge-snapshots/newBFactory.snap @@ -1 +1 @@ -3670893 \ No newline at end of file +4012301 \ No newline at end of file diff --git a/.forge-snapshots/newBPool.snap b/.forge-snapshots/newBPool.snap index fa6c2442..0c307839 100644 --- a/.forge-snapshots/newBPool.snap +++ b/.forge-snapshots/newBPool.snap @@ -1 +1 @@ -3383845 \ No newline at end of file +3404909 \ No newline at end of file diff --git a/.forge-snapshots/settlementCoWSwap.snap b/.forge-snapshots/settlementCoWSwap.snap index f2a54e25..7a658ce0 100644 --- a/.forge-snapshots/settlementCoWSwap.snap +++ b/.forge-snapshots/settlementCoWSwap.snap @@ -1 +1 @@ -186729 \ No newline at end of file +216137 \ No newline at end of file diff --git a/.forge-snapshots/settlementCoWSwapInverse.snap b/.forge-snapshots/settlementCoWSwapInverse.snap index 2b414ad7..4cbb5e9a 100644 --- a/.forge-snapshots/settlementCoWSwapInverse.snap +++ b/.forge-snapshots/settlementCoWSwapInverse.snap @@ -1 +1 @@ -196569 \ No newline at end of file +225977 \ No newline at end of file diff --git a/.forge-snapshots/swapExactAmountIn.snap b/.forge-snapshots/swapExactAmountIn.snap index 89f23e83..afe764e6 100644 --- a/.forge-snapshots/swapExactAmountIn.snap +++ b/.forge-snapshots/swapExactAmountIn.snap @@ -1 +1 @@ -82842 \ No newline at end of file +105194 \ No newline at end of file diff --git a/.forge-snapshots/swapExactAmountInInverse.snap b/.forge-snapshots/swapExactAmountInInverse.snap index c1b3dc0b..2c7381e5 100644 --- a/.forge-snapshots/swapExactAmountInInverse.snap +++ b/.forge-snapshots/swapExactAmountInInverse.snap @@ -1 +1 @@ -92501 \ No newline at end of file +114841 \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d75456bf..cdb26d10 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -64,14 +64,15 @@ jobs: - name: Run tests run: yarn test:integration + - name: Ensure gas snapshots were updated + run: git diff --exit-code -- .forge-snapshots/ + lint: name: Static Analysis runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - with: - fetch-depth: 0 - uses: wagoid/commitlint-github-action@v5 diff --git a/test/integration/DeploymentGas.t.sol b/test/integration/DeploymentGas.t.sol index 5bc4715c..e2b32d95 100644 --- a/test/integration/DeploymentGas.t.sol +++ b/test/integration/DeploymentGas.t.sol @@ -6,7 +6,7 @@ import {BFactory} from 'contracts/BFactory.sol'; import {GasSnapshot} from 'forge-gas-snapshot/GasSnapshot.sol'; import {Test} from 'forge-std/Test.sol'; -contract DeploymentGasTest is Test, GasSnapshot { +contract DeploymentIntegrationGasTest is Test, GasSnapshot { BFactory public factory; function setUp() public { diff --git a/test/smock/MockBFactory.sol b/test/smock/MockBFactory.sol index 541a4876..512eab79 100644 --- a/test/smock/MockBFactory.sol +++ b/test/smock/MockBFactory.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity ^0.8.0; -import {BFactory, BPool, IBFactory, IBPool} from '../../src/contracts/BFactory.sol'; +import {BFactory, BPool, IBFactory, IBPool, SafeERC20} from '../../src/contracts/BFactory.sol'; import {Test} from 'forge-std/Test.sol'; contract MockBFactory is BFactory, Test { diff --git a/test/smock/MockBPool.sol b/test/smock/MockBPool.sol index f2f73396..ffc39659 100644 --- a/test/smock/MockBPool.sol +++ b/test/smock/MockBPool.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity ^0.8.0; -import {BMath, BPool, BToken, IBPool, IERC20} from '../../src/contracts/BPool.sol'; +import {BMath, BPool, BToken, IBPool, IERC20, SafeERC20} from '../../src/contracts/BPool.sol'; import {Test} from 'forge-std/Test.sol'; contract MockBPool is BPool, Test { From 75b94730309ce9870f4c2d5fd36d6d055dfe88c4 Mon Sep 17 00:00:00 2001 From: teddy Date: Fri, 28 Jun 2024 15:30:21 -0300 Subject: [PATCH 04/48] fix: run all foundry tests regardless of their name (#124) --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 26f0309a..79f2f007 100644 --- a/package.json +++ b/package.json @@ -26,9 +26,9 @@ "prepare": "husky install", "smock": "smock-foundry --contracts src/contracts", "test": "forge test -vvv", - "test:integration": "forge test --match-contract Integration -vvv --isolate", + "test:integration": "forge test --match-path 'test/integration/**' -vvv --isolate", "test:local": "FOUNDRY_FUZZ_RUNS=100 forge test -vvv", - "test:unit": "forge test --match-contract Unit -vvv", + "test:unit": "forge test --match-path 'test/unit/**' -vvv", "test:unit:deep": "FOUNDRY_FUZZ_RUNS=5000 yarn test:unit" }, "lint-staged": { From d7762dbbeeccb78b5424a34077da3ade0f0e3b23 Mon Sep 17 00:00:00 2001 From: teddy Date: Mon, 1 Jul 2024 12:52:16 -0300 Subject: [PATCH 05/48] test: btt tests for b{,cow}factory (#120) * chore: checkout btt bfactory tests from feat/new-unit-tests * chore: update BFactory tests for custom errors * chore: get BCoWFactory tests to run for the time being * test: mock _newBPool and test it separately * test: unit test methods defined in BCoWFactory * fix: feedback from review * chore: setBLabs -> BLabs * fix: feedback from review --- test/manual-smock/MockBCoWFactory.sol | 16 +- test/unit/BCoWFactory.t.sol | 98 ++++----- test/unit/BCoWFactory.tree | 17 ++ test/unit/BFactory.t.sol | 290 ++++++++------------------ test/unit/BFactory.tree | 32 +++ 5 files changed, 177 insertions(+), 276 deletions(-) create mode 100644 test/unit/BCoWFactory.tree create mode 100644 test/unit/BFactory.tree diff --git a/test/manual-smock/MockBCoWFactory.sol b/test/manual-smock/MockBCoWFactory.sol index 2f581c3a..804b37f8 100644 --- a/test/manual-smock/MockBCoWFactory.sol +++ b/test/manual-smock/MockBCoWFactory.sol @@ -1,16 +1,16 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity ^0.8.0; -import {BCoWFactory, IBPool} from '../../src/contracts/BCoWFactory.sol'; +import {BCoWFactory, BCoWPool, BFactory, IBCoWFactory, IBPool} from '../../src/contracts/BCoWFactory.sol'; import {Test} from 'forge-std/Test.sol'; contract MockBCoWFactory is BCoWFactory, Test { - function set__isBPool(address _key0, bool _value) public { - _isBPool[_key0] = _value; - } - constructor(address _solutionSettler, bytes32 _appData) BCoWFactory(_solutionSettler, _appData) {} + function mock_call_logBCoWPool() public { + vm.mockCall(address(this), abi.encodeWithSignature('logBCoWPool()'), abi.encode()); + } + function mock_call__newBPool(IBPool _pool) public { vm.mockCall(address(this), abi.encodeWithSignature('_newBPool()'), abi.encode(_pool)); } @@ -30,7 +30,9 @@ contract MockBCoWFactory is BCoWFactory, Test { vm.expectCall(address(this), abi.encodeWithSignature('_newBPool()')); } - function mock_call_logBCoWPool() public { - vm.mockCall(address(this), abi.encodeWithSignature('logBCoWPool()'), abi.encode()); + // MockBFactory methods + + function set__isBPool(address _key0, bool _value) public { + _isBPool[_key0] = _value; } } diff --git a/test/unit/BCoWFactory.t.sol b/test/unit/BCoWFactory.t.sol index 3a1cfb43..6b702f0c 100644 --- a/test/unit/BCoWFactory.t.sol +++ b/test/unit/BCoWFactory.t.sol @@ -1,92 +1,64 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.25; -import { - Base, - BaseBFactory_Internal_NewBPool, - BaseBFactory_Unit_Constructor, - BaseBFactory_Unit_NewBPool -} from './BFactory.t.sol'; -import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; - import {BCoWPool} from 'contracts/BCoWPool.sol'; +import {Test} from 'forge-std/Test.sol'; import {IBCoWFactory} from 'interfaces/IBCoWFactory.sol'; import {IBCoWPool} from 'interfaces/IBCoWPool.sol'; -import {IBFactory} from 'interfaces/IBFactory.sol'; import {ISettlement} from 'interfaces/ISettlement.sol'; import {MockBCoWFactory} from 'test/manual-smock/MockBCoWFactory.sol'; -abstract contract BCoWFactoryTest is Base { +contract BCoWFactoryTest is Test { + address public factoryDeployer = makeAddr('factoryDeployer'); address public solutionSettler = makeAddr('solutionSettler'); bytes32 public appData = bytes32('appData'); - function _configureBFactory() internal override returns (IBFactory) { + MockBCoWFactory factory; + + function setUp() external { + vm.prank(factoryDeployer); + factory = new MockBCoWFactory(solutionSettler, appData); vm.mockCall(solutionSettler, abi.encodePacked(ISettlement.domainSeparator.selector), abi.encode(bytes32(0))); vm.mockCall( solutionSettler, abi.encodePacked(ISettlement.vaultRelayer.selector), abi.encode(makeAddr('vault relayer')) ); - vm.prank(owner); - return new MockBCoWFactory(solutionSettler, appData); - } - - function _bPoolBytecode() internal virtual override returns (bytes memory _bytecode) { - // NOTE: "runtimeCode" is not available for contracts containing immutable variables. - // so we the easiest way to know the bytecode is to deploy it with the same - // parameters the factory would - return address(new BCoWPool(solutionSettler, appData)).code; - } -} - -contract BCoWFactory_Unit_Constructor is BaseBFactory_Unit_Constructor, BCoWFactoryTest { - function test_Set_SolutionSettler(address _settler) public { - MockBCoWFactory factory = new MockBCoWFactory(_settler, appData); - assertEq(factory.SOLUTION_SETTLER(), _settler); - } - - function test_Set_AppData(bytes32 _appData) public { - MockBCoWFactory factory = new MockBCoWFactory(solutionSettler, _appData); - assertEq(factory.APP_DATA(), _appData); } -} -contract BCoWFactory_Unit_NewBPool is BaseBFactory_Unit_NewBPool, BCoWFactoryTest { - function test_Set_SolutionSettler(address _settler) public { - assumeNotForgeAddress(_settler); - bFactory = new MockBCoWFactory(_settler, appData); - vm.mockCall(_settler, abi.encodePacked(ISettlement.domainSeparator.selector), abi.encode(bytes32(0))); - vm.mockCall(_settler, abi.encodePacked(ISettlement.vaultRelayer.selector), abi.encode(makeAddr('vault relayer'))); - IBCoWPool bCoWPool = IBCoWPool(address(bFactory.newBPool())); - assertEq(address(bCoWPool.SOLUTION_SETTLER()), _settler); + function test_ConstructorWhenCalled(address _blabs, address _newSettler, bytes32 _appData) external { + vm.prank(_blabs); + MockBCoWFactory _newFactory = new MockBCoWFactory(_newSettler, _appData); + // it should set solution settler + assertEq(_newFactory.SOLUTION_SETTLER(), _newSettler); + // it should set app data + assertEq(_newFactory.APP_DATA(), _appData); + // it should set blabs + assertEq(_newFactory.getBLabs(), _blabs); } - function test_Set_AppData(bytes32 _appData) public { - bFactory = new MockBCoWFactory(solutionSettler, _appData); - IBCoWPool bCoWPool = IBCoWPool(address(bFactory.newBPool())); - assertEq(bCoWPool.APP_DATA(), _appData); + function test__newBPoolWhenCalled() external { + bytes memory _expectedCode = address(new BCoWPool(solutionSettler, appData)).code; + IBCoWPool _newPool = IBCoWPool(address(factory.call__newBPool())); + // it should set the new BCoWPool solution settler + assertEq(address(_newPool.SOLUTION_SETTLER()), solutionSettler); + // it should set the new BCoWPool app data + assertEq(_newPool.APP_DATA(), appData); + // it should deploy a new BCoWPool + assertEq(address(_newPool).code, _expectedCode); } -} - -contract BCoWPoolFactory_Unit_LogBCoWPool is BCoWFactoryTest { - function test_Revert_NotValidBCoWPool(address _pool) public { - bFactory = new MockBCoWFactory(solutionSettler, appData); - MockBCoWFactory(address(bFactory)).set__isBPool(address(_pool), false); + function test_LogBCowPoolRevertWhen_TheSenderIsNotAValidPool(address _caller) external { + // it should revert vm.expectRevert(IBCoWFactory.BCoWFactory_NotValidBCoWPool.selector); - - vm.prank(_pool); - IBCoWFactory(address(bFactory)).logBCoWPool(); + vm.prank(_caller); + factory.logBCoWPool(); } - function test_Emit_COWAMMPoolCreated(address _pool) public { - bFactory = new MockBCoWFactory(solutionSettler, appData); - MockBCoWFactory(address(bFactory)).set__isBPool(address(_pool), true); - vm.expectEmit(address(bFactory)); + function test_LogBCowPoolWhenTheSenderIsAValidPool(address _pool) external { + factory.set__isBPool(address(_pool), true); + // it should emit a COWAMMPoolCreated event + vm.expectEmit(address(factory)); emit IBCoWFactory.COWAMMPoolCreated(_pool); - vm.prank(_pool); - IBCoWFactory(address(bFactory)).logBCoWPool(); + IBCoWFactory(address(factory)).logBCoWPool(); } } - -// solhint-disable-next-line no-empty-blocks -contract BCoWFactory_Internal_NewBPool is BaseBFactory_Internal_NewBPool, BCoWFactoryTest {} diff --git a/test/unit/BCoWFactory.tree b/test/unit/BCoWFactory.tree new file mode 100644 index 00000000..37160019 --- /dev/null +++ b/test/unit/BCoWFactory.tree @@ -0,0 +1,17 @@ +BCoWFactoryTest::constructor +└── when called + ├── it should set solution settler + ├── it should set app data + └── it should set blabs + +BCoWFactoryTest::_newBPool +└── when called + ├── it should set BCoWPool solution settler + ├── it should set BCoWPool app data + └── it should deploy a new BCoWPool + +BCoWFactoryTest::logBCowPool +├── when the sender is not a valid pool +│ └── it should revert +└── when the sender is a valid pool + └── it should emit a COWAMMPoolCreated event diff --git a/test/unit/BFactory.t.sol b/test/unit/BFactory.t.sol index cbc3c4e4..945470f0 100644 --- a/test/unit/BFactory.t.sol +++ b/test/unit/BFactory.t.sol @@ -1,251 +1,129 @@ -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: UNLICENSED pragma solidity 0.8.25; -import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; +import {IERC20} from '@openzeppelin/contracts/token/ERC20/ERC20.sol'; import {SafeERC20} from '@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol'; -import {BFactory} from 'contracts/BFactory.sol'; + import {BPool} from 'contracts/BPool.sol'; + import {Test} from 'forge-std/Test.sol'; import {IBFactory} from 'interfaces/IBFactory.sol'; import {IBPool} from 'interfaces/IBPool.sol'; - import {MockBFactory} from 'test/smock/MockBFactory.sol'; -abstract contract Base is Test { - IBFactory public bFactory; - address public owner = makeAddr('owner'); +contract BFactoryTest is Test { + address factoryDeployer = makeAddr('factoryDeployer'); - function _configureBFactory() internal virtual returns (IBFactory); + MockBFactory factory; - function _bPoolBytecode() internal virtual returns (bytes memory); - - function setUp() public virtual { - bFactory = _configureBFactory(); + function setUp() external { + vm.prank(factoryDeployer); + factory = new MockBFactory(); } -} -abstract contract BFactoryTest is Base { - function _configureBFactory() internal override returns (IBFactory) { - vm.prank(owner); - return new MockBFactory(); + function test_ConstructorWhenCalled(address _blabs) external { + vm.prank(_blabs); + MockBFactory newFactory = new MockBFactory(); + // it should set BLabs + assertEq(newFactory.getBLabs(), _blabs); } - function _bPoolBytecode() internal pure virtual override returns (bytes memory) { - return type(BPool).runtimeCode; - } -} + function test_NewBPoolWhenCalled(address _deployer, address _newBPool) external { + assumeNotForgeAddress(_newBPool); + vm.mockCall(_newBPool, abi.encodePacked(IBPool.setController.selector), abi.encode()); + factory.mock_call__newBPool(IBPool(_newBPool)); + // it should call _newBPool + factory.expectCall__newBPool(); + // it should set the controller of the newBPool to the caller + vm.expectCall(_newBPool, abi.encodeCall(IBPool.setController, (_deployer))); + // it should emit a PoolCreated event + vm.expectEmit(address(factory)); + emit IBFactory.LOG_NEW_POOL(_deployer, _newBPool); -abstract contract BaseBFactory_Unit_Constructor is Base { - /** - * @notice Test that the owner is set correctly - */ - function test_Deploy() public view { - assertEq(owner, bFactory.getBLabs()); - } -} + vm.prank(_deployer); + IBPool pool = factory.newBPool(); -// solhint-disable-next-line no-empty-blocks -contract BFactory_Unit_Constructor is BFactoryTest, BaseBFactory_Unit_Constructor {} - -contract BFactory_Unit_IsBPool is BFactoryTest { - /** - * @notice Test that a valid pool is present on the mapping - */ - function test_Returns_IsValidPool(address _pool) public { - // Writing TRUE (1) to the mapping with the `_pool` key - vm.store(address(bFactory), keccak256(abi.encode(_pool, uint256(0))), bytes32(uint256(1))); - assertTrue(bFactory.isBPool(address(_pool))); + // it should add the newBPool to the list of pools + assertTrue(factory.isBPool(address(_newBPool))); + // it should return the address of the new BPool + assertEq(address(pool), _newBPool); } - /** - * @notice Test that a invalid pool is not present on the mapping - */ - function test_Returns_IsInvalidPool(address _randomPool) public view { - vm.assume(_randomPool != address(0)); - assertFalse(bFactory.isBPool(_randomPool)); - } -} - -abstract contract BaseBFactory_Unit_NewBPool is Base { - /** - * @notice Test that the pool is set on the mapping - */ - function test_Set_Pool() public { - IBPool _pool = bFactory.newBPool(); - assertTrue(bFactory.isBPool(address(_pool))); - } - - /** - * @notice Test that event is emitted - */ - function test_Emit_Log(address _randomCaller) public { - assumeNotForgeAddress(_randomCaller); - - vm.expectEmit(); - address _expectedPoolAddress = vm.computeCreateAddress(address(bFactory), 1); - emit IBFactory.LOG_NEW_POOL(_randomCaller, _expectedPoolAddress); - vm.prank(_randomCaller); - bFactory.newBPool(); + function test__newBPoolWhenCalled() external { + address _futurePool = vm.computeCreateAddress(address(factory), 1); + address _newBPool = address(factory.call__newBPool()); + assertEq(_newBPool, _futurePool); + // it should deploy a new BPool + assertEq(_newBPool.code, address(new BPool()).code); } - /** - * @notice Test that msg.sender is set as the controller - */ - function test_Set_Controller(address _randomCaller) public { - assumeNotForgeAddress(_randomCaller); + function test_SetBLabsRevertWhen_TheSenderIsNotTheCurrentBLabs(address _caller) external { + vm.assume(_caller != factoryDeployer); - vm.prank(_randomCaller); - IBPool _pool = bFactory.newBPool(); - assertEq(_randomCaller, _pool.getController()); - } + // it should revert + vm.expectRevert(IBFactory.BFactory_NotBLabs.selector); - /** - * @notice Test that the pool address is returned - */ - function test_Returns_Pool() public { - address _expectedPoolAddress = vm.computeCreateAddress(address(bFactory), 1); - IBPool _pool = bFactory.newBPool(); - assertEq(_expectedPoolAddress, address(_pool)); + vm.prank(_caller); + factory.setBLabs(makeAddr('newBLabs')); } - /** - * @notice Test that the internal function is called - */ - function test_Call_NewBPool(address _bPool) public { - assumeNotForgeAddress(_bPool); - MockBFactory(address(bFactory)).mock_call__newBPool(IBPool(_bPool)); - MockBFactory(address(bFactory)).expectCall__newBPool(); - vm.mockCall(_bPool, abi.encodeWithSignature('setController(address)'), abi.encode()); + function test_SetBLabsWhenTheSenderIsTheCurrentBLabs(address _newBLabs) external { + // it should emit a BLabsSet event + vm.expectEmit(address(factory)); + emit IBFactory.LOG_BLABS(factoryDeployer, _newBLabs); - IBPool _pool = bFactory.newBPool(); + vm.prank(factoryDeployer); + factory.setBLabs(_newBLabs); - assertEq(_bPool, address(_pool)); + // it should set the new setBLabs address + assertEq(factory.getBLabs(), _newBLabs); } -} -// solhint-disable-next-line no-empty-blocks -contract BFactory_Unit_NewBPool is BFactoryTest, BaseBFactory_Unit_NewBPool {} - -contract BFactory_Unit_GetBLabs is BFactoryTest { - /** - * @notice Test that the correct owner is returned - */ - function test_Set_Owner(address _randomDeployer) public { - vm.prank(_randomDeployer); - BFactory _bFactory = new BFactory(); - assertEq(_randomDeployer, _bFactory.getBLabs()); - } -} + function test_CollectRevertWhen_TheSenderIsNotTheCurrentBLabs(address _caller) external { + vm.assume(_caller != factoryDeployer); -contract BFactory_Unit_SetBLabs is BFactoryTest { - /** - * @notice Test that only the owner can set the BLabs - */ - function test_Revert_NotLabs(address _randomCaller) public { - vm.assume(_randomCaller != owner); + // it should revert vm.expectRevert(IBFactory.BFactory_NotBLabs.selector); - vm.prank(_randomCaller); - bFactory.setBLabs(_randomCaller); - } - /** - * @notice Test that event is emitted - */ - function test_Emit_Log(address _addressToSet) public { - vm.expectEmit(); - emit IBFactory.LOG_BLABS(owner, _addressToSet); - vm.prank(owner); - bFactory.setBLabs(_addressToSet); + vm.prank(_caller); + factory.collect(IBPool(makeAddr('pool'))); } - /** - * @notice Test that the BLabs is set correctly - */ - function test_Set_BLabs(address _addressToSet) public { - vm.prank(owner); - bFactory.setBLabs(_addressToSet); - assertEq(_addressToSet, bFactory.getBLabs()); + modifier whenTheSenderIsTheCurrentBLabs() { + vm.startPrank(factoryDeployer); + _; } -} -contract BFactory_Unit_Collect is BFactoryTest { - /** - * @notice Test that only the owner can collect - */ - function test_Revert_NotLabs(address _randomCaller) public { - vm.assume(_randomCaller != owner); - vm.expectRevert(IBFactory.BFactory_NotBLabs.selector); - vm.prank(_randomCaller); - bFactory.collect(IBPool(address(0))); - } + function test_CollectWhenTheSenderIsTheCurrentBLabs(uint256 _factoryBTBalance) + external + whenTheSenderIsTheCurrentBLabs + { + address _mockPool = makeAddr('pool'); + vm.mockCall(_mockPool, abi.encodeCall(IERC20.balanceOf, address(factory)), abi.encode(_factoryBTBalance)); + vm.mockCall(_mockPool, abi.encodeCall(IERC20.transfer, (factoryDeployer, _factoryBTBalance)), abi.encode(true)); - /** - * @notice Test that LP token `balanceOf` function is called - */ - function test_Call_BalanceOf(address _lpToken, uint256 _toCollect) public { - assumeNotForgeAddress(_lpToken); + // it should get the pool's btoken balance of the factory + vm.expectCall(_mockPool, abi.encodeCall(IERC20.balanceOf, address(factory))); - vm.mockCall(_lpToken, abi.encodeWithSelector(IERC20.balanceOf.selector, address(bFactory)), abi.encode(_toCollect)); - vm.mockCall(_lpToken, abi.encodeWithSelector(IERC20.transfer.selector, owner, _toCollect), abi.encode(true)); + // it should transfer the btoken balance of the factory to BLabs + vm.expectCall(_mockPool, abi.encodeCall(IERC20.transfer, (factoryDeployer, _factoryBTBalance))); - vm.expectCall(_lpToken, abi.encodeWithSelector(IERC20.balanceOf.selector, address(bFactory))); - vm.prank(owner); - bFactory.collect(IBPool(_lpToken)); + factory.collect(IBPool(_mockPool)); } - /** - * @notice Test that LP token `transfer` function is called - */ - function test_Call_Transfer(address _lpToken, uint256 _toCollect) public { - assumeNotForgeAddress(_lpToken); - - vm.mockCall(_lpToken, abi.encodeWithSelector(IERC20.balanceOf.selector, address(bFactory)), abi.encode(_toCollect)); - vm.mockCall(_lpToken, abi.encodeWithSelector(IERC20.transfer.selector, owner, _toCollect), abi.encode(true)); + function test_CollectRevertWhen_TheBtokenTransferFails(uint256 _factoryBTBalance) + external + whenTheSenderIsTheCurrentBLabs + { + address _mockPool = makeAddr('pool'); + vm.mockCall(_mockPool, abi.encodeCall(IERC20.balanceOf, address(factory)), abi.encode(_factoryBTBalance)); + vm.expectCall(_mockPool, abi.encodeCall(IERC20.balanceOf, address(factory))); + vm.mockCall(_mockPool, abi.encodeCall(IERC20.transfer, (factoryDeployer, _factoryBTBalance)), abi.encode(false)); + vm.expectCall(_mockPool, abi.encodeCall(IERC20.transfer, (factoryDeployer, _factoryBTBalance))); - vm.expectCall(_lpToken, abi.encodeWithSelector(IERC20.transfer.selector, owner, _toCollect)); - vm.prank(owner); - bFactory.collect(IBPool(_lpToken)); - } + // it should revert + vm.expectRevert(abi.encodeWithSelector(SafeERC20.SafeERC20FailedOperation.selector, _mockPool)); - /** - * @notice Do not couple the collect logic to the known BToken - * implementation. Some ERC20's do not return a `bool` with to indicate - * success and only rely on reverting if there's an error. This test ensures - * we support them. - */ - function test_Succeed_TransferNotReturningBoolean(address _lpToken, uint256 _toCollect) public { - assumeNotForgeAddress(_lpToken); - - vm.mockCall(_lpToken, abi.encodeWithSelector(IERC20.balanceOf.selector, address(bFactory)), abi.encode(_toCollect)); - vm.mockCall(_lpToken, abi.encodeWithSelector(IERC20.transfer.selector, owner, _toCollect), abi.encode()); - - vm.expectCall(_lpToken, abi.encodeWithSelector(IERC20.transfer.selector, owner, _toCollect)); - vm.prank(owner); - bFactory.collect(IBPool(_lpToken)); - } - - /** - * @notice Test the function fails if the transfer failed - */ - function test_Revert_TransferFailed(address _lpToken, uint256 _toCollect) public { - assumeNotForgeAddress(_lpToken); - - vm.mockCall(_lpToken, abi.encodeWithSelector(IERC20.balanceOf.selector, address(bFactory)), abi.encode(_toCollect)); - vm.mockCall(_lpToken, abi.encodeWithSelector(IERC20.transfer.selector, owner, _toCollect), abi.encode(false)); - - vm.expectRevert(abi.encodeWithSelector(SafeERC20.SafeERC20FailedOperation.selector, _lpToken)); - vm.prank(owner); - bFactory.collect(IBPool(_lpToken)); - } -} - -abstract contract BaseBFactory_Internal_NewBPool is Base { - function test_Deploy_NewBPool() public { - IBPool _pool = MockBFactory(address(bFactory)).call__newBPool(); - - assertEq(_bPoolBytecode(), address(_pool).code); + factory.collect(IBPool(_mockPool)); } } - -// solhint-disable-next-line no-empty-blocks -contract BFactory_Internal_NewBPool is BFactoryTest, BaseBFactory_Internal_NewBPool {} diff --git a/test/unit/BFactory.tree b/test/unit/BFactory.tree new file mode 100644 index 00000000..099584f3 --- /dev/null +++ b/test/unit/BFactory.tree @@ -0,0 +1,32 @@ +BFactoryTest::constructor +└── when called + └── it should set the deployer as BLabs + +BFactoryTest::newBPool +└── when called + ├── it should call _newBPool + ├── it should add the newBPool to the mapping of pools + ├── it should return the address of the new BPool + ├── it should emit a PoolCreated event + └── it should set the controller of the new BPool to the caller + +BFactoryTest::_newBPool +└── when called + └── it should deploy a new BPool + +BFactoryTest::setBLabs +├── when the sender is not the current BLabs +│ └── it should revert +└── when the sender is the current BLabs + ├── it should set the new BLabs address + └── it should emit a BLabsSet event + + +BFactoryTest::collect +├── when the sender is not the current BLabs +│ └── it should revert +└── when the sender is the current BLabs + ├── it should get the pool's btoken balance of the factory + ├── it should transfer the btoken balance of the factory to BLabs + └── when the btoken transfer fails + └── it should revert From 528b5ab61e9d49b74bc434ac9de5c39b236b69e3 Mon Sep 17 00:00:00 2001 From: teddy Date: Mon, 1 Jul 2024 12:52:58 -0300 Subject: [PATCH 06/48] ci: enable no-empty-blocks solhint rule with error severity (#125) --- .solhint.json | 1 + 1 file changed, 1 insertion(+) diff --git a/.solhint.json b/.solhint.json index b5849fdc..2038d16f 100644 --- a/.solhint.json +++ b/.solhint.json @@ -4,6 +4,7 @@ "avoid-low-level-calls": "off", "constructor-syntax": "warn", "func-visibility": ["warn", { "ignoreConstructors": true }], + "no-empty-blocks": "error", "no-inline-assembly": "off", "ordering": "warn", "private-vars-leading-underscore": ["warn", { "strict": false }], From 9db52dcbd4372c088289ed6fc101b19638921be4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wei=C3=9Fer=20Hase?= Date: Tue, 2 Jul 2024 16:53:57 +0200 Subject: [PATCH 07/48] test: adding BTT tests for BNum (cont. #122) (#126) * chore: adding BNum test files from #122 * fix: inheriting BConst * fix: reusing constants * feat: improving btt test with fuzzing * chore: cleanup tree and tests * feat: minor improvements to tree + fuzz * fix: minor tweaks to tree and comments * fix: clean up bound * fix: test:scaffold script * fix: applying tree updated names * fix: bdiv tests * fix: improving tree branching and expectations * fix: rm unnecessary conditional * fix: revert dumb bun break * fix: example parenthesis --- package.json | 1 + test/manual-smock/MockBNum.sol | 46 +++++ test/unit/BNum.t.sol | 345 +++++++++++++++++++++++++++++++++ test/unit/BNum.tree | 93 +++++++++ 4 files changed, 485 insertions(+) create mode 100644 test/manual-smock/MockBNum.sol create mode 100644 test/unit/BNum.t.sol create mode 100644 test/unit/BNum.tree diff --git a/package.json b/package.json index 79f2f007..49ddd2a3 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "test": "forge test -vvv", "test:integration": "forge test --match-path 'test/integration/**' -vvv --isolate", "test:local": "FOUNDRY_FUZZ_RUNS=100 forge test -vvv", + "test:scaffold": "bulloak check --fix test/unit/*.tree && forge fmt", "test:unit": "forge test --match-path 'test/unit/**' -vvv", "test:unit:deep": "FOUNDRY_FUZZ_RUNS=5000 yarn test:unit" }, diff --git a/test/manual-smock/MockBNum.sol b/test/manual-smock/MockBNum.sol new file mode 100644 index 00000000..259e23a8 --- /dev/null +++ b/test/manual-smock/MockBNum.sol @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.0; + +import {BConst, BNum} from '../../src/contracts/BNum.sol'; + +contract MockBNum is BNum { + function call_btoi(uint256 a) public returns (uint256 _returnParam0) { + return btoi(a); + } + + function call_bfloor(uint256 a) public returns (uint256 _returnParam0) { + return bfloor(a); + } + + function call_badd(uint256 a, uint256 b) public returns (uint256 _returnParam0) { + return badd(a, b); + } + + function call_bsub(uint256 a, uint256 b) public returns (uint256 _returnParam0) { + return bsub(a, b); + } + + function call_bsubSign(uint256 a, uint256 b) public returns (uint256 _returnParam0, bool _returnParam1) { + return bsubSign(a, b); + } + + function call_bmul(uint256 a, uint256 b) public returns (uint256 _returnParam0) { + return bmul(a, b); + } + + function call_bdiv(uint256 a, uint256 b) public returns (uint256 _returnParam0) { + return bdiv(a, b); + } + + function call_bpowi(uint256 a, uint256 n) public returns (uint256 _returnParam0) { + return bpowi(a, n); + } + + function call_bpow(uint256 base, uint256 exp) public returns (uint256 _returnParam0) { + return bpow(base, exp); + } + + function call_bpowApprox(uint256 base, uint256 exp, uint256 precision) public returns (uint256 _returnParam0) { + return bpowApprox(base, exp, precision); + } +} diff --git a/test/unit/BNum.t.sol b/test/unit/BNum.t.sol new file mode 100644 index 00000000..58041f73 --- /dev/null +++ b/test/unit/BNum.t.sol @@ -0,0 +1,345 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.25; + +import {BConst} from 'contracts/BConst.sol'; +import {BNum} from 'contracts/BNum.sol'; +import {Test} from 'forge-std/Test.sol'; +import {MockBNum} from 'test/manual-smock/MockBNum.sol'; + +contract BNumTest is Test, BConst { + MockBNum bNum; + + function setUp() public { + bNum = new MockBNum(); + } + + function test_BtoiWhenPassingZero() external { + uint256 _result = bNum.call_btoi(0); + + // it should return zero + assertEq(_result, 0); + } + + function test_BtoiWhenPassingBONE() external { + uint256 _result = bNum.call_btoi(BONE); + + // it should return one + assertEq(_result, 1); + } + + function test_BtoiWhenPassingALessThanBONE(uint256 _a) external { + _a = bound(_a, 0, BONE - 1); + + uint256 _result = bNum.call_btoi(_a); + + // it should return zero + assertEq(_result, 0); + } + + function test_BtoiWhenUsingKnownValues() external { + // it should return correct value + // btoi(4 * BONE + 1) = 4 + uint256 _a = 4e18 + 1; + + uint256 _result = bNum.call_btoi(_a); + + assertEq(_result, 4); + } + + function test_BfloorWhenPassingZero() external { + uint256 _result = bNum.call_bfloor(0); + + // it should return zero + assertEq(_result, 0); + } + + function test_BfloorWhenPassingALessThanBONE(uint256 _a) external { + _a = bound(_a, 0, BONE - 1); + + uint256 _result = bNum.call_bfloor(_a); + + // it should return zero + assertEq(_result, 0); + } + + function test_BfloorWhenUsingKnownValues() external { + // it should return correct value + // bfloor(4 * BONE + 1) = 4e18 + + uint256 _a = 4e18 + 1; + + uint256 _result = bNum.call_bfloor(_a); + + assertEq(_result, 4e18); + } + + function test_BaddWhenPassingZeroAndZero() external { + uint256 _result = bNum.call_badd(0, 0); + + // it should return zero + assertEq(_result, 0); + } + + function test_BaddRevertWhen_PassingAAndBTooBig(uint256 _a, uint256 _b) external { + _a = bound(_a, 1, type(uint256).max); + _b = bound(_b, type(uint256).max - _a + 1, type(uint256).max); + + // it should revert + // a + b > uint256 max + vm.expectRevert(BNum.BNum_AddOverflow.selector); + + bNum.call_badd(_a, _b); + } + + function test_BaddWhenPassingKnownValues() external { + // it should return correct value + // 1.25 + 1.25 = 2.5 + uint256 _a = 1.25e18; + uint256 _b = 1.25e18; + + uint256 _result = bNum.call_badd(_a, _b); + + assertEq(_result, 2.5e18); + } + + function test_BsubWhenPassingZeroAndZero() external { + uint256 _result = bNum.call_bsub(0, 0); + + // it should return zero + assertEq(_result, 0); + } + + function test_BsubRevertWhen_PassingALessThanB(uint256 _a, uint256 _b) external { + _a = bound(_a, 0, type(uint256).max - 1); + _b = bound(_b, _a + 1, type(uint256).max); + + // it should revert + vm.expectRevert(BNum.BNum_SubUnderflow.selector); + + bNum.call_bsub(_a, _b); + } + + function test_BsubWhenPassingKnownValues() external { + // it should return correct value + // 5 - 4.01 = 0.99 + uint256 _a = 5e18; + uint256 _b = 4.01e18; + + uint256 _result = bNum.call_bsub(_a, _b); + + assertEq(_result, 0.99e18); + } + + function test_BsubSignWhenPassingZeroAndZero() external { + (uint256 _result, bool _flag) = bNum.call_bsubSign(0, 0); + + // it should return zero and false + assertEq(_result, 0); + assertFalse(_flag); + } + + function test_BsubSignWhenPassingALessThanB(uint256 _a, uint256 _b) external { + _a = bound(_a, 0, type(uint256).max - 1); + _b = bound(_b, _a + 1, type(uint256).max); + + (uint256 _result, bool _flag) = bNum.call_bsubSign(_a, _b); + + // it should return correct value and true + assertEq(_result, _b - _a); + assertTrue(_flag); + } + + function test_BsubSignWhenPassingKnownValues() external { + (uint256 _result, bool _flag) = bNum.call_bsubSign(5e18, 3e18); + + // it should return correct value + assertEq(_result, 2e18); + assertFalse(_flag); + } + + function test_BmulWhenPassingZeroAndZero() external { + uint256 _result = bNum.call_bmul(0, 0); + + // it should return zero + assertEq(_result, 0); + } + + function test_BmulRevertWhen_PassingAAndBTooBig(uint256 _a, uint256 _b) external { + _a = bound(_a, 1, type(uint256).max); + _b = bound(_b, _a == 1 ? type(uint256).max : type(uint256).max / _a + 1, type(uint256).max); + + // it should revert + // a * b > uint256 max + vm.expectRevert(BNum.BNum_MulOverflow.selector); + + bNum.call_bmul(_a, _b); + } + + function test_BmulRevertWhen_PassingAMulBTooBig(uint256 _a, uint256 _b) external { + _a = bound(_a, 1, type(uint256).max); + _b = bound(_b, (type(uint256).max - (BONE / 2)) / _a + 1, type(uint256).max); + + // it should revert + // a * b + BONE / 2 > uint256 max + vm.expectRevert(BNum.BNum_MulOverflow.selector); + + bNum.call_bmul(_a, _b); + } + + function test_BmulWhenPassingKnownValues() external { + // it should return correct value + // 1.25 * 4.75 = 5.9375 + + uint256 _a = 1.25e18; + uint256 _b = 4.75e18; + + uint256 _result = bNum.call_bmul(_a, _b); + + assertEq(_result, 5.9375e18); + } + + function test_BdivRevertWhen_PassingBAsZero(uint256 _a) external { + _a = bound(_a, 0, type(uint256).max); + + // it should revert + vm.expectRevert(BNum.BNum_DivZero.selector); + + bNum.call_bdiv(_a, 0); + } + + function test_BdivWhenPassingAAsZero(uint256 _b) external { + _b = bound(_b, 1, type(uint256).max); + + // it should return zero + uint256 _result = bNum.call_bdiv(0, _b); + + assertEq(_result, 0); + } + + function test_BdivRevertWhen_PassingATooBig(uint256 _a) external { + _a = bound(_a, type(uint256).max / BONE + 1, type(uint256).max); + + // it should revert + // a*BONE > uint256 max + vm.expectRevert(BNum.BNum_DivInternal.selector); + + bNum.call_bdiv(_a, 1); + } + + function test_BdivRevertWhen_PassingAAndBTooBig(uint256 _a, uint256 _b) external { + _a = bound(_a, type(uint256).max / (2 * BONE) + 1, type(uint256).max / BONE); + _b = bound(_b, 2 * (type(uint256).max - (_a * BONE)) + 2, type(uint256).max); + + // it should revert + // a*BONE + b/2 > uint256 max + vm.expectRevert(BNum.BNum_DivInternal.selector); + + bNum.call_bdiv(_a, _b); + } + + function test_BdivWhenFlooringToZero() external { + // it should return zero + // (1 * BONE) / (2 * BONE + 1) = 0.499.. + uint256 _a = 1; + uint256 _b = 2e18 + 1; + + uint256 _result = bNum.call_bdiv(_a, _b); + + assertEq(_result, 0); + } + + function test_BdivWhenFlooringToZero(uint256 _a, uint256 _b) external { + _a = bound(_a, 1, (type(uint256).max / (BONE * 2)) - 1); + _b = bound(_b, (2 * BONE * _a) + 1, type(uint256).max); + + uint256 _result = bNum.call_bdiv(_a, _b); + + // it should return zero + assertEq(_result, 0); + } + + function test_BdivWhenPassingKnownValues() external { + // it should return correct value + // 5 / 2 = 2.5 + uint256 _a = 5e18; + uint256 _b = 2e18; + + uint256 _result = bNum.call_bdiv(_a, _b); + + assertEq(_result, 2.5e18); + } + + function test_BpowiWhenPassingExponentAsZero(uint256 _base) external { + _base = bound(_base, 0, type(uint256).max); + + uint256 _result = bNum.call_bpowi(_base, 0); + + // it should return BONE + assertEq(_result, BONE); + } + + function test_BpowiWhenPassingBaseAsZero(uint256 _exponent) external { + _exponent = bound(_exponent, 1, type(uint256).max); + + uint256 _result = bNum.call_bpowi(0, _exponent); + + // it should return zero + assertEq(_result, 0); + } + + function test_BpowiWhenPassingBaseAsBONE(uint256 _exponent) external { + _exponent = bound(_exponent, 0, type(uint256).max); + + uint256 _result = bNum.call_bpowi(BONE, _exponent); + + // it should return BONE + assertEq(_result, BONE); + } + + function test_BpowiWhenPassingKnownValues() external { + // it should return correct value + // 4. ^ 12 = 16777216 + uint256 _a = 4e18; + uint256 _b = 12; + + uint256 _result = bNum.call_bpowi(_a, _b); + + assertEq(_result, 16_777_216e18); + } + + function test_BpowWhenPassingExponentAsZero(uint256 _base) external { + _base = bound(_base, MIN_BPOW_BASE, MAX_BPOW_BASE); + uint256 _result = bNum.call_bpow(_base, 0); + + // it should return BONE + assertEq(_result, BONE); + } + + function test_BpowRevertWhen_PassingBaseLteThanMIN_BPOW_BASE(uint256 _base) external { + _base = bound(_base, 0, MIN_BPOW_BASE); + + // it should revert + vm.expectRevert(BNum.BNum_BPowBaseTooLow.selector); + + bNum.call_bpow(0, 3e18); + } + + function test_BpowRevertWhen_PassingBaseGteMAX_BPOW_BASE(uint256 _base) external { + _base = bound(_base, MAX_BPOW_BASE, type(uint256).max); + + // it should revert + vm.expectRevert(BNum.BNum_BPowBaseTooHigh.selector); + + bNum.call_bpow(type(uint256).max, 3e18); + } + + function test_BpowWhenPassingKnownValues() external { + // it should return correct value + // 1.01 ^ 3 = 1.030301 + uint256 _a = 1.01e18; + uint256 _b = 3e18; + + uint256 _result = bNum.call_bpow(_a, _b); + assertEq(_result, 1.030301e18); + } +} diff --git a/test/unit/BNum.tree b/test/unit/BNum.tree new file mode 100644 index 00000000..cfa8464d --- /dev/null +++ b/test/unit/BNum.tree @@ -0,0 +1,93 @@ +BNumTest::btoi +├── when passing zero +│ └── it should return zero +├── when passing BONE +│ └── it should return one +├── when passing a less than BONE +│ └── it should return zero +└── when using known values + └── it should return correct value + + +BNumTest::bfloor +├── when passing zero +│ └── it should return zero +├── when passing a less than BONE +│ └── it should return zero +└── when using known values + └── it should return correct value + + +BNumTest::badd +├── when passing zero and zero +│ └── it should return zero +├── when passing a and b too big +│ └── it should revert // a+b > uint256 max +└── when passing known values + └── it should return correct value + + +BNumTest::bsub +├── when passing zero and zero +│ └── it should return zero +├── when passing a less than b +│ └── it should revert +└── when passing known values + └── it should return correct value + + +BNumTest::bsubSign +├── when passing zero and zero +│ └── it should return zero and false +├── when passing a less than b +│ └── it should return correct value and true +└── when passing known values + └── it should return correct value + + +BNumTest::bmul +├── when passing zero and zero +│ └── it should return zero +├── when passing a and b too big +│ └── it should revert // a*b > uint256 max +├── when passing a mul b too big +│ └── it should revert // a*BONE + b/2 > uint256 max +└── when passing known values + └── it should return correct value + + +BNumTest::bdiv +├── when passing b as zero +│ └── it should revert +├── when passing a as zero +│ └── it should return zero +├── when passing a too big +│ └── it should revert // a*BONE > uint256 max +├── when passing a and b too big +│ └── it should revert // a*BONE + b/2 > uint256 max +├── when flooring to zero +│ └── it should return zero +└── when passing known values + └── it should return correct value + + +BNumTest::bpowi +├── when passing exponent as zero +│ └── it should return BONE +├── when passing base as zero +│ └── it should return zero +├── when passing base as BONE +│ └── it should return BONE +└── when passing known values + └── it should return correct value + + +BNumTest::bpow +├── when passing exponent as zero +│ └── it should return BONE +├── when passing base lte than MIN_BPOW_BASE +│ └── it should revert +├── when passing base gte MAX_BPOW_BASE +│ └── it should revert +└── when passing known values + └── it should return correct value \ No newline at end of file From 96a1303bec5194e0e75d064269a4441e5f6f4141 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wei=C3=9Fer=20Hase?= Date: Tue, 2 Jul 2024 22:09:56 +0200 Subject: [PATCH 08/48] test: adding BTT tests for BMath (cont. #123) (#127) * test: adding BTT tests for BMath (#123) * fix: improving tree and filling empty blocks * fix: solhint json * fix: branch phrasing * fix: parenthesis * fix: rm revert reason branches * fix: improving calc spot price branches * fix: improving calcOutGivenIn branches and tests * feat: improving calc In and Out branches and tests * feat: improving calcPoolOut tree and tests * feat: adding calcSingleIn calcs * feat: adding calcSingleOut calcs * feat: adding caclPoolIn calcs expectations * feat: improving calcPoolIn tests * feat: populating missing tests * refactor: mv setup to file start * feat: cleanup internal branch assumptions * fix: rm fn mutability warnings * feat: adding some missing branches * fix: revert expectation comment --------- Co-authored-by: teddy --- src/contracts/BMath.sol | 10 +- test/.solhint.json | 8 +- test/unit/BMath.t.sol | 541 ++++++++++++++++++++++++++++++++++++++++ test/unit/BMath.tree | 141 +++++++++++ 4 files changed, 694 insertions(+), 6 deletions(-) create mode 100644 test/unit/BMath.t.sol create mode 100644 test/unit/BMath.tree diff --git a/src/contracts/BMath.sol b/src/contracts/BMath.sol index 27c72fea..25d7e123 100644 --- a/src/contracts/BMath.sol +++ b/src/contracts/BMath.sol @@ -142,7 +142,7 @@ contract BMath is BConst, BNum { uint256 swapFee ) public pure returns (uint256 poolAmountOut) { // Charge the trading fee for the proportion of tokenAi - /// which is implicitly traded to the other pool tokens. + // which is implicitly traded to the other pool tokens. // That proportion is (1- weightTokenIn) // tokenAiAfterFee = tAi * (1 - (1-weightTi) * poolFee); uint256 normalizedWeight = bdiv(tokenWeightIn, totalWeight); @@ -173,9 +173,9 @@ contract BMath is BConst, BNum { * pS = poolSupply || --------- | ^ | --------- || * bI - bI * pAo = poolAmountOut \\ pS / \(wI / tW)// * bI = balanceIn tAi = -------------------------------------------- - * wI = weightIn / wI \ - * tW = totalWeight | 1 - ---- * sF | - * sF = swapFee \ tW / + * wI = weightIn / wI \ + * tW = totalWeight 1 - | 1 - ---- | * sF + * sF = swapFee \ tW / */ function calcSingleInGivenPoolOut( uint256 tokenBalanceIn, @@ -263,7 +263,7 @@ contract BMath is BConst, BNum { * @dev Formula: * pAi = poolAmountIn // / tAo \\ / wO \ \ * bO = tokenBalanceOut // | bO - -------------------------- |\ | ---- | \ - * tAo = tokenAmountOut pS - || \ 1 - ((1 - (w0 / tW)) * sF)/ | ^ \ tW / * pS | + * tAo = tokenAmountOut pS - || \ 1 - ((1 - (wO / tW)) * sF)/ | ^ \ tW / * pS | * ps = poolSupply \\ -----------------------------------/ / * wO = tokenWeightOut pAi = \\ bO / / * tW = totalWeight ------------------------------------------------------------- diff --git a/test/.solhint.json b/test/.solhint.json index aed4ae9b..a80f69f8 100644 --- a/test/.solhint.json +++ b/test/.solhint.json @@ -7,6 +7,12 @@ "ordering": "off", "private-vars-leading-underscore": ["off"], "state-visibility": "off", - "style-guide-casing": ["warn", { "ignorePublicFunctions": true,"ignoreStructs": true, "ignoreExternalFunctions": true , "ignoreContracts": true} ] + "style-guide-casing": ["warn", { + "ignorePublicFunctions": true, + "ignoreStructs": true, + "ignoreExternalFunctions": true, + "ignoreContracts": true, + "ignoreConstants": true + }] } } diff --git a/test/unit/BMath.t.sol b/test/unit/BMath.t.sol new file mode 100644 index 00000000..25158305 --- /dev/null +++ b/test/unit/BMath.t.sol @@ -0,0 +1,541 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.25; + +import {BConst} from 'contracts/BConst.sol'; +import {BMath, BNum} from 'contracts/BMath.sol'; +import {Test} from 'forge-std/Test.sol'; + +// Main test contract +contract BMathTest is Test, BConst { + BMath bMath; + + // valid scenario + uint256 constant weightIn = BONE; + uint256 constant weightOut = 2 * BONE; + uint256 constant balanceIn = 20 * BONE; + uint256 constant balanceOut = 30 * BONE; + uint256 constant swapFee = BONE / 10; + uint256 constant amountIn = 5 * BONE; + uint256 constant amountOut = 7 * BONE; + uint256 constant totalWeight = 10 * BONE; + uint256 constant poolSupply = 100 * BONE; + + function setUp() external { + bMath = new BMath(); + } + + function test_CalcSpotPriceRevertWhen_TokenWeightInIsZero() external { + uint256 _weightIn = 0; + + // it should revert + // division by zero + vm.expectRevert(BNum.BNum_DivZero.selector); + + bMath.calcSpotPrice(balanceIn, _weightIn, balanceOut, weightOut, swapFee); + } + + function test_CalcSpotPriceRevertWhen_TokenWeightOutIsZero() external { + uint256 _weightOut = 0; + + // it should revert + // division by zero + vm.expectRevert(BNum.BNum_DivZero.selector); + + bMath.calcSpotPrice(balanceIn, weightIn, balanceOut, _weightOut, swapFee); + } + + function test_CalcSpotPriceRevertWhen_WeightedTokenBalanceOutIsZero(uint256 _balanceOut, uint256 _weightOut) external { + _weightOut = bound(_weightOut, MIN_WEIGHT, MAX_WEIGHT); + _balanceOut = bound(_balanceOut, 0, _weightOut / (2 * BONE + 1)); // floors to zero + + // it should revert + // division by zero + vm.expectRevert(BNum.BNum_DivZero.selector); + + bMath.calcSpotPrice(balanceIn, weightIn, _balanceOut, _weightOut, swapFee); + } + + function test_CalcSpotPriceRevertWhen_SwapFeeGreaterThanBONE(uint256 _swapFee) external { + _swapFee = bound(_swapFee, BONE + 1, type(uint256).max); + + // it should revert + // subtraction underflow + vm.expectRevert(BNum.BNum_SubUnderflow.selector); + + bMath.calcSpotPrice(balanceIn, weightIn, balanceOut, weightOut, _swapFee); + } + + function test_CalcSpotPriceRevertWhen_SwapFeeEqualsBONE() external { + uint256 _swapFee = BONE; + + // it should revert + // division by zero + vm.expectRevert(BNum.BNum_DivZero.selector); + + bMath.calcSpotPrice(balanceIn, weightIn, balanceOut, weightOut, _swapFee); + } + + function test_CalcSpotPriceWhenSwapFeeIsZero() external virtual { + // it should return correct value + // bi/wi * wo/bo + // 20/1 * 2/30 = 1.333333... + uint256 _spotPrice = bMath.calcSpotPrice(balanceIn, weightIn, balanceOut, weightOut, 0); + + assertEq(_spotPrice, 1.333333333333333333e18); + } + + function test_CalcSpotPriceWhenSwapFeeIsNonZero() external virtual { + // it should return correct value + // (bi/wi * wo/bo) * (1 / (1 - sf)) + // (20/1 * 2/30) * (1 / (1 - 0.1)) = 1.481481481... + uint256 _spotPrice = bMath.calcSpotPrice(balanceIn, weightIn, balanceOut, weightOut, swapFee); + + assertEq(_spotPrice, 1.481481481481481481e18); + } + + function test_CalcOutGivenInRevertWhen_TokenWeightOutIsZero() external { + uint256 _weightOut = 0; + + // it should revert + // division by zero + vm.expectRevert(BNum.BNum_DivZero.selector); + + bMath.calcOutGivenIn(balanceIn, weightIn, balanceOut, _weightOut, amountIn, swapFee); + } + + function test_CalcOutGivenInRevertWhen_SwapFeeGreaterThanBONE(uint256 _swapFee) external { + _swapFee = bound(_swapFee, BONE + 1, type(uint256).max); + + // it should revert + // subtraction underflow + vm.expectRevert(BNum.BNum_SubUnderflow.selector); + + 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); + + // it should revert + // ai * (1 - sf) > uint256 max + vm.expectRevert(BNum.BNum_MulOverflow.selector); + + bMath.calcOutGivenIn(balanceIn, weightIn, balanceOut, weightOut, _amountIn, swapFee); + } + + function test_CalcOutGivenInRevertWhen_TokenBalanceInAndAmountInAreZero() external { + uint256 _balanceIn = 0; + uint256 _amountIn = 0; + + // it should revert + // bi + (ai * (1 - swapFee)) = 0 + vm.expectRevert(BNum.BNum_DivZero.selector); + + bMath.calcOutGivenIn(_balanceIn, weightIn, balanceOut, weightOut, _amountIn, swapFee); + } + + function test_CalcOutGivenInRevertWhen_TokenBalanceInIsZeroAndSwapFeeEqualsBONE() external { + uint256 _balanceIn = 0; + uint256 _swapFee = BONE; + + // it should revert + // bi + (ai * (1 - swapFee)) = 0 + vm.expectRevert(BNum.BNum_DivZero.selector); + + bMath.calcOutGivenIn(_balanceIn, weightIn, balanceOut, weightOut, amountIn, _swapFee); + } + + function test_CalcOutGivenInWhenTokenWeightInIsZero() external virtual { + uint256 _weightIn = 0; + + // it should return zero + uint256 _amountOut = bMath.calcOutGivenIn(balanceIn, _weightIn, balanceOut, weightOut, amountIn, swapFee); + + assertEq(_amountOut, 0); + } + + modifier whenTokenWeightsAreEqual() { + _; + } + + function test_CalcOutGivenInWhenEqualWeightsAndSwapFeeIsZero(uint256 _weight) + external + virtual + whenTokenWeightsAreEqual + { + _weight = bound(_weight, MIN_WEIGHT, MAX_WEIGHT); + + // it should return correct value + // bo * (1 - (bi / (bi + ai)) + // 30 * (1 - (20 / (20 + 5))) = 6 + uint256 _amountOut = bMath.calcOutGivenIn(balanceIn, _weight, balanceOut, _weight, amountIn, 0); + + assertEq(_amountOut, 6e18); + } + + function test_CalcOutGivenInWhenEqualWeightsAndSwapFeeIsNonZero(uint256 _weight) + external + virtual + whenTokenWeightsAreEqual + { + _weight = bound(_weight, MIN_WEIGHT, MAX_WEIGHT); + + // it should return correct value + // bo * (1 - (bi / (bi + (ai * (1-sf)))) + // 30 * (1 - (20 / (20 + (5 * (1 - 0.1)))) = 5.5102040816... + uint256 _amountOut = bMath.calcOutGivenIn(balanceIn, _weight, balanceOut, _weight, amountIn, swapFee); + + assertEq(_amountOut, 5.51020408163265306e18); + } + + modifier whenTokenWeightsAreUnequal() { + _; + } + + function test_CalcOutGivenInWhenUnequalWeightsAndSwapFeeIsZero() external virtual whenTokenWeightsAreUnequal { + // it should return correct value + // b0 * (1 - (bi / ((bi + ai)))^(wi/wo)) + // 30 * (1 - (20 / ((20 + 5)))^(1/2)) = 3.16718427... + uint256 _amountOut = bMath.calcOutGivenIn(balanceIn, weightIn, balanceOut, weightOut, amountIn, 0); + + assertEq(_amountOut, 3.16718426981698245e18); + } + + function test_CalcOutGivenInWhenUnequalWeightsAndSwapFeeIsNonZero() external virtual whenTokenWeightsAreUnequal { + // it should return correct value + // b0 * (1 - (bi / ((bi + (ai * (1 - sf)))))^(wi/wo)) + // 30 * (1 - (20 / ((20 + (5 * (1 - 0.1)))))^(1/2)) = 2.8947629128... + uint256 _amountOut = bMath.calcOutGivenIn(balanceIn, weightIn, balanceOut, weightOut, amountIn, swapFee); + + assertEq(_amountOut, 2.89476291247227984e18); + } + + function test_CalcInGivenOutRevertWhen_TokenWeightInIsZero() external { + uint256 _weightIn = 0; + + // it should revert + // division by zero + vm.expectRevert(BNum.BNum_DivZero.selector); + + bMath.calcInGivenOut(balanceIn, _weightIn, balanceOut, weightOut, amountIn, swapFee); + } + + function test_CalcInGivenOutRevertWhen_TokenAmountOutGreaterThanTokenBalanceOut( + uint256 _balanceOut, + uint256 _amountOut + ) external { + _balanceOut = bound(_balanceOut, 1, type(uint256).max / BONE); + _amountOut = bound(_amountOut, _balanceOut + 1, type(uint256).max); + + // it should revert + // subtraction underflow + vm.expectRevert(BNum.BNum_SubUnderflow.selector); + + bMath.calcInGivenOut(balanceIn, weightIn, _balanceOut, weightOut, _amountOut, swapFee); + } + + function test_CalcInGivenOutRevertWhen_TokenAmountOutEqualsTokenBalanceOut(uint256 _amount) external { + _amount = bound(_amount, 1, type(uint256).max / BONE); + + // it should revert + // division by zero + vm.expectRevert(BNum.BNum_DivZero.selector); + + bMath.calcInGivenOut(balanceIn, weightIn, _amount, weightOut, _amount, swapFee); + } + + function test_CalcInGivenOutRevertWhen_SwapFeeGreaterThanBONE(uint256 _swapFee) external { + _swapFee = bound(_swapFee, BONE + 1, type(uint256).max); + + // it should revert + // subtraction underflow + vm.expectRevert(BNum.BNum_SubUnderflow.selector); + + bMath.calcInGivenOut(balanceIn, weightIn, balanceOut, weightOut, amountOut, _swapFee); + } + + function test_CalcInGivenOutRevertWhen_SwapFeeEqualsBONE() external { + uint256 _swapFee = BONE; + + // it should revert + // division by zero + vm.expectRevert(BNum.BNum_DivZero.selector); + + bMath.calcInGivenOut(balanceIn, weightIn, balanceOut, weightOut, amountOut, _swapFee); + } + + function test_CalcInGivenOutWhenTokenWeightOutIsZero() external virtual { + uint256 _weightOut = 0; + + uint256 _amountIn = bMath.calcInGivenOut(balanceIn, weightIn, balanceOut, _weightOut, amountOut, swapFee); + + // it should return zero + assertEq(_amountIn, 0); + } + + function test_CalcInGivenOutWhenEqualWeightsAndSwapFeeIsZero(uint256 _weights) + external + virtual + whenTokenWeightsAreEqual + { + _weights = bound(_weights, MIN_WEIGHT, MAX_WEIGHT); + + // it should return correct value + // bi * ((bo/(bo-ao) - 1))) + // 20 * ((30/(30-7) - 1)) = 6.08695652174... + uint256 _amountIn = bMath.calcInGivenOut(balanceIn, _weights, balanceOut, _weights, amountOut, 0); + + assertEq(_amountIn, 6.08695652173913044e18); + } + + function test_CalcInGivenOutWhenEqualWeightsAndSwapFeeIsNonZero(uint256 _weights) + external + virtual + whenTokenWeightsAreEqual + { + _weights = bound(_weights, MIN_WEIGHT, MAX_WEIGHT); + // it should return correct value + // bi * ((bo/(bo-ao) - 1))) / (1 - sf) + // 20 * ((30/(30-7) - 1)) / (1 - 0.1) = 6.7632850242... + uint256 _amountIn = bMath.calcInGivenOut(balanceIn, _weights, balanceOut, _weights, amountOut, swapFee); + + assertEq(_amountIn, 6.763285024154589378e18); + } + + function test_CalcInGivenOutWhenUnequalWeightsAndSwapFeeIsZero() external virtual whenTokenWeightsAreUnequal { + // it should return correct value + // bi * (((bo/(bo-ao))^(wo/wi) - 1))) + // 20 * (((30/(30-7))^(2/1) - 1)) = 14.02646502836... + uint256 _amountIn = bMath.calcInGivenOut(balanceIn, weightIn, balanceOut, weightOut, amountOut, 0); + + assertEq(_amountIn, 14.02646502835538754e18); + } + + function test_CalcInGivenOutWhenUnequalWeightsAndSwapFeeIsNonZero() external virtual whenTokenWeightsAreUnequal { + // it should return correct value + // bi * (((bo/(bo-ao))^(wo/wi) - 1))) / (1 - sf) + // 20 * (((30/(30-7))^(2/1) - 1)) / (1 - 0.1) = 15.5849611426... + uint256 _amountIn = bMath.calcInGivenOut(balanceIn, weightIn, balanceOut, weightOut, amountOut, swapFee); + + assertEq(_amountIn, 15.584961142617097267e18); + } + + function test_CalcPoolOutGivenSingleInRevertWhen_TokenBalanceInIsZero() external { + uint256 _balanceIn = 0; + + // it should revert + // division by zero + vm.expectRevert(BNum.BNum_DivZero.selector); + + bMath.calcPoolOutGivenSingleIn(_balanceIn, weightIn, poolSupply, totalWeight, amountIn, swapFee); + } + + function test_CalcPoolOutGivenSingleInWhenTokenWeightInIsZero() external virtual { + uint256 _weightIn = 0; + + // it should return zero + uint256 _poolOut = bMath.calcPoolOutGivenSingleIn(balanceIn, _weightIn, poolSupply, totalWeight, amountIn, swapFee); + + assertEq(_poolOut, 0); + } + + function test_CalcPoolOutGivenSingleInRevertWhen_TotalWeightIsZero() external virtual { + uint256 _totalWeight = 0; + + // it should revert + // division by zero + vm.expectRevert(BNum.BNum_DivZero.selector); + + uint256 _poolOut = bMath.calcPoolOutGivenSingleIn(balanceIn, weightIn, poolSupply, _totalWeight, amountIn, swapFee); + + assertEq(_poolOut, 0); + } + + function test_CalcPoolOutGivenSingleInWhenSwapFeeIsZero() external virtual { + // it should return correct value + // ((( ai + bi ) / bi ) ^ (wi/wT)) * pS - pS + // ((( 5 + 20 ) / 20 ) ^ (1/10)) * 100 - 100 = 2.2565182564... + uint256 _poolOut = bMath.calcPoolOutGivenSingleIn(balanceIn, weightIn, poolSupply, totalWeight, amountIn, 0); + + assertEq(_poolOut, 2.2565182579165133e18); + } + + function test_CalcPoolOutGivenSingleInWhenSwapFeeIsNonZero() external virtual { + // it should return correct value + // ((( ai * (1 - ((1-(wi/wT))*sf)) + bi) / bi ) ^ (wi/wT)) * pS - pS + // ((( 5 * (1 - ((1-(1/10))*0.1)) + 20) / 20 ) ^ (1/10)) * 100 - 100 = 2.07094840224... + uint256 _poolOut = bMath.calcPoolOutGivenSingleIn(balanceIn, weightIn, poolSupply, totalWeight, amountIn, swapFee); + + assertEq(_poolOut, 2.0709484026610259e18); + } + + function test_CalcSingleInGivenPoolOutRevertWhen_TotalWeightIsZero() external { + uint256 _totalWeight = 0; + + // it should revert + // division by zero + vm.expectRevert(BNum.BNum_DivZero.selector); + + bMath.calcSingleInGivenPoolOut(balanceIn, weightIn, poolSupply, _totalWeight, amountOut, swapFee); + } + + function test_CalcSingleInGivenPoolOutWhenSwapFeeIsZero() external virtual { + // it should return correct value + // (((pS + ao) / pS) ^ (wT / wi)) * bi - bi + // (((100 + 7) / 100) ^ (10 / 1)) * 20 - 20 = 19.3430271458... + uint256 _amountIn = bMath.calcSingleInGivenPoolOut(balanceIn, weightIn, poolSupply, totalWeight, amountOut, 0); + + assertEq(_amountIn, 19.34302714579130644e18); + } + + function test_CalcSingleInGivenPoolOutWhenSwapFeeIsNonZero() external virtual { + // it should return correct value + // ((((pS + ao) / pS) ^ (wT/wi)) * bi - bi) / (1 - (1 - (wi/wT)) * sf) + // ((((100 + 7) / 100) ^ (10/1)) * 20 - 20) / (1 - (1 - (1/10)) * 0.1) = 21.2560737866... + uint256 _amountIn = bMath.calcSingleInGivenPoolOut(balanceIn, weightIn, poolSupply, totalWeight, amountOut, swapFee); + + assertEq(_amountIn, 21.256073786583853231e18); + } + + function test_CalcSingleOutGivenPoolInRevertWhen_PoolSupplyIsZero() external { + uint256 _poolSupply = 0; + + // it should revert + // division by zero + vm.expectRevert(BNum.BNum_SubUnderflow.selector); + + bMath.calcSingleOutGivenPoolIn(balanceOut, weightOut, _poolSupply, totalWeight, amountIn, swapFee); + } + + function test_CalcSingleOutGivenPoolInRevertWhen_TotalWeightIsZero() external { + uint256 _totalWeight = 0; + + // it should revert + // division by zero + vm.expectRevert(BNum.BNum_DivZero.selector); + + bMath.calcSingleOutGivenPoolIn(balanceOut, weightOut, poolSupply, _totalWeight, amountIn, swapFee); + } + + function test_CalcSingleOutGivenPoolInWhenTokenBalanceOutIsZero() external virtual { + uint256 _balanceOut = 0; + + // it should return zero + uint256 _amountOut = + bMath.calcSingleOutGivenPoolIn(_balanceOut, weightOut, poolSupply, totalWeight, amountIn, swapFee); + + assertEq(_amountOut, 0); + } + + function test_CalcSingleOutGivenPoolInWhenSwapFeeAndExitFeeAreZero() external virtual { + // it should return correct value + // bo - ((pS - ai)/pS)^(wT/wo) * bo + // 30 - ((100 - 5)/100)^(10/2) * 30 = 6.786571875 + uint256 _amountOut = bMath.calcSingleOutGivenPoolIn(balanceOut, weightOut, poolSupply, totalWeight, amountIn, 0); + + assertEq(_amountOut, 6.786571875e18); + } + + function test_CalcSingleOutGivenPoolInWhenSwapFeeIsZeroAndExitFeeIsNonZero() external { + vm.skip(true); // NOTE: EXIT_FEE is hardcoded to 0 + // it should return correct value + // bo - ((pS - (ai * (1 - ef))/pS)^(wT/wo) * bo + } + + function test_CalcSingleOutGivenPoolInWhenSwapFeeIsNonZeroAndExitFeeIsZero() external virtual { + // it should return correct value + // (bo - ((pS - ai/pS)^(wT/wo) * bo) * (1 - ((1 - (wo/wT)) * sf)) + // (30 - ((100 - 5)/100)^(10/2) * 30) * (1 - ((1 - (2/10)) * 0.1)) = 6.243646125... + uint256 _amountOut = + bMath.calcSingleOutGivenPoolIn(balanceOut, weightOut, poolSupply, totalWeight, amountIn, swapFee); + + assertEq(_amountOut, 6.243646125e18); + } + + function test_CalcSingleOutGivenPoolInWhenSwapFeeAndExitFeeAreNonZero() external { + vm.skip(true); // NOTE: EXIT_FEE is hardcoded to 0 + // it should return correct value + // (bo - ((pS - (ai * (1 - ef))/pS)^(wT/wo) * bo) * (1 - ((1 - (wo/wT)) * sf)) + } + + function test_CalcPoolInGivenSingleOutRevertWhen_TokenBalanceOutIsZero() external { + uint256 _balanceOut = 0; + + // it should revert + // subtraction underflow + vm.expectRevert(BNum.BNum_SubUnderflow.selector); + + bMath.calcPoolInGivenSingleOut(_balanceOut, weightOut, poolSupply, totalWeight, amountOut, swapFee); + } + + function test_CalcPoolInGivenSingleOutRevertWhen_SwapFeeIs1AndTokenWeightOutIsZero() external { + uint256 _swapFee = BONE; + uint256 _weightOut = 0; + + // it should revert + // division by zero + vm.expectRevert(BNum.BNum_DivZero.selector); + + bMath.calcPoolInGivenSingleOut(balanceOut, _weightOut, poolSupply, totalWeight, amountOut, _swapFee); + } + + function test_CalcPoolInGivenSingleOutWhenTokenAmountOutIsZero() external virtual { + uint256 _amountOut = 0; + + // it should return zero + uint256 _amountIn = + bMath.calcPoolInGivenSingleOut(balanceOut, weightOut, poolSupply, totalWeight, _amountOut, swapFee); + + assertEq(_amountIn, 0); + } + + function test_CalcPoolInGivenSingleOutWhenPoolSupplyIsZero() external virtual { + uint256 _poolSupply = 0; + + // it should return zero + uint256 _amountIn = + bMath.calcPoolInGivenSingleOut(balanceOut, weightOut, _poolSupply, totalWeight, amountOut, swapFee); + + assertEq(_amountIn, 0); + } + + function test_CalcPoolInGivenSingleOutWhenSwapFeeAndExitFeeAreZero() external virtual { + // it should return correct value + // pS - (( (bo - ao) / bo ) ^ (wo/wT)) * pS + // 100 - (( (30 - 7) / 30 ) ^ (2/10)) * 100 = 5.1753351805... + uint256 _amountIn = bMath.calcPoolInGivenSingleOut(balanceOut, weightOut, poolSupply, totalWeight, amountOut, 0); + + assertEq(_amountIn, 5.1753351791902224e18); + } + + function test_CalcPoolInGivenSingleOutWhenSwapFeeIsZeroAndExitFeeIsNonZero() external { + vm.skip(true); // NOTE: EXIT_FEE is hardcoded to 0 + // it should return correct value + // (pS - (( (bo - ao) / bo ) ^ (wo/wT)) * pS) / (1 - ef) + } + + function test_CalcPoolInGivenSingleOutWhenSwapFeeIsNonZeroAndExitFeeIsZero() external virtual { + // it should return correct value + // pS - (( (bo - (ao / ( 1 - ((1 - (wo/wT)) * sf) ))) / bo ) ^ (wo/wT)) * pS + // 100 - (( (30 - (7 / ( 1 - ((1 - (2/10)) * 0.1) ))) / 30 ) ^ (2/10)) * 100 = 5.682641831... + uint256 _amountIn = + bMath.calcPoolInGivenSingleOut(balanceOut, weightOut, poolSupply, totalWeight, amountOut, swapFee); + + assertEq(_amountIn, 5.6826418299618119e18); + } + + function test_CalcPoolInGivenSingleOutWhenSwapFeeAndExitFeeAreNonZero() external { + vm.skip(true); // NOTE: EXIT_FEE is hardcoded to 0 + // it should return correct value + // (pS - (( (bo - (ao / ( 1 - ((1 - (wo/wT)) * sf) ))) / bo ) ^ (wo/wT)) * pS) / (1 - ef) + } +} diff --git a/test/unit/BMath.tree b/test/unit/BMath.tree new file mode 100644 index 00000000..be0417b9 --- /dev/null +++ b/test/unit/BMath.tree @@ -0,0 +1,141 @@ +BMathTest::calcSpotPrice +├── when token weight in is zero +│ └── it should revert // division by zero +├── when token weight out is zero +│ └── it should revert // division by zero +├── when weighted token balance out is zero +│ └── it should revert // division by zero +├── when swapFee greater than BONE +│ └── it should revert // subtraction underflow +├── when swapFee equals BONE +│ └── it should revert // division by zero +├── when swap fee is zero +│ └── it should return correct value +│ └── bi/wi * wo/bo +└── when swap fee is non zero + └── it should return correct value + └── (bi/wi * wo/bo) * (1 / (1 - sf)) + +BMathTest::calcOutGivenIn +├── when token weight out is zero +│ └── 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 +├── when token balance in is zero and swap fee equals BONE +│ └── it should revert // bi + (ai * (1 - swapFee)) = 0 +├── when token weight in is zero +│ └── it should return zero +├── when token weights are equal +│ ├── when equal weights and swap fee is zero +│ │ └── it should return correct value +│ │ └── bo * (1 - (bi / (bi + ai)) +│ └── when equal weights and swap fee is non zero +│ └── it should return correct value +│ └── bo * (1 - (bi / (bi + (ai * (1-sf)))) +└── when token weights are unequal + ├── when unequal weights and swap fee is zero + │ └── it should return correct value + │ └── b0 * (1 - (bi / ((bi + ai)))^(wi/wo)) + └── when unequal weights and swap fee is non zero + └── it should return correct value + └── b0 * (1 - (bi / ((bi + (ai * (1 - sf)))))^(wi/wo)) + +BMathTest::calcInGivenOut +├── when token weight in is zero +│ └── it should revert // division by zero +├── when token amount out greater than token balance out +│ └── it should revert // subtraction underflow +├── when token amount out equals token balance out +│ └── it should revert // division by zero +├── when swapFee greater than BONE +│ └── it should revert // subtraction underflow +├── when swapFee equals BONE +│ └── it should revert // division by zero +├── when token weight out is zero +│ └── it should return zero +├── when token weights are equal +│ ├── when equal weights and swap fee is zero +│ │ └── it should return correct value +│ │ └── bi * ((bo/(bo-ao) - 1))) +│ └── when equal weights and swap fee is non zero +│ └── it should return correct value +│ └── bi * ((bo/(bo-ao) - 1))) / (1 - sf) +└── when token weights are unequal + ├── when unequal weights and swap fee is zero + │ └── it should return correct value + │ └── bi * (((bo/(bo-ao))^(wo/wi) - 1))) + └── when unequal weights and swap fee is non zero + └── it should return correct value + └── bi * (((bo/(bo-ao))^(wo/wi) - 1))) / (1 - sf) + +BMathTest::calcPoolOutGivenSingleIn +├── when token balance in is zero +│ └── it should revert // division by zero +├── when token weight in is zero +│ └── it should return zero +├── when total weight is zero +│ └── it should revert // division by zero +├── when swap fee is zero +│ └── it should return correct value +│ └── ((( ai + bi ) / bi ) ^ (wi/wT)) * pS - pS +└── when swap fee is non zero + └── it should return correct value + └── ((( ai * (1 - ((1-(wi/wT))*sf)) + bi) / bi ) ^ (wi/wT)) * pS - pS + +BMathTest::calcSingleInGivenPoolOut +├── when total weight is zero +│ └── it should revert // division by zero +├── when swap fee is zero +│ └── it should return correct value +│ └── (((pS + ao) / pS) ^ (wT/wi))*bi - bi +└── when swap fee is non zero + └── it should return correct value + └── ((((pS + ao) / pS) ^ (wT/wi))*bi - bi) / (1 - ((1 - (wi/wT)) * sf)) + +BMathTest::calcSingleOutGivenPoolIn +├── when pool supply is zero +│ └── it should revert // division by zero +├── when total weight is zero +│ └── it should revert // division by zero +├── when token balance out is zero +│ └── it should return zero +├── when swap fee and exit fee are zero +│ └── it should return correct value +│ └── bo - ((pS - ai)/pS)^(wT/wo) * bo +├── when swap fee is zero and exit fee is non zero +│ └── it should return correct value +│ └── bo - ((pS - (ai * (1 - ef))/pS)^(wT/wo) * bo +├── when swap fee is non zero and exit fee is zero +│ └── it should return correct value +│ └── (bo - ((pS - ai/pS)^(wT/wo) * bo) * (1 - ((1 - (wo/wT)) * sf)) +└── when swap fee and exit fee are non zero + └── it should return correct value + └── (bo - ((pS - (ai * (1 - ef))/pS)^(wT/wo) * bo) * (1 - ((1 - (wo/wT)) * sf)) + +BMathTest::calcPoolInGivenSingleOut +├── when token balance out is zero +│ └── it should revert // subtraction underflow +├── when swap fee is 1 and token weight out is zero +│ └── it should revert // division by zero +├── when token amount out is zero +│ └── it should return zero +├── when pool supply is zero +│ └── it should return zero +├── when swap fee and exit fee are zero +│ └── it should return correct value +│ └── pS - (( (bo - ao) / bo ) ^ (wo/wT)) * pS +├── when swap fee is zero and exit fee is non zero +│ └── it should return correct value +│ └── (pS - (( (bo - ao) / bo ) ^ (wo/wT)) * pS) / (1 - ef) +├── when swap fee is non zero and exit fee is zero +│ └── it should return correct value +│ └── pS - (( (bo - (ao / ( 1 - ((1 - (wo/wT)) * sf) ))) / bo ) ^ (wo/wT)) * pS +└── when swap fee and exit fee are non zero + └── it should return correct value + └── (pS - (( (bo - (ao / ( 1 - ((1 - (wo/wT)) * sf) ))) / bo ) ^ (wo/wT)) * pS) / (1 - ef) \ No newline at end of file From 9f8015df765da13cc9d4e8b790f4d9327d341b01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wei=C3=9Fer=20Hase?= Date: Wed, 3 Jul 2024 22:49:45 +0200 Subject: [PATCH 09/48] feat: adding address zero checks in setters (#132) * feat: adding address zero checks in setters * fix: updating gas snapshots * fix: comment in test --- .forge-snapshots/newBFactory.snap | 2 +- .forge-snapshots/newBPool.snap | 2 +- src/contracts/BFactory.sol | 4 ++++ src/contracts/BPool.sol | 4 ++++ src/interfaces/IBFactory.sol | 5 +++++ src/interfaces/IBPool.sol | 5 +++++ test/unit/BFactory.t.sol | 24 ++++++++++++++++-------- test/unit/BFactory.tree | 7 +++++-- test/unit/BPool.t.sol | 6 ++++++ 9 files changed, 47 insertions(+), 12 deletions(-) diff --git a/.forge-snapshots/newBFactory.snap b/.forge-snapshots/newBFactory.snap index ce6f1abf..59ddf9ce 100644 --- a/.forge-snapshots/newBFactory.snap +++ b/.forge-snapshots/newBFactory.snap @@ -1 +1 @@ -4012301 \ No newline at end of file +4029197 \ No newline at end of file diff --git a/.forge-snapshots/newBPool.snap b/.forge-snapshots/newBPool.snap index 0c307839..94357d20 100644 --- a/.forge-snapshots/newBPool.snap +++ b/.forge-snapshots/newBPool.snap @@ -1 +1 @@ -3404909 \ No newline at end of file +3412772 \ No newline at end of file diff --git a/src/contracts/BFactory.sol b/src/contracts/BFactory.sol index 67f57262..e96d1a0d 100644 --- a/src/contracts/BFactory.sol +++ b/src/contracts/BFactory.sol @@ -31,6 +31,10 @@ contract BFactory is IBFactory { /// @inheritdoc IBFactory function setBLabs(address b) external { + if (b == address(0)) { + revert BFactory_AddressZero(); + } + if (msg.sender != _blabs) { revert BFactory_NotBLabs(); } diff --git a/src/contracts/BPool.sol b/src/contracts/BPool.sol index 560b09f7..81e27999 100644 --- a/src/contracts/BPool.sol +++ b/src/contracts/BPool.sol @@ -86,6 +86,10 @@ contract BPool is BToken, BMath, IBPool { /// @inheritdoc IBPool function setController(address manager) external _logs_ _lock_ _controller_ { + if (manager == address(0)) { + revert BPool_AddressZero(); + } + _controller = manager; } diff --git a/src/interfaces/IBFactory.sol b/src/interfaces/IBFactory.sol index d2e64841..e408b470 100644 --- a/src/interfaces/IBFactory.sol +++ b/src/interfaces/IBFactory.sol @@ -18,6 +18,11 @@ interface IBFactory { */ event LOG_BLABS(address indexed caller, address indexed bLabs); + /** + * @notice Thrown when setting a variable to address zero + */ + error BFactory_AddressZero(); + /** * @notice Thrown when caller is not BLabs address */ diff --git a/src/interfaces/IBPool.sol b/src/interfaces/IBPool.sol index fe5e664d..2b3e7a5e 100644 --- a/src/interfaces/IBPool.sol +++ b/src/interfaces/IBPool.sol @@ -56,6 +56,11 @@ interface IBPool is IERC20 { */ event LOG_CALL(bytes4 indexed sig, address indexed caller, bytes data) anonymous; + /** + * @notice Thrown when setting a variable to address zero + */ + error BPool_AddressZero(); + /** * @notice Thrown when a reentrant call is made */ diff --git a/test/unit/BFactory.t.sol b/test/unit/BFactory.t.sol index 945470f0..7f779303 100644 --- a/test/unit/BFactory.t.sol +++ b/test/unit/BFactory.t.sol @@ -67,15 +67,28 @@ contract BFactoryTest is Test { factory.setBLabs(makeAddr('newBLabs')); } - function test_SetBLabsWhenTheSenderIsTheCurrentBLabs(address _newBLabs) external { + modifier whenTheSenderIsTheCurrentBLabs() { + vm.startPrank(factoryDeployer); + _; + } + + function test_SetBLabsRevertWhen_TheAddressIsZero() external whenTheSenderIsTheCurrentBLabs { + // it should revert + vm.expectRevert(IBFactory.BFactory_AddressZero.selector); + + factory.setBLabs(address(0)); + } + + function test_SetBLabsWhenTheAddressIsNotZero(address _newBLabs) external whenTheSenderIsTheCurrentBLabs { + vm.assume(_newBLabs != address(0)); + // it should emit a BLabsSet event vm.expectEmit(address(factory)); emit IBFactory.LOG_BLABS(factoryDeployer, _newBLabs); - vm.prank(factoryDeployer); factory.setBLabs(_newBLabs); - // it should set the new setBLabs address + // it should set the new bLabs address assertEq(factory.getBLabs(), _newBLabs); } @@ -89,11 +102,6 @@ contract BFactoryTest is Test { factory.collect(IBPool(makeAddr('pool'))); } - modifier whenTheSenderIsTheCurrentBLabs() { - vm.startPrank(factoryDeployer); - _; - } - function test_CollectWhenTheSenderIsTheCurrentBLabs(uint256 _factoryBTBalance) external whenTheSenderIsTheCurrentBLabs diff --git a/test/unit/BFactory.tree b/test/unit/BFactory.tree index 099584f3..bd2dce21 100644 --- a/test/unit/BFactory.tree +++ b/test/unit/BFactory.tree @@ -18,8 +18,11 @@ BFactoryTest::setBLabs ├── when the sender is not the current BLabs │ └── it should revert └── when the sender is the current BLabs - ├── it should set the new BLabs address - └── it should emit a BLabsSet event + ├── when the address is zero + │ └── it should revert + └── when the address is not zero + ├── it should set the new BLabs address + └── it should emit a BLabsSet event BFactoryTest::collect diff --git a/test/unit/BPool.t.sol b/test/unit/BPool.t.sol index 3ee5ff5e..6b28fb8e 100644 --- a/test/unit/BPool.t.sol +++ b/test/unit/BPool.t.sol @@ -611,6 +611,12 @@ contract BPool_Unit_SetController is BasePoolTest { bPool.setController(_controller); } + function test_Revert_AddressZero() public { + vm.expectRevert(IBPool.BPool_AddressZero.selector); + + bPool.setController(address(0)); + } + function test_Set_Controller(address _controller) public { bPool.setController(_controller); From 8c0dfcbc875db35dc920fd4a8d2bce8de41de2f2 Mon Sep 17 00:00:00 2001 From: teddy Date: Thu, 4 Jul 2024 09:33:51 -0300 Subject: [PATCH 10/48] ci: run bulloak check (#130) * ci: setup bulloak in CI * fix: use explicit search instead of depending on bulloaks globbing --- .github/workflows/ci.yml | 8 ++++++++ package.json | 1 + 2 files changed, 9 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cdb26d10..5436cfb8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -87,11 +87,19 @@ jobs: node-version: 18.x cache: 'yarn' + - name: Install bulloak + uses: baptiste0928/cargo-install@v3 + with: + crate: bulloak + - name: Install dependencies run: yarn --frozen-lockfile --network-concurrency 1 - name: Run forge-fmt && solhint run: yarn lint:check + - name: Run bulloak check + run: yarn lint:bulloak + - name: Run natspec-smells run: yarn lint:natspec 2>&1 >/dev/null | grep 'No issues found' diff --git a/package.json b/package.json index 49ddd2a3..c3eef8f5 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "deploy:bcowfactory:testnet": "bash -c 'source .env && forge script DeployBCoWFactory -vvvvv --rpc-url $SEPOLIA_RPC --broadcast --chain sepolia --private-key $SEPOLIA_DEPLOYER_PK --verify --etherscan-api-key $ETHERSCAN_API_KEY'", "deploy:bfactory:mainnet": "bash -c 'source .env && forge script DeployBFactory -vvvvv --rpc-url $MAINNET_RPC --broadcast --chain mainnet --private-key $MAINNET_DEPLOYER_PK --verify --etherscan-api-key $ETHERSCAN_API_KEY'", "deploy:bfactory:testnet": "bash -c 'source .env && forge script DeployBFactory -vvvvv --rpc-url $SEPOLIA_RPC --broadcast --chain sepolia --private-key $SEPOLIA_DEPLOYER_PK --verify --etherscan-api-key $ETHERSCAN_API_KEY'", + "lint:bulloak": "find test/unit -name '*.tree' | xargs bulloak check", "lint:check": "solhint 'src/**/*.sol' 'test/**/*.sol' 'script/**/*.sol' && forge fmt --check", "lint:fix": "solhint --fix 'src/**/*.sol' 'test/**/*.sol' 'script/**/*.sol' && sort-package-json && forge fmt", "lint:natspec": "npx @defi-wonderland/natspec-smells --config natspec-smells.config.js", From 79b8d58d6715147550cb7cab26569288599bc2e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wei=C3=9Fer=20Hase?= Date: Thu, 4 Jul 2024 16:20:58 +0200 Subject: [PATCH 11/48] feat: addressing Certora gas optimizations (#131) * feat: adding join and exit snaps * feat: caching array.length into memory * feat: changing /2 for >>1 when possible * feat: making join/exit integration test 3-token * feat: making factory immutable in BPool * fix: comments * fix: failing bytecode match tests * fix: optimizing _afterFinalize loop * fix: wbtc units in integration test --- .forge-snapshots/exitPool.snap | 1 + .forge-snapshots/joinPool.snap | 1 + .forge-snapshots/newBFactory.snap | 2 +- .forge-snapshots/newBPool.snap | 2 +- .forge-snapshots/settlementCoWSwap.snap | 2 +- .../settlementCoWSwapInverse.snap | 2 +- .forge-snapshots/swapExactAmountIn.snap | 2 +- .../swapExactAmountInInverse.snap | 2 +- src/contracts/BCoWPool.sol | 5 +- src/contracts/BConst.sol | 2 +- src/contracts/BNum.sol | 6 +- src/contracts/BPool.sol | 16 ++-- test/integration/BCowPool.t.sol | 14 +++- .../{PoolSwap.t.sol => BPool.t.sol} | 75 +++++++++++++++++-- test/manual-smock/MockBCoWPool.sol | 9 +-- test/{smock => manual-smock}/MockBPool.sol | 9 +-- test/unit/BCoWFactory.t.sol | 6 +- test/unit/BCoWPool.t.sol | 10 +-- test/unit/BFactory.t.sol | 7 +- test/unit/BPool.t.sol | 10 +-- 20 files changed, 132 insertions(+), 51 deletions(-) create mode 100644 .forge-snapshots/exitPool.snap create mode 100644 .forge-snapshots/joinPool.snap rename test/integration/{PoolSwap.t.sol => BPool.t.sol} (64%) rename test/{smock => manual-smock}/MockBPool.sol (98%) diff --git a/.forge-snapshots/exitPool.snap b/.forge-snapshots/exitPool.snap new file mode 100644 index 00000000..c14f6c20 --- /dev/null +++ b/.forge-snapshots/exitPool.snap @@ -0,0 +1 @@ +175039 \ No newline at end of file diff --git a/.forge-snapshots/joinPool.snap b/.forge-snapshots/joinPool.snap new file mode 100644 index 00000000..18934479 --- /dev/null +++ b/.forge-snapshots/joinPool.snap @@ -0,0 +1 @@ +139119 \ No newline at end of file diff --git a/.forge-snapshots/newBFactory.snap b/.forge-snapshots/newBFactory.snap index 59ddf9ce..7a7a98dd 100644 --- a/.forge-snapshots/newBFactory.snap +++ b/.forge-snapshots/newBFactory.snap @@ -1 +1 @@ -4029197 \ No newline at end of file +4039479 \ No newline at end of file diff --git a/.forge-snapshots/newBPool.snap b/.forge-snapshots/newBPool.snap index 94357d20..372b2857 100644 --- a/.forge-snapshots/newBPool.snap +++ b/.forge-snapshots/newBPool.snap @@ -1 +1 @@ -3412772 \ No newline at end of file +3398313 \ No newline at end of file diff --git a/.forge-snapshots/settlementCoWSwap.snap b/.forge-snapshots/settlementCoWSwap.snap index 7a658ce0..4b4fb6d8 100644 --- a/.forge-snapshots/settlementCoWSwap.snap +++ b/.forge-snapshots/settlementCoWSwap.snap @@ -1 +1 @@ -216137 \ No newline at end of file +216083 \ No newline at end of file diff --git a/.forge-snapshots/settlementCoWSwapInverse.snap b/.forge-snapshots/settlementCoWSwapInverse.snap index 4cbb5e9a..36c5de86 100644 --- a/.forge-snapshots/settlementCoWSwapInverse.snap +++ b/.forge-snapshots/settlementCoWSwapInverse.snap @@ -1 +1 @@ -225977 \ No newline at end of file +225931 \ No newline at end of file diff --git a/.forge-snapshots/swapExactAmountIn.snap b/.forge-snapshots/swapExactAmountIn.snap index afe764e6..de32f053 100644 --- a/.forge-snapshots/swapExactAmountIn.snap +++ b/.forge-snapshots/swapExactAmountIn.snap @@ -1 +1 @@ -105194 \ No newline at end of file +105149 \ No newline at end of file diff --git a/.forge-snapshots/swapExactAmountInInverse.snap b/.forge-snapshots/swapExactAmountInInverse.snap index 2c7381e5..2e508114 100644 --- a/.forge-snapshots/swapExactAmountInInverse.snap +++ b/.forge-snapshots/swapExactAmountInInverse.snap @@ -1 +1 @@ -114841 \ No newline at end of file +114804 \ No newline at end of file diff --git a/src/contracts/BCoWPool.sol b/src/contracts/BCoWPool.sol index d5af3a28..f00585d6 100644 --- a/src/contracts/BCoWPool.sol +++ b/src/contracts/BCoWPool.sol @@ -137,7 +137,8 @@ contract BCoWPool is IERC1271, IBCoWPool, BPool, BCoWConst { * pool after the finalization of the setup. Also emits COWAMMPoolCreated() event. */ function _afterFinalize() internal override { - for (uint256 i; i < _tokens.length; i++) { + uint256 _tokensLength = _tokens.length; + for (uint256 i; i < _tokensLength; i++) { IERC20(_tokens[i]).approve(VAULT_RELAYER, type(uint256).max); } @@ -145,7 +146,7 @@ contract BCoWPool is IERC1271, IBCoWPool, BPool, BCoWConst { // If this pool was not deployed using a bCoWFactory, this will revert and catch // And the event will be emitted by this contract instead // solhint-disable-next-line no-empty-blocks - try IBCoWFactory(_factory).logBCoWPool() {} + try IBCoWFactory(_FACTORY).logBCoWPool() {} catch { emit IBCoWFactory.COWAMMPoolCreated(address(this)); } diff --git a/src/contracts/BConst.sol b/src/contracts/BConst.sol index c6f8cd30..3a184a01 100644 --- a/src/contracts/BConst.sol +++ b/src/contracts/BConst.sol @@ -41,7 +41,7 @@ contract BConst { uint256 public constant BPOW_PRECISION = BONE / 10 ** 10; /// @notice The maximum ratio of input tokens vs the current pool balance. - uint256 public constant MAX_IN_RATIO = BONE / 2; + uint256 public constant MAX_IN_RATIO = BONE >> 1; /// @notice The maximum ratio of output tokens vs the current pool balance. uint256 public constant MAX_OUT_RATIO = (BONE / 3) + 1 wei; diff --git a/src/contracts/BNum.sol b/src/contracts/BNum.sol index 52429928..99824543 100644 --- a/src/contracts/BNum.sol +++ b/src/contracts/BNum.sol @@ -94,7 +94,8 @@ contract BNum is BConst { if (a != 0 && c0 / a != b) { revert BNum_MulOverflow(); } - uint256 c1 = c0 + (BONE / 2); + // NOTE: using >> 1 instead of / 2 + uint256 c1 = c0 + (BONE >> 1); if (c1 < c0) { revert BNum_MulOverflow(); } @@ -112,7 +113,8 @@ contract BNum is BConst { if (a != 0 && c0 / a != BONE) { revert BNum_DivInternal(); // bmul overflow } - uint256 c1 = c0 + (b / 2); + // NOTE: using >> 1 instead of / 2 + uint256 c1 = c0 + (b >> 1); if (c1 < c0) { revert BNum_DivInternal(); // badd require } diff --git a/src/contracts/BPool.sol b/src/contracts/BPool.sol index 81e27999..10603de3 100644 --- a/src/contracts/BPool.sol +++ b/src/contracts/BPool.sol @@ -15,7 +15,7 @@ contract BPool is BToken, BMath, IBPool { using SafeERC20 for IERC20; /// @dev BFactory address to push token exitFee to - address internal _factory; + address internal immutable _FACTORY; /// @dev Has CONTROL role address internal _controller; /// @dev Fee for swapping @@ -65,7 +65,7 @@ contract BPool is BToken, BMath, IBPool { constructor() { _controller = msg.sender; - _factory = msg.sender; + _FACTORY = msg.sender; _swapFee = MIN_FEE; _finalized = false; } @@ -178,7 +178,8 @@ contract BPool is BToken, BMath, IBPool { revert BPool_InvalidPoolRatio(); } - for (uint256 i = 0; i < _tokens.length; i++) { + uint256 _tokensLength = _tokens.length; + for (uint256 i = 0; i < _tokensLength; i++) { address t = _tokens[i]; uint256 bal = IERC20(t).balanceOf(address(this)); uint256 tokenAmountIn = bmul(ratio, bal); @@ -210,10 +211,11 @@ contract BPool is BToken, BMath, IBPool { } _pullPoolShare(msg.sender, poolAmountIn); - _pushPoolShare(_factory, exitFee); + _pushPoolShare(_FACTORY, exitFee); _burnPoolShare(pAiAfterExitFee); - for (uint256 i = 0; i < _tokens.length; i++) { + uint256 _tokensLength = _tokens.length; + for (uint256 i = 0; i < _tokensLength; i++) { address t = _tokens[i]; uint256 bal = IERC20(t).balanceOf(address(this)); uint256 tokenAmountOut = bmul(ratio, bal); @@ -456,7 +458,7 @@ contract BPool is BToken, BMath, IBPool { _pullPoolShare(msg.sender, poolAmountIn); _burnPoolShare(bsub(poolAmountIn, exitFee)); - _pushPoolShare(_factory, exitFee); + _pushPoolShare(_FACTORY, exitFee); _pushUnderlying(tokenOut, msg.sender, tokenAmountOut); return tokenAmountOut; @@ -496,7 +498,7 @@ contract BPool is BToken, BMath, IBPool { _pullPoolShare(msg.sender, poolAmountIn); _burnPoolShare(bsub(poolAmountIn, exitFee)); - _pushPoolShare(_factory, exitFee); + _pushPoolShare(_FACTORY, exitFee); _pushUnderlying(tokenOut, msg.sender, tokenAmountOut); return poolAmountIn; diff --git a/test/integration/BCowPool.t.sol b/test/integration/BCowPool.t.sol index cd34318b..83a16922 100644 --- a/test/integration/BCowPool.t.sol +++ b/test/integration/BCowPool.t.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.25; -import {PoolSwapIntegrationTest} from './PoolSwap.t.sol'; +import {BPoolIntegrationTest} from './BPool.t.sol'; import {GPv2TradeEncoder} from '@composable-cow/test/vendored/GPv2TradeEncoder.sol'; import {IERC20} from '@cowprotocol/interfaces/IERC20.sol'; import {GPv2Interaction} from '@cowprotocol/libraries/GPv2Interaction.sol'; @@ -15,7 +15,7 @@ import {IBCoWPool} from 'interfaces/IBCoWPool.sol'; import {IBFactory} from 'interfaces/IBFactory.sol'; import {ISettlement} from 'interfaces/ISettlement.sol'; -contract BCowPoolIntegrationTest is PoolSwapIntegrationTest, BCoWConst { +contract BCowPoolIntegrationTest is BPoolIntegrationTest, BCoWConst { using GPv2Order for GPv2Order.Data; address public solver = address(0xa5559C2E1302c5Ce82582A6b1E4Aec562C2FbCf4); @@ -239,4 +239,14 @@ contract BCowPoolIntegrationTest is PoolSwapIntegrationTest, BCoWConst { settlement.settle(tokens, clearingPrices, trades, interactions); snapEnd(); } + + // NOTE: not implemented in Balancer CoW flow + function _makeJoin() internal override { + vm.skip(true); + } + + // NOTE: not implemented in Balancer CoW flow + function _makeExit() internal override { + vm.skip(true); + } } diff --git a/test/integration/PoolSwap.t.sol b/test/integration/BPool.t.sol similarity index 64% rename from test/integration/PoolSwap.t.sol rename to test/integration/BPool.t.sol index 625069a5..36deb047 100644 --- a/test/integration/PoolSwap.t.sol +++ b/test/integration/BPool.t.sol @@ -8,17 +8,20 @@ import {Test, Vm} from 'forge-std/Test.sol'; import {IBFactory} from 'interfaces/IBFactory.sol'; import {IBPool} from 'interfaces/IBPool.sol'; -abstract contract PoolSwapIntegrationTest is Test, GasSnapshot { +abstract contract BPoolIntegrationTest is Test, GasSnapshot { IBPool public pool; IBFactory public factory; IERC20 public dai = IERC20(0x6B175474E89094C44Da98b954EedeAC495271d0F); IERC20 public weth = IERC20(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); + IERC20 public wbtc = IERC20(0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599); address public lp = makeAddr('lp'); Vm.Wallet swapper = vm.createWallet('swapper'); Vm.Wallet swapperInverse = vm.createWallet('swapperInverse'); + Vm.Wallet joiner = vm.createWallet('joiner'); + Vm.Wallet exiter = vm.createWallet('exiter'); /** * For the simplicity of this test, a 1000 DAI:1 ETH reference quote is used. @@ -32,12 +35,14 @@ abstract contract PoolSwapIntegrationTest is Test, GasSnapshot { // unit amounts uint256 public constant ONE_TENTH_UNIT = 0.1 ether; uint256 public constant ONE_UNIT = 1 ether; + uint256 public constant ONE_UNIT_8_DECIMALS = 1e8; uint256 public constant HUNDRED_UNITS = 100 ether; uint256 public constant FOUR_THOUSAND_UNITS = 4000 ether; // pool amounts uint256 public constant DAI_LP_AMOUNT = FOUR_THOUSAND_UNITS; uint256 public constant WETH_LP_AMOUNT = ONE_UNIT; + uint256 public constant WBTC_LP_AMOUNT = ONE_UNIT_8_DECIMALS; // swap amounts IN uint256 public constant DAI_AMOUNT = HUNDRED_UNITS; @@ -53,19 +58,27 @@ abstract contract PoolSwapIntegrationTest is Test, GasSnapshot { factory = _deployFactory(); + vm.startPrank(lp); + pool = factory.newBPool(); + deal(address(dai), lp, DAI_LP_AMOUNT); deal(address(weth), lp, WETH_LP_AMOUNT); + deal(address(wbtc), lp, WBTC_LP_AMOUNT); deal(address(dai), swapper.addr, DAI_AMOUNT); deal(address(weth), swapperInverse.addr, WETH_AMOUNT_INVERSE); - vm.startPrank(lp); - pool = factory.newBPool(); + deal(address(dai), joiner.addr, DAI_LP_AMOUNT); + deal(address(weth), joiner.addr, WETH_LP_AMOUNT); + deal(address(wbtc), joiner.addr, WBTC_LP_AMOUNT); + deal(address(pool), exiter.addr, ONE_UNIT, false); dai.approve(address(pool), type(uint256).max); weth.approve(address(pool), type(uint256).max); + wbtc.approve(address(pool), type(uint256).max); pool.bind(address(dai), DAI_LP_AMOUNT, 8e18); // 80% weight pool.bind(address(weth), WETH_LP_AMOUNT, 2e18); // 20% weight + pool.bind(address(wbtc), WBTC_LP_AMOUNT, 10e18); // +100% weight (unused in swaps) // finalize pool.finalize(); } @@ -78,10 +91,11 @@ abstract contract PoolSwapIntegrationTest is Test, GasSnapshot { vm.startPrank(lp); uint256 lpBalance = pool.balanceOf(lp); - pool.exitPool(lpBalance, new uint256[](2)); + pool.exitPool(lpBalance, new uint256[](3)); assertEq(dai.balanceOf(lp), DAI_LP_AMOUNT + DAI_AMOUNT); // initial 4k + 100 dai assertEq(weth.balanceOf(lp), WETH_LP_AMOUNT - WETH_OUT_AMOUNT); // initial 1 - ~0.09 weth + assertEq(wbtc.balanceOf(lp), WBTC_LP_AMOUNT); // initial 1 wbtc } function testSimpleSwapInverse() public { @@ -92,10 +106,25 @@ abstract contract PoolSwapIntegrationTest is Test, GasSnapshot { vm.startPrank(lp); uint256 lpBalance = pool.balanceOf(address(lp)); - pool.exitPool(lpBalance, new uint256[](2)); + pool.exitPool(lpBalance, new uint256[](3)); assertEq(dai.balanceOf(address(lp)), DAI_LP_AMOUNT - DAI_OUT_AMOUNT_INVERSE); // initial 4k - ~100 dai assertEq(weth.balanceOf(address(lp)), WETH_LP_AMOUNT + WETH_AMOUNT_INVERSE); // initial 1 + 0.1 eth + assertEq(wbtc.balanceOf(lp), WBTC_LP_AMOUNT); // initial 1 wbtc + } + + function testSimpleJoin() public { + _makeJoin(); + assertEq(dai.balanceOf(joiner.addr), 0); + assertEq(weth.balanceOf(joiner.addr), 0); + assertEq(wbtc.balanceOf(joiner.addr), 0); + } + + function testSimpleExit() public { + _makeExit(); + assertEq(dai.balanceOf(exiter.addr), DAI_LP_AMOUNT / 100); + assertEq(weth.balanceOf(exiter.addr), WETH_LP_AMOUNT / 100); + assertEq(wbtc.balanceOf(exiter.addr), WBTC_LP_AMOUNT / 100); } function _deployFactory() internal virtual returns (IBFactory); @@ -103,9 +132,13 @@ abstract contract PoolSwapIntegrationTest is Test, GasSnapshot { function _makeSwap() internal virtual; function _makeSwapInverse() internal virtual; + + function _makeJoin() internal virtual; + + function _makeExit() internal virtual; } -contract DirectPoolSwapIntegrationTest is PoolSwapIntegrationTest { +contract DirectBPoolIntegrationTest is BPoolIntegrationTest { function _deployFactory() internal override returns (IBFactory) { return new BFactory(); } @@ -133,4 +166,34 @@ contract DirectPoolSwapIntegrationTest is PoolSwapIntegrationTest { vm.stopPrank(); } + + function _makeJoin() internal override { + vm.startPrank(joiner.addr); + dai.approve(address(pool), type(uint256).max); + weth.approve(address(pool), type(uint256).max); + wbtc.approve(address(pool), type(uint256).max); + + uint256[] memory maxAmountsIn = new uint256[](3); + maxAmountsIn[0] = type(uint256).max; + maxAmountsIn[1] = type(uint256).max; + maxAmountsIn[2] = type(uint256).max; + + snapStart('joinPool'); + pool.joinPool(pool.totalSupply(), maxAmountsIn); + snapEnd(); + + vm.stopPrank(); + } + + function _makeExit() internal override { + vm.startPrank(exiter.addr); + + uint256[] memory minAmountsOut = new uint256[](3); + + snapStart('exitPool'); + pool.exitPool(ONE_UNIT, minAmountsOut); + snapEnd(); + + vm.stopPrank(); + } } diff --git a/test/manual-smock/MockBCoWPool.sol b/test/manual-smock/MockBCoWPool.sol index 095842e3..5693743d 100644 --- a/test/manual-smock/MockBCoWPool.sol +++ b/test/manual-smock/MockBCoWPool.sol @@ -70,12 +70,9 @@ contract MockBCoWPool is BCoWPool, Test { vm.expectCall(address(this), abi.encodeWithSignature('_getLock()')); } - function set__factory(address __factory) public { - _factory = __factory; - } - - function call__factory() public view returns (address) { - return _factory; + // NOTE: manually added method + function call__FACTORY() public view returns (address) { + return _FACTORY; } function set__controller(address __controller) public { diff --git a/test/smock/MockBPool.sol b/test/manual-smock/MockBPool.sol similarity index 98% rename from test/smock/MockBPool.sol rename to test/manual-smock/MockBPool.sol index ffc39659..414fca48 100644 --- a/test/smock/MockBPool.sol +++ b/test/manual-smock/MockBPool.sol @@ -5,12 +5,9 @@ import {BMath, BPool, BToken, IBPool, IERC20, SafeERC20} from '../../src/contrac import {Test} from 'forge-std/Test.sol'; contract MockBPool is BPool, Test { - function set__factory(address __factory) public { - _factory = __factory; - } - - function call__factory() public view returns (address) { - return _factory; + // NOTE: manually added method + function call__FACTORY() public view returns (address) { + return _FACTORY; } function set__controller(address __controller) public { diff --git a/test/unit/BCoWFactory.t.sol b/test/unit/BCoWFactory.t.sol index 6b702f0c..1ff8070a 100644 --- a/test/unit/BCoWFactory.t.sol +++ b/test/unit/BCoWFactory.t.sol @@ -36,8 +36,12 @@ contract BCoWFactoryTest is Test { } function test__newBPoolWhenCalled() external { - bytes memory _expectedCode = address(new BCoWPool(solutionSettler, appData)).code; + vm.prank(address(factory)); + bytes memory _expectedCode = address(new BCoWPool(solutionSettler, appData)).code; // NOTE: uses nonce 1 + address _futurePool = vm.computeCreateAddress(address(factory), 2); + IBCoWPool _newPool = IBCoWPool(address(factory.call__newBPool())); + assertEq(address(_newPool), _futurePool); // it should set the new BCoWPool solution settler assertEq(address(_newPool.SOLUTION_SETTLER()), solutionSettler); // it should set the new BCoWPool app data diff --git a/test/unit/BCoWPool.t.sol b/test/unit/BCoWPool.t.sol index 37c13573..aea6167c 100644 --- a/test/unit/BCoWPool.t.sol +++ b/test/unit/BCoWPool.t.sol @@ -11,7 +11,7 @@ import {IBCoWPool} from 'interfaces/IBCoWPool.sol'; import {IBPool} from 'interfaces/IBPool.sol'; import {ISettlement} from 'interfaces/ISettlement.sol'; import {MockBCoWPool} from 'test/manual-smock/MockBCoWPool.sol'; -import {MockBPool} from 'test/smock/MockBPool.sol'; +import {MockBPool} from 'test/manual-smock/MockBPool.sol'; abstract contract BaseCoWPoolTest is BasePoolTest, BCoWConst { address public cowSolutionSettler = makeAddr('cowSolutionSettler'); @@ -87,7 +87,7 @@ contract BCoWPool_Unit_Finalize is BaseCoWPoolTest { } vm.mockCall( - address(bCoWPool.call__factory()), abi.encodeWithSelector(IBCoWFactory.logBCoWPool.selector), abi.encode() + address(bCoWPool.call__FACTORY()), abi.encodeWithSelector(IBCoWFactory.logBCoWPool.selector), abi.encode() ); } @@ -100,7 +100,7 @@ contract BCoWPool_Unit_Finalize is BaseCoWPoolTest { function test_Log_IfRevert() public { vm.mockCallRevert( - address(bCoWPool.call__factory()), abi.encodeWithSelector(IBCoWFactory.logBCoWPool.selector), abi.encode() + address(bCoWPool.call__FACTORY()), abi.encodeWithSelector(IBCoWFactory.logBCoWPool.selector), abi.encode() ); vm.expectEmit(address(bCoWPool)); @@ -110,7 +110,7 @@ contract BCoWPool_Unit_Finalize is BaseCoWPoolTest { } function test_Call_LogBCoWPool() public { - vm.expectCall(address(bCoWPool.call__factory()), abi.encodeWithSelector(IBCoWFactory.logBCoWPool.selector), 1); + vm.expectCall(address(bCoWPool.call__FACTORY()), abi.encodeWithSelector(IBCoWFactory.logBCoWPool.selector), 1); bCoWPool.finalize(); } } @@ -292,7 +292,7 @@ contract BCoWPool_Unit_IsValidSignature is BaseCoWPoolTest { vm.mockCall(tokens[i], abi.encodePacked(IERC20.approve.selector), abi.encode(true)); } vm.mockCall( - address(bCoWPool.call__factory()), abi.encodeWithSelector(IBCoWFactory.logBCoWPool.selector), abi.encode() + address(bCoWPool.call__FACTORY()), abi.encodeWithSelector(IBCoWFactory.logBCoWPool.selector), abi.encode() ); bCoWPool.finalize(); } diff --git a/test/unit/BFactory.t.sol b/test/unit/BFactory.t.sol index 7f779303..b1426432 100644 --- a/test/unit/BFactory.t.sol +++ b/test/unit/BFactory.t.sol @@ -50,11 +50,14 @@ contract BFactoryTest is Test { } function test__newBPoolWhenCalled() external { - address _futurePool = vm.computeCreateAddress(address(factory), 1); + vm.prank(address(factory)); + bytes memory _expectedCode = address(new BPool()).code; // NOTE: uses nonce 1 + address _futurePool = vm.computeCreateAddress(address(factory), 2); + address _newBPool = address(factory.call__newBPool()); assertEq(_newBPool, _futurePool); // it should deploy a new BPool - assertEq(_newBPool.code, address(new BPool()).code); + assertEq(_newBPool.code, _expectedCode); } function test_SetBLabsRevertWhen_TheSenderIsNotTheCurrentBLabs(address _caller) external { diff --git a/test/unit/BPool.t.sol b/test/unit/BPool.t.sol index 6b28fb8e..c193bdb8 100644 --- a/test/unit/BPool.t.sol +++ b/test/unit/BPool.t.sol @@ -6,7 +6,7 @@ import {SafeERC20} from '@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol import {BPool} from 'contracts/BPool.sol'; import {IBPool} from 'interfaces/IBPool.sol'; -import {MockBPool} from 'test/smock/MockBPool.sol'; +import {MockBPool} from 'test/manual-smock/MockBPool.sol'; import {BConst} from 'contracts/BConst.sol'; import {BMath} from 'contracts/BMath.sol'; @@ -363,7 +363,7 @@ contract BPool_Unit_Constructor is BasePoolTest { MockBPool _newBPool = new MockBPool(); assertEq(_newBPool.call__controller(), _deployer); - assertEq(_newBPool.call__factory(), _deployer); + assertEq(_newBPool.call__FACTORY(), _deployer); assertEq(_newBPool.call__swapFee(), MIN_FEE); assertEq(_newBPool.call__finalized(), false); } @@ -1401,7 +1401,7 @@ contract BPool_Unit_ExitPool is BasePoolTest { } function test_Push_PoolShare(ExitPool_FuzzScenario memory _fuzz) public happyPath(_fuzz) { - address _factoryAddress = bPool.call__factory(); + address _factoryAddress = bPool.call__FACTORY(); uint256 _exitFee = bmul(_fuzz.poolAmountIn, EXIT_FEE); uint256 _balanceBefore = bPool.balanceOf(_factoryAddress); @@ -2690,7 +2690,7 @@ contract BPool_Unit_ExitswapPoolAmountIn is BasePoolTest { } function test_Push_PoolShare(ExitswapPoolAmountIn_FuzzScenario memory _fuzz) public happyPath(_fuzz) { - address _factoryAddress = bPool.call__factory(); + address _factoryAddress = bPool.call__FACTORY(); uint256 _balanceBefore = bPool.balanceOf(_factoryAddress); uint256 _exitFee = bmul(_fuzz.poolAmountIn, EXIT_FEE); @@ -2938,7 +2938,7 @@ contract BPool_Unit_ExitswapExternAmountOut is BasePoolTest { } function test_Push_PoolShare(ExitswapExternAmountOut_FuzzScenario memory _fuzz) public happyPath(_fuzz) { - address _factoryAddress = bPool.call__factory(); + address _factoryAddress = bPool.call__FACTORY(); uint256 _balanceBefore = bPool.balanceOf(_factoryAddress); uint256 _poolAmountIn = calcPoolInGivenSingleOut( _fuzz.tokenOutBalance, From 84cdbbf4e20c79fee6af2faece72b814ead58d93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wei=C3=9Fer=20Hase?= Date: Thu, 4 Jul 2024 19:05:06 +0200 Subject: [PATCH 12/48] fix: failing tests on setController (address zero) (#137) * fix: failing tests on setController (address zero) * fix: update gas snapshot --- .forge-snapshots/newBFactory.snap | 2 +- src/contracts/BPool.sol | 6 +++--- test/unit/BPool.t.sol | 3 +++ 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/.forge-snapshots/newBFactory.snap b/.forge-snapshots/newBFactory.snap index 7a7a98dd..db1134a1 100644 --- a/.forge-snapshots/newBFactory.snap +++ b/.forge-snapshots/newBFactory.snap @@ -1 +1 @@ -4039479 \ No newline at end of file +4039491 \ No newline at end of file diff --git a/src/contracts/BPool.sol b/src/contracts/BPool.sol index 10603de3..c0d5733d 100644 --- a/src/contracts/BPool.sol +++ b/src/contracts/BPool.sol @@ -85,12 +85,12 @@ contract BPool is BToken, BMath, IBPool { } /// @inheritdoc IBPool - function setController(address manager) external _logs_ _lock_ _controller_ { - if (manager == address(0)) { + function setController(address newController) external _logs_ _lock_ _controller_ { + if (newController == address(0)) { revert BPool_AddressZero(); } - _controller = manager; + _controller = newController; } /// @inheritdoc IBPool diff --git a/test/unit/BPool.t.sol b/test/unit/BPool.t.sol index c193bdb8..46a9f14b 100644 --- a/test/unit/BPool.t.sol +++ b/test/unit/BPool.t.sol @@ -598,6 +598,7 @@ contract BPool_Unit_SetSwapFee is BasePoolTest { contract BPool_Unit_SetController is BasePoolTest { function test_Revert_NotController(address _controller, address _caller, address _newController) public { + vm.assume(_newController != address(0)); vm.assume(_controller != _caller); bPool.set__controller(_controller); @@ -618,12 +619,14 @@ contract BPool_Unit_SetController is BasePoolTest { } function test_Set_Controller(address _controller) public { + vm.assume(_controller != address(0)); bPool.setController(_controller); assertEq(bPool.call__controller(), _controller); } function test_Emit_LogCall(address _controller) public { + vm.assume(_controller != address(0)); vm.expectEmit(); bytes memory _data = abi.encodeWithSelector(BPool.setController.selector, _controller); emit IBPool.LOG_CALL(BPool.setController.selector, address(this), _data); From b490996d83ed5cb994e0408590bcd29bbc68cd09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wei=C3=9Fer=20Hase?= Date: Thu, 4 Jul 2024 21:20:15 +0200 Subject: [PATCH 13/48] fix: typos and nits (#133) * fix: typos in IBCoWPool * fix: replace erc20 for token * fix: increase/decrease approval arg names * chore: update gas snapshots * fix: consistent argument naming * fix: rm global ffi and fix yarn:test * fix: consistent _newBPool return * fix: rm obvious return arg names * fix: consistent returns in BPool * fix: using trailing underscore for orderHash * fix: adding strict linter rule and fixing issues * feat: adding deny warnings to foundry toml * fix: update gas snapshot * fix: failing tests * fix: rm deny warnings * feat: raising optimizer to 500 runs * fix: failing test --- .forge-snapshots/exitPool.snap | 2 +- .forge-snapshots/joinPool.snap | 2 +- .forge-snapshots/newBFactory.snap | 2 +- .forge-snapshots/newBPool.snap | 2 +- .forge-snapshots/settlementCoWSwap.snap | 2 +- .../settlementCoWSwapInverse.snap | 2 +- .forge-snapshots/swapExactAmountIn.snap | 2 +- .../swapExactAmountInInverse.snap | 2 +- .solhint.json | 2 +- foundry.toml | 3 +- package.json | 4 +- script/DeployBCoWFactory.s.sol | 6 +-- script/DeployBFactory.s.sol | 4 +- src/contracts/BCoWFactory.sol | 12 +++--- src/contracts/BCoWPool.sol | 24 +++++------ src/contracts/BFactory.sol | 41 +++++++++--------- src/contracts/BPool.sol | 42 +++++++++++-------- src/contracts/BToken.sol | 38 ++++++++--------- src/interfaces/IBCoWFactory.sol | 8 ++-- src/interfaces/IBCoWPool.sol | 24 +++++------ src/interfaces/IBFactory.sol | 16 +++---- src/interfaces/ISettlement.sol | 8 ++-- test/manual-smock/MockBCoWPool.sol | 2 +- test/manual-smock/MockBPool.sol | 2 +- test/smock/MockBFactory.sol | 8 ++-- test/unit/BPool.t.sol | 9 ++-- 26 files changed, 138 insertions(+), 131 deletions(-) diff --git a/.forge-snapshots/exitPool.snap b/.forge-snapshots/exitPool.snap index c14f6c20..1453e021 100644 --- a/.forge-snapshots/exitPool.snap +++ b/.forge-snapshots/exitPool.snap @@ -1 +1 @@ -175039 \ No newline at end of file +174721 \ No newline at end of file diff --git a/.forge-snapshots/joinPool.snap b/.forge-snapshots/joinPool.snap index 18934479..020d521b 100644 --- a/.forge-snapshots/joinPool.snap +++ b/.forge-snapshots/joinPool.snap @@ -1 +1 @@ -139119 \ No newline at end of file +138967 \ No newline at end of file diff --git a/.forge-snapshots/newBFactory.snap b/.forge-snapshots/newBFactory.snap index db1134a1..353be171 100644 --- a/.forge-snapshots/newBFactory.snap +++ b/.forge-snapshots/newBFactory.snap @@ -1 +1 @@ -4039491 \ No newline at end of file +4118828 \ No newline at end of file diff --git a/.forge-snapshots/newBPool.snap b/.forge-snapshots/newBPool.snap index 372b2857..3fb1e826 100644 --- a/.forge-snapshots/newBPool.snap +++ b/.forge-snapshots/newBPool.snap @@ -1 +1 @@ -3398313 \ No newline at end of file +3467519 \ No newline at end of file diff --git a/.forge-snapshots/settlementCoWSwap.snap b/.forge-snapshots/settlementCoWSwap.snap index 4b4fb6d8..b79c8eef 100644 --- a/.forge-snapshots/settlementCoWSwap.snap +++ b/.forge-snapshots/settlementCoWSwap.snap @@ -1 +1 @@ -216083 \ No newline at end of file +215790 \ No newline at end of file diff --git a/.forge-snapshots/settlementCoWSwapInverse.snap b/.forge-snapshots/settlementCoWSwapInverse.snap index 36c5de86..786e0dc8 100644 --- a/.forge-snapshots/settlementCoWSwapInverse.snap +++ b/.forge-snapshots/settlementCoWSwapInverse.snap @@ -1 +1 @@ -225931 \ No newline at end of file +225638 \ No newline at end of file diff --git a/.forge-snapshots/swapExactAmountIn.snap b/.forge-snapshots/swapExactAmountIn.snap index de32f053..47859bf3 100644 --- a/.forge-snapshots/swapExactAmountIn.snap +++ b/.forge-snapshots/swapExactAmountIn.snap @@ -1 +1 @@ -105149 \ No newline at end of file +104892 \ No newline at end of file diff --git a/.forge-snapshots/swapExactAmountInInverse.snap b/.forge-snapshots/swapExactAmountInInverse.snap index 2e508114..16eb128e 100644 --- a/.forge-snapshots/swapExactAmountInInverse.snap +++ b/.forge-snapshots/swapExactAmountInInverse.snap @@ -1 +1 @@ -114804 \ No newline at end of file +114561 \ No newline at end of file diff --git a/.solhint.json b/.solhint.json index 2038d16f..4c8efadd 100644 --- a/.solhint.json +++ b/.solhint.json @@ -7,7 +7,7 @@ "no-empty-blocks": "error", "no-inline-assembly": "off", "ordering": "warn", - "private-vars-leading-underscore": ["warn", { "strict": false }], + "private-vars-leading-underscore": ["warn", { "strict": true }], "quotes": "off", "one-contract-per-file": "warn", "style-guide-casing": ["warn", { "ignoreEvents": true } ] diff --git a/foundry.toml b/foundry.toml index fead9a16..979d4d49 100644 --- a/foundry.toml +++ b/foundry.toml @@ -11,8 +11,7 @@ sort_imports = true [profile.default] solc_version = '0.8.25' libs = ["node_modules", "lib"] -optimizer_runs = 50 # TODO: increase for production and add via-ir -ffi = true +optimizer_runs = 500 evm_version = 'cancun' fs_permissions = [{ access = "read-write", path = ".forge-snapshots/"}] diff --git a/package.json b/package.json index c3eef8f5..04721367 100644 --- a/package.json +++ b/package.json @@ -26,8 +26,8 @@ "lint:natspec": "npx @defi-wonderland/natspec-smells --config natspec-smells.config.js", "prepare": "husky install", "smock": "smock-foundry --contracts src/contracts", - "test": "forge test -vvv", - "test:integration": "forge test --match-path 'test/integration/**' -vvv --isolate", + "test": "yarn test:integration && yarn test:unit", + "test:integration": "forge test --ffi --match-path 'test/integration/**' -vvv --isolate", "test:local": "FOUNDRY_FUZZ_RUNS=100 forge test -vvv", "test:scaffold": "bulloak check --fix test/unit/*.tree && forge fmt", "test:unit": "forge test --match-path 'test/unit/**' -vvv", diff --git a/script/DeployBCoWFactory.s.sol b/script/DeployBCoWFactory.s.sol index 99be7268..a2abfbdd 100644 --- a/script/DeployBCoWFactory.s.sol +++ b/script/DeployBCoWFactory.s.sol @@ -7,11 +7,11 @@ import {Params} from 'script/Params.s.sol'; contract DeployBCoWFactory is Script, Params { function run() public { - BCoWFactoryDeploymentParams memory _params = _bCoWFactoryDeploymentParams[block.chainid]; + BCoWFactoryDeploymentParams memory params = _bCoWFactoryDeploymentParams[block.chainid]; vm.startBroadcast(); - BCoWFactory bCoWFactory = new BCoWFactory(_params.settlement, _params.appData); - bCoWFactory.setBLabs(_params.bLabs); + BCoWFactory bCoWFactory = new BCoWFactory(params.settlement, params.appData); + bCoWFactory.setBLabs(params.bLabs); vm.stopBroadcast(); } } diff --git a/script/DeployBFactory.s.sol b/script/DeployBFactory.s.sol index 6987cc81..5d0c33bc 100644 --- a/script/DeployBFactory.s.sol +++ b/script/DeployBFactory.s.sol @@ -7,11 +7,11 @@ import {Params} from 'script/Params.s.sol'; contract DeployBFactory is Script, Params { function run() public { - BFactoryDeploymentParams memory _params = _bFactoryDeploymentParams[block.chainid]; + BFactoryDeploymentParams memory params = _bFactoryDeploymentParams[block.chainid]; vm.startBroadcast(); BFactory bFactory = new BFactory(); - bFactory.setBLabs(_params.bLabs); + bFactory.setBLabs(params.bLabs); vm.stopBroadcast(); } } diff --git a/src/contracts/BCoWFactory.sol b/src/contracts/BCoWFactory.sol index 29576b9c..1eadeefe 100644 --- a/src/contracts/BCoWFactory.sol +++ b/src/contracts/BCoWFactory.sol @@ -18,9 +18,9 @@ contract BCoWFactory is BFactory, IBCoWFactory { /// @inheritdoc IBCoWFactory bytes32 public immutable APP_DATA; - constructor(address _solutionSettler, bytes32 _appData) BFactory() { - SOLUTION_SETTLER = _solutionSettler; - APP_DATA = _appData; + constructor(address solutionSettler, bytes32 appData) BFactory() { + SOLUTION_SETTLER = solutionSettler; + APP_DATA = appData; } /// @inheritdoc IBCoWFactory @@ -31,9 +31,9 @@ contract BCoWFactory is BFactory, IBCoWFactory { /** * @dev Deploys a BCoWPool instead of a regular BPool. - * @return _pool The deployed BCoWPool + * @return bCoWPool The deployed BCoWPool */ - function _newBPool() internal virtual override returns (IBPool _pool) { - return new BCoWPool(SOLUTION_SETTLER, APP_DATA); + function _newBPool() internal virtual override returns (IBPool bCoWPool) { + bCoWPool = new BCoWPool(SOLUTION_SETTLER, APP_DATA); } } diff --git a/src/contracts/BCoWPool.sol b/src/contracts/BCoWPool.sol index f00585d6..1186405b 100644 --- a/src/contracts/BCoWPool.sol +++ b/src/contracts/BCoWPool.sol @@ -46,11 +46,11 @@ contract BCoWPool is IERC1271, IBCoWPool, BPool, BCoWConst { /// @inheritdoc IBCoWPool bytes32 public immutable APP_DATA; - constructor(address _cowSolutionSettler, bytes32 _appData) BPool() { - SOLUTION_SETTLER = ISettlement(_cowSolutionSettler); - SOLUTION_SETTLER_DOMAIN_SEPARATOR = ISettlement(_cowSolutionSettler).domainSeparator(); - VAULT_RELAYER = ISettlement(_cowSolutionSettler).vaultRelayer(); - APP_DATA = _appData; + constructor(address cowSolutionSettler, bytes32 appData) BPool() { + SOLUTION_SETTLER = ISettlement(cowSolutionSettler); + SOLUTION_SETTLER_DOMAIN_SEPARATOR = ISettlement(cowSolutionSettler).domainSeparator(); + VAULT_RELAYER = ISettlement(cowSolutionSettler).vaultRelayer(); + APP_DATA = appData; } /// @inheritdoc IBCoWPool @@ -65,19 +65,19 @@ contract BCoWPool is IERC1271, IBCoWPool, BPool, BCoWConst { * @inheritdoc IERC1271 * @dev this function reverts if the order hash does not match the current commitment */ - function isValidSignature(bytes32 _hash, bytes memory signature) external view returns (bytes4) { + function isValidSignature(bytes32 orderHash, bytes memory signature) external view returns (bytes4 magicValue) { (GPv2Order.Data memory order) = abi.decode(signature, (GPv2Order.Data)); if (order.appData != APP_DATA) { revert AppDataDoesNotMatch(); } - bytes32 orderHash = order.hash(SOLUTION_SETTLER_DOMAIN_SEPARATOR); - if (orderHash != _hash) { + bytes32 orderHash_ = order.hash(SOLUTION_SETTLER_DOMAIN_SEPARATOR); + if (orderHash_ != orderHash) { revert OrderDoesNotMatchMessageHash(); } - if (orderHash != _getLock()) { + if (orderHash_ != _getLock()) { revert OrderDoesNotMatchCommitmentHash(); } @@ -85,7 +85,7 @@ contract BCoWPool is IERC1271, IBCoWPool, BPool, BCoWConst { // A signature is valid according to EIP-1271 if this function returns // its selector as the so-called "magic value". - return this.isValidSignature.selector; + magicValue = this.isValidSignature.selector; } /// @inheritdoc IBCoWPool @@ -137,8 +137,8 @@ contract BCoWPool is IERC1271, IBCoWPool, BPool, BCoWConst { * pool after the finalization of the setup. Also emits COWAMMPoolCreated() event. */ function _afterFinalize() internal override { - uint256 _tokensLength = _tokens.length; - for (uint256 i; i < _tokensLength; i++) { + uint256 tokensLength = _tokens.length; + for (uint256 i; i < tokensLength; i++) { IERC20(_tokens[i]).approve(VAULT_RELAYER, type(uint256).max); } diff --git a/src/contracts/BFactory.sol b/src/contracts/BFactory.sol index e96d1a0d..ed832c8f 100644 --- a/src/contracts/BFactory.sol +++ b/src/contracts/BFactory.sol @@ -14,41 +14,40 @@ contract BFactory is IBFactory { /// @dev Mapping indicating whether the address is a BPool. mapping(address => bool) internal _isBPool; /// @dev bLabs address. - address internal _blabs; + address internal _bLabs; constructor() { - _blabs = msg.sender; + _bLabs = msg.sender; } /// @inheritdoc IBFactory - function newBPool() external returns (IBPool _pool) { - IBPool bpool = _newBPool(); - _isBPool[address(bpool)] = true; - emit LOG_NEW_POOL(msg.sender, address(bpool)); - bpool.setController(msg.sender); - return bpool; + function newBPool() external returns (IBPool bPool) { + bPool = _newBPool(); + _isBPool[address(bPool)] = true; + emit LOG_NEW_POOL(msg.sender, address(bPool)); + bPool.setController(msg.sender); } /// @inheritdoc IBFactory - function setBLabs(address b) external { - if (b == address(0)) { + function setBLabs(address bLabs) external { + if (bLabs == address(0)) { revert BFactory_AddressZero(); } - if (msg.sender != _blabs) { + if (msg.sender != _bLabs) { revert BFactory_NotBLabs(); } - emit LOG_BLABS(msg.sender, b); - _blabs = b; + emit LOG_BLABS(msg.sender, bLabs); + _bLabs = bLabs; } /// @inheritdoc IBFactory - function collect(IBPool pool) external { - if (msg.sender != _blabs) { + function collect(IBPool bPool) external { + if (msg.sender != _bLabs) { revert BFactory_NotBLabs(); } - uint256 collected = pool.balanceOf(address(this)); - SafeERC20.safeTransfer(pool, _blabs, collected); + uint256 collected = bPool.balanceOf(address(this)); + SafeERC20.safeTransfer(bPool, _bLabs, collected); } /// @inheritdoc IBFactory @@ -58,15 +57,15 @@ contract BFactory is IBFactory { /// @inheritdoc IBFactory function getBLabs() external view returns (address) { - return _blabs; + return _bLabs; } /** * @notice Deploys a new BPool. * @dev Internal function to allow overriding in derived contracts. - * @return _pool The deployed BPool + * @return bPool The deployed BPool */ - function _newBPool() internal virtual returns (IBPool _pool) { - return new BPool(); + function _newBPool() internal virtual returns (IBPool bPool) { + bPool = new BPool(); } } diff --git a/src/contracts/BPool.sol b/src/contracts/BPool.sol index c0d5733d..02ab08b0 100644 --- a/src/contracts/BPool.sol +++ b/src/contracts/BPool.sol @@ -178,8 +178,8 @@ contract BPool is BToken, BMath, IBPool { revert BPool_InvalidPoolRatio(); } - uint256 _tokensLength = _tokens.length; - for (uint256 i = 0; i < _tokensLength; i++) { + uint256 tokensLength = _tokens.length; + for (uint256 i = 0; i < tokensLength; i++) { address t = _tokens[i]; uint256 bal = IERC20(t).balanceOf(address(this)); uint256 tokenAmountIn = bmul(ratio, bal); @@ -214,8 +214,8 @@ contract BPool is BToken, BMath, IBPool { _pushPoolShare(_FACTORY, exitFee); _burnPoolShare(pAiAfterExitFee); - uint256 _tokensLength = _tokens.length; - for (uint256 i = 0; i < _tokensLength; i++) { + uint256 tokensLength = _tokens.length; + for (uint256 i = 0; i < tokensLength; i++) { address t = _tokens[i]; uint256 bal = IERC20(t).balanceOf(address(this)); uint256 tokenAmountOut = bmul(ratio, bal); @@ -514,13 +514,16 @@ contract BPool is BToken, BMath, IBPool { } Record storage inRecord = _records[tokenIn]; Record storage outRecord = _records[tokenOut]; - return calcSpotPrice( + + spotPrice = calcSpotPrice( IERC20(tokenIn).balanceOf(address(this)), inRecord.denorm, IERC20(tokenOut).balanceOf(address(this)), outRecord.denorm, _swapFee ); + + return spotPrice; } /// @inheritdoc IBPool @@ -533,13 +536,16 @@ contract BPool is BToken, BMath, IBPool { } Record storage inRecord = _records[tokenIn]; Record storage outRecord = _records[tokenOut]; - return calcSpotPrice( + + spotPrice = calcSpotPrice( IERC20(tokenIn).balanceOf(address(this)), inRecord.denorm, IERC20(tokenOut).balanceOf(address(this)), outRecord.denorm, 0 ); + + return spotPrice; } /// @inheritdoc IBPool @@ -612,34 +618,34 @@ contract BPool is BToken, BMath, IBPool { /** * @notice Sets the value of the transient storage slot used for reentrancy locks - * @param _value The value of the transient storage slot used for reentrancy locks. + * @param value The value of the transient storage slot used for reentrancy locks. * @dev Should be set to _MUTEX_FREE after a call, any other value will * be interpreted as locked */ - function _setLock(bytes32 _value) internal virtual { + function _setLock(bytes32 value) internal virtual { assembly ("memory-safe") { - tstore(_MUTEX_TRANSIENT_STORAGE_SLOT, _value) + tstore(_MUTEX_TRANSIENT_STORAGE_SLOT, value) } } /** * @dev Pulls tokens from the sender. Tokens needs to be approved first. Calls are not locked. - * @param erc20 The address of the token to pull + * @param token The address of the token to pull * @param from The address to pull the tokens from * @param amount The amount of tokens to pull */ - function _pullUnderlying(address erc20, address from, uint256 amount) internal virtual { - IERC20(erc20).safeTransferFrom(from, address(this), amount); + function _pullUnderlying(address token, address from, uint256 amount) internal virtual { + IERC20(token).safeTransferFrom(from, address(this), amount); } /** * @dev Pushes tokens to the receiver. Calls are not locked. - * @param erc20 The address of the token to push + * @param token The address of the token to push * @param to The address to push the tokens to * @param amount The amount of tokens to push */ - function _pushUnderlying(address erc20, address to, uint256 amount) internal virtual { - IERC20(erc20).safeTransfer(to, amount); + function _pushUnderlying(address token, address to, uint256 amount) internal virtual { + IERC20(token).safeTransfer(to, amount); } /** @@ -685,13 +691,13 @@ contract BPool is BToken, BMath, IBPool { /** * @notice Gets the value of the transient storage slot used for reentrancy locks - * @return _value Contents of transient storage slot used for reentrancy locks. + * @return value Contents of transient storage slot used for reentrancy locks. * @dev Should only be compared against _MUTEX_FREE for the purposes of * allowing calls */ - function _getLock() internal view virtual returns (bytes32 _value) { + function _getLock() internal view virtual returns (bytes32 value) { assembly ("memory-safe") { - _value := tload(_MUTEX_TRANSIENT_STORAGE_SLOT) + value := tload(_MUTEX_TRANSIENT_STORAGE_SLOT) } } } diff --git a/src/contracts/BToken.sol b/src/contracts/BToken.sol index 55c0e565..0491b17d 100644 --- a/src/contracts/BToken.sol +++ b/src/contracts/BToken.sol @@ -12,27 +12,27 @@ contract BToken is ERC20 { /** * @notice Increase the allowance of the spender. - * @param dst The address which will spend the funds. - * @param amt The amount of tokens to increase the allowance by. - * @return True if the operation is successful. + * @param spender The address which will spend the funds. + * @param amount The amount of tokens to increase the allowance by. + * @return success True if the operation is successful. */ - function increaseApproval(address dst, uint256 amt) external returns (bool) { - _approve(msg.sender, dst, allowance(msg.sender, dst) + amt); + function increaseApproval(address spender, uint256 amount) external returns (bool success) { + _approve(msg.sender, spender, allowance(msg.sender, spender) + amount); return true; } /** * @notice Decrease the allowance of the spender. - * @param dst The address which will spend the funds. - * @param amt The amount of tokens to decrease the allowance by. - * @return True if the operation is successful. + * @param spender The address which will spend the funds. + * @param amount The amount of tokens to decrease the allowance by. + * @return success True if the operation is successful. */ - function decreaseApproval(address dst, uint256 amt) external returns (bool) { - uint256 oldValue = allowance(msg.sender, dst); - if (amt > oldValue) { - _approve(msg.sender, dst, 0); + function decreaseApproval(address spender, uint256 amount) external returns (bool success) { + uint256 oldValue = allowance(msg.sender, spender); + if (amount > oldValue) { + _approve(msg.sender, spender, 0); } else { - _approve(msg.sender, dst, oldValue - amt); + _approve(msg.sender, spender, oldValue - amount); } return true; } @@ -40,18 +40,18 @@ contract BToken is ERC20 { /** * @notice Transfer tokens from one this contract to another. * @param to The address which you want to transfer to. - * @param amt The amount of tokens to be transferred. + * @param amount The amount of tokens to be transferred. */ - function _push(address to, uint256 amt) internal virtual { - _transfer(address(this), to, amt); + function _push(address to, uint256 amount) internal virtual { + _transfer(address(this), to, amount); } /** * @notice Pull tokens from another address to this contract. * @param from The address which you want to transfer from. - * @param amt The amount of tokens to be transferred. + * @param amount The amount of tokens to be transferred. */ - function _pull(address from, uint256 amt) internal virtual { - _transfer(from, address(this), amt); + function _pull(address from, uint256 amount) internal virtual { + _transfer(from, address(this), amount); } } diff --git a/src/interfaces/IBCoWFactory.sol b/src/interfaces/IBCoWFactory.sol index b7ef368f..da757ef4 100644 --- a/src/interfaces/IBCoWFactory.sol +++ b/src/interfaces/IBCoWFactory.sol @@ -23,16 +23,16 @@ interface IBCoWFactory is IBFactory { /** * @notice The address of the CoW Protocol settlement contract. It is the * only address that can set commitments. - * @return _solutionSettler The address of the solution settler. + * @return solutionSettler The address of the solution settler. */ // solhint-disable-next-line style-guide-casing - function SOLUTION_SETTLER() external view returns (address _solutionSettler); + function SOLUTION_SETTLER() external view returns (address solutionSettler); /** * @notice The identifier describing which `GPv2Order.AppData` currently * apply to this AMM. - * @return _appData The 32 bytes identifier of the allowed GPv2Order AppData. + * @return appData The 32 bytes identifier of the allowed GPv2Order AppData. */ // solhint-disable-next-line style-guide-casing - function APP_DATA() external view returns (bytes32 _appData); + function APP_DATA() external view returns (bytes32 appData); } diff --git a/src/interfaces/IBCoWPool.sol b/src/interfaces/IBCoWPool.sol index 5d8d60d5..0c362221 100644 --- a/src/interfaces/IBCoWPool.sol +++ b/src/interfaces/IBCoWPool.sol @@ -8,22 +8,22 @@ import {ISettlement} from 'interfaces/ISettlement.sol'; interface IBCoWPool is IERC1271, IBPool { /** - * @notice thrown when a CoW order has a non-zero fee + * @notice Thrown when a CoW order has a non-zero fee */ error BCoWPool_FeeMustBeZero(); /** - * @notice thrown when a CoW order is executed after its deadline + * @notice Thrown when a CoW order is executed after its deadline */ error BCoWPool_OrderValidityTooLong(); /** - * @notice thrown when a CoW order has an unkown type (must be GPv2Order.KIND_SELL) + * @notice Thrown when a CoW order has an unknown type (must be GPv2Order.KIND_SELL) */ error BCoWPool_InvalidOperation(); /** - * @notice thrown when a CoW order has an invalid balance marker. BCoWPool + * @notice Thrown when a CoW order has an invalid balance marker. BCoWPool * only supports BALANCE_ERC20, instructing to use the underlying ERC20 * balance directly instead of balancer's internal accounting */ @@ -73,33 +73,33 @@ interface IBCoWPool is IERC1271, IBPool { /** * @notice The address that can pull funds from the AMM vault to execute an order - * @return _vaultRelayer The address of the vault relayer. + * @return vaultRelayer The address of the vault relayer. */ // solhint-disable-next-line style-guide-casing - function VAULT_RELAYER() external view returns (address _vaultRelayer); + function VAULT_RELAYER() external view returns (address vaultRelayer); /** * @notice The domain separator used for hashing CoW Protocol orders. - * @return _solutionSettlerDomainSeparator The domain separator. + * @return solutionSettlerDomainSeparator The domain separator. */ // solhint-disable-next-line style-guide-casing - function SOLUTION_SETTLER_DOMAIN_SEPARATOR() external view returns (bytes32 _solutionSettlerDomainSeparator); + function SOLUTION_SETTLER_DOMAIN_SEPARATOR() external view returns (bytes32 solutionSettlerDomainSeparator); /** * @notice The address of the CoW Protocol settlement contract. It is the * only address that can set commitments. - * @return _solutionSettler The address of the solution settler. + * @return solutionSettler The address of the solution settler. */ // solhint-disable-next-line style-guide-casing - function SOLUTION_SETTLER() external view returns (ISettlement _solutionSettler); + function SOLUTION_SETTLER() external view returns (ISettlement solutionSettler); /** * @notice The identifier describing which `GPv2Order.AppData` currently * apply to this AMM. - * @return _appData The 32 bytes identifier of the allowed GPv2Order AppData. + * @return appData The 32 bytes identifier of the allowed GPv2Order AppData. */ // solhint-disable-next-line style-guide-casing - function APP_DATA() external view returns (bytes32 _appData); + function APP_DATA() external view returns (bytes32 appData); /** * @notice This function checks that the input order is admissible for the diff --git a/src/interfaces/IBFactory.sol b/src/interfaces/IBFactory.sol index e408b470..b9305031 100644 --- a/src/interfaces/IBFactory.sol +++ b/src/interfaces/IBFactory.sol @@ -7,9 +7,9 @@ interface IBFactory { /** * @notice Emitted when creating a new pool * @param caller The caller of the function that will be set as the controller - * @param pool The address of the new pool + * @param bPool The address of the new pool */ - event LOG_NEW_POOL(address indexed caller, address indexed pool); + event LOG_NEW_POOL(address indexed caller, address indexed bPool); /** * @notice Emitted when setting the BLabs address @@ -30,21 +30,21 @@ interface IBFactory { /** * @notice Creates a new BPool, assigning the caller as the pool controller - * @return _pool The new BPool + * @return bPool The new BPool */ - function newBPool() external returns (IBPool _pool); + function newBPool() external returns (IBPool bPool); /** * @notice Sets the BLabs address in the factory - * @param b The new BLabs address + * @param bLabs The new BLabs address */ - function setBLabs(address b) external; + function setBLabs(address bLabs) external; /** * @notice Collects the fees of a pool and transfers it to BLabs address - * @param pool The address of the pool to collect fees from + * @param bPool The address of the pool to collect fees from */ - function collect(IBPool pool) external; + function collect(IBPool bPool) external; /** * @notice Checks if an address is a BPool created from this factory diff --git a/src/interfaces/ISettlement.sol b/src/interfaces/ISettlement.sol index 086cfae2..1b176cc1 100644 --- a/src/interfaces/ISettlement.sol +++ b/src/interfaces/ISettlement.sol @@ -25,14 +25,14 @@ interface ISettlement { ) external; /** - * @return The domain separator for IERC1271 signature + * @return domainSeparator The domain separator for IERC1271 signature * @dev Immutable value, would not change on chain forks */ - function domainSeparator() external view returns (bytes32); + function domainSeparator() external view returns (bytes32 domainSeparator); /** - * @return The address that'll use the pool liquidity in CoWprotocol swaps + * @return vaultRelayer The address that'll use the pool liquidity in CoWprotocol swaps * @dev Address that will transfer and transferFrom the pool. Has an infinite allowance. */ - function vaultRelayer() external view returns (address); + function vaultRelayer() external view returns (address vaultRelayer); } diff --git a/test/manual-smock/MockBCoWPool.sol b/test/manual-smock/MockBCoWPool.sol index 5693743d..5df14933 100644 --- a/test/manual-smock/MockBCoWPool.sol +++ b/test/manual-smock/MockBCoWPool.sol @@ -62,7 +62,7 @@ contract MockBCoWPool is BCoWPool, Test { else return super._getLock(); } - function call__getLock() public returns (bytes32 _value) { + function call__getLock() public view returns (bytes32 _value) { return _getLock(); } diff --git a/test/manual-smock/MockBPool.sol b/test/manual-smock/MockBPool.sol index 414fca48..bd6d3546 100644 --- a/test/manual-smock/MockBPool.sol +++ b/test/manual-smock/MockBPool.sol @@ -357,7 +357,7 @@ contract MockBPool is BPool, Test { else return super._getLock(); } - function call__getLock() public returns (bytes32 _value) { + function call__getLock() public view returns (bytes32 _value) { return _getLock(); } diff --git a/test/smock/MockBFactory.sol b/test/smock/MockBFactory.sol index 512eab79..ae3f48ff 100644 --- a/test/smock/MockBFactory.sol +++ b/test/smock/MockBFactory.sol @@ -13,12 +13,12 @@ contract MockBFactory is BFactory, Test { return _isBPool[_key0]; } - function set__blabs(address __blabs) public { - _blabs = __blabs; + function set__bLabs(address __bLabs) public { + _bLabs = __bLabs; } - function call__blabs() public view returns (address) { - return _blabs; + function call__bLabs() public view returns (address) { + return _bLabs; } constructor() BFactory() {} diff --git a/test/unit/BPool.t.sol b/test/unit/BPool.t.sol index 46a9f14b..b46be656 100644 --- a/test/unit/BPool.t.sol +++ b/test/unit/BPool.t.sol @@ -635,6 +635,7 @@ contract BPool_Unit_SetController is BasePoolTest { } function test_Set_ReentrancyLock(address _controller) public { + vm.assume(_controller != address(0)); _expectSetReentrancyLock(); bPool.setController(_controller); } @@ -2590,12 +2591,13 @@ contract BPool_Unit_ExitswapPoolAmountIn is BasePoolTest { function test_Revert_NotBound( ExitswapPoolAmountIn_FuzzScenario memory _fuzz, - address _tokenIn + address _tokenOut ) public happyPath(_fuzz) { - assumeNotForgeAddress(_tokenIn); + vm.assume(_tokenOut != tokenOut); + assumeNotForgeAddress(_tokenOut); vm.expectRevert(IBPool.BPool_TokenNotBound.selector); - bPool.exitswapPoolAmountIn(_tokenIn, _fuzz.poolAmountIn, 0); + bPool.exitswapPoolAmountIn(_tokenOut, _fuzz.poolAmountIn, 0); } function test_Revert_TokenAmountOutBelowMinAmountOut( @@ -2848,6 +2850,7 @@ contract BPool_Unit_ExitswapExternAmountOut is BasePoolTest { ExitswapExternAmountOut_FuzzScenario memory _fuzz, address _tokenOut ) public happyPath(_fuzz) { + vm.assume(_tokenOut != tokenOut); assumeNotForgeAddress(_tokenOut); vm.expectRevert(IBPool.BPool_TokenNotBound.selector); From 4ea568e2604186a55f9ec66f089402857d5c2bd6 Mon Sep 17 00:00:00 2001 From: teddy Date: Fri, 5 Jul 2024 14:39:22 -0300 Subject: [PATCH 14/48] test: btt tests for bpool bind & unbind (#129) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * test: migrate some easy methods to BTT * test: btt tests for bind * test: btt tests for unbind * refactor: move BPoolBase onto its own file to please bulloak * fix: fix bind/unbind tree * refactor: manually mock every bind side effect for unbind * chore: renames in bpool tree * fix: various renames in .tree and .t.sol from code review * refactor: every non-trivial method has its own tree now * refactor: better mocking * refactor: flatten trees * fix: small fixes from code review * test: fuzz start total weight and existing tokens * test: test the entire behaviour of reentrancy locks * fix: consistent bind and unbind trees (#140) * fix: consistent bind and unbind trees * fix: reentrancy lock expectations * fix: unfuzzing initial weight in bind --------- Co-authored-by: Weißer Hase --- test/unit/BPool.t.sol | 339 ----------------------------- test/unit/BPool/BPool.t.sol | 53 +++++ test/unit/BPool/BPool.tree | 22 ++ test/unit/BPool/BPoolBase.sol | 44 ++++ test/unit/BPool/BPool_Bind.t.sol | 101 +++++++++ test/unit/BPool/BPool_Bind.tree | 28 +++ test/unit/BPool/BPool_Unbind.t.sol | 99 +++++++++ test/unit/BPool/BPool_Unbind.tree | 24 ++ 8 files changed, 371 insertions(+), 339 deletions(-) create mode 100644 test/unit/BPool/BPool.t.sol create mode 100644 test/unit/BPool/BPool.tree create mode 100644 test/unit/BPool/BPoolBase.sol create mode 100644 test/unit/BPool/BPool_Bind.t.sol create mode 100644 test/unit/BPool/BPool_Bind.tree create mode 100644 test/unit/BPool/BPool_Unbind.t.sol create mode 100644 test/unit/BPool/BPool_Unbind.tree diff --git a/test/unit/BPool.t.sol b/test/unit/BPool.t.sol index b46be656..c5b9341a 100644 --- a/test/unit/BPool.t.sol +++ b/test/unit/BPool.t.sol @@ -357,42 +357,6 @@ abstract contract SwapExactAmountInUtils is BasePoolTest { } } -contract BPool_Unit_Constructor is BasePoolTest { - function test_Deploy(address _deployer) public { - vm.prank(_deployer); - MockBPool _newBPool = new MockBPool(); - - assertEq(_newBPool.call__controller(), _deployer); - assertEq(_newBPool.call__FACTORY(), _deployer); - assertEq(_newBPool.call__swapFee(), MIN_FEE); - assertEq(_newBPool.call__finalized(), false); - } -} - -contract BPool_Unit_IsFinalized is BasePoolTest { - function test_Returns_IsFinalized(bool _isFinalized) public { - bPool.set__finalized(_isFinalized); - assertEq(bPool.isFinalized(), _isFinalized); - } -} - -contract BPool_Unit_IsBound is BasePoolTest { - function test_Returns_IsBound(address _token, bool _isBound) public { - _setRecord(_token, IBPool.Record({bound: _isBound, index: 0, denorm: 0})); - assertEq(bPool.isBound(_token), _isBound); - } -} - -contract BPool_Unit_GetNumTokens is BasePoolTest { - function test_Returns_NumTokens(uint256 _tokensToAdd) public { - vm.assume(_tokensToAdd > 0); - vm.assume(_tokensToAdd <= MAX_BOUND_TOKENS); - _setRandomTokens(_tokensToAdd); - - assertEq(bPool.getNumTokens(), _tokensToAdd); - } -} - contract BPool_Unit_GetCurrentTokens is BasePoolTest { function test_Returns_CurrentTokens(uint256 _length) public { vm.assume(_length > 0); @@ -718,309 +682,6 @@ contract BPool_Unit_Finalize is BasePoolTest { } } -contract BPool_Unit_Bind is BasePoolTest { - struct Bind_FuzzScenario { - address token; - uint256 balance; - uint256 denorm; - uint256 previousTokensAmount; - uint256 totalWeight; - } - - function _setValues(Bind_FuzzScenario memory _fuzz) internal { - // Create mocks - _mockTransferFrom(_fuzz.token); - _mockPoolBalance(_fuzz.token, 0); - - // Set tokens - _setRandomTokens(_fuzz.previousTokensAmount); - - // Set finalize - _setFinalize(false); - // Set totalWeight - _setTotalWeight(_fuzz.totalWeight); - } - - function _assumeHappyPath(Bind_FuzzScenario memory _fuzz) internal { - assumeNotForgeAddress(_fuzz.token); - - _fuzz.previousTokensAmount = bound(_fuzz.previousTokensAmount, 0, MAX_BOUND_TOKENS - 1); - - address[] memory _tokenArray = _getDeterministicTokenArray(_fuzz.previousTokensAmount); - for (uint256 i = 0; i < _fuzz.previousTokensAmount; i++) { - vm.assume(_fuzz.token != _tokenArray[i]); - } - - _fuzz.balance = bound(_fuzz.balance, MIN_BALANCE, type(uint256).max); - _fuzz.totalWeight = bound(_fuzz.totalWeight, 0, MAX_TOTAL_WEIGHT - MIN_WEIGHT); - _fuzz.denorm = bound(_fuzz.denorm, MIN_WEIGHT, MAX_TOTAL_WEIGHT - _fuzz.totalWeight); - } - - modifier happyPath(Bind_FuzzScenario memory _fuzz) { - _assumeHappyPath(_fuzz); - _setValues(_fuzz); - _; - } - - function test_Revert_NotController( - Bind_FuzzScenario memory _fuzz, - address _controller, - address _caller - ) public happyPath(_fuzz) { - vm.assume(_controller != _caller); - bPool.set__controller(_controller); - - vm.prank(_caller); - vm.expectRevert(IBPool.BPool_CallerIsNotController.selector); - bPool.bind(_fuzz.token, _fuzz.balance, _fuzz.denorm); - } - - function test_Revert_IsBound(Bind_FuzzScenario memory _fuzz, address _token) public happyPath(_fuzz) { - _setRecord(_token, IBPool.Record({bound: true, index: 0, denorm: 0})); - - vm.expectRevert(IBPool.BPool_TokenAlreadyBound.selector); - bPool.bind(_token, _fuzz.balance, _fuzz.denorm); - } - - function test_Revert_Finalized(Bind_FuzzScenario memory _fuzz) public happyPath(_fuzz) { - _setFinalize(true); - - vm.expectRevert(IBPool.BPool_PoolIsFinalized.selector); - bPool.bind(_fuzz.token, _fuzz.balance, _fuzz.denorm); - } - - function test_Revert_MaxPoolTokens(Bind_FuzzScenario memory _fuzz) public happyPath(_fuzz) { - address[] memory _tokens = _setRandomTokens(MAX_BOUND_TOKENS); - for (uint256 i = 0; i < _tokens.length; i++) { - vm.assume(_fuzz.token != _tokens[i]); - } - - vm.expectRevert(IBPool.BPool_TokensAboveMaximum.selector); - bPool.bind(_fuzz.token, _fuzz.balance, _fuzz.denorm); - } - - function test_Set_ReentrancyLock(Bind_FuzzScenario memory _fuzz) public happyPath(_fuzz) { - _expectSetReentrancyLock(); - bPool.bind(_fuzz.token, _fuzz.balance, _fuzz.denorm); - } - - function test_Set_Record(Bind_FuzzScenario memory _fuzz) public happyPath(_fuzz) { - bPool.bind(_fuzz.token, _fuzz.balance, _fuzz.denorm); - - assertTrue(bPool.isBound(_fuzz.token)); - assertEq(bPool.call__records(_fuzz.token).index, _fuzz.previousTokensAmount); - } - - function test_PushArray_TokenArray(Bind_FuzzScenario memory _fuzz) public happyPath(_fuzz) { - bPool.bind(_fuzz.token, _fuzz.balance, _fuzz.denorm); - - assertEq(bPool.getCurrentTokens().length, _fuzz.previousTokensAmount + 1); - assertEq(bPool.getCurrentTokens()[_fuzz.previousTokensAmount], _fuzz.token); - } - - function test_Emit_LogCall(Bind_FuzzScenario memory _fuzz) public happyPath(_fuzz) { - vm.expectEmit(); - bytes memory _data = abi.encodeWithSelector(BPool.bind.selector, _fuzz.token, _fuzz.balance, _fuzz.denorm); - emit IBPool.LOG_CALL(BPool.bind.selector, address(this), _data); - - bPool.bind(_fuzz.token, _fuzz.balance, _fuzz.denorm); - } - - function test_Revert_MinWeight(Bind_FuzzScenario memory _fuzz, uint256 _denorm) public happyPath(_fuzz) { - vm.assume(_denorm < MIN_WEIGHT); - - vm.expectRevert(IBPool.BPool_WeightBelowMinimum.selector); - bPool.bind(_fuzz.token, _fuzz.balance, _denorm); - } - - function test_Revert_MaxWeight(Bind_FuzzScenario memory _fuzz, uint256 _denorm) public happyPath(_fuzz) { - vm.assume(_denorm > MAX_WEIGHT); - - vm.expectRevert(IBPool.BPool_WeightAboveMaximum.selector); - bPool.bind(_fuzz.token, _fuzz.balance, _denorm); - } - - function test_Revert_MinBalance(Bind_FuzzScenario memory _fuzz, uint256 _balance) public happyPath(_fuzz) { - vm.assume(_balance < MIN_BALANCE); - - vm.expectRevert(IBPool.BPool_BalanceBelowMinimum.selector); - bPool.bind(_fuzz.token, _balance, _fuzz.denorm); - } - - function test_Revert_Reentrancy(Bind_FuzzScenario memory _fuzz) public happyPath(_fuzz) { - _expectRevertByReentrancy(); - bPool.bind(_fuzz.token, _fuzz.balance, _fuzz.denorm); - } - - function test_Set_TotalWeight(Bind_FuzzScenario memory _fuzz) public happyPath(_fuzz) { - bPool.bind(_fuzz.token, _fuzz.balance, _fuzz.denorm); - - assertEq(bPool.call__totalWeight(), _fuzz.totalWeight + _fuzz.denorm); - } - - function test_Revert_MaxTotalWeight(Bind_FuzzScenario memory _fuzz, uint256 _denorm) public happyPath(_fuzz) { - _denorm = bound(_denorm, MIN_WEIGHT, MAX_WEIGHT); - _setTotalWeight(MAX_TOTAL_WEIGHT); - - vm.expectRevert(IBPool.BPool_TotalWeightAboveMaximum.selector); - bPool.bind(_fuzz.token, _fuzz.balance, _denorm); - } - - function test_Set_Denorm(Bind_FuzzScenario memory _fuzz) public happyPath(_fuzz) { - bPool.bind(_fuzz.token, _fuzz.balance, _fuzz.denorm); - - assertEq(bPool.call__records(_fuzz.token).denorm, _fuzz.denorm); - } - - function test_Pull_Balance(Bind_FuzzScenario memory _fuzz) public happyPath(_fuzz) { - vm.expectCall( - address(_fuzz.token), - abi.encodeWithSelector(IERC20.transferFrom.selector, address(this), address(bPool), _fuzz.balance) - ); - - bPool.bind(_fuzz.token, _fuzz.balance, _fuzz.denorm); - } -} - -contract BPool_Unit_Unbind is BasePoolTest { - struct Unbind_FuzzScenario { - address token; - uint256 tokenIndex; - uint256 balance; - uint256 denorm; - uint256 previousTokensAmount; - uint256 totalWeight; - } - - function _setValues(Unbind_FuzzScenario memory _fuzz) internal { - // Create mocks - _mockTransfer(_fuzz.token); - - // Set tokens - _setRandomTokens(_fuzz.previousTokensAmount); - - // Set denorm and balance - _setRecord(_fuzz.token, IBPool.Record({bound: true, index: _fuzz.tokenIndex, denorm: _fuzz.denorm})); - _mockPoolBalance(_fuzz.token, _fuzz.balance); - - // Set finalize - _setFinalize(false); - // Set totalWeight - _setTotalWeight(_fuzz.totalWeight); - } - - function _assumeHappyPath(Unbind_FuzzScenario memory _fuzz) internal pure { - assumeNotForgeAddress(_fuzz.token); - - _fuzz.balance = bound(_fuzz.balance, MIN_BALANCE, type(uint256).max); - _fuzz.totalWeight = bound(_fuzz.totalWeight, MIN_WEIGHT, MAX_TOTAL_WEIGHT - MIN_WEIGHT); - // The token to unbind will be included inside the array - _fuzz.previousTokensAmount = bound(_fuzz.previousTokensAmount, 1, MAX_BOUND_TOKENS); - _fuzz.tokenIndex = bound(_fuzz.tokenIndex, 0, _fuzz.previousTokensAmount - 1); - _fuzz.denorm = bound(_fuzz.denorm, MIN_WEIGHT, _fuzz.totalWeight); - } - - modifier happyPath(Unbind_FuzzScenario memory _fuzz) { - _assumeHappyPath(_fuzz); - _setValues(_fuzz); - _; - } - - function test_Revert_NotController( - Unbind_FuzzScenario memory _fuzz, - address _controller, - address _caller - ) public happyPath(_fuzz) { - vm.assume(_controller != _caller); - bPool.set__controller(_controller); - - vm.prank(_caller); - vm.expectRevert(IBPool.BPool_CallerIsNotController.selector); - bPool.unbind(_fuzz.token); - } - - function test_Revert_NotBound(Unbind_FuzzScenario memory _fuzz, address _token) public happyPath(_fuzz) { - _setRecord(_token, IBPool.Record({bound: false, index: _fuzz.tokenIndex, denorm: _fuzz.denorm})); - - vm.expectRevert(IBPool.BPool_TokenNotBound.selector); - bPool.unbind(_token); - } - - function test_Revert_Finalized(Unbind_FuzzScenario memory _fuzz) public happyPath(_fuzz) { - _setFinalize(true); - - vm.expectRevert(IBPool.BPool_PoolIsFinalized.selector); - bPool.unbind(_fuzz.token); - } - - function test_Revert_Reentrancy(Unbind_FuzzScenario memory _fuzz) public happyPath(_fuzz) { - _expectRevertByReentrancy(); - bPool.unbind(_fuzz.token); - } - - function test_Set_TotalWeight(Unbind_FuzzScenario memory _fuzz) public happyPath(_fuzz) { - bPool.unbind(_fuzz.token); - - assertEq(bPool.call__totalWeight(), _fuzz.totalWeight - _fuzz.denorm); - } - - function test_Set_ReentrancyLock(Unbind_FuzzScenario memory _fuzz) public happyPath(_fuzz) { - _expectSetReentrancyLock(); - bPool.unbind(_fuzz.token); - } - - function test_Set_TokenArray(Unbind_FuzzScenario memory _fuzz) public happyPath(_fuzz) { - address _lastTokenBefore = bPool.call__tokens()[bPool.call__tokens().length - 1]; - - bPool.unbind(_fuzz.token); - - // Only check if the token is not the last of the array (that item is always poped out) - if (_fuzz.tokenIndex != _fuzz.previousTokensAmount - 1) { - address _tokenToUnbindAfter = bPool.call__tokens()[_fuzz.tokenIndex]; - assertEq(_tokenToUnbindAfter, _lastTokenBefore); - } - } - - function test_Set_Index(Unbind_FuzzScenario memory _fuzz) public happyPath(_fuzz) { - address _lastTokenBefore = bPool.call__tokens()[bPool.call__tokens().length - 1]; - - bPool.unbind(_fuzz.token); - - // Only check if the token is not the last of the array (that item is always poped out) - if (_fuzz.tokenIndex != _fuzz.previousTokensAmount - 1) { - assertEq(bPool.call__records(_lastTokenBefore).index, _fuzz.tokenIndex); - } - } - - function test_PopArray_TokenArray(Unbind_FuzzScenario memory _fuzz) public happyPath(_fuzz) { - bPool.unbind(_fuzz.token); - - assertEq(bPool.call__tokens().length, _fuzz.previousTokensAmount - 1); - } - - function test_Set_Record(Unbind_FuzzScenario memory _fuzz) public happyPath(_fuzz) { - bPool.unbind(_fuzz.token); - - assertEq(bPool.call__records(_fuzz.token).index, 0); - assertEq(bPool.call__records(_fuzz.token).bound, false); - assertEq(bPool.call__records(_fuzz.token).denorm, 0); - } - - function test_Push_UnderlyingBalance(Unbind_FuzzScenario memory _fuzz) public happyPath(_fuzz) { - vm.expectCall(_fuzz.token, abi.encodeWithSelector(IERC20.transfer.selector, address(this), _fuzz.balance)); - - bPool.unbind(_fuzz.token); - } - - function test_Emit_LogCall(Unbind_FuzzScenario memory _fuzz) public happyPath(_fuzz) { - vm.expectEmit(); - bytes memory _data = abi.encodeWithSelector(BPool.unbind.selector, _fuzz.token); - emit IBPool.LOG_CALL(BPool.unbind.selector, address(this), _data); - - bPool.unbind(_fuzz.token); - } -} - contract BPool_Unit_GetSpotPrice is BasePoolTest { struct GetSpotPrice_FuzzScenario { address tokenIn; diff --git a/test/unit/BPool/BPool.t.sol b/test/unit/BPool/BPool.t.sol new file mode 100644 index 00000000..0c5b8937 --- /dev/null +++ b/test/unit/BPool/BPool.t.sol @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.25; + +import {BPoolBase} from './BPoolBase.sol'; +import {IBPool} from 'interfaces/IBPool.sol'; +import {MockBPool} from 'test/manual-smock/MockBPool.sol'; + +contract BPool is BPoolBase { + function test_ConstructorWhenCalled(address _deployer) external { + vm.prank(_deployer); + MockBPool _newBPool = new MockBPool(); + + // it sets caller as controller + assertEq(_newBPool.call__controller(), _deployer); + // it sets caller as factory + assertEq(_newBPool.call__FACTORY(), _deployer); + // it sets swap fee to MIN_FEE + assertEq(_newBPool.call__swapFee(), MIN_FEE); + // it does NOT finalize the pool + assertEq(_newBPool.call__finalized(), false); + } + + function test_IsFinalizedWhenPoolIsFinalized() external { + bPool.set__finalized(true); + // it returns true + assertTrue(bPool.isFinalized()); + } + + function test_IsFinalizedWhenPoolIsNOTFinalized() external { + bPool.set__finalized(false); + // it returns false + assertFalse(bPool.isFinalized()); + } + + function test_IsBoundWhenTokenIsBound(address _token) external { + _setRecord(_token, IBPool.Record({bound: true, index: 0, denorm: 0})); + // it returns true + assertTrue(bPool.isBound(_token)); + } + + function test_IsBoundWhenTokenIsNOTBound(address _token) external { + _setRecord(_token, IBPool.Record({bound: false, index: 0, denorm: 0})); + // it returns false + assertFalse(bPool.isBound(_token)); + } + + function test_GetNumTokensWhenCalled(uint256 _tokensToAdd) external { + _tokensToAdd = bound(_tokensToAdd, 0, MAX_BOUND_TOKENS); + _setRandomTokens(_tokensToAdd); + // it returns number of tokens + assertEq(bPool.getNumTokens(), _tokensToAdd); + } +} diff --git a/test/unit/BPool/BPool.tree b/test/unit/BPool/BPool.tree new file mode 100644 index 00000000..379bec03 --- /dev/null +++ b/test/unit/BPool/BPool.tree @@ -0,0 +1,22 @@ +BPool::constructor +└── when called + ├── it sets caller as controller + ├── it sets caller as factory + ├── it sets swap fee to MIN_FEE + └── it does NOT finalize the pool + +BPool::isFinalized +├── when pool is finalized +│ └── it returns true +└── when pool is NOT finalized + └── it returns false + +BPool::isBound +├── when token is bound +│ └── it returns true +└── when token is NOT bound + └── it returns false + +BPool::getNumTokens +└── when called + └── it returns number of tokens diff --git a/test/unit/BPool/BPoolBase.sol b/test/unit/BPool/BPoolBase.sol new file mode 100644 index 00000000..1b324c59 --- /dev/null +++ b/test/unit/BPool/BPoolBase.sol @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.25; + +import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; +import {BConst} from 'contracts/BConst.sol'; +import {Test} from 'forge-std/Test.sol'; +import {IBPool} from 'interfaces/IBPool.sol'; +import {MockBPool} from 'test/manual-smock/MockBPool.sol'; +import {Utils} from 'test/utils/Utils.sol'; + +contract BPoolBase is Test, BConst, Utils { + MockBPool public bPool; + address public deployer = makeAddr('deployer'); + + address public token = makeAddr('token'); + uint256 public tokenBindBalance = 100e18; + uint256 public tokenWeight = 1e18; + uint256 public totalWeight = 10e18; + + function setUp() public virtual { + vm.prank(deployer); + bPool = new MockBPool(); + + vm.mockCall(token, abi.encodePacked(IERC20.transferFrom.selector), abi.encode()); + vm.mockCall(token, abi.encodePacked(IERC20.transfer.selector), abi.encode()); + vm.mockCall(token, abi.encodePacked(IERC20.balanceOf.selector), abi.encode(tokenBindBalance)); + } + + function _setRandomTokens(uint256 _length) internal returns (address[] memory _tokensToAdd) { + _tokensToAdd = _getDeterministicTokenArray(_length); + for (uint256 i = 0; i < _length; i++) { + _setRecord(_tokensToAdd[i], IBPool.Record({bound: true, index: i, denorm: 0})); + } + _setTokens(_tokensToAdd); + } + + function _setTokens(address[] memory _tokens) internal { + bPool.set__tokens(_tokens); + } + + function _setRecord(address _token, IBPool.Record memory _record) internal { + bPool.set__records(_token, _record); + } +} diff --git a/test/unit/BPool/BPool_Bind.t.sol b/test/unit/BPool/BPool_Bind.t.sol new file mode 100644 index 00000000..a1cf7152 --- /dev/null +++ b/test/unit/BPool/BPool_Bind.t.sol @@ -0,0 +1,101 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.25; + +import {BPoolBase} from './BPoolBase.sol'; +import {IBPool} from 'interfaces/IBPool.sol'; + +contract BPoolBind is BPoolBase { + function test_RevertWhen_ReentrancyLockIsSet() external { + bPool.call__setLock(_MUTEX_TAKEN); + vm.expectRevert(IBPool.BPool_Reentrancy.selector); + // it should revert + bPool.bind(token, tokenBindBalance, tokenWeight); + } + + function test_RevertWhen_CallerIsNOTController(address _caller) external { + // it should revert + vm.assume(_caller != deployer); + vm.prank(_caller); + vm.expectRevert(IBPool.BPool_CallerIsNotController.selector); + bPool.bind(token, tokenBindBalance, tokenWeight); + } + + modifier whenCallerIsController() { + vm.startPrank(deployer); + _; + } + + function test_RevertWhen_TokenIsAlreadyBound() external whenCallerIsController { + _setRecord(token, IBPool.Record({bound: true, index: 0, denorm: tokenWeight})); + // it should revert + vm.expectRevert(IBPool.BPool_TokenAlreadyBound.selector); + bPool.bind(token, tokenBindBalance, tokenWeight); + } + + function test_RevertWhen_PoolIsFinalized() external whenCallerIsController { + bPool.set__finalized(true); + // it should revert + vm.expectRevert(IBPool.BPool_PoolIsFinalized.selector); + bPool.bind(token, tokenBindBalance, tokenWeight); + } + + function test_RevertWhen_MAX_BOUND_TOKENSTokensAreAlreadyBound() external whenCallerIsController { + _setRandomTokens(MAX_BOUND_TOKENS); + // it should revert + vm.expectRevert(IBPool.BPool_TokensAboveMaximum.selector); + bPool.bind(token, tokenBindBalance, tokenWeight); + } + + function test_RevertWhen_TokenWeightIsTooLow() external whenCallerIsController { + // it should revert + vm.expectRevert(IBPool.BPool_WeightBelowMinimum.selector); + bPool.bind(token, tokenBindBalance, MIN_WEIGHT - 1); + } + + function test_RevertWhen_TokenWeightIsTooHigh() external whenCallerIsController { + // it should revert + vm.expectRevert(IBPool.BPool_WeightAboveMaximum.selector); + bPool.bind(token, tokenBindBalance, MAX_WEIGHT + 1); + } + + function test_RevertWhen_TooLittleBalanceIsProvided() external whenCallerIsController { + // it should revert + vm.expectRevert(IBPool.BPool_BalanceBelowMinimum.selector); + bPool.bind(token, MIN_BALANCE - 1, tokenWeight); + } + + function test_RevertWhen_WeightSumExceedsMAX_TOTAL_WEIGHT() external whenCallerIsController { + bPool.set__totalWeight(2 * MAX_TOTAL_WEIGHT / 3); + // it should revert + vm.expectRevert(IBPool.BPool_TotalWeightAboveMaximum.selector); + bPool.bind(token, tokenBindBalance, MAX_TOTAL_WEIGHT / 2); + } + + function test_WhenTokenCanBeBound(uint256 _existingTokens) external whenCallerIsController { + _existingTokens = bound(_existingTokens, 0, MAX_BOUND_TOKENS - 1); + bPool.set__tokens(_getDeterministicTokenArray(_existingTokens)); + + bPool.set__totalWeight(totalWeight); + // it calls _pullUnderlying + bPool.expectCall__pullUnderlying(token, deployer, tokenBindBalance); + // it sets the reentrancy lock + bPool.expectCall__setLock(_MUTEX_TAKEN); + // it emits LOG_CALL event + vm.expectEmit(); + bytes memory _data = abi.encodeWithSelector(IBPool.bind.selector, token, tokenBindBalance, tokenWeight); + emit IBPool.LOG_CALL(IBPool.bind.selector, deployer, _data); + + bPool.bind(token, tokenBindBalance, tokenWeight); + + // it clears the reentrancy lock + assertEq(bPool.call__getLock(), _MUTEX_FREE); + // it adds token to the tokens array + assertEq(bPool.call__tokens()[_existingTokens], token); + // it sets the token record + assertEq(bPool.call__records(token).bound, true); + assertEq(bPool.call__records(token).denorm, tokenWeight); + assertEq(bPool.call__records(token).index, _existingTokens); + // it sets total weight + assertEq(bPool.call__totalWeight(), totalWeight + tokenWeight); + } +} diff --git a/test/unit/BPool/BPool_Bind.tree b/test/unit/BPool/BPool_Bind.tree new file mode 100644 index 00000000..f0ab3278 --- /dev/null +++ b/test/unit/BPool/BPool_Bind.tree @@ -0,0 +1,28 @@ +BPool::Bind +├── when reentrancy lock is set +│ └── it should revert +├── when caller is NOT controller +│ └── it should revert +└── when caller is controller + ├── when token is already bound + │ └── it should revert + ├── when pool is finalized + │ └── it should revert + ├── when MAX_BOUND_TOKENS tokens are already bound + │ └── it should revert + ├── when token weight is too low + │ └── it should revert + ├── when token weight is too high + │ └── it should revert + ├── when too little balance is provided + │ └── it should revert + ├── when weight sum exceeds MAX_TOTAL_WEIGHT + │ └── it should revert + └── when token can be bound + ├── it sets the reentrancy lock + ├── it emits LOG_CALL event + ├── it increments _totalWeight + ├── it calls _pullUnderlying + ├── it adds token to the tokens array + ├── it sets the token record + └── it clears the reentrancy lock diff --git a/test/unit/BPool/BPool_Unbind.t.sol b/test/unit/BPool/BPool_Unbind.t.sol new file mode 100644 index 00000000..0695dde9 --- /dev/null +++ b/test/unit/BPool/BPool_Unbind.t.sol @@ -0,0 +1,99 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.25; + +import {BPoolBase} from './BPoolBase.sol'; + +import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; +import {IBPool} from 'interfaces/IBPool.sol'; + +contract BPoolUnbind is BPoolBase { + address public secondToken = makeAddr('secondToken'); + + function setUp() public virtual override { + super.setUp(); + vm.mockCall(secondToken, abi.encodePacked(IERC20.transferFrom.selector), abi.encode()); + } + + function test_RevertWhen_ReentrancyLockIsSet() external { + bPool.call__setLock(_MUTEX_TAKEN); + vm.expectRevert(IBPool.BPool_Reentrancy.selector); + // it should revert + bPool.unbind(token); + } + + function test_RevertWhen_CallerIsNOTController(address _caller) external { + // it should revert + vm.assume(_caller != deployer); + vm.prank(_caller); + vm.expectRevert(IBPool.BPool_CallerIsNotController.selector); + bPool.unbind(token); + } + + modifier whenCallerIsController() { + vm.startPrank(deployer); + _; + } + + function test_RevertWhen_TokenIsNotBound() external whenCallerIsController { + vm.expectRevert(IBPool.BPool_TokenNotBound.selector); + // it should revert + bPool.unbind(token); + } + + function test_RevertWhen_PoolIsFinalized() external whenCallerIsController { + _setRecord(token, IBPool.Record({bound: true, index: 0, denorm: 0})); + bPool.set__finalized(true); + // it should revert + vm.expectRevert(IBPool.BPool_PoolIsFinalized.selector); + bPool.unbind(token); + } + + modifier whenTokenCanBeUnbound() { + _setRecord(token, IBPool.Record({bound: true, index: 0, denorm: tokenWeight})); + bPool.set__totalWeight(totalWeight); + address[] memory tokens = new address[](1); + tokens[0] = token; + bPool.set__tokens(tokens); + _; + } + + function test_WhenTokenIsLastOnTheTokensArray() external whenCallerIsController whenTokenCanBeUnbound { + // it sets the reentrancy lock + bPool.expectCall__setLock(_MUTEX_TAKEN); + // it calls _pushUnderlying + bPool.expectCall__pushUnderlying(token, deployer, tokenBindBalance); + + // it emits LOG_CALL event + vm.expectEmit(); + bytes memory _data = abi.encodeWithSelector(IBPool.unbind.selector, token); + emit IBPool.LOG_CALL(IBPool.unbind.selector, deployer, _data); + bPool.unbind(token); + + // it clears the reentrancy lock + assertEq(bPool.call__getLock(), _MUTEX_FREE); + // it removes the token record + assertFalse(bPool.call__records(token).bound); + // it pops from the array + assertEq(bPool.getNumTokens(), 0); + // it decreases the total weight + assertEq(bPool.call__totalWeight(), totalWeight - tokenWeight); + } + + function test_WhenTokenIsNOTLastOnTheTokensArray() external whenCallerIsController whenTokenCanBeUnbound { + _setRecord(secondToken, IBPool.Record({bound: true, index: 0, denorm: tokenWeight})); + address[] memory tokens = new address[](2); + tokens[0] = token; + tokens[1] = secondToken; + bPool.set__tokens(tokens); + bPool.unbind(token); + // it removes the token record + assertFalse(bPool.call__records(token).bound); + // it removes the token from the array + assertEq(bPool.getNumTokens(), 1); + // it keeps other tokens in the array + assertEq(bPool.call__tokens()[0], secondToken); + assertTrue(bPool.call__records(secondToken).bound); + // it updates records to point to the new indices + assertEq(bPool.call__records(secondToken).index, 0); + } +} diff --git a/test/unit/BPool/BPool_Unbind.tree b/test/unit/BPool/BPool_Unbind.tree new file mode 100644 index 00000000..0f8fd8e7 --- /dev/null +++ b/test/unit/BPool/BPool_Unbind.tree @@ -0,0 +1,24 @@ +BPool::Unbind +├── when reentrancy lock is set +│ └── it should revert +├── when caller is NOT controller +│ └── it should revert +└── when caller is controller + ├── when token is not bound + │ └── it should revert + ├── when pool is finalized + │ └── it should revert + └── when token can be unbound + ├── when token is last on the tokens array + │ ├── it sets the reentrancy lock + │ ├── it emits LOG_CALL event + │ ├── it calls _pushUnderlying + │ ├── it removes the token record + │ ├── it decreases the total weight + │ ├── it pops from the array + │ └── it clears the reentrancy lock + └── when token is NOT last on the tokens array + ├── it removes the token record + ├── it removes the token from the array + ├── it keeps other tokens in the array + └── it updates records to point to the correct indices From 4d4344d15fd5bb385ccd1abb33ae0763c3e8955c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wei=C3=9Fer=20Hase?= Date: Mon, 8 Jul 2024 18:06:01 +0200 Subject: [PATCH 15/48] feat: adding finalized and notFinalized modifiers to BPool (#134) * feat: adding finalized and notFinalized modifiers to BPool * fix: rm global ffi and fix yarn:test * fix: setSwapFee is notFinalized * fix: rm remanent changes --- src/contracts/BPool.sol | 85 ++++++++++++++--------------------------- 1 file changed, 29 insertions(+), 56 deletions(-) diff --git a/src/contracts/BPool.sol b/src/contracts/BPool.sol index 02ab08b0..e92ba123 100644 --- a/src/contracts/BPool.sol +++ b/src/contracts/BPool.sol @@ -53,6 +53,22 @@ contract BPool is BToken, BMath, IBPool { _; } + /// @dev Throws an error if pool is not finalized + modifier _finalized_() { + if (!_finalized) { + revert BPool_PoolNotFinalized(); + } + _; + } + + /// @dev Throws an error if pool is finalized + modifier _notFinalized_() { + if (_finalized) { + revert BPool_PoolIsFinalized(); + } + _; + } + /** * @notice Throws an error if caller is not controller */ @@ -71,10 +87,7 @@ contract BPool is BToken, BMath, IBPool { } /// @inheritdoc IBPool - function setSwapFee(uint256 swapFee) external _logs_ _lock_ _controller_ { - if (_finalized) { - revert BPool_PoolIsFinalized(); - } + function setSwapFee(uint256 swapFee) external _logs_ _lock_ _controller_ _notFinalized_ { if (swapFee < MIN_FEE) { revert BPool_FeeBelowMinimum(); } @@ -94,10 +107,7 @@ contract BPool is BToken, BMath, IBPool { } /// @inheritdoc IBPool - function finalize() external _logs_ _lock_ _controller_ { - if (_finalized) { - revert BPool_PoolIsFinalized(); - } + function finalize() external _logs_ _lock_ _controller_ _notFinalized_ { if (_tokens.length < MIN_BOUND_TOKENS) { revert BPool_TokensBelowMinimum(); } @@ -110,18 +120,13 @@ contract BPool is BToken, BMath, IBPool { } /// @inheritdoc IBPool - function bind(address token, uint256 balance, uint256 denorm) external _logs_ _lock_ _controller_ { + function bind(address token, uint256 balance, uint256 denorm) external _logs_ _lock_ _controller_ _notFinalized_ { if (_records[token].bound) { revert BPool_TokenAlreadyBound(); } - if (_finalized) { - revert BPool_PoolIsFinalized(); - } - if (_tokens.length >= MAX_BOUND_TOKENS) { revert BPool_TokensAboveMaximum(); } - if (denorm < MIN_WEIGHT) { revert BPool_WeightBelowMinimum(); } @@ -144,13 +149,10 @@ contract BPool is BToken, BMath, IBPool { } /// @inheritdoc IBPool - function unbind(address token) external _logs_ _lock_ _controller_ { + function unbind(address token) external _logs_ _lock_ _controller_ _notFinalized_ { if (!_records[token].bound) { revert BPool_TokenNotBound(); } - if (_finalized) { - revert BPool_PoolIsFinalized(); - } _totalWeight = bsub(_totalWeight, _records[token].denorm); @@ -167,11 +169,7 @@ contract BPool is BToken, BMath, IBPool { } /// @inheritdoc IBPool - function joinPool(uint256 poolAmountOut, uint256[] calldata maxAmountsIn) external _logs_ _lock_ { - if (!_finalized) { - revert BPool_PoolNotFinalized(); - } - + function joinPool(uint256 poolAmountOut, uint256[] calldata maxAmountsIn) external _logs_ _lock_ _finalized_ { uint256 poolTotal = totalSupply(); uint256 ratio = bdiv(poolAmountOut, poolTotal); if (ratio == 0) { @@ -197,11 +195,7 @@ contract BPool is BToken, BMath, IBPool { } /// @inheritdoc IBPool - function exitPool(uint256 poolAmountIn, uint256[] calldata minAmountsOut) external _logs_ _lock_ { - if (!_finalized) { - revert BPool_PoolNotFinalized(); - } - + function exitPool(uint256 poolAmountIn, uint256[] calldata minAmountsOut) external _logs_ _lock_ _finalized_ { uint256 poolTotal = totalSupply(); uint256 exitFee = bmul(poolAmountIn, EXIT_FEE); uint256 pAiAfterExitFee = bsub(poolAmountIn, exitFee); @@ -237,16 +231,13 @@ contract BPool is BToken, BMath, IBPool { address tokenOut, uint256 minAmountOut, uint256 maxPrice - ) external _logs_ _lock_ returns (uint256 tokenAmountOut, uint256 spotPriceAfter) { + ) external _logs_ _lock_ _finalized_ returns (uint256 tokenAmountOut, uint256 spotPriceAfter) { if (!_records[tokenIn].bound) { revert BPool_TokenNotBound(); } if (!_records[tokenOut].bound) { revert BPool_TokenNotBound(); } - if (!_finalized) { - revert BPool_PoolNotFinalized(); - } Record storage inRecord = _records[address(tokenIn)]; Record storage outRecord = _records[address(tokenOut)]; @@ -299,16 +290,13 @@ contract BPool is BToken, BMath, IBPool { address tokenOut, uint256 tokenAmountOut, uint256 maxPrice - ) external _logs_ _lock_ returns (uint256 tokenAmountIn, uint256 spotPriceAfter) { + ) external _logs_ _lock_ _finalized_ returns (uint256 tokenAmountIn, uint256 spotPriceAfter) { if (!_records[tokenIn].bound) { revert BPool_TokenNotBound(); } if (!_records[tokenOut].bound) { revert BPool_TokenNotBound(); } - if (!_finalized) { - revert BPool_PoolNotFinalized(); - } Record storage inRecord = _records[address(tokenIn)]; Record storage outRecord = _records[address(tokenOut)]; @@ -359,10 +347,7 @@ contract BPool is BToken, BMath, IBPool { address tokenIn, uint256 tokenAmountIn, uint256 minPoolAmountOut - ) external _logs_ _lock_ returns (uint256 poolAmountOut) { - if (!_finalized) { - revert BPool_PoolNotFinalized(); - } + ) external _logs_ _lock_ _finalized_ returns (uint256 poolAmountOut) { if (!_records[tokenIn].bound) { revert BPool_TokenNotBound(); } @@ -393,10 +378,7 @@ contract BPool is BToken, BMath, IBPool { address tokenIn, uint256 poolAmountOut, uint256 maxAmountIn - ) external _logs_ _lock_ returns (uint256 tokenAmountIn) { - if (!_finalized) { - revert BPool_PoolNotFinalized(); - } + ) external _logs_ _lock_ _finalized_ returns (uint256 tokenAmountIn) { if (!_records[tokenIn].bound) { revert BPool_TokenNotBound(); } @@ -431,10 +413,7 @@ contract BPool is BToken, BMath, IBPool { address tokenOut, uint256 poolAmountIn, uint256 minAmountOut - ) external _logs_ _lock_ returns (uint256 tokenAmountOut) { - if (!_finalized) { - revert BPool_PoolNotFinalized(); - } + ) external _logs_ _lock_ _finalized_ returns (uint256 tokenAmountOut) { if (!_records[tokenOut].bound) { revert BPool_TokenNotBound(); } @@ -469,10 +448,7 @@ contract BPool is BToken, BMath, IBPool { address tokenOut, uint256 tokenAmountOut, uint256 maxPoolAmountIn - ) external _logs_ _lock_ returns (uint256 poolAmountIn) { - if (!_finalized) { - revert BPool_PoolNotFinalized(); - } + ) external _logs_ _lock_ _finalized_ returns (uint256 poolAmountIn) { if (!_records[tokenOut].bound) { revert BPool_TokenNotBound(); } @@ -569,10 +545,7 @@ contract BPool is BToken, BMath, IBPool { } /// @inheritdoc IBPool - function getFinalTokens() external view _viewlock_ returns (address[] memory tokens) { - if (!_finalized) { - revert BPool_PoolNotFinalized(); - } + function getFinalTokens() external view _viewlock_ _finalized_ returns (address[] memory tokens) { return _tokens; } From 899c2a5d5a06d7624492fb294527543675f16ed5 Mon Sep 17 00:00:00 2001 From: teddy Date: Mon, 8 Jul 2024 14:30:57 -0300 Subject: [PATCH 16/48] ci: ensure smock files are up to date (#142) * ci: ensure smock files are up to date * chore: update existing smocked files --- .github/workflows/ci.yml | 3 +++ test/smock/MockBFactory.sol | 20 ++++++++--------- test/smock/MockBToken.sol | 45 +++++++++++++++++++------------------ 3 files changed, 36 insertions(+), 32 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5436cfb8..0b0cee1d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -101,5 +101,8 @@ jobs: - name: Run bulloak check run: yarn lint:bulloak + - name: Ensure auto-generated smocks are up to date + run: yarn smock && git diff --exit-code -- test/smock + - name: Run natspec-smells run: yarn lint:natspec 2>&1 >/dev/null | grep 'No issues found' diff --git a/test/smock/MockBFactory.sol b/test/smock/MockBFactory.sol index ae3f48ff..d8195aa9 100644 --- a/test/smock/MockBFactory.sol +++ b/test/smock/MockBFactory.sol @@ -23,16 +23,16 @@ contract MockBFactory is BFactory, Test { constructor() BFactory() {} - function mock_call_newBPool(IBPool _pool) public { - vm.mockCall(address(this), abi.encodeWithSignature('newBPool()'), abi.encode(_pool)); + function mock_call_newBPool(IBPool bPool) public { + vm.mockCall(address(this), abi.encodeWithSignature('newBPool()'), abi.encode(bPool)); } - function mock_call_setBLabs(address b) public { - vm.mockCall(address(this), abi.encodeWithSignature('setBLabs(address)', b), abi.encode()); + function mock_call_setBLabs(address bLabs) public { + vm.mockCall(address(this), abi.encodeWithSignature('setBLabs(address)', bLabs), abi.encode()); } - function mock_call_collect(IBPool pool) public { - vm.mockCall(address(this), abi.encodeWithSignature('collect(IBPool)', pool), abi.encode()); + function mock_call_collect(IBPool bPool) public { + vm.mockCall(address(this), abi.encodeWithSignature('collect(IBPool)', bPool), abi.encode()); } function mock_call_isBPool(address b, bool _returnParam0) public { @@ -43,18 +43,18 @@ contract MockBFactory is BFactory, Test { vm.mockCall(address(this), abi.encodeWithSignature('getBLabs()'), abi.encode(_returnParam0)); } - function mock_call__newBPool(IBPool _pool) public { - vm.mockCall(address(this), abi.encodeWithSignature('_newBPool()'), abi.encode(_pool)); + function mock_call__newBPool(IBPool bPool) public { + vm.mockCall(address(this), abi.encodeWithSignature('_newBPool()'), abi.encode(bPool)); } - function _newBPool() internal override returns (IBPool _pool) { + function _newBPool() internal override returns (IBPool bPool) { (bool _success, bytes memory _data) = address(this).call(abi.encodeWithSignature('_newBPool()')); if (_success) return abi.decode(_data, (IBPool)); else return super._newBPool(); } - function call__newBPool() public returns (IBPool _pool) { + function call__newBPool() public returns (IBPool bPool) { return _newBPool(); } diff --git a/test/smock/MockBToken.sol b/test/smock/MockBToken.sol index 697d4069..d1a2b3a7 100644 --- a/test/smock/MockBToken.sol +++ b/test/smock/MockBToken.sol @@ -7,54 +7,55 @@ import {Test} from 'forge-std/Test.sol'; contract MockBToken is BToken, Test { constructor() BToken() {} - function mock_call_increaseApproval(address dst, uint256 amt, bool _returnParam0) public { + function mock_call_increaseApproval(address spender, uint256 amount, bool success) public { vm.mockCall( - address(this), abi.encodeWithSignature('increaseApproval(address,uint256)', dst, amt), abi.encode(_returnParam0) + address(this), abi.encodeWithSignature('increaseApproval(address,uint256)', spender, amount), abi.encode(success) ); } - function mock_call_decreaseApproval(address dst, uint256 amt, bool _returnParam0) public { + function mock_call_decreaseApproval(address spender, uint256 amount, bool success) public { vm.mockCall( - address(this), abi.encodeWithSignature('decreaseApproval(address,uint256)', dst, amt), abi.encode(_returnParam0) + address(this), abi.encodeWithSignature('decreaseApproval(address,uint256)', spender, amount), abi.encode(success) ); } - function mock_call__push(address to, uint256 amt) public { - vm.mockCall(address(this), abi.encodeWithSignature('_push(address,uint256)', to, amt), abi.encode()); + function mock_call__push(address to, uint256 amount) public { + vm.mockCall(address(this), abi.encodeWithSignature('_push(address,uint256)', to, amount), abi.encode()); } - function _push(address to, uint256 amt) internal override { - (bool _success, bytes memory _data) = address(this).call(abi.encodeWithSignature('_push(address,uint256)', to, amt)); + function _push(address to, uint256 amount) internal override { + (bool _success, bytes memory _data) = + address(this).call(abi.encodeWithSignature('_push(address,uint256)', to, amount)); if (_success) return abi.decode(_data, ()); - else return super._push(to, amt); + else return super._push(to, amount); } - function call__push(address to, uint256 amt) public { - return _push(to, amt); + function call__push(address to, uint256 amount) public { + return _push(to, amount); } - function expectCall__push(address to, uint256 amt) public { - vm.expectCall(address(this), abi.encodeWithSignature('_push(address,uint256)', to, amt)); + function expectCall__push(address to, uint256 amount) public { + vm.expectCall(address(this), abi.encodeWithSignature('_push(address,uint256)', to, amount)); } - function mock_call__pull(address from, uint256 amt) public { - vm.mockCall(address(this), abi.encodeWithSignature('_pull(address,uint256)', from, amt), abi.encode()); + function mock_call__pull(address from, uint256 amount) public { + vm.mockCall(address(this), abi.encodeWithSignature('_pull(address,uint256)', from, amount), abi.encode()); } - function _pull(address from, uint256 amt) internal override { + function _pull(address from, uint256 amount) internal override { (bool _success, bytes memory _data) = - address(this).call(abi.encodeWithSignature('_pull(address,uint256)', from, amt)); + address(this).call(abi.encodeWithSignature('_pull(address,uint256)', from, amount)); if (_success) return abi.decode(_data, ()); - else return super._pull(from, amt); + else return super._pull(from, amount); } - function call__pull(address from, uint256 amt) public { - return _pull(from, amt); + function call__pull(address from, uint256 amount) public { + return _pull(from, amount); } - function expectCall__pull(address from, uint256 amt) public { - vm.expectCall(address(this), abi.encodeWithSignature('_pull(address,uint256)', from, amt)); + function expectCall__pull(address from, uint256 amount) public { + vm.expectCall(address(this), abi.encodeWithSignature('_pull(address,uint256)', from, amount)); } } From d4043565189c83aa7debe8cf7809fb34a973ed55 Mon Sep 17 00:00:00 2001 From: teddy Date: Mon, 8 Jul 2024 14:42:52 -0300 Subject: [PATCH 17/48] chore: treat compiler warnings as errors, ignoring known ones (#144) --- foundry.toml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/foundry.toml b/foundry.toml index 979d4d49..9fd206be 100644 --- a/foundry.toml +++ b/foundry.toml @@ -14,6 +14,11 @@ libs = ["node_modules", "lib"] optimizer_runs = 500 evm_version = 'cancun' fs_permissions = [{ access = "read-write", path = ".forge-snapshots/"}] +# 2018: function can be view, so far only caused by mocks +# 2394: solc insists on reporting on every transient storage use +# 5574, 3860: bytecode size limit, so far only caused by test contracts +ignored_error_codes = [2018, 2394, 5574, 3860] +deny_warnings = true [profile.optimized] via_ir = true From 5c7d33f5a250d44ad353ebb241d37a18d4be3427 Mon Sep 17 00:00:00 2001 From: teddy Date: Mon, 8 Jul 2024 15:38:42 -0300 Subject: [PATCH 18/48] feat: make the pool's factory public (#143) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: make a pools factory public * chore: use autogenerated mockbpool * fix: nits * fix: gas snapshot --------- Co-authored-by: Weißer Hase Co-authored-by: Weißer Hase --- .forge-snapshots/exitPool.snap | 2 +- .forge-snapshots/joinPool.snap | 2 +- .forge-snapshots/newBFactory.snap | 2 +- .forge-snapshots/newBPool.snap | 2 +- .forge-snapshots/swapExactAmountIn.snap | 2 +- .../swapExactAmountInInverse.snap | 2 +- src/contracts/BCoWPool.sol | 2 +- src/contracts/BPool.sol | 11 +-- src/interfaces/IBPool.sol | 7 ++ test/manual-smock/MockBCoWPool.sol | 5 -- test/{manual-smock => smock}/MockBPool.sol | 71 +++++++++---------- test/unit/BCoWPool.t.sol | 14 ++-- test/unit/BPool.t.sol | 8 +-- test/unit/BPool/BPool.t.sol | 4 +- test/unit/BPool/BPoolBase.sol | 2 +- 15 files changed, 65 insertions(+), 71 deletions(-) rename test/{manual-smock => smock}/MockBPool.sol (84%) diff --git a/.forge-snapshots/exitPool.snap b/.forge-snapshots/exitPool.snap index 1453e021..a39bedae 100644 --- a/.forge-snapshots/exitPool.snap +++ b/.forge-snapshots/exitPool.snap @@ -1 +1 @@ -174721 \ No newline at end of file +174743 \ No newline at end of file diff --git a/.forge-snapshots/joinPool.snap b/.forge-snapshots/joinPool.snap index 020d521b..f586e9ee 100644 --- a/.forge-snapshots/joinPool.snap +++ b/.forge-snapshots/joinPool.snap @@ -1 +1 @@ -138967 \ No newline at end of file +138985 \ No newline at end of file diff --git a/.forge-snapshots/newBFactory.snap b/.forge-snapshots/newBFactory.snap index 353be171..c54b6046 100644 --- a/.forge-snapshots/newBFactory.snap +++ b/.forge-snapshots/newBFactory.snap @@ -1 +1 @@ -4118828 \ No newline at end of file +4130633 \ No newline at end of file diff --git a/.forge-snapshots/newBPool.snap b/.forge-snapshots/newBPool.snap index 3fb1e826..0f67b99b 100644 --- a/.forge-snapshots/newBPool.snap +++ b/.forge-snapshots/newBPool.snap @@ -1 +1 @@ -3467519 \ No newline at end of file +3477592 \ No newline at end of file diff --git a/.forge-snapshots/swapExactAmountIn.snap b/.forge-snapshots/swapExactAmountIn.snap index 47859bf3..6273edbd 100644 --- a/.forge-snapshots/swapExactAmountIn.snap +++ b/.forge-snapshots/swapExactAmountIn.snap @@ -1 +1 @@ -104892 \ No newline at end of file +104914 \ No newline at end of file diff --git a/.forge-snapshots/swapExactAmountInInverse.snap b/.forge-snapshots/swapExactAmountInInverse.snap index 16eb128e..e2f5743c 100644 --- a/.forge-snapshots/swapExactAmountInInverse.snap +++ b/.forge-snapshots/swapExactAmountInInverse.snap @@ -1 +1 @@ -114561 \ No newline at end of file +114583 \ No newline at end of file diff --git a/src/contracts/BCoWPool.sol b/src/contracts/BCoWPool.sol index 1186405b..eb25c441 100644 --- a/src/contracts/BCoWPool.sol +++ b/src/contracts/BCoWPool.sol @@ -146,7 +146,7 @@ contract BCoWPool is IERC1271, IBCoWPool, BPool, BCoWConst { // If this pool was not deployed using a bCoWFactory, this will revert and catch // And the event will be emitted by this contract instead // solhint-disable-next-line no-empty-blocks - try IBCoWFactory(_FACTORY).logBCoWPool() {} + try IBCoWFactory(FACTORY).logBCoWPool() {} catch { emit IBCoWFactory.COWAMMPoolCreated(address(this)); } diff --git a/src/contracts/BPool.sol b/src/contracts/BPool.sol index e92ba123..0d48a6d4 100644 --- a/src/contracts/BPool.sol +++ b/src/contracts/BPool.sol @@ -15,7 +15,8 @@ contract BPool is BToken, BMath, IBPool { using SafeERC20 for IERC20; /// @dev BFactory address to push token exitFee to - address internal immutable _FACTORY; + /// @inheritdoc IBPool + address public immutable FACTORY; /// @dev Has CONTROL role address internal _controller; /// @dev Fee for swapping @@ -81,7 +82,7 @@ contract BPool is BToken, BMath, IBPool { constructor() { _controller = msg.sender; - _FACTORY = msg.sender; + FACTORY = msg.sender; _swapFee = MIN_FEE; _finalized = false; } @@ -205,7 +206,7 @@ contract BPool is BToken, BMath, IBPool { } _pullPoolShare(msg.sender, poolAmountIn); - _pushPoolShare(_FACTORY, exitFee); + _pushPoolShare(FACTORY, exitFee); _burnPoolShare(pAiAfterExitFee); uint256 tokensLength = _tokens.length; @@ -437,7 +438,7 @@ contract BPool is BToken, BMath, IBPool { _pullPoolShare(msg.sender, poolAmountIn); _burnPoolShare(bsub(poolAmountIn, exitFee)); - _pushPoolShare(_FACTORY, exitFee); + _pushPoolShare(FACTORY, exitFee); _pushUnderlying(tokenOut, msg.sender, tokenAmountOut); return tokenAmountOut; @@ -474,7 +475,7 @@ contract BPool is BToken, BMath, IBPool { _pullPoolShare(msg.sender, poolAmountIn); _burnPoolShare(bsub(poolAmountIn, exitFee)); - _pushPoolShare(_FACTORY, exitFee); + _pushPoolShare(FACTORY, exitFee); _pushUnderlying(tokenOut, msg.sender, tokenAmountOut); return poolAmountIn; diff --git a/src/interfaces/IBPool.sol b/src/interfaces/IBPool.sol index 2b3e7a5e..2f5e59f0 100644 --- a/src/interfaces/IBPool.sol +++ b/src/interfaces/IBPool.sol @@ -424,4 +424,11 @@ interface IBPool is IERC20 { * @return controller The controller of the pool */ function getController() external view returns (address controller); + + /** + * @notice Gets the BFactory address that deployed the pool + * @return factory The address of the factory + */ + // solhint-disable-next-line style-guide-casing + function FACTORY() external view returns (address factory); } diff --git a/test/manual-smock/MockBCoWPool.sol b/test/manual-smock/MockBCoWPool.sol index 5df14933..f9023bfa 100644 --- a/test/manual-smock/MockBCoWPool.sol +++ b/test/manual-smock/MockBCoWPool.sol @@ -70,11 +70,6 @@ contract MockBCoWPool is BCoWPool, Test { vm.expectCall(address(this), abi.encodeWithSignature('_getLock()')); } - // NOTE: manually added method - function call__FACTORY() public view returns (address) { - return _FACTORY; - } - function set__controller(address __controller) public { _controller = __controller; } diff --git a/test/manual-smock/MockBPool.sol b/test/smock/MockBPool.sol similarity index 84% rename from test/manual-smock/MockBPool.sol rename to test/smock/MockBPool.sol index bd6d3546..f4514e46 100644 --- a/test/manual-smock/MockBPool.sol +++ b/test/smock/MockBPool.sol @@ -5,11 +5,6 @@ import {BMath, BPool, BToken, IBPool, IERC20, SafeERC20} from '../../src/contrac import {Test} from 'forge-std/Test.sol'; contract MockBPool is BPool, Test { - // NOTE: manually added method - function call__FACTORY() public view returns (address) { - return _FACTORY; - } - function set__controller(address __controller) public { _controller = __controller; } @@ -64,8 +59,8 @@ contract MockBPool is BPool, Test { vm.mockCall(address(this), abi.encodeWithSignature('setSwapFee(uint256)', swapFee), abi.encode()); } - function mock_call_setController(address manager) public { - vm.mockCall(address(this), abi.encodeWithSignature('setController(address)', manager), abi.encode()); + function mock_call_setController(address newController) public { + vm.mockCall(address(this), abi.encodeWithSignature('setController(address)', newController), abi.encode()); } function mock_call_finalize() public { @@ -258,73 +253,73 @@ contract MockBPool is BPool, Test { vm.mockCall(address(this), abi.encodeWithSignature('getController()'), abi.encode(_returnParam0)); } - function mock_call__setLock(bytes32 _value) public { - vm.mockCall(address(this), abi.encodeWithSignature('_setLock(bytes32)', _value), abi.encode()); + function mock_call__setLock(bytes32 value) public { + vm.mockCall(address(this), abi.encodeWithSignature('_setLock(bytes32)', value), abi.encode()); } - function _setLock(bytes32 _value) internal override { - (bool _success, bytes memory _data) = address(this).call(abi.encodeWithSignature('_setLock(bytes32)', _value)); + function _setLock(bytes32 value) internal override { + (bool _success, bytes memory _data) = address(this).call(abi.encodeWithSignature('_setLock(bytes32)', value)); if (_success) return abi.decode(_data, ()); - else return super._setLock(_value); + else return super._setLock(value); } - function call__setLock(bytes32 _value) public { - return _setLock(_value); + function call__setLock(bytes32 value) public { + return _setLock(value); } - function expectCall__setLock(bytes32 _value) public { - vm.expectCall(address(this), abi.encodeWithSignature('_setLock(bytes32)', _value)); + function expectCall__setLock(bytes32 value) public { + vm.expectCall(address(this), abi.encodeWithSignature('_setLock(bytes32)', value)); } - function mock_call__pullUnderlying(address erc20, address from, uint256 amount) public { + function mock_call__pullUnderlying(address token, address from, uint256 amount) public { vm.mockCall( address(this), - abi.encodeWithSignature('_pullUnderlying(address,address,uint256)', erc20, from, amount), + abi.encodeWithSignature('_pullUnderlying(address,address,uint256)', token, from, amount), abi.encode() ); } - function _pullUnderlying(address erc20, address from, uint256 amount) internal override { + function _pullUnderlying(address token, address from, uint256 amount) internal override { (bool _success, bytes memory _data) = - address(this).call(abi.encodeWithSignature('_pullUnderlying(address,address,uint256)', erc20, from, amount)); + address(this).call(abi.encodeWithSignature('_pullUnderlying(address,address,uint256)', token, from, amount)); if (_success) return abi.decode(_data, ()); - else return super._pullUnderlying(erc20, from, amount); + else return super._pullUnderlying(token, from, amount); } - function call__pullUnderlying(address erc20, address from, uint256 amount) public { - return _pullUnderlying(erc20, from, amount); + function call__pullUnderlying(address token, address from, uint256 amount) public { + return _pullUnderlying(token, from, amount); } - function expectCall__pullUnderlying(address erc20, address from, uint256 amount) public { + function expectCall__pullUnderlying(address token, address from, uint256 amount) public { vm.expectCall( - address(this), abi.encodeWithSignature('_pullUnderlying(address,address,uint256)', erc20, from, amount) + address(this), abi.encodeWithSignature('_pullUnderlying(address,address,uint256)', token, from, amount) ); } - function mock_call__pushUnderlying(address erc20, address to, uint256 amount) public { + function mock_call__pushUnderlying(address token, address to, uint256 amount) public { vm.mockCall( address(this), - abi.encodeWithSignature('_pushUnderlying(address,address,uint256)', erc20, to, amount), + abi.encodeWithSignature('_pushUnderlying(address,address,uint256)', token, to, amount), abi.encode() ); } - function _pushUnderlying(address erc20, address to, uint256 amount) internal override { + function _pushUnderlying(address token, address to, uint256 amount) internal override { (bool _success, bytes memory _data) = - address(this).call(abi.encodeWithSignature('_pushUnderlying(address,address,uint256)', erc20, to, amount)); + address(this).call(abi.encodeWithSignature('_pushUnderlying(address,address,uint256)', token, to, amount)); if (_success) return abi.decode(_data, ()); - else return super._pushUnderlying(erc20, to, amount); + else return super._pushUnderlying(token, to, amount); } - function call__pushUnderlying(address erc20, address to, uint256 amount) public { - return _pushUnderlying(erc20, to, amount); + function call__pushUnderlying(address token, address to, uint256 amount) public { + return _pushUnderlying(token, to, amount); } - function expectCall__pushUnderlying(address erc20, address to, uint256 amount) public { - vm.expectCall(address(this), abi.encodeWithSignature('_pushUnderlying(address,address,uint256)', erc20, to, amount)); + function expectCall__pushUnderlying(address token, address to, uint256 amount) public { + vm.expectCall(address(this), abi.encodeWithSignature('_pushUnderlying(address,address,uint256)', token, to, amount)); } function mock_call__afterFinalize() public { @@ -346,18 +341,18 @@ contract MockBPool is BPool, Test { vm.expectCall(address(this), abi.encodeWithSignature('_afterFinalize()')); } - function mock_call__getLock(bytes32 _value) public { - vm.mockCall(address(this), abi.encodeWithSignature('_getLock()'), abi.encode(_value)); + function mock_call__getLock(bytes32 value) public { + vm.mockCall(address(this), abi.encodeWithSignature('_getLock()'), abi.encode(value)); } - function _getLock() internal view override returns (bytes32 _value) { + function _getLock() internal view override returns (bytes32 value) { (bool _success, bytes memory _data) = address(this).staticcall(abi.encodeWithSignature('_getLock()')); if (_success) return abi.decode(_data, (bytes32)); else return super._getLock(); } - function call__getLock() public view returns (bytes32 _value) { + function call__getLock() public returns (bytes32 value) { return _getLock(); } diff --git a/test/unit/BCoWPool.t.sol b/test/unit/BCoWPool.t.sol index aea6167c..7b77420f 100644 --- a/test/unit/BCoWPool.t.sol +++ b/test/unit/BCoWPool.t.sol @@ -11,7 +11,7 @@ import {IBCoWPool} from 'interfaces/IBCoWPool.sol'; import {IBPool} from 'interfaces/IBPool.sol'; import {ISettlement} from 'interfaces/ISettlement.sol'; import {MockBCoWPool} from 'test/manual-smock/MockBCoWPool.sol'; -import {MockBPool} from 'test/manual-smock/MockBPool.sol'; +import {MockBPool} from 'test/smock/MockBPool.sol'; abstract contract BaseCoWPoolTest is BasePoolTest, BCoWConst { address public cowSolutionSettler = makeAddr('cowSolutionSettler'); @@ -86,9 +86,7 @@ contract BCoWPool_Unit_Finalize is BaseCoWPoolTest { vm.mockCall(tokens[i], abi.encodePacked(IERC20.approve.selector), abi.encode(true)); } - vm.mockCall( - address(bCoWPool.call__FACTORY()), abi.encodeWithSelector(IBCoWFactory.logBCoWPool.selector), abi.encode() - ); + vm.mockCall(address(bCoWPool.FACTORY()), abi.encodeWithSelector(IBCoWFactory.logBCoWPool.selector), abi.encode()); } function test_Set_Approvals() public { @@ -100,7 +98,7 @@ contract BCoWPool_Unit_Finalize is BaseCoWPoolTest { function test_Log_IfRevert() public { vm.mockCallRevert( - address(bCoWPool.call__FACTORY()), abi.encodeWithSelector(IBCoWFactory.logBCoWPool.selector), abi.encode() + address(bCoWPool.FACTORY()), abi.encodeWithSelector(IBCoWFactory.logBCoWPool.selector), abi.encode() ); vm.expectEmit(address(bCoWPool)); @@ -110,7 +108,7 @@ contract BCoWPool_Unit_Finalize is BaseCoWPoolTest { } function test_Call_LogBCoWPool() public { - vm.expectCall(address(bCoWPool.call__FACTORY()), abi.encodeWithSelector(IBCoWFactory.logBCoWPool.selector), 1); + vm.expectCall(address(bCoWPool.FACTORY()), abi.encodeWithSelector(IBCoWFactory.logBCoWPool.selector), 1); bCoWPool.finalize(); } } @@ -291,9 +289,7 @@ contract BCoWPool_Unit_IsValidSignature is BaseCoWPoolTest { for (uint256 i = 0; i < TOKENS_AMOUNT; i++) { vm.mockCall(tokens[i], abi.encodePacked(IERC20.approve.selector), abi.encode(true)); } - vm.mockCall( - address(bCoWPool.call__FACTORY()), abi.encodeWithSelector(IBCoWFactory.logBCoWPool.selector), abi.encode() - ); + vm.mockCall(address(bCoWPool.FACTORY()), abi.encodeWithSelector(IBCoWFactory.logBCoWPool.selector), abi.encode()); bCoWPool.finalize(); } diff --git a/test/unit/BPool.t.sol b/test/unit/BPool.t.sol index c5b9341a..cc6629b8 100644 --- a/test/unit/BPool.t.sol +++ b/test/unit/BPool.t.sol @@ -6,7 +6,7 @@ import {SafeERC20} from '@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol import {BPool} from 'contracts/BPool.sol'; import {IBPool} from 'interfaces/IBPool.sol'; -import {MockBPool} from 'test/manual-smock/MockBPool.sol'; +import {MockBPool} from 'test/smock/MockBPool.sol'; import {BConst} from 'contracts/BConst.sol'; import {BMath} from 'contracts/BMath.sol'; @@ -1066,7 +1066,7 @@ contract BPool_Unit_ExitPool is BasePoolTest { } function test_Push_PoolShare(ExitPool_FuzzScenario memory _fuzz) public happyPath(_fuzz) { - address _factoryAddress = bPool.call__FACTORY(); + address _factoryAddress = bPool.FACTORY(); uint256 _exitFee = bmul(_fuzz.poolAmountIn, EXIT_FEE); uint256 _balanceBefore = bPool.balanceOf(_factoryAddress); @@ -2356,7 +2356,7 @@ contract BPool_Unit_ExitswapPoolAmountIn is BasePoolTest { } function test_Push_PoolShare(ExitswapPoolAmountIn_FuzzScenario memory _fuzz) public happyPath(_fuzz) { - address _factoryAddress = bPool.call__FACTORY(); + address _factoryAddress = bPool.FACTORY(); uint256 _balanceBefore = bPool.balanceOf(_factoryAddress); uint256 _exitFee = bmul(_fuzz.poolAmountIn, EXIT_FEE); @@ -2605,7 +2605,7 @@ contract BPool_Unit_ExitswapExternAmountOut is BasePoolTest { } function test_Push_PoolShare(ExitswapExternAmountOut_FuzzScenario memory _fuzz) public happyPath(_fuzz) { - address _factoryAddress = bPool.call__FACTORY(); + address _factoryAddress = bPool.FACTORY(); uint256 _balanceBefore = bPool.balanceOf(_factoryAddress); uint256 _poolAmountIn = calcPoolInGivenSingleOut( _fuzz.tokenOutBalance, diff --git a/test/unit/BPool/BPool.t.sol b/test/unit/BPool/BPool.t.sol index 0c5b8937..b3bebdee 100644 --- a/test/unit/BPool/BPool.t.sol +++ b/test/unit/BPool/BPool.t.sol @@ -3,7 +3,7 @@ pragma solidity 0.8.25; import {BPoolBase} from './BPoolBase.sol'; import {IBPool} from 'interfaces/IBPool.sol'; -import {MockBPool} from 'test/manual-smock/MockBPool.sol'; +import {MockBPool} from 'test/smock/MockBPool.sol'; contract BPool is BPoolBase { function test_ConstructorWhenCalled(address _deployer) external { @@ -13,7 +13,7 @@ contract BPool is BPoolBase { // it sets caller as controller assertEq(_newBPool.call__controller(), _deployer); // it sets caller as factory - assertEq(_newBPool.call__FACTORY(), _deployer); + assertEq(_newBPool.FACTORY(), _deployer); // it sets swap fee to MIN_FEE assertEq(_newBPool.call__swapFee(), MIN_FEE); // it does NOT finalize the pool diff --git a/test/unit/BPool/BPoolBase.sol b/test/unit/BPool/BPoolBase.sol index 1b324c59..30779910 100644 --- a/test/unit/BPool/BPoolBase.sol +++ b/test/unit/BPool/BPoolBase.sol @@ -5,7 +5,7 @@ import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; import {BConst} from 'contracts/BConst.sol'; import {Test} from 'forge-std/Test.sol'; import {IBPool} from 'interfaces/IBPool.sol'; -import {MockBPool} from 'test/manual-smock/MockBPool.sol'; +import {MockBPool} from 'test/smock/MockBPool.sol'; import {Utils} from 'test/utils/Utils.sol'; contract BPoolBase is Test, BConst, Utils { From d986d923c89a2ea29646d8f35cddc8ea0bcac642 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wei=C3=9Fer=20Hase?= Date: Mon, 8 Jul 2024 21:49:14 +0200 Subject: [PATCH 19/48] fix: more informational findings from Certora (#138) * fix: max order duration check * fix: rm SpotPriceAfterBelowMaxPrice error for SpotPriceAboveMaxPrice * fix: test names * fix: gas snapshot --- .forge-snapshots/newBFactory.snap | 2 +- .forge-snapshots/settlementCoWSwap.snap | 2 +- .forge-snapshots/settlementCoWSwapInverse.snap | 2 +- src/contracts/BCoWPool.sol | 2 +- src/contracts/BPool.sol | 4 ++-- src/interfaces/IBPool.sol | 7 +------ test/unit/BCoWPool.t.sol | 2 +- test/unit/BPool.t.sol | 8 ++++---- 8 files changed, 12 insertions(+), 17 deletions(-) diff --git a/.forge-snapshots/newBFactory.snap b/.forge-snapshots/newBFactory.snap index c54b6046..95e984ca 100644 --- a/.forge-snapshots/newBFactory.snap +++ b/.forge-snapshots/newBFactory.snap @@ -1 +1 @@ -4130633 \ No newline at end of file +4130621 \ No newline at end of file diff --git a/.forge-snapshots/settlementCoWSwap.snap b/.forge-snapshots/settlementCoWSwap.snap index b79c8eef..0e09cffc 100644 --- a/.forge-snapshots/settlementCoWSwap.snap +++ b/.forge-snapshots/settlementCoWSwap.snap @@ -1 +1 @@ -215790 \ No newline at end of file +215793 \ No newline at end of file diff --git a/.forge-snapshots/settlementCoWSwapInverse.snap b/.forge-snapshots/settlementCoWSwapInverse.snap index 786e0dc8..a7aae089 100644 --- a/.forge-snapshots/settlementCoWSwapInverse.snap +++ b/.forge-snapshots/settlementCoWSwapInverse.snap @@ -1 +1 @@ -225638 \ No newline at end of file +225641 \ No newline at end of file diff --git a/src/contracts/BCoWPool.sol b/src/contracts/BCoWPool.sol index eb25c441..6ac87bec 100644 --- a/src/contracts/BCoWPool.sol +++ b/src/contracts/BCoWPool.sol @@ -99,7 +99,7 @@ contract BCoWPool is IERC1271, IBCoWPool, BPool, BCoWConst { if (order.receiver != GPv2Order.RECEIVER_SAME_AS_OWNER) { revert BCoWPool_ReceiverIsNotBCoWPool(); } - if (order.validTo >= block.timestamp + MAX_ORDER_DURATION) { + if (order.validTo > block.timestamp + MAX_ORDER_DURATION) { revert BCoWPool_OrderValidityTooLong(); } if (order.feeAmount != 0) { diff --git a/src/contracts/BPool.sol b/src/contracts/BPool.sol index 0d48a6d4..f941e674 100644 --- a/src/contracts/BPool.sol +++ b/src/contracts/BPool.sol @@ -270,7 +270,7 @@ contract BPool is BToken, BMath, IBPool { revert BPool_SpotPriceAfterBelowSpotPriceBefore(); } if (spotPriceAfter > maxPrice) { - revert BPool_SpotPriceAfterBelowMaxPrice(); + revert BPool_SpotPriceAboveMaxPrice(); } if (spotPriceBefore > bdiv(tokenAmountIn, tokenAmountOut)) { revert BPool_SpotPriceBeforeAboveTokenRatio(); @@ -329,7 +329,7 @@ contract BPool is BToken, BMath, IBPool { revert BPool_SpotPriceAfterBelowSpotPriceBefore(); } if (spotPriceAfter > maxPrice) { - revert BPool_SpotPriceAfterBelowMaxPrice(); + revert BPool_SpotPriceAboveMaxPrice(); } if (spotPriceBefore > bdiv(tokenAmountIn, tokenAmountOut)) { revert BPool_SpotPriceBeforeAboveTokenRatio(); diff --git a/src/interfaces/IBPool.sol b/src/interfaces/IBPool.sol index 2f5e59f0..9b18cfa4 100644 --- a/src/interfaces/IBPool.sol +++ b/src/interfaces/IBPool.sol @@ -162,7 +162,7 @@ interface IBPool is IERC20 { error BPool_TokenAmountInAboveMaxRatio(); /** - * @notice Thrown when the spot price before the swap is above the max allowed by the caller + * @notice Thrown when the spot price before or after the swap is above the max allowed by the caller */ error BPool_SpotPriceAboveMaxPrice(); @@ -176,11 +176,6 @@ interface IBPool is IERC20 { */ error BPool_SpotPriceAfterBelowSpotPriceBefore(); - /** - * @notice Thrown when the spot price after the swap is above the max allowed by the caller - */ - error BPool_SpotPriceAfterBelowMaxPrice(); - /** * @notice Thrown when the spot price before the swap is above the ratio between the two tokens in the pool */ diff --git a/test/unit/BCoWPool.t.sol b/test/unit/BCoWPool.t.sol index 7b77420f..7c7e566c 100644 --- a/test/unit/BCoWPool.t.sol +++ b/test/unit/BCoWPool.t.sol @@ -203,7 +203,7 @@ contract BCoWPool_Unit_Verify is BaseCoWPoolTest, SwapExactAmountInUtils { } function test_Revert_LargeDurationOrder(uint256 _timeOffset) public { - _timeOffset = bound(_timeOffset, MAX_ORDER_DURATION, type(uint32).max - block.timestamp); + _timeOffset = bound(_timeOffset, MAX_ORDER_DURATION + 1, type(uint32).max - block.timestamp); GPv2Order.Data memory order = correctOrder; order.validTo = uint32(block.timestamp + _timeOffset); vm.expectRevert(IBCoWPool.BCoWPool_OrderValidityTooLong.selector); diff --git a/test/unit/BPool.t.sol b/test/unit/BPool.t.sol index cc6629b8..52ae592e 100644 --- a/test/unit/BPool.t.sol +++ b/test/unit/BPool.t.sol @@ -1250,7 +1250,7 @@ contract BPool_Unit_SwapExactAmountIn is SwapExactAmountInUtils { // TODO: this revert might be unreachable. Find a way to test it or remove the revert in the code. } - function test_Revert_SpotPriceAfterBelowMaxPrice(SwapExactAmountIn_FuzzScenario memory _fuzz) public happyPath(_fuzz) { + function test_Revert_SpotPriceAfterAboveMaxPrice(SwapExactAmountIn_FuzzScenario memory _fuzz) public happyPath(_fuzz) { uint256 _tokenAmountOut = calcOutGivenIn( _fuzz.tokenInBalance, _fuzz.tokenInDenorm, @@ -1271,7 +1271,7 @@ contract BPool_Unit_SwapExactAmountIn is SwapExactAmountInUtils { ); vm.assume(_spotPriceAfter > _spotPriceBefore); - vm.expectRevert(IBPool.BPool_SpotPriceAfterBelowMaxPrice.selector); + vm.expectRevert(IBPool.BPool_SpotPriceAboveMaxPrice.selector); bPool.swapExactAmountIn(tokenIn, _fuzz.tokenAmountIn, tokenOut, 0, _spotPriceBefore); } @@ -1589,7 +1589,7 @@ contract BPool_Unit_SwapExactAmountOut is BasePoolTest { // TODO: this revert might be unreachable. Find a way to test it or remove the revert in the code. } - function test_Revert_SpotPriceAfterBelowMaxPrice(SwapExactAmountOut_FuzzScenario memory _fuzz) + function test_Revert_SpotPriceAfterAboveMaxPrice(SwapExactAmountOut_FuzzScenario memory _fuzz) public happyPath(_fuzz) { @@ -1613,7 +1613,7 @@ contract BPool_Unit_SwapExactAmountOut is BasePoolTest { ); vm.assume(_spotPriceAfter > _spotPriceBefore); - vm.expectRevert(IBPool.BPool_SpotPriceAfterBelowMaxPrice.selector); + vm.expectRevert(IBPool.BPool_SpotPriceAboveMaxPrice.selector); bPool.swapExactAmountOut(tokenIn, type(uint256).max, tokenOut, _fuzz.tokenAmountOut, _spotPriceBefore); } From 723b529f650b230d6d743057ed27f61c0a05ee2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wei=C3=9Fer=20Hase?= Date: Mon, 8 Jul 2024 21:50:02 +0200 Subject: [PATCH 20/48] fix: fixing nits in input argument names and mocks (#139) * fix: isBPool argument name * fix: updating smock contracts to latest changes * fix: gas snapshot * fix: fmt --- src/contracts/BFactory.sol | 4 +- src/interfaces/IBFactory.sol | 4 +- test/manual-smock/MockBCoWFactory.sol | 10 +- test/manual-smock/MockBCoWPool.sol | 153 +++++++++++++------------- test/manual-smock/MockBNum.sol | 20 ++-- test/smock/MockBFactory.sol | 4 +- 6 files changed, 100 insertions(+), 95 deletions(-) diff --git a/src/contracts/BFactory.sol b/src/contracts/BFactory.sol index ed832c8f..60619bb6 100644 --- a/src/contracts/BFactory.sol +++ b/src/contracts/BFactory.sol @@ -51,8 +51,8 @@ contract BFactory is IBFactory { } /// @inheritdoc IBFactory - function isBPool(address b) external view returns (bool) { - return _isBPool[b]; + function isBPool(address bPool) external view returns (bool) { + return _isBPool[bPool]; } /// @inheritdoc IBFactory diff --git a/src/interfaces/IBFactory.sol b/src/interfaces/IBFactory.sol index b9305031..edaff060 100644 --- a/src/interfaces/IBFactory.sol +++ b/src/interfaces/IBFactory.sol @@ -48,10 +48,10 @@ interface IBFactory { /** * @notice Checks if an address is a BPool created from this factory - * @param b The address to check + * @param bPool The address to check * @return isBPool True if the address is a BPool, False otherwise */ - function isBPool(address b) external view returns (bool isBPool); + function isBPool(address bPool) external view returns (bool isBPool); /** * @notice Gets the BLabs address diff --git a/test/manual-smock/MockBCoWFactory.sol b/test/manual-smock/MockBCoWFactory.sol index 804b37f8..5e7baa5a 100644 --- a/test/manual-smock/MockBCoWFactory.sol +++ b/test/manual-smock/MockBCoWFactory.sol @@ -5,24 +5,24 @@ import {BCoWFactory, BCoWPool, BFactory, IBCoWFactory, IBPool} from '../../src/c import {Test} from 'forge-std/Test.sol'; contract MockBCoWFactory is BCoWFactory, Test { - constructor(address _solutionSettler, bytes32 _appData) BCoWFactory(_solutionSettler, _appData) {} + constructor(address solutionSettler, bytes32 appData) BCoWFactory(solutionSettler, appData) {} function mock_call_logBCoWPool() public { vm.mockCall(address(this), abi.encodeWithSignature('logBCoWPool()'), abi.encode()); } - function mock_call__newBPool(IBPool _pool) public { - vm.mockCall(address(this), abi.encodeWithSignature('_newBPool()'), abi.encode(_pool)); + function mock_call__newBPool(IBPool bCoWPool) public { + vm.mockCall(address(this), abi.encodeWithSignature('_newBPool()'), abi.encode(bCoWPool)); } - function _newBPool() internal override returns (IBPool _pool) { + function _newBPool() internal override returns (IBPool bCoWPool) { (bool _success, bytes memory _data) = address(this).call(abi.encodeWithSignature('_newBPool()')); if (_success) return abi.decode(_data, (IBPool)); else return super._newBPool(); } - function call__newBPool() public returns (IBPool _pool) { + function call__newBPool() public returns (IBPool bCoWPool) { return _newBPool(); } diff --git a/test/manual-smock/MockBCoWPool.sol b/test/manual-smock/MockBCoWPool.sol index f9023bfa..de851269 100644 --- a/test/manual-smock/MockBCoWPool.sol +++ b/test/manual-smock/MockBCoWPool.sol @@ -7,69 +7,41 @@ import {GPv2Order} from '@cowprotocol/libraries/GPv2Order.sol'; import {Test} from 'forge-std/Test.sol'; contract MockBCoWPool is BCoWPool, Test { + // NOTE: manually added method (public overrides not supported in smock) + function verify(GPv2Order.Data memory order) public view override { + (bool _success, bytes memory _data) = + address(this).staticcall(abi.encodeWithSignature('verify(GPv2Order.Data)', order)); + + if (_success) return abi.decode(_data, ()); + else return super.verify(order); + } + + // NOTE: manually added method (public overrides not supported in smock) + function expectCall_verify(GPv2Order.Data memory order) public { + vm.expectCall(address(this), abi.encodeWithSignature('verify(GPv2Order.Data)', order)); + } + /// MockBCoWPool mock methods - constructor(address _cowSolutionSettler, bytes32 _appData) BCoWPool(_cowSolutionSettler, _appData) {} + constructor(address cowSolutionSettler, bytes32 appData) BCoWPool(cowSolutionSettler, appData) {} function mock_call_commit(bytes32 orderHash) public { vm.mockCall(address(this), abi.encodeWithSignature('commit(bytes32)', orderHash), abi.encode()); } - function mock_call_isValidSignature(bytes32 _hash, bytes memory signature, bytes4 _returnParam0) public { + function mock_call_isValidSignature(bytes32 orderHash, bytes memory signature, bytes4 magicValue) public { vm.mockCall( address(this), - abi.encodeWithSignature('isValidSignature(bytes32,bytes)', _hash, signature), - abi.encode(_returnParam0) + abi.encodeWithSignature('isValidSignature(bytes32,bytes)', orderHash, signature), + abi.encode(magicValue) ); } - function mock_call_commitment(bytes32 value) public { - vm.mockCall(address(this), abi.encodeWithSignature('commitment()'), abi.encode(value)); - } - function mock_call_verify(GPv2Order.Data memory order) public { vm.mockCall(address(this), abi.encodeWithSignature('verify(GPv2Order.Data)', order), abi.encode()); } - function mock_call_hash(bytes32 appData, bytes32 _returnParam0) public { - vm.mockCall(address(this), abi.encodeWithSignature('hash(bytes32)', appData), abi.encode(_returnParam0)); - } /// BPool Mocked methods - - function _setLock(bytes32 _value) internal override { - (bool _success, bytes memory _data) = address(this).call(abi.encodeWithSignature('_setLock(bytes32)', _value)); - - if (_success) return abi.decode(_data, ()); - else return super._setLock(_value); - } - - function call__setLock(bytes32 _value) public { - return _setLock(_value); - } - - function expectCall__setLock(bytes32 _value) public { - vm.expectCall(address(this), abi.encodeWithSignature('_setLock(bytes32)', _value)); - } - - function mock_call__getLock(bytes32 _value) public { - vm.mockCall(address(this), abi.encodeWithSignature('_getLock()'), abi.encode(_value)); - } - - function _getLock() internal view override returns (bytes32 _value) { - (bool _success, bytes memory _data) = address(this).staticcall(abi.encodeWithSignature('_getLock()')); - - if (_success) return abi.decode(_data, (bytes32)); - else return super._getLock(); - } - - function call__getLock() public view returns (bytes32 _value) { - return _getLock(); - } - - function expectCall__getLock() public { - vm.expectCall(address(this), abi.encodeWithSignature('_getLock()')); - } - function set__controller(address __controller) public { _controller = __controller; } @@ -122,8 +94,8 @@ contract MockBCoWPool is BCoWPool, Test { vm.mockCall(address(this), abi.encodeWithSignature('setSwapFee(uint256)', swapFee), abi.encode()); } - function mock_call_setController(address manager) public { - vm.mockCall(address(this), abi.encodeWithSignature('setController(address)', manager), abi.encode()); + function mock_call_setController(address newController) public { + vm.mockCall(address(this), abi.encodeWithSignature('setController(address)', newController), abi.encode()); } function mock_call_finalize() public { @@ -316,66 +288,99 @@ contract MockBCoWPool is BCoWPool, Test { vm.mockCall(address(this), abi.encodeWithSignature('getController()'), abi.encode(_returnParam0)); } - function mock_call__pullUnderlying(address erc20, address from, uint256 amount) public { + function mock_call__setLock(bytes32 value) public { + vm.mockCall(address(this), abi.encodeWithSignature('_setLock(bytes32)', value), abi.encode()); + } + + function _setLock(bytes32 value) internal override { + (bool _success, bytes memory _data) = address(this).call(abi.encodeWithSignature('_setLock(bytes32)', value)); + + if (_success) return abi.decode(_data, ()); + else return super._setLock(value); + } + + function call__setLock(bytes32 value) public { + return _setLock(value); + } + + function expectCall__setLock(bytes32 value) public { + vm.expectCall(address(this), abi.encodeWithSignature('_setLock(bytes32)', value)); + } + + function mock_call__pullUnderlying(address token, address from, uint256 amount) public { vm.mockCall( address(this), - abi.encodeWithSignature('_pullUnderlying(address,address,uint256)', erc20, from, amount), + abi.encodeWithSignature('_pullUnderlying(address,address,uint256)', token, from, amount), abi.encode() ); } - function _pullUnderlying(address erc20, address from, uint256 amount) internal override { + function _pullUnderlying(address token, address from, uint256 amount) internal override { (bool _success, bytes memory _data) = - address(this).call(abi.encodeWithSignature('_pullUnderlying(address,address,uint256)', erc20, from, amount)); + address(this).call(abi.encodeWithSignature('_pullUnderlying(address,address,uint256)', token, from, amount)); if (_success) return abi.decode(_data, ()); - else return super._pullUnderlying(erc20, from, amount); + else return super._pullUnderlying(token, from, amount); } - function call__pullUnderlying(address erc20, address from, uint256 amount) public { - return _pullUnderlying(erc20, from, amount); + function call__pullUnderlying(address token, address from, uint256 amount) public { + return _pullUnderlying(token, from, amount); } - function expectCall__pullUnderlying(address erc20, address from, uint256 amount) public { + function expectCall__pullUnderlying(address token, address from, uint256 amount) public { vm.expectCall( - address(this), abi.encodeWithSignature('_pullUnderlying(address,address,uint256)', erc20, from, amount) + address(this), abi.encodeWithSignature('_pullUnderlying(address,address,uint256)', token, from, amount) ); } - function mock_call__pushUnderlying(address erc20, address to, uint256 amount) public { + function mock_call__pushUnderlying(address token, address to, uint256 amount) public { vm.mockCall( address(this), - abi.encodeWithSignature('_pushUnderlying(address,address,uint256)', erc20, to, amount), + abi.encodeWithSignature('_pushUnderlying(address,address,uint256)', token, to, amount), abi.encode() ); } - function _pushUnderlying(address erc20, address to, uint256 amount) internal override { + function _pushUnderlying(address token, address to, uint256 amount) internal override { (bool _success, bytes memory _data) = - address(this).call(abi.encodeWithSignature('_pushUnderlying(address,address,uint256)', erc20, to, amount)); + address(this).call(abi.encodeWithSignature('_pushUnderlying(address,address,uint256)', token, to, amount)); if (_success) return abi.decode(_data, ()); - else return super._pushUnderlying(erc20, to, amount); + else return super._pushUnderlying(token, to, amount); } - function call__pushUnderlying(address erc20, address to, uint256 amount) public { - return _pushUnderlying(erc20, to, amount); + function call__pushUnderlying(address token, address to, uint256 amount) public { + return _pushUnderlying(token, to, amount); } - function expectCall__pushUnderlying(address erc20, address to, uint256 amount) public { - vm.expectCall(address(this), abi.encodeWithSignature('_pushUnderlying(address,address,uint256)', erc20, to, amount)); + function expectCall__pushUnderlying(address token, address to, uint256 amount) public { + vm.expectCall(address(this), abi.encodeWithSignature('_pushUnderlying(address,address,uint256)', token, to, amount)); } - // BCoWPool overrides - function verify(GPv2Order.Data memory order) public view override { - (bool _success, bytes memory _data) = - address(this).staticcall(abi.encodeWithSignature('verify(GPv2Order.Data)', order)); + function call__afterFinalize() public { + return _afterFinalize(); + } - if (_success) return abi.decode(_data, ()); - else return super.verify(order); + function expectCall__afterFinalize() public { + vm.expectCall(address(this), abi.encodeWithSignature('_afterFinalize()')); } - function expectCall_verify(GPv2Order.Data memory order) public { - vm.expectCall(address(this), abi.encodeWithSignature('verify(GPv2Order.Data)', order)); + function mock_call__getLock(bytes32 value) public { + vm.mockCall(address(this), abi.encodeWithSignature('_getLock()'), abi.encode(value)); + } + + function _getLock() internal view override returns (bytes32 value) { + (bool _success, bytes memory _data) = address(this).staticcall(abi.encodeWithSignature('_getLock()')); + + if (_success) return abi.decode(_data, (bytes32)); + else return super._getLock(); + } + + function call__getLock() public returns (bytes32 value) { + return _getLock(); + } + + function expectCall__getLock() public { + vm.expectCall(address(this), abi.encodeWithSignature('_getLock()')); } } diff --git a/test/manual-smock/MockBNum.sol b/test/manual-smock/MockBNum.sol index 259e23a8..4a3ecbab 100644 --- a/test/manual-smock/MockBNum.sol +++ b/test/manual-smock/MockBNum.sol @@ -4,43 +4,43 @@ pragma solidity ^0.8.0; import {BConst, BNum} from '../../src/contracts/BNum.sol'; contract MockBNum is BNum { - function call_btoi(uint256 a) public returns (uint256 _returnParam0) { + function call_btoi(uint256 a) public pure returns (uint256 _returnParam0) { return btoi(a); } - function call_bfloor(uint256 a) public returns (uint256 _returnParam0) { + function call_bfloor(uint256 a) public pure returns (uint256 _returnParam0) { return bfloor(a); } - function call_badd(uint256 a, uint256 b) public returns (uint256 _returnParam0) { + function call_badd(uint256 a, uint256 b) public pure returns (uint256 _returnParam0) { return badd(a, b); } - function call_bsub(uint256 a, uint256 b) public returns (uint256 _returnParam0) { + function call_bsub(uint256 a, uint256 b) public pure returns (uint256 _returnParam0) { return bsub(a, b); } - function call_bsubSign(uint256 a, uint256 b) public returns (uint256 _returnParam0, bool _returnParam1) { + function call_bsubSign(uint256 a, uint256 b) public pure returns (uint256 _returnParam0, bool _returnParam1) { return bsubSign(a, b); } - function call_bmul(uint256 a, uint256 b) public returns (uint256 _returnParam0) { + function call_bmul(uint256 a, uint256 b) public pure returns (uint256 _returnParam0) { return bmul(a, b); } - function call_bdiv(uint256 a, uint256 b) public returns (uint256 _returnParam0) { + function call_bdiv(uint256 a, uint256 b) public pure returns (uint256 _returnParam0) { return bdiv(a, b); } - function call_bpowi(uint256 a, uint256 n) public returns (uint256 _returnParam0) { + function call_bpowi(uint256 a, uint256 n) public pure returns (uint256 _returnParam0) { return bpowi(a, n); } - function call_bpow(uint256 base, uint256 exp) public returns (uint256 _returnParam0) { + function call_bpow(uint256 base, uint256 exp) public pure returns (uint256 _returnParam0) { return bpow(base, exp); } - function call_bpowApprox(uint256 base, uint256 exp, uint256 precision) public returns (uint256 _returnParam0) { + function call_bpowApprox(uint256 base, uint256 exp, uint256 precision) public pure returns (uint256 _returnParam0) { return bpowApprox(base, exp, precision); } } diff --git a/test/smock/MockBFactory.sol b/test/smock/MockBFactory.sol index d8195aa9..7689e20e 100644 --- a/test/smock/MockBFactory.sol +++ b/test/smock/MockBFactory.sol @@ -35,8 +35,8 @@ contract MockBFactory is BFactory, Test { vm.mockCall(address(this), abi.encodeWithSignature('collect(IBPool)', bPool), abi.encode()); } - function mock_call_isBPool(address b, bool _returnParam0) public { - vm.mockCall(address(this), abi.encodeWithSignature('isBPool(address)', b), abi.encode(_returnParam0)); + function mock_call_isBPool(address bPool, bool _returnParam0) public { + vm.mockCall(address(this), abi.encodeWithSignature('isBPool(address)', bPool), abi.encode(_returnParam0)); } function mock_call_getBLabs(address _returnParam0) public { From ce1b925d296ac1bb5dbaf5c688c8592d43f64e36 Mon Sep 17 00:00:00 2001 From: teddy Date: Mon, 8 Jul 2024 16:56:59 -0300 Subject: [PATCH 21/48] test: btt tests for joinPool (#141) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: mock internal bpool methods * chore: move common secondToken up to base contract * test: btt joinPool tests * refactor: move bound token balance out of the base contract * fix: feedback from review * fix: update tree * chore: remove preexisting unit tests replaced by ones in this pr * fix: consistent tree indent * fix: join btt test file (#145) * fix: improving join BTT test file * fix: minor final touches * fix: using _mintPoolShare instead of deal --------- Co-authored-by: Weißer Hase --- .forge-snapshots/newBFactory.snap | 2 +- src/contracts/BPool.sol | 8 +- test/smock/MockBPool.sol | 78 +++++++++++++ test/unit/BPool.t.sol | 162 --------------------------- test/unit/BPool/BPoolBase.sol | 3 +- test/unit/BPool/BPool_Bind.t.sol | 2 + test/unit/BPool/BPool_JoinPool.t.sol | 119 ++++++++++++++++++++ test/unit/BPool/BPool_JoinPool.tree | 21 ++++ test/unit/BPool/BPool_Unbind.t.sol | 5 +- 9 files changed, 229 insertions(+), 171 deletions(-) create mode 100644 test/unit/BPool/BPool_JoinPool.t.sol create mode 100644 test/unit/BPool/BPool_JoinPool.tree diff --git a/.forge-snapshots/newBFactory.snap b/.forge-snapshots/newBFactory.snap index 95e984ca..c54b6046 100644 --- a/.forge-snapshots/newBFactory.snap +++ b/.forge-snapshots/newBFactory.snap @@ -1 +1 @@ -4130621 \ No newline at end of file +4130633 \ No newline at end of file diff --git a/src/contracts/BPool.sol b/src/contracts/BPool.sol index f941e674..2af48596 100644 --- a/src/contracts/BPool.sol +++ b/src/contracts/BPool.sol @@ -634,7 +634,7 @@ contract BPool is BToken, BMath, IBPool { * @param from The address to pull the pool tokens from * @param amount The amount of pool tokens to pull */ - function _pullPoolShare(address from, uint256 amount) internal { + function _pullPoolShare(address from, uint256 amount) internal virtual { _pull(from, amount); } @@ -643,7 +643,7 @@ contract BPool is BToken, BMath, IBPool { * @param to The address to push the pool tokens to * @param amount The amount of pool tokens to push */ - function _pushPoolShare(address to, uint256 amount) internal { + function _pushPoolShare(address to, uint256 amount) internal virtual { _push(to, amount); } @@ -651,7 +651,7 @@ contract BPool is BToken, BMath, IBPool { * @dev Mints an amount of pool tokens. * @param amount The amount of pool tokens to mint */ - function _mintPoolShare(uint256 amount) internal { + function _mintPoolShare(uint256 amount) internal virtual { _mint(address(this), amount); } @@ -659,7 +659,7 @@ contract BPool is BToken, BMath, IBPool { * @dev Burns an amount of pool tokens. * @param amount The amount of pool tokens to burn */ - function _burnPoolShare(uint256 amount) internal { + function _burnPoolShare(uint256 amount) internal virtual { _burn(address(this), amount); } diff --git a/test/smock/MockBPool.sol b/test/smock/MockBPool.sol index f4514e46..a4eb6d08 100644 --- a/test/smock/MockBPool.sol +++ b/test/smock/MockBPool.sol @@ -341,6 +341,84 @@ contract MockBPool is BPool, Test { vm.expectCall(address(this), abi.encodeWithSignature('_afterFinalize()')); } + function mock_call__pullPoolShare(address from, uint256 amount) public { + vm.mockCall(address(this), abi.encodeWithSignature('_pullPoolShare(address,uint256)', from, amount), abi.encode()); + } + + function _pullPoolShare(address from, uint256 amount) internal override { + (bool _success, bytes memory _data) = + address(this).call(abi.encodeWithSignature('_pullPoolShare(address,uint256)', from, amount)); + + if (_success) return abi.decode(_data, ()); + else return super._pullPoolShare(from, amount); + } + + function call__pullPoolShare(address from, uint256 amount) public { + return _pullPoolShare(from, amount); + } + + function expectCall__pullPoolShare(address from, uint256 amount) public { + vm.expectCall(address(this), abi.encodeWithSignature('_pullPoolShare(address,uint256)', from, amount)); + } + + function mock_call__pushPoolShare(address to, uint256 amount) public { + vm.mockCall(address(this), abi.encodeWithSignature('_pushPoolShare(address,uint256)', to, amount), abi.encode()); + } + + function _pushPoolShare(address to, uint256 amount) internal override { + (bool _success, bytes memory _data) = + address(this).call(abi.encodeWithSignature('_pushPoolShare(address,uint256)', to, amount)); + + if (_success) return abi.decode(_data, ()); + else return super._pushPoolShare(to, amount); + } + + function call__pushPoolShare(address to, uint256 amount) public { + return _pushPoolShare(to, amount); + } + + function expectCall__pushPoolShare(address to, uint256 amount) public { + vm.expectCall(address(this), abi.encodeWithSignature('_pushPoolShare(address,uint256)', to, amount)); + } + + function mock_call__mintPoolShare(uint256 amount) public { + vm.mockCall(address(this), abi.encodeWithSignature('_mintPoolShare(uint256)', amount), abi.encode()); + } + + function _mintPoolShare(uint256 amount) internal override { + (bool _success, bytes memory _data) = address(this).call(abi.encodeWithSignature('_mintPoolShare(uint256)', amount)); + + if (_success) return abi.decode(_data, ()); + else return super._mintPoolShare(amount); + } + + function call__mintPoolShare(uint256 amount) public { + return _mintPoolShare(amount); + } + + function expectCall__mintPoolShare(uint256 amount) public { + vm.expectCall(address(this), abi.encodeWithSignature('_mintPoolShare(uint256)', amount)); + } + + function mock_call__burnPoolShare(uint256 amount) public { + vm.mockCall(address(this), abi.encodeWithSignature('_burnPoolShare(uint256)', amount), abi.encode()); + } + + function _burnPoolShare(uint256 amount) internal override { + (bool _success, bytes memory _data) = address(this).call(abi.encodeWithSignature('_burnPoolShare(uint256)', amount)); + + if (_success) return abi.decode(_data, ()); + else return super._burnPoolShare(amount); + } + + function call__burnPoolShare(uint256 amount) public { + return _burnPoolShare(amount); + } + + function expectCall__burnPoolShare(uint256 amount) public { + vm.expectCall(address(this), abi.encodeWithSignature('_burnPoolShare(uint256)', amount)); + } + function mock_call__getLock(bytes32 value) public { vm.mockCall(address(this), abi.encodeWithSignature('_getLock()'), abi.encode(value)); } diff --git a/test/unit/BPool.t.sol b/test/unit/BPool.t.sol index 52ae592e..4611daf6 100644 --- a/test/unit/BPool.t.sol +++ b/test/unit/BPool.t.sol @@ -818,168 +818,6 @@ contract BPool_Unit_GetSpotPriceSansFee is BasePoolTest { } } -contract BPool_Unit_JoinPool is BasePoolTest { - struct JoinPool_FuzzScenario { - uint256 poolAmountOut; - uint256 initPoolSupply; - uint256[TOKENS_AMOUNT] balance; - } - - function _setValues(JoinPool_FuzzScenario memory _fuzz) internal { - // Create mocks - for (uint256 i = 0; i < tokens.length; i++) { - _mockTransfer(tokens[i]); - _mockTransferFrom(tokens[i]); - } - - // Set tokens - _setTokens(_tokensToMemory()); - - // Set balances - for (uint256 i = 0; i < tokens.length; i++) { - _setRecord( - tokens[i], - IBPool.Record({ - bound: true, - index: 0, // NOTE: irrelevant for this method - denorm: 0 // NOTE: irrelevant for this method - }) - ); - _mockPoolBalance(tokens[i], _fuzz.balance[i]); - } - - // Set finalize - _setFinalize(true); - // Set totalSupply - _setTotalSupply(_fuzz.initPoolSupply); - } - - function _assumeHappyPath(JoinPool_FuzzScenario memory _fuzz) internal pure { - _fuzz.initPoolSupply = bound(_fuzz.initPoolSupply, INIT_POOL_SUPPLY, type(uint256).max / BONE); - _fuzz.poolAmountOut = bound(_fuzz.poolAmountOut, _fuzz.initPoolSupply, type(uint256).max / BONE); - vm.assume(_fuzz.poolAmountOut * BONE < type(uint256).max - (_fuzz.initPoolSupply / 2)); - - uint256 _ratio = bdiv(_fuzz.poolAmountOut, _fuzz.initPoolSupply); - uint256 _maxTokenAmountIn = (type(uint256).max / _ratio) - (BONE / 2); - - for (uint256 i = 0; i < _fuzz.balance.length; i++) { - _fuzz.balance[i] = bound(_fuzz.balance[i], MIN_BALANCE, _maxTokenAmountIn); - } - } - - modifier happyPath(JoinPool_FuzzScenario memory _fuzz) { - _assumeHappyPath(_fuzz); - _setValues(_fuzz); - _; - } - - function test_Revert_NotFinalized(JoinPool_FuzzScenario memory _fuzz) public { - _setFinalize(false); - - vm.expectRevert(IBPool.BPool_PoolNotFinalized.selector); - bPool.joinPool(_fuzz.poolAmountOut, _maxArray(tokens.length)); - } - - function test_Revert_InvalidPoolRatio( - JoinPool_FuzzScenario memory _fuzz, - uint256 _poolAmountOut - ) public happyPath(_fuzz) { - _poolAmountOut = bound(_poolAmountOut, 0, (INIT_POOL_SUPPLY / 2 / BONE) - 1); // bdiv rounds up - - vm.expectRevert(IBPool.BPool_InvalidPoolRatio.selector); - bPool.joinPool(_poolAmountOut, _maxArray(tokens.length)); - } - - function test_Revert_InvalidTokenAmountIn(JoinPool_FuzzScenario memory _fuzz, uint256 _tokenIndex) public { - _assumeHappyPath(_fuzz); - _tokenIndex = bound(_tokenIndex, 0, TOKENS_AMOUNT - 1); - _fuzz.balance[_tokenIndex] = 0; - _setValues(_fuzz); - - vm.expectRevert(IBPool.BPool_InvalidTokenAmountIn.selector); - bPool.joinPool(_fuzz.poolAmountOut, _maxArray(tokens.length)); - } - - function test_Revert_TokenAmountInAboveMaxAmountIn( - JoinPool_FuzzScenario memory _fuzz, - uint256 _tokenIndex, - uint256[TOKENS_AMOUNT] memory _maxAmountsIn - ) public happyPath(_fuzz) { - _tokenIndex = bound(_tokenIndex, 0, TOKENS_AMOUNT - 1); - - uint256 _ratio = bdiv(_fuzz.poolAmountOut, _fuzz.initPoolSupply); - for (uint256 i = 0; i < _fuzz.balance.length; i++) { - uint256 _tokenAmountIn = bmul(_ratio, _fuzz.balance[i]); - _maxAmountsIn[i] = _tokenIndex == i ? _tokenAmountIn - 1 : _tokenAmountIn; - } - - vm.expectRevert(IBPool.BPool_TokenAmountInAboveMaxAmountIn.selector); - bPool.joinPool(_fuzz.poolAmountOut, _staticToDynamicUintArray(_maxAmountsIn)); - } - - function test_Revert_Reentrancy(JoinPool_FuzzScenario memory _fuzz) public happyPath(_fuzz) { - _expectRevertByReentrancy(); - bPool.joinPool(_fuzz.poolAmountOut, _maxArray(tokens.length)); - } - - function test_Emit_TokenArrayLogJoin(JoinPool_FuzzScenario memory _fuzz) public happyPath(_fuzz) { - uint256 _poolTotal = _fuzz.initPoolSupply; - uint256 _ratio = bdiv(_fuzz.poolAmountOut, _poolTotal); - - for (uint256 i = 0; i < tokens.length; i++) { - uint256 _bal = _fuzz.balance[i]; - uint256 _tokenAmountIn = bmul(_ratio, _bal); - vm.expectEmit(); - emit IBPool.LOG_JOIN(address(this), tokens[i], _tokenAmountIn); - } - bPool.joinPool(_fuzz.poolAmountOut, _maxArray(tokens.length)); - } - - function test_Set_ReentrancyLock(JoinPool_FuzzScenario memory _fuzz) public happyPath(_fuzz) { - _expectSetReentrancyLock(); - bPool.joinPool(_fuzz.poolAmountOut, _maxArray(tokens.length)); - } - - function test_Pull_TokenArrayTokenAmountIn(JoinPool_FuzzScenario memory _fuzz) public happyPath(_fuzz) { - uint256 _poolTotal = _fuzz.initPoolSupply; - uint256 _ratio = bdiv(_fuzz.poolAmountOut, _poolTotal); - - for (uint256 i = 0; i < tokens.length; i++) { - uint256 _bal = _fuzz.balance[i]; - uint256 _tokenAmountIn = bmul(_ratio, _bal); - vm.expectCall( - address(tokens[i]), - abi.encodeWithSelector(IERC20.transferFrom.selector, address(this), address(bPool), _tokenAmountIn) - ); - } - bPool.joinPool(_fuzz.poolAmountOut, _maxArray(tokens.length)); - } - - function test_Mint_PoolShare(JoinPool_FuzzScenario memory _fuzz) public happyPath(_fuzz) { - bPool.joinPool(_fuzz.poolAmountOut, _maxArray(tokens.length)); - - assertEq(bPool.totalSupply(), _fuzz.initPoolSupply + _fuzz.poolAmountOut); - } - - function test_Push_PoolShare(JoinPool_FuzzScenario memory _fuzz, address _caller) public happyPath(_fuzz) { - assumeNotForgeAddress(_caller); - vm.assume(_caller != address(0)); - - vm.prank(_caller); - bPool.joinPool(_fuzz.poolAmountOut, _maxArray(tokens.length)); - - assertEq(bPool.balanceOf(_caller), _fuzz.poolAmountOut); - } - - function test_Emit_LogCall(JoinPool_FuzzScenario memory _fuzz) public happyPath(_fuzz) { - vm.expectEmit(); - bytes memory _data = abi.encodeWithSelector(BPool.joinPool.selector, _fuzz.poolAmountOut, _maxArray(tokens.length)); - emit IBPool.LOG_CALL(BPool.joinPool.selector, address(this), _data); - - bPool.joinPool(_fuzz.poolAmountOut, _maxArray(tokens.length)); - } -} - contract BPool_Unit_ExitPool is BasePoolTest { struct ExitPool_FuzzScenario { uint256 poolAmountIn; diff --git a/test/unit/BPool/BPoolBase.sol b/test/unit/BPool/BPoolBase.sol index 30779910..41dd6c88 100644 --- a/test/unit/BPool/BPoolBase.sol +++ b/test/unit/BPool/BPoolBase.sol @@ -13,9 +13,9 @@ contract BPoolBase is Test, BConst, Utils { address public deployer = makeAddr('deployer'); address public token = makeAddr('token'); - uint256 public tokenBindBalance = 100e18; uint256 public tokenWeight = 1e18; uint256 public totalWeight = 10e18; + address public secondToken = makeAddr('secondToken'); function setUp() public virtual { vm.prank(deployer); @@ -23,7 +23,6 @@ contract BPoolBase is Test, BConst, Utils { vm.mockCall(token, abi.encodePacked(IERC20.transferFrom.selector), abi.encode()); vm.mockCall(token, abi.encodePacked(IERC20.transfer.selector), abi.encode()); - vm.mockCall(token, abi.encodePacked(IERC20.balanceOf.selector), abi.encode(tokenBindBalance)); } function _setRandomTokens(uint256 _length) internal returns (address[] memory _tokensToAdd) { diff --git a/test/unit/BPool/BPool_Bind.t.sol b/test/unit/BPool/BPool_Bind.t.sol index a1cf7152..95ede7d6 100644 --- a/test/unit/BPool/BPool_Bind.t.sol +++ b/test/unit/BPool/BPool_Bind.t.sol @@ -5,6 +5,8 @@ import {BPoolBase} from './BPoolBase.sol'; import {IBPool} from 'interfaces/IBPool.sol'; contract BPoolBind is BPoolBase { + uint256 public tokenBindBalance = 100e18; + function test_RevertWhen_ReentrancyLockIsSet() external { bPool.call__setLock(_MUTEX_TAKEN); vm.expectRevert(IBPool.BPool_Reentrancy.selector); diff --git a/test/unit/BPool/BPool_JoinPool.t.sol b/test/unit/BPool/BPool_JoinPool.t.sol new file mode 100644 index 00000000..6cc9f9a9 --- /dev/null +++ b/test/unit/BPool/BPool_JoinPool.t.sol @@ -0,0 +1,119 @@ +// 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 BPoolJoinPool is BPoolBase { + // Valid scenario + uint256 public constant SHARE_PROPORTION = 10; + uint256 public poolAmountOut = INIT_POOL_SUPPLY / SHARE_PROPORTION; + uint256 public token0Balance = 10e18; + uint256 public token1Balance = 30e18; + uint256[] maxAmountsIn; + + // when minting n pool shares, enough amount X of every token t should be provided to statisfy + // Xt = n/BPT.totalSupply() * t.balanceOf(BPT) + uint256 public requiredToken0In = token0Balance / SHARE_PROPORTION; + uint256 public requiredToken1In = token1Balance / SHARE_PROPORTION; + + function setUp() public virtual override { + super.setUp(); + bPool.set__finalized(true); + // mint an initial amount of pool shares (expected to happen at _finalize) + bPool.call__mintPoolShare(INIT_POOL_SUPPLY); + address[] memory _tokens = new address[](2); + _tokens[0] = token; + _tokens[1] = secondToken; + bPool.set__tokens(_tokens); + // token weights are not used for all-token joins + _setRecord(token, IBPool.Record({bound: true, index: 0, denorm: 0})); + _setRecord(secondToken, IBPool.Record({bound: true, index: 1, denorm: 0})); + // underlying balances are used instead + vm.mockCall(token, abi.encodePacked(IERC20.balanceOf.selector), abi.encode(uint256(token0Balance))); + vm.mockCall(secondToken, abi.encodePacked(IERC20.balanceOf.selector), abi.encode(uint256(token1Balance))); + + maxAmountsIn = new uint256[](2); + maxAmountsIn[0] = requiredToken0In; + maxAmountsIn[1] = requiredToken1In; + } + + function test_RevertWhen_ReentrancyLockIsSet() external { + bPool.call__setLock(_MUTEX_TAKEN); + // it should revert + vm.expectRevert(IBPool.BPool_Reentrancy.selector); + bPool.joinPool(0, new uint256[](2)); + } + + function test_RevertWhen_PoolIsNotFinalized() external { + bPool.set__finalized(false); + // it should revert + vm.expectRevert(IBPool.BPool_PoolNotFinalized.selector); + bPool.joinPool(0, new uint256[](2)); + } + + // should not happen in the real world since finalization mints 100 tokens + // and sends them to controller + function test_RevertWhen_TotalSupplyIsZero() external { + // natively burn the pool shares initially minted to the pool + bPool.call__burnPoolShare(INIT_POOL_SUPPLY); + // it should revert + // division by zero + vm.expectRevert(BNum.BNum_DivZero.selector); + bPool.joinPool(0, new uint256[](2)); + } + + function test_RevertWhen_PoolAmountOutIsTooSmall(uint256 amountOut) external { + amountOut = bound(amountOut, 0, (INIT_POOL_SUPPLY / 1e18) / 2 - 1); + // it should revert + vm.expectRevert(IBPool.BPool_InvalidPoolRatio.selector); + bPool.joinPool(amountOut, new uint256[](2)); + } + + function test_RevertWhen_BalanceOfPoolInAnyTokenIsZero() external { + vm.mockCall(secondToken, abi.encodePacked(IERC20.balanceOf.selector), abi.encode(uint256(0))); + // it should revert + vm.expectRevert(IBPool.BPool_InvalidTokenAmountIn.selector); + bPool.joinPool(poolAmountOut, maxAmountsIn); + } + + function test_RevertWhen_RequiredAmountOfATokenIsMoreThanMaxAmountsIn() external { + maxAmountsIn[0] -= 1; + // it should revert + vm.expectRevert(IBPool.BPool_TokenAmountInAboveMaxAmountIn.selector); + bPool.joinPool(poolAmountOut, maxAmountsIn); + } + + function test_WhenPreconditionsAreMet() external { + // it sets reentrancy lock + bPool.expectCall__setLock(_MUTEX_TAKEN); + // it calls _pullUnderlying for every token + bPool.mock_call__pullUnderlying(token, address(this), requiredToken0In); + bPool.expectCall__pullUnderlying(token, address(this), requiredToken0In); + bPool.mock_call__pullUnderlying(secondToken, address(this), requiredToken1In); + bPool.expectCall__pullUnderlying(secondToken, address(this), requiredToken1In); + // it mints the pool shares + bPool.expectCall__mintPoolShare(poolAmountOut); + // it sends pool shares to caller + bPool.expectCall__pushPoolShare(address(this), poolAmountOut); + uint256[] memory maxAmounts = new uint256[](2); + maxAmounts[0] = requiredToken0In; + maxAmounts[1] = requiredToken1In; + + // it emits LOG_CALL event + bytes memory _data = abi.encodeWithSelector(IBPool.joinPool.selector, poolAmountOut, maxAmounts); + vm.expectEmit(); + emit IBPool.LOG_CALL(IBPool.joinPool.selector, address(this), _data); + // it emits LOG_JOIN event for every token + vm.expectEmit(); + emit IBPool.LOG_JOIN(address(this), token, requiredToken0In); + vm.expectEmit(); + emit IBPool.LOG_JOIN(address(this), secondToken, requiredToken1In); + bPool.joinPool(poolAmountOut, maxAmounts); + // it clears the reentrancy lock + assertEq(_MUTEX_FREE, bPool.call__getLock()); + } +} diff --git a/test/unit/BPool/BPool_JoinPool.tree b/test/unit/BPool/BPool_JoinPool.tree new file mode 100644 index 00000000..5fbbc9e3 --- /dev/null +++ b/test/unit/BPool/BPool_JoinPool.tree @@ -0,0 +1,21 @@ +BPool::JoinPool +├── when reentrancy lock is set +│ └── it should revert +├── when pool is not finalized +│ └── it should revert +├── when total supply is zero +│ └── it should revert // division by zero +├── when pool amount out is too small +│ └── it should revert +├── when balance of pool in any token is zero +│ └── it should revert +├── when required amount of a token is more than maxAmountsIn +│ └── it should revert +└── when preconditions are met + ├── it emits LOG_CALL event + ├── it sets the reentrancy lock + ├── it emits LOG_JOIN event for every token + ├── it calls _pullUnderlying for every token + ├── it mints the pool shares + ├── it sends pool shares to caller + └── it clears the reentrancy lock diff --git a/test/unit/BPool/BPool_Unbind.t.sol b/test/unit/BPool/BPool_Unbind.t.sol index 0695dde9..f854c0c7 100644 --- a/test/unit/BPool/BPool_Unbind.t.sol +++ b/test/unit/BPool/BPool_Unbind.t.sol @@ -7,10 +7,11 @@ import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; import {IBPool} from 'interfaces/IBPool.sol'; contract BPoolUnbind is BPoolBase { - address public secondToken = makeAddr('secondToken'); + uint256 public boundTokenAmount = 100e18; function setUp() public virtual override { super.setUp(); + vm.mockCall(token, abi.encodePacked(IERC20.balanceOf.selector), abi.encode(boundTokenAmount)); vm.mockCall(secondToken, abi.encodePacked(IERC20.transferFrom.selector), abi.encode()); } @@ -61,7 +62,7 @@ contract BPoolUnbind is BPoolBase { // it sets the reentrancy lock bPool.expectCall__setLock(_MUTEX_TAKEN); // it calls _pushUnderlying - bPool.expectCall__pushUnderlying(token, deployer, tokenBindBalance); + bPool.expectCall__pushUnderlying(token, deployer, boundTokenAmount); // it emits LOG_CALL event vm.expectEmit(); From bbc79ca2c67d6417ff5344876a4bdc22e1b347bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wei=C3=9Fer=20Hase?= Date: Tue, 9 Jul 2024 16:45:38 +0200 Subject: [PATCH 22/48] fix: replace bLabs for bDao (#147) --- .forge-snapshots/newBFactory.snap | 2 +- script/DeployBCoWFactory.s.sol | 2 +- script/DeployBFactory.s.sol | 2 +- script/Params.s.sol | 4 +-- src/contracts/BFactory.sol | 28 +++++++++--------- src/interfaces/IBFactory.sol | 26 ++++++++--------- test/smock/MockBFactory.sol | 16 +++++------ test/unit/BCoWFactory.t.sol | 8 +++--- test/unit/BCoWFactory.tree | 2 +- test/unit/BFactory.t.sol | 47 +++++++++++++++---------------- test/unit/BFactory.tree | 18 ++++++------ 11 files changed, 76 insertions(+), 79 deletions(-) diff --git a/.forge-snapshots/newBFactory.snap b/.forge-snapshots/newBFactory.snap index c54b6046..95e984ca 100644 --- a/.forge-snapshots/newBFactory.snap +++ b/.forge-snapshots/newBFactory.snap @@ -1 +1 @@ -4130633 \ No newline at end of file +4130621 \ No newline at end of file diff --git a/script/DeployBCoWFactory.s.sol b/script/DeployBCoWFactory.s.sol index a2abfbdd..57905392 100644 --- a/script/DeployBCoWFactory.s.sol +++ b/script/DeployBCoWFactory.s.sol @@ -11,7 +11,7 @@ contract DeployBCoWFactory is Script, Params { vm.startBroadcast(); BCoWFactory bCoWFactory = new BCoWFactory(params.settlement, params.appData); - bCoWFactory.setBLabs(params.bLabs); + bCoWFactory.setBDao(params.bDao); vm.stopBroadcast(); } } diff --git a/script/DeployBFactory.s.sol b/script/DeployBFactory.s.sol index 5d0c33bc..1d7a0157 100644 --- a/script/DeployBFactory.s.sol +++ b/script/DeployBFactory.s.sol @@ -11,7 +11,7 @@ contract DeployBFactory is Script, Params { vm.startBroadcast(); BFactory bFactory = new BFactory(); - bFactory.setBLabs(params.bLabs); + bFactory.setBDao(params.bDao); vm.stopBroadcast(); } } diff --git a/script/Params.s.sol b/script/Params.s.sol index b2f0eeab..9df9a8f0 100644 --- a/script/Params.s.sol +++ b/script/Params.s.sol @@ -3,11 +3,11 @@ pragma solidity 0.8.25; contract Params { struct BFactoryDeploymentParams { - address bLabs; + address bDao; } struct BCoWFactoryDeploymentParams { - address bLabs; + address bDao; address settlement; bytes32 appData; } diff --git a/src/contracts/BFactory.sol b/src/contracts/BFactory.sol index 60619bb6..19450439 100644 --- a/src/contracts/BFactory.sol +++ b/src/contracts/BFactory.sol @@ -13,11 +13,11 @@ import {IBPool} from 'interfaces/IBPool.sol'; contract BFactory is IBFactory { /// @dev Mapping indicating whether the address is a BPool. mapping(address => bool) internal _isBPool; - /// @dev bLabs address. - address internal _bLabs; + /// @dev bDao address. + address internal _bDao; constructor() { - _bLabs = msg.sender; + _bDao = msg.sender; } /// @inheritdoc IBFactory @@ -29,25 +29,25 @@ contract BFactory is IBFactory { } /// @inheritdoc IBFactory - function setBLabs(address bLabs) external { - if (bLabs == address(0)) { + function setBDao(address bDao) external { + if (bDao == address(0)) { revert BFactory_AddressZero(); } - if (msg.sender != _bLabs) { - revert BFactory_NotBLabs(); + if (msg.sender != _bDao) { + revert BFactory_NotBDao(); } - emit LOG_BLABS(msg.sender, bLabs); - _bLabs = bLabs; + emit LOG_BDAO(msg.sender, bDao); + _bDao = bDao; } /// @inheritdoc IBFactory function collect(IBPool bPool) external { - if (msg.sender != _bLabs) { - revert BFactory_NotBLabs(); + if (msg.sender != _bDao) { + revert BFactory_NotBDao(); } uint256 collected = bPool.balanceOf(address(this)); - SafeERC20.safeTransfer(bPool, _bLabs, collected); + SafeERC20.safeTransfer(bPool, _bDao, collected); } /// @inheritdoc IBFactory @@ -56,8 +56,8 @@ contract BFactory is IBFactory { } /// @inheritdoc IBFactory - function getBLabs() external view returns (address) { - return _bLabs; + function getBDao() external view returns (address) { + return _bDao; } /** diff --git a/src/interfaces/IBFactory.sol b/src/interfaces/IBFactory.sol index edaff060..672ba7ca 100644 --- a/src/interfaces/IBFactory.sol +++ b/src/interfaces/IBFactory.sol @@ -12,11 +12,11 @@ interface IBFactory { event LOG_NEW_POOL(address indexed caller, address indexed bPool); /** - * @notice Emitted when setting the BLabs address - * @param caller The caller of the set BLabs function - * @param bLabs The address of the new BLabs + * @notice Emitted when setting the BDao address + * @param caller The caller of the set BDao function + * @param bDao The address of the new BDao */ - event LOG_BLABS(address indexed caller, address indexed bLabs); + event LOG_BDAO(address indexed caller, address indexed bDao); /** * @notice Thrown when setting a variable to address zero @@ -24,9 +24,9 @@ interface IBFactory { error BFactory_AddressZero(); /** - * @notice Thrown when caller is not BLabs address + * @notice Thrown when caller is not BDao address */ - error BFactory_NotBLabs(); + error BFactory_NotBDao(); /** * @notice Creates a new BPool, assigning the caller as the pool controller @@ -35,13 +35,13 @@ interface IBFactory { function newBPool() external returns (IBPool bPool); /** - * @notice Sets the BLabs address in the factory - * @param bLabs The new BLabs address + * @notice Sets the BDao address in the factory + * @param bDao The new BDao address */ - function setBLabs(address bLabs) external; + function setBDao(address bDao) external; /** - * @notice Collects the fees of a pool and transfers it to BLabs address + * @notice Collects the fees of a pool and transfers it to BDao address * @param bPool The address of the pool to collect fees from */ function collect(IBPool bPool) external; @@ -54,8 +54,8 @@ interface IBFactory { function isBPool(address bPool) external view returns (bool isBPool); /** - * @notice Gets the BLabs address - * @return bLabs The address of the BLabs + * @notice Gets the BDao address + * @return bDao The address of the BDao */ - function getBLabs() external view returns (address bLabs); + function getBDao() external view returns (address bDao); } diff --git a/test/smock/MockBFactory.sol b/test/smock/MockBFactory.sol index 7689e20e..ab754fa5 100644 --- a/test/smock/MockBFactory.sol +++ b/test/smock/MockBFactory.sol @@ -13,12 +13,12 @@ contract MockBFactory is BFactory, Test { return _isBPool[_key0]; } - function set__bLabs(address __bLabs) public { - _bLabs = __bLabs; + function set__bDao(address __bDao) public { + _bDao = __bDao; } - function call__bLabs() public view returns (address) { - return _bLabs; + function call__bDao() public view returns (address) { + return _bDao; } constructor() BFactory() {} @@ -27,8 +27,8 @@ contract MockBFactory is BFactory, Test { vm.mockCall(address(this), abi.encodeWithSignature('newBPool()'), abi.encode(bPool)); } - function mock_call_setBLabs(address bLabs) public { - vm.mockCall(address(this), abi.encodeWithSignature('setBLabs(address)', bLabs), abi.encode()); + function mock_call_setBDao(address bDao) public { + vm.mockCall(address(this), abi.encodeWithSignature('setBDao(address)', bDao), abi.encode()); } function mock_call_collect(IBPool bPool) public { @@ -39,8 +39,8 @@ contract MockBFactory is BFactory, Test { vm.mockCall(address(this), abi.encodeWithSignature('isBPool(address)', bPool), abi.encode(_returnParam0)); } - function mock_call_getBLabs(address _returnParam0) public { - vm.mockCall(address(this), abi.encodeWithSignature('getBLabs()'), abi.encode(_returnParam0)); + function mock_call_getBDao(address _returnParam0) public { + vm.mockCall(address(this), abi.encodeWithSignature('getBDao()'), abi.encode(_returnParam0)); } function mock_call__newBPool(IBPool bPool) public { diff --git a/test/unit/BCoWFactory.t.sol b/test/unit/BCoWFactory.t.sol index 1ff8070a..9c2bfca2 100644 --- a/test/unit/BCoWFactory.t.sol +++ b/test/unit/BCoWFactory.t.sol @@ -24,15 +24,15 @@ contract BCoWFactoryTest is Test { ); } - function test_ConstructorWhenCalled(address _blabs, address _newSettler, bytes32 _appData) external { - vm.prank(_blabs); + function test_ConstructorWhenCalled(address _bDao, address _newSettler, bytes32 _appData) external { + vm.prank(_bDao); MockBCoWFactory _newFactory = new MockBCoWFactory(_newSettler, _appData); // it should set solution settler assertEq(_newFactory.SOLUTION_SETTLER(), _newSettler); // it should set app data assertEq(_newFactory.APP_DATA(), _appData); - // it should set blabs - assertEq(_newFactory.getBLabs(), _blabs); + // it should set BDao + assertEq(_newFactory.getBDao(), _bDao); } function test__newBPoolWhenCalled() external { diff --git a/test/unit/BCoWFactory.tree b/test/unit/BCoWFactory.tree index 37160019..daa4549a 100644 --- a/test/unit/BCoWFactory.tree +++ b/test/unit/BCoWFactory.tree @@ -2,7 +2,7 @@ BCoWFactoryTest::constructor └── when called ├── it should set solution settler ├── it should set app data - └── it should set blabs + └── it should set BDao BCoWFactoryTest::_newBPool └── when called diff --git a/test/unit/BFactory.t.sol b/test/unit/BFactory.t.sol index b1426432..24e0ed78 100644 --- a/test/unit/BFactory.t.sol +++ b/test/unit/BFactory.t.sol @@ -21,11 +21,11 @@ contract BFactoryTest is Test { factory = new MockBFactory(); } - function test_ConstructorWhenCalled(address _blabs) external { - vm.prank(_blabs); + function test_ConstructorWhenCalled(address _bDao) external { + vm.prank(_bDao); MockBFactory newFactory = new MockBFactory(); - // it should set BLabs - assertEq(newFactory.getBLabs(), _blabs); + // it should set BDao + assertEq(newFactory.getBDao(), _bDao); } function test_NewBPoolWhenCalled(address _deployer, address _newBPool) external { @@ -60,55 +60,52 @@ contract BFactoryTest is Test { assertEq(_newBPool.code, _expectedCode); } - function test_SetBLabsRevertWhen_TheSenderIsNotTheCurrentBLabs(address _caller) external { + function test_SetBDaoRevertWhen_TheSenderIsNotTheCurrentBDao(address _caller) external { vm.assume(_caller != factoryDeployer); // it should revert - vm.expectRevert(IBFactory.BFactory_NotBLabs.selector); + vm.expectRevert(IBFactory.BFactory_NotBDao.selector); vm.prank(_caller); - factory.setBLabs(makeAddr('newBLabs')); + factory.setBDao(makeAddr('newBDao')); } - modifier whenTheSenderIsTheCurrentBLabs() { + modifier whenTheSenderIsTheCurrentBDao() { vm.startPrank(factoryDeployer); _; } - function test_SetBLabsRevertWhen_TheAddressIsZero() external whenTheSenderIsTheCurrentBLabs { + function test_SetBDaoRevertWhen_TheAddressIsZero() external whenTheSenderIsTheCurrentBDao { // it should revert vm.expectRevert(IBFactory.BFactory_AddressZero.selector); - factory.setBLabs(address(0)); + factory.setBDao(address(0)); } - function test_SetBLabsWhenTheAddressIsNotZero(address _newBLabs) external whenTheSenderIsTheCurrentBLabs { - vm.assume(_newBLabs != address(0)); + function test_SetBDaoWhenTheAddressIsNotZero(address _newBDao) external whenTheSenderIsTheCurrentBDao { + vm.assume(_newBDao != address(0)); - // it should emit a BLabsSet event + // it should emit a BDaoSet event vm.expectEmit(address(factory)); - emit IBFactory.LOG_BLABS(factoryDeployer, _newBLabs); + emit IBFactory.LOG_BDAO(factoryDeployer, _newBDao); - factory.setBLabs(_newBLabs); + factory.setBDao(_newBDao); - // it should set the new bLabs address - assertEq(factory.getBLabs(), _newBLabs); + // it should set the new bDao address + assertEq(factory.getBDao(), _newBDao); } - function test_CollectRevertWhen_TheSenderIsNotTheCurrentBLabs(address _caller) external { + function test_CollectRevertWhen_TheSenderIsNotTheCurrentBDao(address _caller) external { vm.assume(_caller != factoryDeployer); // it should revert - vm.expectRevert(IBFactory.BFactory_NotBLabs.selector); + vm.expectRevert(IBFactory.BFactory_NotBDao.selector); vm.prank(_caller); factory.collect(IBPool(makeAddr('pool'))); } - function test_CollectWhenTheSenderIsTheCurrentBLabs(uint256 _factoryBTBalance) - external - whenTheSenderIsTheCurrentBLabs - { + function test_CollectWhenTheSenderIsTheCurrentBDao(uint256 _factoryBTBalance) external whenTheSenderIsTheCurrentBDao { address _mockPool = makeAddr('pool'); vm.mockCall(_mockPool, abi.encodeCall(IERC20.balanceOf, address(factory)), abi.encode(_factoryBTBalance)); vm.mockCall(_mockPool, abi.encodeCall(IERC20.transfer, (factoryDeployer, _factoryBTBalance)), abi.encode(true)); @@ -116,7 +113,7 @@ contract BFactoryTest is Test { // it should get the pool's btoken balance of the factory vm.expectCall(_mockPool, abi.encodeCall(IERC20.balanceOf, address(factory))); - // it should transfer the btoken balance of the factory to BLabs + // it should transfer the btoken balance of the factory to BDao vm.expectCall(_mockPool, abi.encodeCall(IERC20.transfer, (factoryDeployer, _factoryBTBalance))); factory.collect(IBPool(_mockPool)); @@ -124,7 +121,7 @@ contract BFactoryTest is Test { function test_CollectRevertWhen_TheBtokenTransferFails(uint256 _factoryBTBalance) external - whenTheSenderIsTheCurrentBLabs + whenTheSenderIsTheCurrentBDao { address _mockPool = makeAddr('pool'); vm.mockCall(_mockPool, abi.encodeCall(IERC20.balanceOf, address(factory)), abi.encode(_factoryBTBalance)); diff --git a/test/unit/BFactory.tree b/test/unit/BFactory.tree index bd2dce21..02f1fd16 100644 --- a/test/unit/BFactory.tree +++ b/test/unit/BFactory.tree @@ -1,6 +1,6 @@ BFactoryTest::constructor └── when called - └── it should set the deployer as BLabs + └── it should set the deployer as BDao BFactoryTest::newBPool └── when called @@ -14,22 +14,22 @@ BFactoryTest::_newBPool └── when called └── it should deploy a new BPool -BFactoryTest::setBLabs -├── when the sender is not the current BLabs +BFactoryTest::setBDao +├── when the sender is not the current BDao │ └── it should revert -└── when the sender is the current BLabs +└── when the sender is the current BDao ├── when the address is zero │ └── it should revert └── when the address is not zero - ├── it should set the new BLabs address - └── it should emit a BLabsSet event + ├── it should set the new BDao address + └── it should emit a BDaoSet event BFactoryTest::collect -├── when the sender is not the current BLabs +├── when the sender is not the current BDao │ └── it should revert -└── when the sender is the current BLabs +└── when the sender is the current BDao ├── it should get the pool's btoken balance of the factory - ├── it should transfer the btoken balance of the factory to BLabs + ├── it should transfer the btoken balance of the factory to BDao └── when the btoken transfer fails └── it should revert From c11c3117ddd127b12273bdc313ae21e7e7ed07fd Mon Sep 17 00:00:00 2001 From: teddy Date: Thu, 11 Jul 2024 05:42:38 -0300 Subject: [PATCH 23/48] fix: run all unit tests for coverage report (#152) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 04721367..836757e0 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "scripts": { "build": "forge build", "build:optimized": "FOUNDRY_PROFILE=optimized forge build", - "coverage": "forge coverage --match-contract Unit", + "coverage": "forge coverage --match-path 'test/unit/**'", "deploy:bcowfactory:mainnet": "bash -c 'source .env && forge script DeployBCoWFactory -vvvvv --rpc-url $MAINNET_RPC --broadcast --chain mainnet --private-key $MAINNET_DEPLOYER_PK --verify --etherscan-api-key $ETHERSCAN_API_KEY'", "deploy:bcowfactory:testnet": "bash -c 'source .env && forge script DeployBCoWFactory -vvvvv --rpc-url $SEPOLIA_RPC --broadcast --chain sepolia --private-key $SEPOLIA_DEPLOYER_PK --verify --etherscan-api-key $ETHERSCAN_API_KEY'", "deploy:bfactory:mainnet": "bash -c 'source .env && forge script DeployBFactory -vvvvv --rpc-url $MAINNET_RPC --broadcast --chain mainnet --private-key $MAINNET_DEPLOYER_PK --verify --etherscan-api-key $ETHERSCAN_API_KEY'", From b795bfb84fa7986d4846b9ce25c34693d05a7177 Mon Sep 17 00:00:00 2001 From: teddy Date: Thu, 11 Jul 2024 06:13:21 -0300 Subject: [PATCH 24/48] test: btt bpool exitPool (#148) * test: btt tests for exitPool * chore: delete preexisting tests * refactor: consistently use tokens array for tree tests * test: assume behaviour of _pullPoolShare correct * test: exitFee behaviour * test: small fixes from feedback * fix: only mock transfers where required --- test/unit/BPool.t.sol | 193 +-------------------------- test/unit/BPool/BPoolBase.sol | 8 +- test/unit/BPool/BPool_Bind.t.sol | 42 +++--- test/unit/BPool/BPool_ExitPool.t.sol | 120 +++++++++++++++++ test/unit/BPool/BPool_ExitPool.tree | 22 +++ test/unit/BPool/BPool_JoinPool.t.sol | 27 ++-- test/unit/BPool/BPool_Unbind.t.sol | 43 +++--- test/utils/Utils.sol | 2 +- 8 files changed, 204 insertions(+), 253 deletions(-) create mode 100644 test/unit/BPool/BPool_ExitPool.t.sol create mode 100644 test/unit/BPool/BPool_ExitPool.tree diff --git a/test/unit/BPool.t.sol b/test/unit/BPool.t.sol index 4611daf6..08ace5cb 100644 --- a/test/unit/BPool.t.sol +++ b/test/unit/BPool.t.sol @@ -26,8 +26,8 @@ abstract contract BasePoolTest is Test, BConst, Utils, BMath { // Create fake tokens address[] memory _tokensToAdd = _getDeterministicTokenArray(TOKENS_AMOUNT); - for (uint256 i = 0; i < tokens.length; i++) { - tokens[i] = _tokensToAdd[i]; + for (uint256 i = 0; i < _tokensToAdd.length; i++) { + tokens.push(_tokensToAdd[i]); } } @@ -818,195 +818,6 @@ contract BPool_Unit_GetSpotPriceSansFee is BasePoolTest { } } -contract BPool_Unit_ExitPool is BasePoolTest { - struct ExitPool_FuzzScenario { - uint256 poolAmountIn; - uint256 initPoolSupply; - uint256[TOKENS_AMOUNT] balance; - } - - function _setValues(ExitPool_FuzzScenario memory _fuzz) internal { - // Create mocks - for (uint256 i = 0; i < tokens.length; i++) { - _mockTransfer(tokens[i]); - } - - // Set tokens - _setTokens(_tokensToMemory()); - - // Set balances - for (uint256 i = 0; i < tokens.length; i++) { - _setRecord( - tokens[i], - IBPool.Record({ - bound: true, - index: 0, // NOTE: irrelevant for this method - denorm: 0 // NOTE: irrelevant for this method - }) - ); - _mockPoolBalance(tokens[i], _fuzz.balance[i]); - } - - // Set LP token balance - _setPoolBalance(address(this), _fuzz.poolAmountIn); // give LP tokens to fn caller - // Set totalSupply - _setTotalSupply(_fuzz.initPoolSupply - _fuzz.poolAmountIn); - // Set finalize - _setFinalize(true); - } - - function _assumeHappyPath(ExitPool_FuzzScenario memory _fuzz) internal pure { - uint256 _maxInitSupply = type(uint256).max / BONE; - _fuzz.initPoolSupply = bound(_fuzz.initPoolSupply, INIT_POOL_SUPPLY, _maxInitSupply); - - uint256 _poolAmountInAfterFee = _fuzz.poolAmountIn - (_fuzz.poolAmountIn * EXIT_FEE); - vm.assume(_poolAmountInAfterFee <= _fuzz.initPoolSupply); - vm.assume(_poolAmountInAfterFee * BONE > _fuzz.initPoolSupply); - vm.assume(_poolAmountInAfterFee * BONE < type(uint256).max - (_fuzz.initPoolSupply / 2)); - - uint256 _ratio = bdiv(_poolAmountInAfterFee, _fuzz.initPoolSupply); - uint256 _maxBalance = type(uint256).max / (_ratio * BONE); - - for (uint256 i = 0; i < _fuzz.balance.length; i++) { - _fuzz.balance[i] = bound(_fuzz.balance[i], BONE, _maxBalance); - } - } - - modifier happyPath(ExitPool_FuzzScenario memory _fuzz) { - _assumeHappyPath(_fuzz); - _setValues(_fuzz); - _; - } - - function test_Revert_NotFinalized(ExitPool_FuzzScenario memory _fuzz) public happyPath(_fuzz) { - _setFinalize(false); - - vm.expectRevert(IBPool.BPool_PoolNotFinalized.selector); - bPool.exitPool(_fuzz.poolAmountIn, _zeroArray(tokens.length)); - } - - function test_Revert_InvalidPoolRatio( - ExitPool_FuzzScenario memory _fuzz, - uint256 _poolAmountIn - ) public happyPath(_fuzz) { - _poolAmountIn = bound(_poolAmountIn, 0, (INIT_POOL_SUPPLY / 2 / BONE) - 1); // bdiv rounds up - - vm.expectRevert(IBPool.BPool_InvalidPoolRatio.selector); - bPool.exitPool(_poolAmountIn, _zeroArray(tokens.length)); - } - - function test_Pull_PoolShare(ExitPool_FuzzScenario memory _fuzz) public happyPath(_fuzz) { - assertEq(bPool.balanceOf(address(this)), _fuzz.poolAmountIn); - - bPool.exitPool(_fuzz.poolAmountIn, _zeroArray(tokens.length)); - - assertEq(bPool.balanceOf(address(this)), 0); - } - - function test_Push_PoolShare(ExitPool_FuzzScenario memory _fuzz) public happyPath(_fuzz) { - address _factoryAddress = bPool.FACTORY(); - uint256 _exitFee = bmul(_fuzz.poolAmountIn, EXIT_FEE); - uint256 _balanceBefore = bPool.balanceOf(_factoryAddress); - - bPool.exitPool(_fuzz.poolAmountIn, _zeroArray(tokens.length)); - - assertEq(bPool.balanceOf(_factoryAddress), _balanceBefore - _fuzz.poolAmountIn + _exitFee); - } - - function test_Burn_PoolShare(ExitPool_FuzzScenario memory _fuzz) public happyPath(_fuzz) { - uint256 _exitFee = bmul(_fuzz.poolAmountIn, EXIT_FEE); - uint256 _pAiAfterExitFee = bsub(_fuzz.poolAmountIn, _exitFee); - uint256 _totalSupplyBefore = bPool.totalSupply(); - - bPool.exitPool(_fuzz.poolAmountIn, _zeroArray(tokens.length)); - - assertEq(bPool.totalSupply(), _totalSupplyBefore - _pAiAfterExitFee); - } - - function test_Set_ReentrancyLock(ExitPool_FuzzScenario memory _fuzz) public happyPath(_fuzz) { - _expectSetReentrancyLock(); - bPool.exitPool(_fuzz.poolAmountIn, _zeroArray(tokens.length)); - } - - function test_Revert_InvalidTokenAmountOut( - ExitPool_FuzzScenario memory _fuzz, - uint256 _tokenIndex - ) public happyPath(_fuzz) { - _assumeHappyPath(_fuzz); - _tokenIndex = bound(_tokenIndex, 0, TOKENS_AMOUNT - 1); - _fuzz.balance[_tokenIndex] = 0; - _setValues(_fuzz); - - vm.expectRevert(IBPool.BPool_InvalidTokenAmountOut.selector); - bPool.exitPool(_fuzz.poolAmountIn, _zeroArray(tokens.length)); - } - - function test_Revert_TokenAmountOutBelowMinAmountOut( - ExitPool_FuzzScenario memory _fuzz, - uint256 _tokenIndex, - uint256[TOKENS_AMOUNT] memory _minAmountsOut - ) public happyPath(_fuzz) { - _tokenIndex = bound(_tokenIndex, 0, TOKENS_AMOUNT - 1); - - uint256 _poolTotal = _fuzz.initPoolSupply; - uint256 _exitFee = bmul(_fuzz.poolAmountIn, EXIT_FEE); - uint256 _pAiAfterExitFee = bsub(_fuzz.poolAmountIn, _exitFee); - uint256 _ratio = bdiv(_pAiAfterExitFee, _poolTotal); - - for (uint256 i = 0; i < tokens.length; i++) { - uint256 _bal = _fuzz.balance[i]; - uint256 _tokenAmountOut = bmul(_ratio, _bal); - - _minAmountsOut[i] = _tokenIndex == i ? _tokenAmountOut + 1 : _tokenAmountOut; - } - - vm.expectRevert(IBPool.BPool_TokenAmountOutBelowMinAmountOut.selector); - bPool.exitPool(_fuzz.poolAmountIn, _staticToDynamicUintArray(_minAmountsOut)); - } - - function test_Revert_Reentrancy(ExitPool_FuzzScenario memory _fuzz) public happyPath(_fuzz) { - _expectRevertByReentrancy(); - bPool.exitPool(_fuzz.poolAmountIn, _zeroArray(tokens.length)); - } - - function test_Emit_TokenArrayLogExit(ExitPool_FuzzScenario memory _fuzz) public happyPath(_fuzz) { - uint256 _exitFee = bmul(_fuzz.poolAmountIn, EXIT_FEE); - uint256 _pAiAfterExitFee = bsub(_fuzz.poolAmountIn, _exitFee); - uint256 _ratio = bdiv(_pAiAfterExitFee, _fuzz.initPoolSupply); - - for (uint256 i = 0; i < tokens.length; i++) { - uint256 _bal = _fuzz.balance[i]; - uint256 _tokenAmountOut = bmul(_ratio, _bal); - vm.expectEmit(); - emit IBPool.LOG_EXIT(address(this), tokens[i], _tokenAmountOut); - } - bPool.exitPool(_fuzz.poolAmountIn, _zeroArray(tokens.length)); - } - - function test_Push_TokenArrayTokenAmountOut(ExitPool_FuzzScenario memory _fuzz) public happyPath(_fuzz) { - uint256 _exitFee = bmul(_fuzz.poolAmountIn, EXIT_FEE); - uint256 _pAiAfterExitFee = bsub(_fuzz.poolAmountIn, _exitFee); - uint256 _ratio = bdiv(_pAiAfterExitFee, _fuzz.initPoolSupply); - - for (uint256 i = 0; i < tokens.length; i++) { - uint256 _bal = _fuzz.balance[i]; - uint256 _tokenAmountOut = bmul(_ratio, _bal); - vm.expectCall( - address(tokens[i]), abi.encodeWithSelector(IERC20.transfer.selector, address(this), _tokenAmountOut) - ); - } - bPool.exitPool(_fuzz.poolAmountIn, _zeroArray(tokens.length)); - } - - function test_Emit_LogCall(ExitPool_FuzzScenario memory _fuzz) public happyPath(_fuzz) { - vm.expectEmit(); - bytes memory _data = abi.encodeWithSelector(BPool.exitPool.selector, _fuzz.poolAmountIn, _zeroArray(tokens.length)); - emit IBPool.LOG_CALL(BPool.exitPool.selector, address(this), _data); - - bPool.exitPool(_fuzz.poolAmountIn, _zeroArray(tokens.length)); - } -} - contract BPool_Unit_SwapExactAmountIn is SwapExactAmountInUtils { function test_Revert_NotBoundTokenIn( SwapExactAmountIn_FuzzScenario memory _fuzz, diff --git a/test/unit/BPool/BPoolBase.sol b/test/unit/BPool/BPoolBase.sol index 41dd6c88..0516fcec 100644 --- a/test/unit/BPool/BPoolBase.sol +++ b/test/unit/BPool/BPoolBase.sol @@ -1,7 +1,6 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.25; -import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; import {BConst} from 'contracts/BConst.sol'; import {Test} from 'forge-std/Test.sol'; import {IBPool} from 'interfaces/IBPool.sol'; @@ -12,17 +11,14 @@ contract BPoolBase is Test, BConst, Utils { MockBPool public bPool; address public deployer = makeAddr('deployer'); - address public token = makeAddr('token'); uint256 public tokenWeight = 1e18; uint256 public totalWeight = 10e18; - address public secondToken = makeAddr('secondToken'); function setUp() public virtual { vm.prank(deployer); bPool = new MockBPool(); - - vm.mockCall(token, abi.encodePacked(IERC20.transferFrom.selector), abi.encode()); - vm.mockCall(token, abi.encodePacked(IERC20.transfer.selector), abi.encode()); + tokens.push(makeAddr('token0')); + tokens.push(makeAddr('token1')); } function _setRandomTokens(uint256 _length) internal returns (address[] memory _tokensToAdd) { diff --git a/test/unit/BPool/BPool_Bind.t.sol b/test/unit/BPool/BPool_Bind.t.sol index 95ede7d6..0b146ac7 100644 --- a/test/unit/BPool/BPool_Bind.t.sol +++ b/test/unit/BPool/BPool_Bind.t.sol @@ -2,16 +2,24 @@ pragma solidity 0.8.25; import {BPoolBase} from './BPoolBase.sol'; +import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; import {IBPool} from 'interfaces/IBPool.sol'; contract BPoolBind is BPoolBase { uint256 public tokenBindBalance = 100e18; + function setUp() public virtual override { + super.setUp(); + + vm.mockCall(tokens[0], abi.encodePacked(IERC20.transferFrom.selector), abi.encode()); + vm.mockCall(tokens[0], abi.encodePacked(IERC20.transfer.selector), abi.encode()); + } + function test_RevertWhen_ReentrancyLockIsSet() external { bPool.call__setLock(_MUTEX_TAKEN); vm.expectRevert(IBPool.BPool_Reentrancy.selector); // it should revert - bPool.bind(token, tokenBindBalance, tokenWeight); + bPool.bind(tokens[0], tokenBindBalance, tokenWeight); } function test_RevertWhen_CallerIsNOTController(address _caller) external { @@ -19,7 +27,7 @@ contract BPoolBind is BPoolBase { vm.assume(_caller != deployer); vm.prank(_caller); vm.expectRevert(IBPool.BPool_CallerIsNotController.selector); - bPool.bind(token, tokenBindBalance, tokenWeight); + bPool.bind(tokens[0], tokenBindBalance, tokenWeight); } modifier whenCallerIsController() { @@ -28,49 +36,49 @@ contract BPoolBind is BPoolBase { } function test_RevertWhen_TokenIsAlreadyBound() external whenCallerIsController { - _setRecord(token, IBPool.Record({bound: true, index: 0, denorm: tokenWeight})); + _setRecord(tokens[0], IBPool.Record({bound: true, index: 0, denorm: tokenWeight})); // it should revert vm.expectRevert(IBPool.BPool_TokenAlreadyBound.selector); - bPool.bind(token, tokenBindBalance, tokenWeight); + bPool.bind(tokens[0], tokenBindBalance, tokenWeight); } function test_RevertWhen_PoolIsFinalized() external whenCallerIsController { bPool.set__finalized(true); // it should revert vm.expectRevert(IBPool.BPool_PoolIsFinalized.selector); - bPool.bind(token, tokenBindBalance, tokenWeight); + bPool.bind(tokens[0], tokenBindBalance, tokenWeight); } function test_RevertWhen_MAX_BOUND_TOKENSTokensAreAlreadyBound() external whenCallerIsController { _setRandomTokens(MAX_BOUND_TOKENS); // it should revert vm.expectRevert(IBPool.BPool_TokensAboveMaximum.selector); - bPool.bind(token, tokenBindBalance, tokenWeight); + bPool.bind(tokens[0], tokenBindBalance, tokenWeight); } function test_RevertWhen_TokenWeightIsTooLow() external whenCallerIsController { // it should revert vm.expectRevert(IBPool.BPool_WeightBelowMinimum.selector); - bPool.bind(token, tokenBindBalance, MIN_WEIGHT - 1); + bPool.bind(tokens[0], tokenBindBalance, MIN_WEIGHT - 1); } function test_RevertWhen_TokenWeightIsTooHigh() external whenCallerIsController { // it should revert vm.expectRevert(IBPool.BPool_WeightAboveMaximum.selector); - bPool.bind(token, tokenBindBalance, MAX_WEIGHT + 1); + bPool.bind(tokens[0], tokenBindBalance, MAX_WEIGHT + 1); } function test_RevertWhen_TooLittleBalanceIsProvided() external whenCallerIsController { // it should revert vm.expectRevert(IBPool.BPool_BalanceBelowMinimum.selector); - bPool.bind(token, MIN_BALANCE - 1, tokenWeight); + bPool.bind(tokens[0], MIN_BALANCE - 1, tokenWeight); } function test_RevertWhen_WeightSumExceedsMAX_TOTAL_WEIGHT() external whenCallerIsController { bPool.set__totalWeight(2 * MAX_TOTAL_WEIGHT / 3); // it should revert vm.expectRevert(IBPool.BPool_TotalWeightAboveMaximum.selector); - bPool.bind(token, tokenBindBalance, MAX_TOTAL_WEIGHT / 2); + bPool.bind(tokens[0], tokenBindBalance, MAX_TOTAL_WEIGHT / 2); } function test_WhenTokenCanBeBound(uint256 _existingTokens) external whenCallerIsController { @@ -79,24 +87,24 @@ contract BPoolBind is BPoolBase { bPool.set__totalWeight(totalWeight); // it calls _pullUnderlying - bPool.expectCall__pullUnderlying(token, deployer, tokenBindBalance); + bPool.expectCall__pullUnderlying(tokens[0], deployer, tokenBindBalance); // it sets the reentrancy lock bPool.expectCall__setLock(_MUTEX_TAKEN); // it emits LOG_CALL event vm.expectEmit(); - bytes memory _data = abi.encodeWithSelector(IBPool.bind.selector, token, tokenBindBalance, tokenWeight); + bytes memory _data = abi.encodeWithSelector(IBPool.bind.selector, tokens[0], tokenBindBalance, tokenWeight); emit IBPool.LOG_CALL(IBPool.bind.selector, deployer, _data); - bPool.bind(token, tokenBindBalance, tokenWeight); + bPool.bind(tokens[0], tokenBindBalance, tokenWeight); // it clears the reentrancy lock assertEq(bPool.call__getLock(), _MUTEX_FREE); // it adds token to the tokens array - assertEq(bPool.call__tokens()[_existingTokens], token); + assertEq(bPool.call__tokens()[_existingTokens], tokens[0]); // it sets the token record - assertEq(bPool.call__records(token).bound, true); - assertEq(bPool.call__records(token).denorm, tokenWeight); - assertEq(bPool.call__records(token).index, _existingTokens); + assertEq(bPool.call__records(tokens[0]).bound, true); + assertEq(bPool.call__records(tokens[0]).denorm, tokenWeight); + assertEq(bPool.call__records(tokens[0]).index, _existingTokens); // it sets total weight assertEq(bPool.call__totalWeight(), totalWeight + tokenWeight); } diff --git a/test/unit/BPool/BPool_ExitPool.t.sol b/test/unit/BPool/BPool_ExitPool.t.sol new file mode 100644 index 00000000..fac066ce --- /dev/null +++ b/test/unit/BPool/BPool_ExitPool.t.sol @@ -0,0 +1,120 @@ +// 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 BPoolExitPool is BPoolBase, BNum { + // Valid scenario + uint256 public constant SHARE_PROPORTION = 20; + uint256 public poolAmountIn = INIT_POOL_SUPPLY / SHARE_PROPORTION; + uint256 public token0Balance = 20e18; + uint256 public token1Balance = 50e18; + // currently hard-coded to zero + uint256 public exitFee = bmul(poolAmountIn, EXIT_FEE); + uint256[] minAmountsOut; + + // when buring n pool shares, caller expects enough amount X of every token t + // should be sent to statisfy: + // Xt = n/BPT.totalSupply() * t.balanceOf(BPT) + uint256 public expectedToken0Out = token0Balance / SHARE_PROPORTION; + uint256 public expectedToken1Out = token1Balance / SHARE_PROPORTION; + + function setUp() public virtual override { + super.setUp(); + bPool.set__finalized(true); + // mint an initial amount of pool shares (expected to happen at _finalize) + bPool.call__mintPoolShare(INIT_POOL_SUPPLY); + bPool.set__tokens(_tokensToMemory()); + // token weights are not used for all-token exits + _setRecord(tokens[0], IBPool.Record({bound: true, index: 0, denorm: 0})); + _setRecord(tokens[1], IBPool.Record({bound: true, index: 1, denorm: 0})); + // underlying balances are used instead + vm.mockCall(tokens[0], abi.encodePacked(IERC20.balanceOf.selector), abi.encode(uint256(token0Balance))); + vm.mockCall(tokens[1], abi.encodePacked(IERC20.balanceOf.selector), abi.encode(uint256(token1Balance))); + + // caller not having enough pool shares would revert inside `_pullPoolShare` + bPool.mock_call__pullPoolShare(address(this), poolAmountIn); + + minAmountsOut = new uint256[](2); + minAmountsOut[0] = expectedToken0Out; + minAmountsOut[1] = expectedToken1Out; + } + + function test_RevertWhen_ReentrancyLockIsSet() external { + bPool.call__setLock(_MUTEX_TAKEN); + // it should revert + vm.expectRevert(IBPool.BPool_Reentrancy.selector); + bPool.exitPool(0, minAmountsOut); + } + + function test_RevertWhen_PoolIsNotFinalized() external { + bPool.set__finalized(false); + // it should revert + vm.expectRevert(IBPool.BPool_PoolNotFinalized.selector); + bPool.exitPool(0, minAmountsOut); + } + + function test_RevertWhen_TotalSupplyIsZero() external { + bPool.call__burnPoolShare(INIT_POOL_SUPPLY); + // it should revert + // division by zero + vm.expectRevert(BNum.BNum_DivZero.selector); + bPool.exitPool(0, minAmountsOut); + } + + function test_RevertWhen_PoolAmountInIsTooSmall(uint256 amountIn) external { + amountIn = bound(amountIn, 0, (INIT_POOL_SUPPLY / 1e18) / 2 - 1); + // it should revert + vm.expectRevert(IBPool.BPool_InvalidPoolRatio.selector); + bPool.exitPool(amountIn, minAmountsOut); + } + + function test_RevertWhen_BalanceOfPoolInAnyTokenIsZero() external { + vm.mockCall(tokens[1], abi.encodePacked(IERC20.balanceOf.selector), abi.encode(uint256(0))); + // it should revert + vm.expectRevert(IBPool.BPool_InvalidTokenAmountOut.selector); + bPool.exitPool(poolAmountIn, minAmountsOut); + } + + function test_RevertWhen_ReturnedAmountOfATokenIsLessThanMinAmountsOut() external { + minAmountsOut[1] += 1; + // it should revert + vm.expectRevert(IBPool.BPool_TokenAmountOutBelowMinAmountOut.selector); + bPool.exitPool(poolAmountIn, minAmountsOut); + } + + function test_WhenPreconditionsAreMet() external { + // it pulls poolAmountIn shares + bPool.expectCall__pullPoolShare(address(this), poolAmountIn); + // it sends exitFee to factory + bPool.expectCall__pushPoolShare(deployer, exitFee); + // it burns poolAmountIn - exitFee shares + bPool.expectCall__burnPoolShare(poolAmountIn - exitFee); + // it calls _pushUnderlying for every token + bPool.mock_call__pushUnderlying(tokens[0], address(this), expectedToken0Out); + bPool.expectCall__pushUnderlying(tokens[0], address(this), expectedToken0Out); + bPool.mock_call__pushUnderlying(tokens[1], address(this), expectedToken1Out); + bPool.expectCall__pushUnderlying(tokens[1], address(this), expectedToken1Out); + // it sets the reentrancy lock + bPool.expectCall__setLock(_MUTEX_TAKEN); + // it emits LOG_CALL event + bytes memory _data = abi.encodeWithSelector(IBPool.exitPool.selector, poolAmountIn, minAmountsOut); + vm.expectEmit(); + emit IBPool.LOG_CALL(IBPool.exitPool.selector, address(this), _data); + // it emits LOG_EXIT event for every token + vm.expectEmit(); + emit IBPool.LOG_EXIT(address(this), tokens[0], expectedToken0Out); + vm.expectEmit(); + emit IBPool.LOG_EXIT(address(this), tokens[1], expectedToken1Out); + + bPool.exitPool(poolAmountIn, minAmountsOut); + + // it clears the reentrancy lock + assertEq(_MUTEX_FREE, bPool.call__getLock()); + } +} diff --git a/test/unit/BPool/BPool_ExitPool.tree b/test/unit/BPool/BPool_ExitPool.tree new file mode 100644 index 00000000..b13d6c20 --- /dev/null +++ b/test/unit/BPool/BPool_ExitPool.tree @@ -0,0 +1,22 @@ +BPool::ExitPool +├── when reentrancy lock is set +│ └── it should revert +├── when pool is not finalized +│ └── it should revert +├── when total supply is zero +│ └── it should revert // division by zero +├── when pool amount in is too small +│ └── it should revert +├── when balance of pool in any token is zero +│ └── it should revert +├── when returned amount of a token is less than minAmountsOut +│ └── it should revert +└── when preconditions are met + ├── it emits LOG_CALL event + ├── it sets the reentrancy lock + ├── it pulls poolAmountIn shares + ├── it sends exitFee to factory + ├── it burns poolAmountIn - exitFee shares + ├── it calls _pushUnderlying for every token + ├── it emits LOG_EXIT event for every token + └── it clears the reentrancy lock diff --git a/test/unit/BPool/BPool_JoinPool.t.sol b/test/unit/BPool/BPool_JoinPool.t.sol index 6cc9f9a9..1f501275 100644 --- a/test/unit/BPool/BPool_JoinPool.t.sol +++ b/test/unit/BPool/BPool_JoinPool.t.sol @@ -25,16 +25,13 @@ contract BPoolJoinPool is BPoolBase { bPool.set__finalized(true); // mint an initial amount of pool shares (expected to happen at _finalize) bPool.call__mintPoolShare(INIT_POOL_SUPPLY); - address[] memory _tokens = new address[](2); - _tokens[0] = token; - _tokens[1] = secondToken; - bPool.set__tokens(_tokens); + bPool.set__tokens(_tokensToMemory()); // token weights are not used for all-token joins - _setRecord(token, IBPool.Record({bound: true, index: 0, denorm: 0})); - _setRecord(secondToken, IBPool.Record({bound: true, index: 1, denorm: 0})); + _setRecord(tokens[0], IBPool.Record({bound: true, index: 0, denorm: 0})); + _setRecord(tokens[1], IBPool.Record({bound: true, index: 1, denorm: 0})); // underlying balances are used instead - vm.mockCall(token, abi.encodePacked(IERC20.balanceOf.selector), abi.encode(uint256(token0Balance))); - vm.mockCall(secondToken, abi.encodePacked(IERC20.balanceOf.selector), abi.encode(uint256(token1Balance))); + vm.mockCall(tokens[0], abi.encodePacked(IERC20.balanceOf.selector), abi.encode(uint256(token0Balance))); + vm.mockCall(tokens[1], abi.encodePacked(IERC20.balanceOf.selector), abi.encode(uint256(token1Balance))); maxAmountsIn = new uint256[](2); maxAmountsIn[0] = requiredToken0In; @@ -74,7 +71,7 @@ contract BPoolJoinPool is BPoolBase { } function test_RevertWhen_BalanceOfPoolInAnyTokenIsZero() external { - vm.mockCall(secondToken, abi.encodePacked(IERC20.balanceOf.selector), abi.encode(uint256(0))); + vm.mockCall(tokens[1], abi.encodePacked(IERC20.balanceOf.selector), abi.encode(uint256(0))); // it should revert vm.expectRevert(IBPool.BPool_InvalidTokenAmountIn.selector); bPool.joinPool(poolAmountOut, maxAmountsIn); @@ -91,10 +88,10 @@ contract BPoolJoinPool is BPoolBase { // it sets reentrancy lock bPool.expectCall__setLock(_MUTEX_TAKEN); // it calls _pullUnderlying for every token - bPool.mock_call__pullUnderlying(token, address(this), requiredToken0In); - bPool.expectCall__pullUnderlying(token, address(this), requiredToken0In); - bPool.mock_call__pullUnderlying(secondToken, address(this), requiredToken1In); - bPool.expectCall__pullUnderlying(secondToken, address(this), requiredToken1In); + bPool.mock_call__pullUnderlying(tokens[0], address(this), requiredToken0In); + bPool.expectCall__pullUnderlying(tokens[0], address(this), requiredToken0In); + bPool.mock_call__pullUnderlying(tokens[1], address(this), requiredToken1In); + bPool.expectCall__pullUnderlying(tokens[1], address(this), requiredToken1In); // it mints the pool shares bPool.expectCall__mintPoolShare(poolAmountOut); // it sends pool shares to caller @@ -109,9 +106,9 @@ contract BPoolJoinPool is BPoolBase { emit IBPool.LOG_CALL(IBPool.joinPool.selector, address(this), _data); // it emits LOG_JOIN event for every token vm.expectEmit(); - emit IBPool.LOG_JOIN(address(this), token, requiredToken0In); + emit IBPool.LOG_JOIN(address(this), tokens[0], requiredToken0In); vm.expectEmit(); - emit IBPool.LOG_JOIN(address(this), secondToken, requiredToken1In); + emit IBPool.LOG_JOIN(address(this), tokens[1], requiredToken1In); bPool.joinPool(poolAmountOut, maxAmounts); // it clears the reentrancy lock assertEq(_MUTEX_FREE, bPool.call__getLock()); diff --git a/test/unit/BPool/BPool_Unbind.t.sol b/test/unit/BPool/BPool_Unbind.t.sol index f854c0c7..19eb9ced 100644 --- a/test/unit/BPool/BPool_Unbind.t.sol +++ b/test/unit/BPool/BPool_Unbind.t.sol @@ -11,15 +11,15 @@ contract BPoolUnbind is BPoolBase { function setUp() public virtual override { super.setUp(); - vm.mockCall(token, abi.encodePacked(IERC20.balanceOf.selector), abi.encode(boundTokenAmount)); - vm.mockCall(secondToken, abi.encodePacked(IERC20.transferFrom.selector), abi.encode()); + vm.mockCall(tokens[0], abi.encodePacked(IERC20.balanceOf.selector), abi.encode(boundTokenAmount)); + vm.mockCall(tokens[1], abi.encodePacked(IERC20.transferFrom.selector), abi.encode()); } function test_RevertWhen_ReentrancyLockIsSet() external { bPool.call__setLock(_MUTEX_TAKEN); vm.expectRevert(IBPool.BPool_Reentrancy.selector); // it should revert - bPool.unbind(token); + bPool.unbind(tokens[0]); } function test_RevertWhen_CallerIsNOTController(address _caller) external { @@ -27,7 +27,7 @@ contract BPoolUnbind is BPoolBase { vm.assume(_caller != deployer); vm.prank(_caller); vm.expectRevert(IBPool.BPool_CallerIsNotController.selector); - bPool.unbind(token); + bPool.unbind(tokens[0]); } modifier whenCallerIsController() { @@ -38,22 +38,22 @@ contract BPoolUnbind is BPoolBase { function test_RevertWhen_TokenIsNotBound() external whenCallerIsController { vm.expectRevert(IBPool.BPool_TokenNotBound.selector); // it should revert - bPool.unbind(token); + bPool.unbind(tokens[0]); } function test_RevertWhen_PoolIsFinalized() external whenCallerIsController { - _setRecord(token, IBPool.Record({bound: true, index: 0, denorm: 0})); + _setRecord(tokens[0], IBPool.Record({bound: true, index: 0, denorm: 0})); bPool.set__finalized(true); // it should revert vm.expectRevert(IBPool.BPool_PoolIsFinalized.selector); - bPool.unbind(token); + bPool.unbind(tokens[0]); } modifier whenTokenCanBeUnbound() { - _setRecord(token, IBPool.Record({bound: true, index: 0, denorm: tokenWeight})); + _setRecord(tokens[0], IBPool.Record({bound: true, index: 0, denorm: tokenWeight})); bPool.set__totalWeight(totalWeight); address[] memory tokens = new address[](1); - tokens[0] = token; + tokens[0] = tokens[0]; bPool.set__tokens(tokens); _; } @@ -62,18 +62,18 @@ contract BPoolUnbind is BPoolBase { // it sets the reentrancy lock bPool.expectCall__setLock(_MUTEX_TAKEN); // it calls _pushUnderlying - bPool.expectCall__pushUnderlying(token, deployer, boundTokenAmount); + bPool.expectCall__pushUnderlying(tokens[0], deployer, boundTokenAmount); // it emits LOG_CALL event vm.expectEmit(); - bytes memory _data = abi.encodeWithSelector(IBPool.unbind.selector, token); + bytes memory _data = abi.encodeWithSelector(IBPool.unbind.selector, tokens[0]); emit IBPool.LOG_CALL(IBPool.unbind.selector, deployer, _data); - bPool.unbind(token); + bPool.unbind(tokens[0]); // it clears the reentrancy lock assertEq(bPool.call__getLock(), _MUTEX_FREE); // it removes the token record - assertFalse(bPool.call__records(token).bound); + assertFalse(bPool.call__records(tokens[0]).bound); // it pops from the array assertEq(bPool.getNumTokens(), 0); // it decreases the total weight @@ -81,20 +81,17 @@ contract BPoolUnbind is BPoolBase { } function test_WhenTokenIsNOTLastOnTheTokensArray() external whenCallerIsController whenTokenCanBeUnbound { - _setRecord(secondToken, IBPool.Record({bound: true, index: 0, denorm: tokenWeight})); - address[] memory tokens = new address[](2); - tokens[0] = token; - tokens[1] = secondToken; - bPool.set__tokens(tokens); - bPool.unbind(token); + _setRecord(tokens[1], IBPool.Record({bound: true, index: 0, denorm: tokenWeight})); + bPool.set__tokens(_tokensToMemory()); + bPool.unbind(tokens[0]); // it removes the token record - assertFalse(bPool.call__records(token).bound); + assertFalse(bPool.call__records(tokens[0]).bound); // it removes the token from the array assertEq(bPool.getNumTokens(), 1); // it keeps other tokens in the array - assertEq(bPool.call__tokens()[0], secondToken); - assertTrue(bPool.call__records(secondToken).bound); + assertEq(bPool.call__tokens()[0], tokens[1]); + assertTrue(bPool.call__records(tokens[1]).bound); // it updates records to point to the new indices - assertEq(bPool.call__records(secondToken).index, 0); + assertEq(bPool.call__records(tokens[1]).index, 0); } } diff --git a/test/utils/Utils.sol b/test/utils/Utils.sol index 19225f9d..59a4a135 100644 --- a/test/utils/Utils.sol +++ b/test/utils/Utils.sol @@ -9,7 +9,7 @@ contract Utils is Test { uint256 public constant TOKENS_AMOUNT = 3; - address[TOKENS_AMOUNT] public tokens; + address[] public tokens; function _getDeterministicTokenArray(uint256 _length) internal returns (address[] memory _tokenArray) { _tokenArray = new address[](_length); From 06efb5435a35d298b50db93bb6e8e2e965743b73 Mon Sep 17 00:00:00 2001 From: teddy Date: Fri, 12 Jul 2024 04:39:40 -0300 Subject: [PATCH 25/48] test: btt tests for swap exact amount in (#149) * 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/unit/BPool.t.sol | 224 ------------------ test/unit/BPool/BPool_SwapExactAmountIn.t.sol | 151 ++++++++++++ test/unit/BPool/BPool_SwapExactAmountIn.tree | 30 +++ 3 files changed, 181 insertions(+), 224 deletions(-) create mode 100644 test/unit/BPool/BPool_SwapExactAmountIn.t.sol create mode 100644 test/unit/BPool/BPool_SwapExactAmountIn.tree diff --git a/test/unit/BPool.t.sol b/test/unit/BPool.t.sol index 08ace5cb..16868e32 100644 --- a/test/unit/BPool.t.sol +++ b/test/unit/BPool.t.sol @@ -818,230 +818,6 @@ contract BPool_Unit_GetSpotPriceSansFee is BasePoolTest { } } -contract BPool_Unit_SwapExactAmountIn is SwapExactAmountInUtils { - function test_Revert_NotBoundTokenIn( - SwapExactAmountIn_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.swapExactAmountIn(_tokenIn, _fuzz.tokenAmountIn, tokenOut, 0, type(uint256).max); - } - - function test_Revert_NotBoundTokenOut( - SwapExactAmountIn_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.swapExactAmountIn(tokenIn, _fuzz.tokenAmountIn, _tokenOut, 0, type(uint256).max); - } - - function test_Revert_NotFinalized(SwapExactAmountIn_FuzzScenario memory _fuzz) public happyPath(_fuzz) { - _setFinalize(false); - - vm.expectRevert(IBPool.BPool_PoolNotFinalized.selector); - bPool.swapExactAmountIn(tokenIn, _fuzz.tokenAmountIn, tokenOut, 0, type(uint256).max); - } - - function test_Revert_TokenAmountInAboveMaxIn(SwapExactAmountIn_FuzzScenario memory _fuzz) public happyPath(_fuzz) { - uint256 _tokenAmountIn = bmul(_fuzz.tokenInBalance, MAX_IN_RATIO) + 1; - - vm.expectRevert(IBPool.BPool_TokenAmountInAboveMaxRatio.selector); - bPool.swapExactAmountIn(tokenIn, _tokenAmountIn, tokenOut, 0, type(uint256).max); - } - - function test_Revert_SpotPriceAboveMaxPrice( - SwapExactAmountIn_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.swapExactAmountIn(tokenIn, _fuzz.tokenAmountIn, tokenOut, 0, _maxPrice); - } - - function test_Revert_TokenAmountOutBelowMinOut( - SwapExactAmountIn_FuzzScenario memory _fuzz, - uint256 _minAmountOut - ) public happyPath(_fuzz) { - uint256 _tokenAmountOut = calcOutGivenIn( - _fuzz.tokenInBalance, - _fuzz.tokenInDenorm, - _fuzz.tokenOutBalance, - _fuzz.tokenOutDenorm, - _fuzz.tokenAmountIn, - _fuzz.swapFee - ); - _minAmountOut = bound(_minAmountOut, _tokenAmountOut + 1, type(uint256).max); - - vm.expectRevert(IBPool.BPool_TokenAmountOutBelowMinOut.selector); - bPool.swapExactAmountIn(tokenIn, _fuzz.tokenAmountIn, tokenOut, _minAmountOut, type(uint256).max); - } - - function test_Revert_Reentrancy(SwapExactAmountIn_FuzzScenario memory _fuzz) public happyPath(_fuzz) { - _expectRevertByReentrancy(); - bPool.swapExactAmountIn(tokenIn, _fuzz.tokenAmountIn, tokenOut, 0, type(uint256).max); - } - - function test_Revert_SpotPriceAfterBelowSpotPriceBefore() 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(SwapExactAmountIn_FuzzScenario memory _fuzz) public happyPath(_fuzz) { - uint256 _tokenAmountOut = calcOutGivenIn( - _fuzz.tokenInBalance, - _fuzz.tokenInDenorm, - _fuzz.tokenOutBalance, - _fuzz.tokenOutDenorm, - _fuzz.tokenAmountIn, - _fuzz.swapFee - ); - uint256 _spotPriceBefore = calcSpotPrice( - _fuzz.tokenInBalance, _fuzz.tokenInDenorm, _fuzz.tokenOutBalance, _fuzz.tokenOutDenorm, _fuzz.swapFee - ); - uint256 _spotPriceAfter = calcSpotPrice( - _fuzz.tokenInBalance + _fuzz.tokenAmountIn, - _fuzz.tokenInDenorm, - _fuzz.tokenOutBalance - _tokenAmountOut, - _fuzz.tokenOutDenorm, - _fuzz.swapFee - ); - vm.assume(_spotPriceAfter > _spotPriceBefore); - - vm.expectRevert(IBPool.BPool_SpotPriceAboveMaxPrice.selector); - bPool.swapExactAmountIn(tokenIn, _fuzz.tokenAmountIn, tokenOut, 0, _spotPriceBefore); - } - - function test_Revert_SpotPriceBeforeAboveTokenRatio(SwapExactAmountIn_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.tokenInDenorm); - _fuzz.tokenOutBalance = bound(_fuzz.tokenOutBalance, MIN_BALANCE, type(uint256).max / _fuzz.tokenOutDenorm); - vm.assume(_fuzz.tokenAmountIn < type(uint256).max - _fuzz.tokenInBalance); - vm.assume(_fuzz.tokenInBalance + _fuzz.tokenAmountIn < type(uint256).max / _fuzz.tokenInDenorm); - _assumeCalcSpotPrice( - _fuzz.tokenInBalance, _fuzz.tokenInDenorm, _fuzz.tokenOutBalance, _fuzz.tokenOutDenorm, _fuzz.swapFee - ); - vm.assume(_fuzz.tokenAmountIn <= bmul(_fuzz.tokenInBalance, MAX_IN_RATIO)); - uint256 _spotPriceBefore = calcSpotPrice( - _fuzz.tokenInBalance, _fuzz.tokenInDenorm, _fuzz.tokenOutBalance, _fuzz.tokenOutDenorm, _fuzz.swapFee - ); - _assumeCalcOutGivenIn(_fuzz.tokenInBalance, _fuzz.tokenAmountIn, _fuzz.swapFee); - uint256 _tokenAmountOut = calcOutGivenIn( - _fuzz.tokenInBalance, - _fuzz.tokenInDenorm, - _fuzz.tokenOutBalance, - _fuzz.tokenOutDenorm, - _fuzz.tokenAmountIn, - _fuzz.swapFee - ); - vm.assume(_tokenAmountOut > BONE); - _assumeCalcSpotPrice( - _fuzz.tokenInBalance + _fuzz.tokenAmountIn, - _fuzz.tokenInDenorm, - _fuzz.tokenOutBalance - _tokenAmountOut, - _fuzz.tokenOutDenorm, - _fuzz.swapFee - ); - vm.assume(_spotPriceBefore > bdiv(_fuzz.tokenAmountIn, _tokenAmountOut)); - - _setValues(_fuzz); - - vm.expectRevert(IBPool.BPool_SpotPriceBeforeAboveTokenRatio.selector); - bPool.swapExactAmountIn(tokenIn, _fuzz.tokenAmountIn, tokenOut, 0, type(uint256).max); - } - - function test_Emit_LogSwap(SwapExactAmountIn_FuzzScenario memory _fuzz) public happyPath(_fuzz) { - uint256 _tokenAmountOut = calcOutGivenIn( - _fuzz.tokenInBalance, - _fuzz.tokenInDenorm, - _fuzz.tokenOutBalance, - _fuzz.tokenOutDenorm, - _fuzz.tokenAmountIn, - _fuzz.swapFee - ); - - vm.expectEmit(); - emit IBPool.LOG_SWAP(address(this), tokenIn, tokenOut, _fuzz.tokenAmountIn, _tokenAmountOut); - bPool.swapExactAmountIn(tokenIn, _fuzz.tokenAmountIn, tokenOut, 0, type(uint256).max); - } - - function test_Set_ReentrancyLock(SwapExactAmountIn_FuzzScenario memory _fuzz) public happyPath(_fuzz) { - _expectSetReentrancyLock(); - bPool.swapExactAmountIn(tokenIn, _fuzz.tokenAmountIn, tokenOut, 0, type(uint256).max); - } - - function test_Pull_TokenAmountIn(SwapExactAmountIn_FuzzScenario memory _fuzz) public happyPath(_fuzz) { - vm.expectCall( - address(tokenIn), - abi.encodeWithSelector(IERC20.transferFrom.selector, address(this), address(bPool), _fuzz.tokenAmountIn) - ); - bPool.swapExactAmountIn(tokenIn, _fuzz.tokenAmountIn, tokenOut, 0, type(uint256).max); - } - - function test_Push_TokenAmountOut(SwapExactAmountIn_FuzzScenario memory _fuzz) public happyPath(_fuzz) { - uint256 _tokenAmountOut = calcOutGivenIn( - _fuzz.tokenInBalance, - _fuzz.tokenInDenorm, - _fuzz.tokenOutBalance, - _fuzz.tokenOutDenorm, - _fuzz.tokenAmountIn, - _fuzz.swapFee - ); - - vm.expectCall(address(tokenOut), abi.encodeWithSelector(IERC20.transfer.selector, address(this), _tokenAmountOut)); - bPool.swapExactAmountIn(tokenIn, _fuzz.tokenAmountIn, tokenOut, 0, type(uint256).max); - } - - function test_Returns_AmountAndPrice(SwapExactAmountIn_FuzzScenario memory _fuzz) public happyPath(_fuzz) { - uint256 _expectedTokenAmountOut = calcOutGivenIn( - _fuzz.tokenInBalance, - _fuzz.tokenInDenorm, - _fuzz.tokenOutBalance, - _fuzz.tokenOutDenorm, - _fuzz.tokenAmountIn, - _fuzz.swapFee - ); - uint256 _expectedSpotPriceAfter = calcSpotPrice( - _fuzz.tokenInBalance + _fuzz.tokenAmountIn, - _fuzz.tokenInDenorm, - _fuzz.tokenOutBalance - _expectedTokenAmountOut, - _fuzz.tokenOutDenorm, - _fuzz.swapFee - ); - - (uint256 _tokenAmountOut, uint256 _spotPriceAfter) = - bPool.swapExactAmountIn(tokenIn, _fuzz.tokenAmountIn, tokenOut, 0, type(uint256).max); - - assertEq(_tokenAmountOut, _expectedTokenAmountOut); - assertEq(_spotPriceAfter, _expectedSpotPriceAfter); - } - - function test_Emit_LogCall(SwapExactAmountIn_FuzzScenario memory _fuzz) public happyPath(_fuzz) { - vm.expectEmit(); - bytes memory _data = abi.encodeWithSelector( - BPool.swapExactAmountIn.selector, tokenIn, _fuzz.tokenAmountIn, tokenOut, 0, type(uint256).max - ); - emit IBPool.LOG_CALL(BPool.swapExactAmountIn.selector, address(this), _data); - - bPool.swapExactAmountIn(tokenIn, _fuzz.tokenAmountIn, tokenOut, 0, type(uint256).max); - } -} - contract BPool_Unit_SwapExactAmountOut is BasePoolTest { address tokenIn; address tokenOut; diff --git a/test/unit/BPool/BPool_SwapExactAmountIn.t.sol b/test/unit/BPool/BPool_SwapExactAmountIn.t.sol new file mode 100644 index 00000000..709b3461 --- /dev/null +++ b/test/unit/BPool/BPool_SwapExactAmountIn.t.sol @@ -0,0 +1,151 @@ +// 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 BPoolSwapExactAmountIn is BPoolBase, BNum { + // Valid scenario + address public tokenIn; + uint256 public tokenAmountIn = 3e18; + + uint256 public tokenInBalance = 10e18; + uint256 public tokenOutBalance = 40e18; + // pool is expected to keep 2X the value of tokenIn than tokenOut + uint256 public tokenInWeight = 2e18; + uint256 public tokenOutWeight = 1e18; + + address public tokenOut; + // (tokenInBalance / tokenInWeight) / (tokenOutBalance/ tokenOutWeight) + uint256 public spotPriceBeforeSwapWithoutFee = 0.125e18; + uint256 public spotPriceBeforeSwap = bmul(spotPriceBeforeSwapWithoutFee, bdiv(BONE, bsub(BONE, MIN_FEE))); + // from bmath: 40*(1-(10/(10+3*(1-10^-6)))^2) + uint256 public expectedAmountOut = 16.3313500227545254e18; + // (tokenInBalance / tokenInWeight) / (tokenOutBalance/ tokenOutWeight) + // (13 / 2) / (40-expectedAmountOut/ 1) + uint256 public spotPriceAfterSwapWithoutFee = 0.274624873250014625e18; + 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.swapExactAmountIn(tokenIn, tokenAmountIn, tokenOut, expectedAmountOut, spotPriceAfterSwap); + } + + function test_RevertWhen_PoolIsNotFinalized() external { + bPool.set__finalized(false); + // it should revert + vm.expectRevert(IBPool.BPool_PoolNotFinalized.selector); + bPool.swapExactAmountIn(tokenIn, tokenAmountIn, tokenOut, expectedAmountOut, spotPriceAfterSwap); + } + + function test_RevertWhen_TokenInIsNotBound() external { + // it should revert + vm.expectRevert(IBPool.BPool_TokenNotBound.selector); + bPool.swapExactAmountIn(makeAddr('unknown token'), tokenAmountIn, tokenOut, expectedAmountOut, spotPriceAfterSwap); + } + + function test_RevertWhen_TokenOutIsNotBound() external { + // it should revert + vm.expectRevert(IBPool.BPool_TokenNotBound.selector); + bPool.swapExactAmountIn(tokenIn, tokenAmountIn, makeAddr('unknown token'), expectedAmountOut, spotPriceAfterSwap); + } + + function test_RevertWhen_TokenAmountInExceedsMaxAllowedRatio(uint256 tokenAmountIn_) external { + tokenAmountIn_ = bound(tokenAmountIn_, bmul(tokenInBalance, MAX_IN_RATIO) + 1, type(uint256).max); + // it should revert + vm.expectRevert(IBPool.BPool_TokenAmountInAboveMaxRatio.selector); + bPool.swapExactAmountIn(tokenIn, tokenAmountIn_, tokenOut, 0, 0); + } + + function test_RevertWhen_SpotPriceBeforeSwapExceedsMaxPrice() external { + // it should revert + vm.expectRevert(IBPool.BPool_SpotPriceAboveMaxPrice.selector); + bPool.swapExactAmountIn(tokenIn, tokenAmountIn, tokenOut, expectedAmountOut, spotPriceBeforeSwap - 1); + } + + function test_RevertWhen_CalculatedTokenAmountOutIsLessThanMinAmountOut() external { + // it should revert + vm.expectRevert(IBPool.BPool_TokenAmountOutBelowMinOut.selector); + bPool.swapExactAmountIn(tokenIn, tokenAmountIn, tokenOut, expectedAmountOut + 1, spotPriceAfterSwap); + } + + function test_RevertWhen_SpotPriceAfterSwapExceedsSpotPriceBeforeSwap() 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_RevertWhen_SpotPriceAfterSwapExceedsMaxPrice() external { + // it should revert + vm.expectRevert(IBPool.BPool_SpotPriceAboveMaxPrice.selector); + bPool.swapExactAmountIn(tokenIn, tokenAmountIn, tokenOut, expectedAmountOut, spotPriceAfterSwap - 1); + } + + function test_RevertWhen_SpotPriceBeforeSwapExceedsTokenRatioAfterSwap() external { + uint256 tokenAmountIn_ = 30e18; + uint256 balanceTokenIn_ = 36_830_000_000_000_000_000_000_000_000_000; + uint256 weightTokenIn_ = 1e18; + uint256 balanceTokenOut_ = 18_100_000_000_000_000_000_000_000_000_000; + uint256 weightTokenOut_ = 1e18; + uint256 swapFee_ = 0.019e18; + vm.mockCall(tokenIn, abi.encodePacked(IERC20.balanceOf.selector), abi.encode(uint256(balanceTokenIn_))); + vm.mockCall(tokenOut, abi.encodePacked(IERC20.balanceOf.selector), abi.encode(uint256(balanceTokenOut_))); + bPool.set__records(tokenIn, IBPool.Record({bound: true, index: 0, denorm: weightTokenIn_})); + bPool.set__records(tokenOut, IBPool.Record({bound: true, index: 1, denorm: weightTokenOut_})); + bPool.set__swapFee(swapFee_); + // it should revert + vm.expectRevert(IBPool.BPool_SpotPriceBeforeAboveTokenRatio.selector); + bPool.swapExactAmountIn(tokenIn, tokenAmountIn_, tokenOut, 0, type(uint256).max); + } + + 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), tokenAmountIn); + bPool.expectCall__pullUnderlying(tokenIn, address(this), tokenAmountIn); + // it calls _pushUnderlying for tokenOut + bPool.mock_call__pushUnderlying(tokenOut, address(this), expectedAmountOut); + bPool.expectCall__pushUnderlying(tokenOut, address(this), expectedAmountOut); + // it emits a LOG_CALL event + bytes memory _data = abi.encodeCall( + IBPool.swapExactAmountIn, (tokenIn, tokenAmountIn, tokenOut, expectedAmountOut, spotPriceAfterSwap) + ); + vm.expectEmit(); + emit IBPool.LOG_CALL(IBPool.swapExactAmountIn.selector, address(this), _data); + // it emits a LOG_SWAP event + vm.expectEmit(); + emit IBPool.LOG_SWAP(address(this), tokenIn, tokenOut, tokenAmountIn, expectedAmountOut); + + // it returns the tokenOut amount swapped + // it returns the spot price after the swap + (uint256 out, uint256 priceAfter) = + bPool.swapExactAmountIn(tokenIn, tokenAmountIn, tokenOut, expectedAmountOut, spotPriceAfterSwap); + assertEq(out, expectedAmountOut); + assertEq(priceAfter, spotPriceAfterSwap); + // it clears the reeentrancy lock + assertEq(bPool.call__getLock(), _MUTEX_FREE); + } +} diff --git a/test/unit/BPool/BPool_SwapExactAmountIn.tree b/test/unit/BPool/BPool_SwapExactAmountIn.tree new file mode 100644 index 00000000..8d78fd2e --- /dev/null +++ b/test/unit/BPool/BPool_SwapExactAmountIn.tree @@ -0,0 +1,30 @@ +BPool::SwapExactAmountIn +├── 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 amount in exceeds max allowed ratio +│ └── it should revert +├── when spot price before swap exceeds maxPrice +│ └── it should revert +├── when calculated token amount out is less than minAmountOut +│ └── it should revert +├── when spot price after swap exceeds spot price before swap +│ └── it should revert +├── when spot price after swap exceeds maxPrice +│ └── it should revert +├── when spot price before swap exceeds token ratio after 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 tokenOut amount swapped + ├── it returns the spot price after the swap + └── it clears the reeentrancy lock From d00b1eb3e192f9fda0a09ff6ed2416c9932583cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wei=C3=9Fer=20Hase?= Date: Fri, 12 Jul 2024 09:40:25 +0200 Subject: [PATCH 26/48] feat: adding bCoW deployment snapshots and updating snaps (#151) * feat: adding bCoW deployment snapshots and updating snaps * fix: re-updated snaps * fix: manually updating BFactory snap to match --- .forge-snapshots/newBCoWFactory.snap | 1 + .forge-snapshots/newBCoWPool.snap | 1 + .github/workflows/ci.yml | 16 +++++++-------- test/integration/DeploymentGas.t.sol | 29 ++++++++++++++++++++++++---- 4 files changed, 35 insertions(+), 12 deletions(-) create mode 100644 .forge-snapshots/newBCoWFactory.snap create mode 100644 .forge-snapshots/newBCoWPool.snap diff --git a/.forge-snapshots/newBCoWFactory.snap b/.forge-snapshots/newBCoWFactory.snap new file mode 100644 index 00000000..65bdb362 --- /dev/null +++ b/.forge-snapshots/newBCoWFactory.snap @@ -0,0 +1 @@ +4889580 \ No newline at end of file diff --git a/.forge-snapshots/newBCoWPool.snap b/.forge-snapshots/newBCoWPool.snap new file mode 100644 index 00000000..6916b44e --- /dev/null +++ b/.forge-snapshots/newBCoWPool.snap @@ -0,0 +1 @@ +4033896 \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0b0cee1d..fb56bc60 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,20 +18,20 @@ jobs: - uses: actions/checkout@v3 - name: Install Foundry - uses: foundry-rs/foundry-toolchain@v1 + uses: foundry-rs/foundry-toolchain@v1.2.0 with: version: nightly - name: Use Node.js uses: actions/setup-node@v3 with: - node-version: 18.x + node-version: 20.x cache: 'yarn' - name: Install dependencies run: yarn --frozen-lockfile --network-concurrency 1 - - name: Precompile using 0.8.14 and via-ir=false + - name: Precompile contracts run: yarn build - name: Run tests @@ -45,20 +45,20 @@ jobs: - uses: actions/checkout@v3 - name: Install Foundry - uses: foundry-rs/foundry-toolchain@v1 + uses: foundry-rs/foundry-toolchain@v1.2.0 with: version: nightly - name: Use Node.js uses: actions/setup-node@v3 with: - node-version: 18.x + node-version: 20.x cache: 'yarn' - name: Install dependencies run: yarn --frozen-lockfile --network-concurrency 1 - - name: Precompile using 0.8.14 and via-ir=false + - name: Precompile contracts run: yarn build - name: Run tests @@ -77,14 +77,14 @@ jobs: - uses: wagoid/commitlint-github-action@v5 - name: Install Foundry - uses: foundry-rs/foundry-toolchain@v1 + uses: foundry-rs/foundry-toolchain@v1.2.0 with: version: nightly - name: Use Node.js uses: actions/setup-node@v3 with: - node-version: 18.x + node-version: 20.x cache: 'yarn' - name: Install bulloak diff --git a/test/integration/DeploymentGas.t.sol b/test/integration/DeploymentGas.t.sol index e2b32d95..d4604024 100644 --- a/test/integration/DeploymentGas.t.sol +++ b/test/integration/DeploymentGas.t.sol @@ -1,27 +1,48 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.25; +import {BCoWFactory} from 'contracts/BCoWFactory.sol'; import {BFactory} from 'contracts/BFactory.sol'; import {GasSnapshot} from 'forge-gas-snapshot/GasSnapshot.sol'; import {Test} from 'forge-std/Test.sol'; +contract MockSolutionSettler { + address public vaultRelayer; + bytes32 public domainSeparator; +} + contract DeploymentIntegrationGasTest is Test, GasSnapshot { - BFactory public factory; + BFactory public bFactory; + BCoWFactory public bCowFactory; + address solutionSettler; + address deployer = makeAddr('deployer'); function setUp() public { - factory = new BFactory(); + vm.startPrank(deployer); + bFactory = new BFactory(); + + solutionSettler = address(new MockSolutionSettler()); + bCowFactory = new BCoWFactory(solutionSettler, bytes32('appData')); } function testFactoryDeployment() public { snapStart('newBFactory'); new BFactory(); snapEnd(); + + snapStart('newBCoWFactory'); + new BCoWFactory(solutionSettler, bytes32('appData')); + snapEnd(); } - function testDeployment() public { + function testPoolDeployment() public { snapStart('newBPool'); - factory.newBPool(); + bFactory.newBPool(); + snapEnd(); + + snapStart('newBCoWPool'); + bCowFactory.newBPool(); snapEnd(); } } From 4a4f8d3ea44658225023029d965fc6bb9c0ffdd1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wei=C3=9Fer=20Hase?= Date: Mon, 15 Jul 2024 19:42:23 +0200 Subject: [PATCH 27/48] fix: return variable names consistency (#156) * fix: return variable names consistency * fix: rm explicit returns when named * fix: mocks --- .forge-snapshots/newBCoWFactory.snap | 2 +- .forge-snapshots/newBCoWPool.snap | 2 +- .forge-snapshots/newBFactory.snap | 2 +- .forge-snapshots/newBPool.snap | 2 +- .forge-snapshots/swapExactAmountIn.snap | 2 +- .../swapExactAmountInInverse.snap | 2 +- src/contracts/BPool.sol | 32 +++++-------------- src/contracts/BToken.sol | 4 +-- test/manual-smock/MockBCoWPool.sol | 22 +++++++------ test/smock/MockBPool.sol | 22 +++++++------ 10 files changed, 40 insertions(+), 52 deletions(-) diff --git a/.forge-snapshots/newBCoWFactory.snap b/.forge-snapshots/newBCoWFactory.snap index 65bdb362..5f4e5d08 100644 --- a/.forge-snapshots/newBCoWFactory.snap +++ b/.forge-snapshots/newBCoWFactory.snap @@ -1 +1 @@ -4889580 \ No newline at end of file +4890648 \ No newline at end of file diff --git a/.forge-snapshots/newBCoWPool.snap b/.forge-snapshots/newBCoWPool.snap index 6916b44e..5c3c9e3f 100644 --- a/.forge-snapshots/newBCoWPool.snap +++ b/.forge-snapshots/newBCoWPool.snap @@ -1 +1 @@ -4033896 \ No newline at end of file +4034907 \ No newline at end of file diff --git a/.forge-snapshots/newBFactory.snap b/.forge-snapshots/newBFactory.snap index 95e984ca..d28ec6cf 100644 --- a/.forge-snapshots/newBFactory.snap +++ b/.forge-snapshots/newBFactory.snap @@ -1 +1 @@ -4130621 \ No newline at end of file +4131877 \ No newline at end of file diff --git a/.forge-snapshots/newBPool.snap b/.forge-snapshots/newBPool.snap index 0f67b99b..e4fc526d 100644 --- a/.forge-snapshots/newBPool.snap +++ b/.forge-snapshots/newBPool.snap @@ -1 +1 @@ -3477592 \ No newline at end of file +3478592 \ No newline at end of file diff --git a/.forge-snapshots/swapExactAmountIn.snap b/.forge-snapshots/swapExactAmountIn.snap index 6273edbd..ec541304 100644 --- a/.forge-snapshots/swapExactAmountIn.snap +++ b/.forge-snapshots/swapExactAmountIn.snap @@ -1 +1 @@ -104914 \ No newline at end of file +104920 \ No newline at end of file diff --git a/.forge-snapshots/swapExactAmountInInverse.snap b/.forge-snapshots/swapExactAmountInInverse.snap index e2f5743c..004cff8a 100644 --- a/.forge-snapshots/swapExactAmountInInverse.snap +++ b/.forge-snapshots/swapExactAmountInInverse.snap @@ -1 +1 @@ -114583 \ No newline at end of file +114589 \ No newline at end of file diff --git a/src/contracts/BPool.sol b/src/contracts/BPool.sol index 2af48596..7db5cc73 100644 --- a/src/contracts/BPool.sol +++ b/src/contracts/BPool.sol @@ -280,8 +280,6 @@ contract BPool is BToken, BMath, IBPool { _pullUnderlying(tokenIn, msg.sender, tokenAmountIn); _pushUnderlying(tokenOut, msg.sender, tokenAmountOut); - - return (tokenAmountOut, spotPriceAfter); } /// @inheritdoc IBPool @@ -339,8 +337,6 @@ contract BPool is BToken, BMath, IBPool { _pullUnderlying(tokenIn, msg.sender, tokenAmountIn); _pushUnderlying(tokenOut, msg.sender, tokenAmountOut); - - return (tokenAmountIn, spotPriceAfter); } /// @inheritdoc IBPool @@ -370,8 +366,6 @@ contract BPool is BToken, BMath, IBPool { _mintPoolShare(poolAmountOut); _pushPoolShare(msg.sender, poolAmountOut); _pullUnderlying(tokenIn, msg.sender, tokenAmountIn); - - return poolAmountOut; } /// @inheritdoc IBPool @@ -405,8 +399,6 @@ contract BPool is BToken, BMath, IBPool { _mintPoolShare(poolAmountOut); _pushPoolShare(msg.sender, poolAmountOut); _pullUnderlying(tokenIn, msg.sender, tokenAmountIn); - - return tokenAmountIn; } /// @inheritdoc IBPool @@ -440,8 +432,6 @@ contract BPool is BToken, BMath, IBPool { _burnPoolShare(bsub(poolAmountIn, exitFee)); _pushPoolShare(FACTORY, exitFee); _pushUnderlying(tokenOut, msg.sender, tokenAmountOut); - - return tokenAmountOut; } /// @inheritdoc IBPool @@ -477,12 +467,10 @@ contract BPool is BToken, BMath, IBPool { _burnPoolShare(bsub(poolAmountIn, exitFee)); _pushPoolShare(FACTORY, exitFee); _pushUnderlying(tokenOut, msg.sender, tokenAmountOut); - - return poolAmountIn; } /// @inheritdoc IBPool - function getSpotPrice(address tokenIn, address tokenOut) external view _viewlock_ returns (uint256 spotPrice) { + function getSpotPrice(address tokenIn, address tokenOut) external view _viewlock_ returns (uint256) { if (!_records[tokenIn].bound) { revert BPool_TokenNotBound(); } @@ -492,19 +480,17 @@ contract BPool is BToken, BMath, IBPool { Record storage inRecord = _records[tokenIn]; Record storage outRecord = _records[tokenOut]; - spotPrice = calcSpotPrice( + return calcSpotPrice( IERC20(tokenIn).balanceOf(address(this)), inRecord.denorm, IERC20(tokenOut).balanceOf(address(this)), outRecord.denorm, _swapFee ); - - return spotPrice; } /// @inheritdoc IBPool - function getSpotPriceSansFee(address tokenIn, address tokenOut) external view _viewlock_ returns (uint256 spotPrice) { + function getSpotPriceSansFee(address tokenIn, address tokenOut) external view _viewlock_ returns (uint256) { if (!_records[tokenIn].bound) { revert BPool_TokenNotBound(); } @@ -514,15 +500,13 @@ contract BPool is BToken, BMath, IBPool { Record storage inRecord = _records[tokenIn]; Record storage outRecord = _records[tokenOut]; - spotPrice = calcSpotPrice( + return calcSpotPrice( IERC20(tokenIn).balanceOf(address(this)), inRecord.denorm, IERC20(tokenOut).balanceOf(address(this)), outRecord.denorm, 0 ); - - return spotPrice; } /// @inheritdoc IBPool @@ -531,8 +515,8 @@ contract BPool is BToken, BMath, IBPool { } /// @inheritdoc IBPool - function isBound(address t) external view returns (bool) { - return _records[t].bound; + function isBound(address token) external view returns (bool) { + return _records[token].bound; } /// @inheritdoc IBPool @@ -541,12 +525,12 @@ contract BPool is BToken, BMath, IBPool { } /// @inheritdoc IBPool - function getCurrentTokens() external view _viewlock_ returns (address[] memory tokens) { + function getCurrentTokens() external view _viewlock_ returns (address[] memory) { return _tokens; } /// @inheritdoc IBPool - function getFinalTokens() external view _viewlock_ _finalized_ returns (address[] memory tokens) { + function getFinalTokens() external view _viewlock_ _finalized_ returns (address[] memory) { return _tokens; } diff --git a/src/contracts/BToken.sol b/src/contracts/BToken.sol index 0491b17d..157a4440 100644 --- a/src/contracts/BToken.sol +++ b/src/contracts/BToken.sol @@ -18,7 +18,7 @@ contract BToken is ERC20 { */ function increaseApproval(address spender, uint256 amount) external returns (bool success) { _approve(msg.sender, spender, allowance(msg.sender, spender) + amount); - return true; + success = true; } /** @@ -34,7 +34,7 @@ contract BToken is ERC20 { } else { _approve(msg.sender, spender, oldValue - amount); } - return true; + success = true; } /** diff --git a/test/manual-smock/MockBCoWPool.sol b/test/manual-smock/MockBCoWPool.sol index de851269..5ce2c703 100644 --- a/test/manual-smock/MockBCoWPool.sol +++ b/test/manual-smock/MockBCoWPool.sol @@ -226,17 +226,19 @@ contract MockBCoWPool is BCoWPool, Test { ); } - function mock_call_getSpotPrice(address tokenIn, address tokenOut, uint256 spotPrice) public { + function mock_call_getSpotPrice(address tokenIn, address tokenOut, uint256 _returnParam0) public { vm.mockCall( - address(this), abi.encodeWithSignature('getSpotPrice(address,address)', tokenIn, tokenOut), abi.encode(spotPrice) + address(this), + abi.encodeWithSignature('getSpotPrice(address,address)', tokenIn, tokenOut), + abi.encode(_returnParam0) ); } - function mock_call_getSpotPriceSansFee(address tokenIn, address tokenOut, uint256 spotPrice) public { + function mock_call_getSpotPriceSansFee(address tokenIn, address tokenOut, uint256 _returnParam0) public { vm.mockCall( address(this), abi.encodeWithSignature('getSpotPriceSansFee(address,address)', tokenIn, tokenOut), - abi.encode(spotPrice) + abi.encode(_returnParam0) ); } @@ -244,20 +246,20 @@ contract MockBCoWPool is BCoWPool, Test { vm.mockCall(address(this), abi.encodeWithSignature('isFinalized()'), abi.encode(_returnParam0)); } - function mock_call_isBound(address t, bool _returnParam0) public { - vm.mockCall(address(this), abi.encodeWithSignature('isBound(address)', t), abi.encode(_returnParam0)); + function mock_call_isBound(address token, bool _returnParam0) public { + vm.mockCall(address(this), abi.encodeWithSignature('isBound(address)', token), abi.encode(_returnParam0)); } function mock_call_getNumTokens(uint256 _returnParam0) public { vm.mockCall(address(this), abi.encodeWithSignature('getNumTokens()'), abi.encode(_returnParam0)); } - function mock_call_getCurrentTokens(address[] memory tokens) public { - vm.mockCall(address(this), abi.encodeWithSignature('getCurrentTokens()'), abi.encode(tokens)); + function mock_call_getCurrentTokens(address[] memory _returnParam0) public { + vm.mockCall(address(this), abi.encodeWithSignature('getCurrentTokens()'), abi.encode(_returnParam0)); } - function mock_call_getFinalTokens(address[] memory tokens) public { - vm.mockCall(address(this), abi.encodeWithSignature('getFinalTokens()'), abi.encode(tokens)); + function mock_call_getFinalTokens(address[] memory _returnParam0) public { + vm.mockCall(address(this), abi.encodeWithSignature('getFinalTokens()'), abi.encode(_returnParam0)); } function mock_call_getDenormalizedWeight(address token, uint256 _returnParam0) public { diff --git a/test/smock/MockBPool.sol b/test/smock/MockBPool.sol index a4eb6d08..4bb4ef49 100644 --- a/test/smock/MockBPool.sol +++ b/test/smock/MockBPool.sol @@ -191,17 +191,19 @@ contract MockBPool is BPool, Test { ); } - function mock_call_getSpotPrice(address tokenIn, address tokenOut, uint256 spotPrice) public { + function mock_call_getSpotPrice(address tokenIn, address tokenOut, uint256 _returnParam0) public { vm.mockCall( - address(this), abi.encodeWithSignature('getSpotPrice(address,address)', tokenIn, tokenOut), abi.encode(spotPrice) + address(this), + abi.encodeWithSignature('getSpotPrice(address,address)', tokenIn, tokenOut), + abi.encode(_returnParam0) ); } - function mock_call_getSpotPriceSansFee(address tokenIn, address tokenOut, uint256 spotPrice) public { + function mock_call_getSpotPriceSansFee(address tokenIn, address tokenOut, uint256 _returnParam0) public { vm.mockCall( address(this), abi.encodeWithSignature('getSpotPriceSansFee(address,address)', tokenIn, tokenOut), - abi.encode(spotPrice) + abi.encode(_returnParam0) ); } @@ -209,20 +211,20 @@ contract MockBPool is BPool, Test { vm.mockCall(address(this), abi.encodeWithSignature('isFinalized()'), abi.encode(_returnParam0)); } - function mock_call_isBound(address t, bool _returnParam0) public { - vm.mockCall(address(this), abi.encodeWithSignature('isBound(address)', t), abi.encode(_returnParam0)); + function mock_call_isBound(address token, bool _returnParam0) public { + vm.mockCall(address(this), abi.encodeWithSignature('isBound(address)', token), abi.encode(_returnParam0)); } function mock_call_getNumTokens(uint256 _returnParam0) public { vm.mockCall(address(this), abi.encodeWithSignature('getNumTokens()'), abi.encode(_returnParam0)); } - function mock_call_getCurrentTokens(address[] memory tokens) public { - vm.mockCall(address(this), abi.encodeWithSignature('getCurrentTokens()'), abi.encode(tokens)); + function mock_call_getCurrentTokens(address[] memory _returnParam0) public { + vm.mockCall(address(this), abi.encodeWithSignature('getCurrentTokens()'), abi.encode(_returnParam0)); } - function mock_call_getFinalTokens(address[] memory tokens) public { - vm.mockCall(address(this), abi.encodeWithSignature('getFinalTokens()'), abi.encode(tokens)); + function mock_call_getFinalTokens(address[] memory _returnParam0) public { + vm.mockCall(address(this), abi.encodeWithSignature('getFinalTokens()'), abi.encode(_returnParam0)); } function mock_call_getDenormalizedWeight(address token, uint256 _returnParam0) public { From 28fbf44e2ab44fe67c232871edb3ee2ac19de027 Mon Sep 17 00:00:00 2001 From: teddy Date: Tue, 16 Jul 2024 10:13:50 -0300 Subject: [PATCH 28/48] 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 29/48] 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); From dfbed2610ff249b033f9b938019e647f969750b2 Mon Sep 17 00:00:00 2001 From: teddy Date: Mon, 22 Jul 2024 06:35:31 -0300 Subject: [PATCH 30/48] feat: adding btt test for joinswap extern amount in (#164) * 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/unit/BPool.t.sol | 180 ------------------ test/unit/BPool/BPoolBase.sol | 3 - test/unit/BPool/BPool_Bind.t.sol | 2 + .../BPool/BPool_JoinswapExternAmountIn.t.sol | 95 +++++++++ .../BPool/BPool_JoinswapExternAmountIn.tree | 20 ++ test/unit/BPool/BPool_Unbind.t.sol | 2 + 6 files changed, 119 insertions(+), 183 deletions(-) create mode 100644 test/unit/BPool/BPool_JoinswapExternAmountIn.t.sol create mode 100644 test/unit/BPool/BPool_JoinswapExternAmountIn.tree diff --git a/test/unit/BPool.t.sol b/test/unit/BPool.t.sol index 97ad7488..096e2889 100644 --- a/test/unit/BPool.t.sol +++ b/test/unit/BPool.t.sol @@ -819,186 +819,6 @@ contract BPool_Unit_GetSpotPriceSansFee is BasePoolTest { } } -contract BPool_Unit_JoinswapExternAmountIn is BasePoolTest { - address tokenIn; - - struct JoinswapExternAmountIn_FuzzScenario { - uint256 tokenAmountIn; - uint256 tokenInBalance; - uint256 tokenInDenorm; - uint256 totalSupply; - uint256 totalWeight; - uint256 swapFee; - } - - function _setValues(JoinswapExternAmountIn_FuzzScenario memory _fuzz) internal { - tokenIn = tokens[0]; - - // Create mocks for tokenIn - _mockTransferFrom(tokenIn); - - // Set balances - _setRecord( - tokenIn, - IBPool.Record({ - bound: true, - index: 0, // NOTE: irrelevant for this method - denorm: _fuzz.tokenInDenorm - }) - ); - _mockPoolBalance(tokenIn, _fuzz.tokenInBalance); - - // Set swapFee - _setSwapFee(_fuzz.swapFee); - // Set finalize - _setFinalize(true); - // Set totalSupply - _setTotalSupply(_fuzz.totalSupply); - // Set totalWeight - _setTotalWeight(_fuzz.totalWeight); - } - - function _assumeHappyPath(JoinswapExternAmountIn_FuzzScenario memory _fuzz) internal pure { - // safe bound assumptions - _fuzz.tokenInDenorm = bound(_fuzz.tokenInDenorm, 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); - _fuzz.tokenInBalance = bound(_fuzz.tokenInBalance, MIN_BALANCE, type(uint256).max); - - // max - vm.assume(_fuzz.tokenInBalance < type(uint256).max - _fuzz.tokenAmountIn); - - // MAX_IN_RATIO - vm.assume(_fuzz.tokenInBalance < type(uint256).max / MAX_IN_RATIO); - vm.assume(_fuzz.tokenAmountIn <= bmul(_fuzz.tokenInBalance, MAX_IN_RATIO)); - - // internal calculation for calcPoolOutGivenSingleIn - _assumeCalcPoolOutGivenSingleIn( - _fuzz.tokenInDenorm, - _fuzz.tokenInBalance, - _fuzz.tokenAmountIn, - _fuzz.swapFee, - _fuzz.totalWeight, - _fuzz.totalSupply - ); - } - - modifier happyPath(JoinswapExternAmountIn_FuzzScenario memory _fuzz) { - _assumeHappyPath(_fuzz); - _setValues(_fuzz); - _; - } - - function test_Revert_NotFinalized(JoinswapExternAmountIn_FuzzScenario memory _fuzz) public happyPath(_fuzz) { - _setFinalize(false); - - vm.expectRevert(IBPool.BPool_PoolNotFinalized.selector); - bPool.joinswapExternAmountIn(tokenIn, _fuzz.tokenAmountIn, 0); - } - - function test_Revert_NotBound( - JoinswapExternAmountIn_FuzzScenario memory _fuzz, - address _tokenIn - ) public happyPath(_fuzz) { - assumeNotForgeAddress(_tokenIn); - - vm.expectRevert(IBPool.BPool_TokenNotBound.selector); - bPool.joinswapExternAmountIn(_tokenIn, _fuzz.tokenAmountIn, 0); - } - - function test_Revert_TokenAmountInAboveMaxIn(JoinswapExternAmountIn_FuzzScenario memory _fuzz) - public - happyPath(_fuzz) - { - uint256 _tokenAmountIn = bmul(_fuzz.tokenInBalance, MAX_IN_RATIO); - - vm.expectRevert(IBPool.BPool_TokenAmountInAboveMaxRatio.selector); - bPool.joinswapExternAmountIn(tokenIn, _tokenAmountIn + 1, 0); - } - - function test_Revert_PoolAmountOutBelowMinPoolAmountOut( - JoinswapExternAmountIn_FuzzScenario memory _fuzz, - uint256 _minPoolAmountOut - ) public happyPath(_fuzz) { - uint256 _poolAmountIn = calcPoolOutGivenSingleIn( - _fuzz.tokenInBalance, - _fuzz.tokenInDenorm, - _fuzz.totalSupply, - _fuzz.totalWeight, - _fuzz.tokenAmountIn, - _fuzz.swapFee - ); - _minPoolAmountOut = bound(_minPoolAmountOut, _poolAmountIn + 1, type(uint256).max); - - vm.expectRevert(IBPool.BPool_PoolAmountOutBelowMinPoolAmountOut.selector); - bPool.joinswapExternAmountIn(tokenIn, _fuzz.tokenAmountIn, _minPoolAmountOut); - } - - function test_Revert_Reentrancy(JoinswapExternAmountIn_FuzzScenario memory _fuzz) public { - _expectRevertByReentrancy(); - bPool.joinswapExternAmountIn(tokenIn, _fuzz.tokenAmountIn, 0); - } - - function test_Emit_LogJoin(JoinswapExternAmountIn_FuzzScenario memory _fuzz) public happyPath(_fuzz) { - vm.expectEmit(); - emit IBPool.LOG_JOIN(address(this), tokenIn, _fuzz.tokenAmountIn); - - bPool.joinswapExternAmountIn(tokenIn, _fuzz.tokenAmountIn, 0); - } - - function test_Set_ReentrancyLock(JoinswapExternAmountIn_FuzzScenario memory _fuzz) public happyPath(_fuzz) { - _expectSetReentrancyLock(); - bPool.joinswapExternAmountIn(tokenIn, _fuzz.tokenAmountIn, 0); - } - - function test_Mint_PoolShare(JoinswapExternAmountIn_FuzzScenario memory _fuzz) public happyPath(_fuzz) { - (uint256 _poolAmountOut) = bPool.joinswapExternAmountIn(tokenIn, _fuzz.tokenAmountIn, 0); - - assertEq(bPool.totalSupply(), _fuzz.totalSupply + _poolAmountOut); - } - - function test_Push_PoolShare(JoinswapExternAmountIn_FuzzScenario memory _fuzz) public happyPath(_fuzz) { - uint256 _balanceBefore = bPool.balanceOf(address(this)); - - (uint256 _poolAmountOut) = bPool.joinswapExternAmountIn(tokenIn, _fuzz.tokenAmountIn, 0); - - assertEq(bPool.balanceOf(address(this)), _balanceBefore + _poolAmountOut); - } - - function test_Pull_Underlying(JoinswapExternAmountIn_FuzzScenario memory _fuzz) public happyPath(_fuzz) { - vm.expectCall( - address(tokenIn), - abi.encodeWithSelector(IERC20.transferFrom.selector, address(this), address(bPool), _fuzz.tokenAmountIn) - ); - bPool.joinswapExternAmountIn(tokenIn, _fuzz.tokenAmountIn, 0); - } - - function test_Returns_PoolAmountOut(JoinswapExternAmountIn_FuzzScenario memory _fuzz) public happyPath(_fuzz) { - uint256 _expectedPoolAmountOut = calcPoolOutGivenSingleIn( - _fuzz.tokenInBalance, - _fuzz.tokenInDenorm, - _fuzz.totalSupply, - _fuzz.totalWeight, - _fuzz.tokenAmountIn, - _fuzz.swapFee - ); - - (uint256 _poolAmountOut) = bPool.joinswapExternAmountIn(tokenIn, _fuzz.tokenAmountIn, 0); - - assertEq(_poolAmountOut, _expectedPoolAmountOut); - } - - function test_Emit_LogCall(JoinswapExternAmountIn_FuzzScenario memory _fuzz) public happyPath(_fuzz) { - vm.expectEmit(); - bytes memory _data = abi.encodeWithSelector(BPool.joinswapExternAmountIn.selector, tokenIn, _fuzz.tokenAmountIn, 0); - emit IBPool.LOG_CALL(BPool.joinswapExternAmountIn.selector, address(this), _data); - - bPool.joinswapExternAmountIn(tokenIn, _fuzz.tokenAmountIn, 0); - } -} - contract BPool_Unit_JoinswapPoolAmountOut is BasePoolTest { address tokenIn; diff --git a/test/unit/BPool/BPoolBase.sol b/test/unit/BPool/BPoolBase.sol index 0516fcec..113ddffd 100644 --- a/test/unit/BPool/BPoolBase.sol +++ b/test/unit/BPool/BPoolBase.sol @@ -11,9 +11,6 @@ contract BPoolBase is Test, BConst, Utils { MockBPool public bPool; address public deployer = makeAddr('deployer'); - uint256 public tokenWeight = 1e18; - uint256 public totalWeight = 10e18; - function setUp() public virtual { vm.prank(deployer); bPool = new MockBPool(); diff --git a/test/unit/BPool/BPool_Bind.t.sol b/test/unit/BPool/BPool_Bind.t.sol index 0b146ac7..b3570b0b 100644 --- a/test/unit/BPool/BPool_Bind.t.sol +++ b/test/unit/BPool/BPool_Bind.t.sol @@ -7,6 +7,8 @@ import {IBPool} from 'interfaces/IBPool.sol'; contract BPoolBind is BPoolBase { uint256 public tokenBindBalance = 100e18; + uint256 public tokenWeight = 1e18; + uint256 public totalWeight = 10e18; function setUp() public virtual override { super.setUp(); diff --git a/test/unit/BPool/BPool_JoinswapExternAmountIn.t.sol b/test/unit/BPool/BPool_JoinswapExternAmountIn.t.sol new file mode 100644 index 00000000..04981553 --- /dev/null +++ b/test/unit/BPool/BPool_JoinswapExternAmountIn.t.sol @@ -0,0 +1,95 @@ +// 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 BPoolJoinswapExternAmountIn is BPoolBase, BNum { + address public tokenIn; + + // Valid scenario + uint256 public tokenAmountIn = 1e18; + uint256 public tokenInWeight = 2e18; + uint256 public totalWeight = 10e18; + uint256 public tokenInBalance = 50e18; + + // (((tokenAmountIn*(1-(1-tokenInWeight/totalWeight)*MIN_FEE)+tokenInBalance)/tokenInBalance)^(tokenInWeight/totalWeight))*INIT_POOL_SUPPLY - INIT_POOL_SUPPLY + // (((1*(1-(1-2/10)*(10^-6))+50)/50)^(2/10))*100 - 100 + // 0.396837555601045600 + uint256 public expectedPoolOut = 0.3968375556010456e18; + + function setUp() public virtual override { + super.setUp(); + tokenIn = tokens[0]; + bPool.set__finalized(true); + // mint an initial amount of pool shares (expected to happen at _finalize) + bPool.call__mintPoolShare(INIT_POOL_SUPPLY); + bPool.set__tokens(_tokensToMemory()); + bPool.set__totalWeight(totalWeight); + _setRecord(tokenIn, IBPool.Record({bound: true, index: 0, denorm: tokenInWeight})); + vm.mockCall(tokenIn, abi.encodePacked(IERC20.balanceOf.selector), abi.encode(uint256(tokenInBalance))); + } + + function test_RevertWhen_ReentrancyLockIsSet() external { + bPool.call__setLock(_MUTEX_TAKEN); + // it should revert + vm.expectRevert(IBPool.BPool_Reentrancy.selector); + bPool.joinswapExternAmountIn(tokenIn, tokenAmountIn, expectedPoolOut); + } + + function test_RevertWhen_PoolIsNotFinalized() external { + bPool.set__finalized(false); + // it should revert + vm.expectRevert(IBPool.BPool_PoolNotFinalized.selector); + bPool.joinswapExternAmountIn(tokenIn, tokenAmountIn, expectedPoolOut); + } + + function test_RevertWhen_TokenIsNotBound() external { + // it should revert + vm.expectRevert(IBPool.BPool_TokenNotBound.selector); + bPool.joinswapExternAmountIn(makeAddr('unknown token'), tokenAmountIn, expectedPoolOut); + } + + function test_RevertWhen_TokenAmountInExceedsMaxRatio(uint256 amountIn) external { + amountIn = bound(amountIn, bdiv(INIT_POOL_SUPPLY, MAX_IN_RATIO) + 1, type(uint256).max); + // it should revert + vm.expectRevert(IBPool.BPool_TokenAmountInAboveMaxRatio.selector); + bPool.joinswapExternAmountIn(tokenIn, amountIn, expectedPoolOut); + } + + function test_RevertWhen_CalculatedPoolAmountOutIsLessThanExpected(uint256 expectedPoolOut_) external { + expectedPoolOut_ = bound(expectedPoolOut_, expectedPoolOut + 1, type(uint256).max); + // it should revert + vm.expectRevert(IBPool.BPool_PoolAmountOutBelowMinPoolAmountOut.selector); + bPool.joinswapExternAmountIn(tokenIn, tokenAmountIn, expectedPoolOut_); + } + + function test_WhenPreconditionsAreMet() external { + // it sets reentrancy lock + bPool.expectCall__setLock(_MUTEX_TAKEN); + // it queries the contracts token in balance + vm.expectCall(tokenIn, abi.encodeCall(IERC20.balanceOf, (address(bPool)))); + // it calls _pullUnderlying for token + bPool.mock_call__pullUnderlying(tokenIn, address(this), tokenAmountIn); + bPool.expectCall__pullUnderlying(tokenIn, address(this), tokenAmountIn); + // it mints the pool shares + bPool.expectCall__mintPoolShare(expectedPoolOut); + // it sends pool shares to caller + bPool.expectCall__pushPoolShare(address(this), expectedPoolOut); + // it emits LOG_CALL event + bytes memory _data = + abi.encodeWithSelector(IBPool.joinswapExternAmountIn.selector, tokenIn, tokenAmountIn, expectedPoolOut); + vm.expectEmit(); + emit IBPool.LOG_CALL(IBPool.joinswapExternAmountIn.selector, address(this), _data); + // it emits LOG_JOIN event for token + vm.expectEmit(); + emit IBPool.LOG_JOIN(address(this), tokenIn, tokenAmountIn); + bPool.joinswapExternAmountIn(tokenIn, tokenAmountIn, expectedPoolOut); + + // it clears the reentrancy lock + assertEq(_MUTEX_FREE, bPool.call__getLock()); + } +} diff --git a/test/unit/BPool/BPool_JoinswapExternAmountIn.tree b/test/unit/BPool/BPool_JoinswapExternAmountIn.tree new file mode 100644 index 00000000..6e49f3ab --- /dev/null +++ b/test/unit/BPool/BPool_JoinswapExternAmountIn.tree @@ -0,0 +1,20 @@ +BPool::JoinswapExternAmountIn +├── 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 token amount in exceeds max ratio +│ └── it should revert +├── when calculated pool amount out is less than expected +│ └── it should revert +└── when preconditions are met + ├── it emits LOG_CALL event + ├── it sets the reentrancy lock + ├── it queries the contracts token in balance + ├── it emits LOG_JOIN event for token + ├── it calls _pullUnderlying for token + ├── it mints the pool shares + ├── it sends pool shares to caller + └── it clears the reentrancy lock diff --git a/test/unit/BPool/BPool_Unbind.t.sol b/test/unit/BPool/BPool_Unbind.t.sol index 19eb9ced..8741b776 100644 --- a/test/unit/BPool/BPool_Unbind.t.sol +++ b/test/unit/BPool/BPool_Unbind.t.sol @@ -8,6 +8,8 @@ import {IBPool} from 'interfaces/IBPool.sol'; contract BPoolUnbind is BPoolBase { uint256 public boundTokenAmount = 100e18; + uint256 public tokenWeight = 1e18; + uint256 public totalWeight = 10e18; function setUp() public virtual override { super.setUp(); From cb92f9da93011faa65b98d6448adba82492a7479 Mon Sep 17 00:00:00 2001 From: teddy Date: Mon, 22 Jul 2024 13:27:56 -0300 Subject: [PATCH 31/48] 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 From 6c13de8ac3e156baf7e89b7b30563823e38d72cc Mon Sep 17 00:00:00 2001 From: teddy Date: Mon, 22 Jul 2024 13:46:55 -0300 Subject: [PATCH 32/48] feat: adding bcowpool verify btt tests (#155) * 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 * refactor: get rid of _setRecord * test: btt tests for bcowpool.verify * chore: delete preexisting unit tests * chore: testcase renaming from review * chore: get rid of _setTokens altogether * test: fuzz all possible valid order.sellAmount values * chore: rename correctOrder -> validOrder * fix: rename base file so it is skipped by coverage * test: ensure verify asks for ERC20 balances * chore: make bun happy --- test/unit/BCoWPool.t.sol | 143 +----------------- test/unit/BCoWPool/BCoWPoolBase.t.sol | 28 ++++ test/unit/BCoWPool/BCoWPool_Verify.t.sol | 132 ++++++++++++++++ test/unit/BCoWPool/BCoWPool_Verify.tree | 24 +++ test/unit/BPool.t.sol | 131 ++-------------- test/unit/BPool/BPool.t.sol | 4 +- test/unit/BPool/BPoolBase.sol | 12 +- test/unit/BPool/BPool_Bind.t.sol | 2 +- test/unit/BPool/BPool_ExitPool.t.sol | 4 +- .../BPool/BPool_ExitswapPoolAmountIn.t.sol | 2 +- test/unit/BPool/BPool_JoinPool.t.sol | 4 +- .../BPool/BPool_JoinswapExternAmountIn.t.sol | 2 +- test/unit/BPool/BPool_SwapExactAmountIn.t.sol | 4 +- .../unit/BPool/BPool_SwapExactAmountOut.t.sol | 4 +- test/unit/BPool/BPool_Unbind.t.sol | 6 +- 15 files changed, 212 insertions(+), 290 deletions(-) create mode 100644 test/unit/BCoWPool/BCoWPoolBase.t.sol create mode 100644 test/unit/BCoWPool/BCoWPool_Verify.t.sol create mode 100644 test/unit/BCoWPool/BCoWPool_Verify.tree diff --git a/test/unit/BCoWPool.t.sol b/test/unit/BCoWPool.t.sol index 7c7e566c..0a73ea41 100644 --- a/test/unit/BCoWPool.t.sol +++ b/test/unit/BCoWPool.t.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.25; -import {BasePoolTest, SwapExactAmountInUtils} from './BPool.t.sol'; +import {BasePoolTest} from './BPool.t.sol'; import {IERC20} from '@cowprotocol/interfaces/IERC20.sol'; import {GPv2Order} from '@cowprotocol/libraries/GPv2Order.sol'; import {IERC1271} from '@openzeppelin/contracts/interfaces/IERC1271.sol'; @@ -142,147 +142,6 @@ contract BCoWPool_Unit_Commit is BaseCoWPoolTest { } } -contract BCoWPool_Unit_Verify is BaseCoWPoolTest, SwapExactAmountInUtils { - function setUp() public virtual override(BaseCoWPoolTest, BasePoolTest) { - BaseCoWPoolTest.setUp(); - } - - function _assumeHappyPath(SwapExactAmountIn_FuzzScenario memory _fuzz) internal pure override { - // safe bound assumptions - _fuzz.tokenInDenorm = bound(_fuzz.tokenInDenorm, MIN_WEIGHT, MAX_WEIGHT); - _fuzz.tokenOutDenorm = bound(_fuzz.tokenOutDenorm, MIN_WEIGHT, MAX_WEIGHT); - // LP fee when swapping via CoW will always be zero - _fuzz.swapFee = 0; - - // min - max - calcSpotPrice (spotPriceBefore) - _fuzz.tokenInBalance = bound(_fuzz.tokenInBalance, MIN_BALANCE, type(uint256).max / _fuzz.tokenInDenorm); - _fuzz.tokenOutBalance = bound(_fuzz.tokenOutBalance, MIN_BALANCE, type(uint256).max / _fuzz.tokenOutDenorm); - - // MAX_IN_RATIO - vm.assume(_fuzz.tokenAmountIn <= bmul(_fuzz.tokenInBalance, MAX_IN_RATIO)); - - _assumeCalcOutGivenIn(_fuzz.tokenInBalance, _fuzz.tokenAmountIn, _fuzz.swapFee); - uint256 _tokenAmountOut = calcOutGivenIn( - _fuzz.tokenInBalance, - _fuzz.tokenInDenorm, - _fuzz.tokenOutBalance, - _fuzz.tokenOutDenorm, - _fuzz.tokenAmountIn, - _fuzz.swapFee - ); - vm.assume(_tokenAmountOut > BONE); - } - - modifier assumeNotBoundToken(address _token) { - for (uint256 i = 0; i < TOKENS_AMOUNT; i++) { - vm.assume(tokens[i] != _token); - } - _; - } - - function test_Revert_NonBoundBuyToken(address _otherToken) public assumeNotBoundToken(_otherToken) { - GPv2Order.Data memory order = correctOrder; - order.buyToken = IERC20(_otherToken); - vm.expectRevert(IBPool.BPool_TokenNotBound.selector); - bCoWPool.verify(order); - } - - function test_Revert_NonBoundSellToken(address _otherToken) public assumeNotBoundToken(_otherToken) { - GPv2Order.Data memory order = correctOrder; - order.sellToken = IERC20(_otherToken); - vm.expectRevert(IBPool.BPool_TokenNotBound.selector); - bCoWPool.verify(order); - } - - function test_Revert_ReceiverIsNotBCoWPool(address _receiver) public { - vm.assume(_receiver != GPv2Order.RECEIVER_SAME_AS_OWNER); - GPv2Order.Data memory order = correctOrder; - order.receiver = _receiver; - vm.expectRevert(IBCoWPool.BCoWPool_ReceiverIsNotBCoWPool.selector); - bCoWPool.verify(order); - } - - function test_Revert_LargeDurationOrder(uint256 _timeOffset) public { - _timeOffset = bound(_timeOffset, MAX_ORDER_DURATION + 1, type(uint32).max - block.timestamp); - GPv2Order.Data memory order = correctOrder; - order.validTo = uint32(block.timestamp + _timeOffset); - vm.expectRevert(IBCoWPool.BCoWPool_OrderValidityTooLong.selector); - bCoWPool.verify(order); - } - - function test_Revert_NonZeroFee(uint256 _fee) public { - _fee = bound(_fee, 1, type(uint256).max); - GPv2Order.Data memory order = correctOrder; - order.feeAmount = _fee; - vm.expectRevert(IBCoWPool.BCoWPool_FeeMustBeZero.selector); - bCoWPool.verify(order); - } - - function test_Revert_InvalidOrderKind(bytes32 _orderKind) public { - vm.assume(_orderKind != GPv2Order.KIND_SELL); - GPv2Order.Data memory order = correctOrder; - order.kind = _orderKind; - vm.expectRevert(IBCoWPool.BCoWPool_InvalidOperation.selector); - bCoWPool.verify(order); - } - - function test_Revert_InvalidBuyBalanceKind(bytes32 _balanceKind) public { - vm.assume(_balanceKind != GPv2Order.BALANCE_ERC20); - GPv2Order.Data memory order = correctOrder; - order.buyTokenBalance = _balanceKind; - vm.expectRevert(IBCoWPool.BCoWPool_InvalidBalanceMarker.selector); - bCoWPool.verify(order); - } - - function test_Revert_InvalidSellBalanceKind(bytes32 _balanceKind) public { - vm.assume(_balanceKind != GPv2Order.BALANCE_ERC20); - GPv2Order.Data memory order = correctOrder; - order.sellTokenBalance = _balanceKind; - vm.expectRevert(IBCoWPool.BCoWPool_InvalidBalanceMarker.selector); - bCoWPool.verify(order); - } - - function test_Revert_TokenAmountInAboveMaxIn( - SwapExactAmountIn_FuzzScenario memory _fuzz, - uint256 _offset - ) public happyPath(_fuzz) { - _offset = bound(_offset, 1, type(uint256).max - _fuzz.tokenInBalance); - uint256 _tokenAmountIn = bmul(_fuzz.tokenInBalance, MAX_IN_RATIO) + _offset; - GPv2Order.Data memory order = correctOrder; - order.buyAmount = _tokenAmountIn; - - vm.expectRevert(IBPool.BPool_TokenAmountInAboveMaxRatio.selector); - bCoWPool.verify(order); - } - - function test_Revert_InsufficientReturn( - SwapExactAmountIn_FuzzScenario memory _fuzz, - uint256 _offset - ) public happyPath(_fuzz) { - uint256 _tokenAmountOut = calcOutGivenIn( - _fuzz.tokenInBalance, _fuzz.tokenInDenorm, _fuzz.tokenOutBalance, _fuzz.tokenOutDenorm, _fuzz.tokenAmountIn, 0 - ); - _offset = bound(_offset, 1, _tokenAmountOut); - GPv2Order.Data memory order = correctOrder; - order.buyAmount = _fuzz.tokenAmountIn; - order.sellAmount = _tokenAmountOut + _offset; - - vm.expectRevert(IBPool.BPool_TokenAmountOutBelowMinOut.selector); - bCoWPool.verify(order); - } - - function test_Success_HappyPath(SwapExactAmountIn_FuzzScenario memory _fuzz) public happyPath(_fuzz) { - uint256 _tokenAmountOut = calcOutGivenIn( - _fuzz.tokenInBalance, _fuzz.tokenInDenorm, _fuzz.tokenOutBalance, _fuzz.tokenOutDenorm, _fuzz.tokenAmountIn, 0 - ); - GPv2Order.Data memory order = correctOrder; - order.buyAmount = _fuzz.tokenAmountIn; - order.sellAmount = _tokenAmountOut; - - bCoWPool.verify(order); - } -} - contract BCoWPool_Unit_IsValidSignature is BaseCoWPoolTest { function setUp() public virtual override { super.setUp(); diff --git a/test/unit/BCoWPool/BCoWPoolBase.t.sol b/test/unit/BCoWPool/BCoWPoolBase.t.sol new file mode 100644 index 00000000..3418e59e --- /dev/null +++ b/test/unit/BCoWPool/BCoWPoolBase.t.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.25; + +import {BPoolBase} from '../BPool/BPoolBase.sol'; +import {BCoWConst} from 'contracts/BCoWConst.sol'; +import {BNum} from 'contracts/BNum.sol'; + +import {ISettlement} from 'interfaces/ISettlement.sol'; +import {MockBCoWPool} from 'test/manual-smock/MockBCoWPool.sol'; + +contract BCoWPoolBase is BPoolBase, BCoWConst, BNum { + bytes32 public appData = bytes32('appData'); + address public cowSolutionSettler = makeAddr('cowSolutionSettler'); + bytes32 public domainSeparator = bytes32(bytes2(0xf00b)); + address public vaultRelayer = makeAddr('vaultRelayer'); + address public tokenIn; + address public tokenOut; + MockBCoWPool bCoWPool; + + function setUp() public virtual override { + super.setUp(); + tokenIn = tokens[0]; + tokenOut = tokens[1]; + vm.mockCall(cowSolutionSettler, abi.encodePacked(ISettlement.domainSeparator.selector), abi.encode(domainSeparator)); + vm.mockCall(cowSolutionSettler, abi.encodePacked(ISettlement.vaultRelayer.selector), abi.encode(vaultRelayer)); + bCoWPool = new MockBCoWPool(cowSolutionSettler, appData); + } +} diff --git a/test/unit/BCoWPool/BCoWPool_Verify.t.sol b/test/unit/BCoWPool/BCoWPool_Verify.t.sol new file mode 100644 index 00000000..3af73f50 --- /dev/null +++ b/test/unit/BCoWPool/BCoWPool_Verify.t.sol @@ -0,0 +1,132 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.25; + +import {IERC20} from '@cowprotocol/interfaces/IERC20.sol'; +import {GPv2Order} from '@cowprotocol/libraries/GPv2Order.sol'; + +import {BCoWPoolBase} from './BCoWPoolBase.t.sol'; +import {IBCoWPool} from 'interfaces/IBCoWPool.sol'; +import {IBPool} from 'interfaces/IBPool.sol'; + +contract BCoWPoolVerify is BCoWPoolBase { + // Valid scenario: + uint256 public tokenAmountIn = 1e18; + uint256 public tokenInBalance = 100e18; + uint256 public tokenOutBalance = 80e18; + // pool is expected to keep 4X the value of tokenIn than tokenOut + uint256 public tokenInWeight = 4e18; + uint256 public tokenOutWeight = 1e18; + // from bmath: (with fee zero) 80*(1-(100/(100+1))^(4)) + uint256 public expectedAmountOut = 3.12157244137469736e18; + GPv2Order.Data validOrder; + + function setUp() public virtual override { + super.setUp(); + bCoWPool.set__tokens(tokens); + bCoWPool.set__records(tokenIn, IBPool.Record({bound: true, index: 0, denorm: tokenInWeight})); + bCoWPool.set__records(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))); + + validOrder = GPv2Order.Data({ + sellToken: IERC20(tokenOut), + buyToken: IERC20(tokenIn), + receiver: GPv2Order.RECEIVER_SAME_AS_OWNER, + sellAmount: expectedAmountOut, + buyAmount: tokenAmountIn, + validTo: uint32(block.timestamp + 1 minutes), + appData: appData, + feeAmount: 0, + kind: GPv2Order.KIND_SELL, + partiallyFillable: false, + sellTokenBalance: GPv2Order.BALANCE_ERC20, + buyTokenBalance: GPv2Order.BALANCE_ERC20 + }); + } + + function test_RevertWhen_BuyTokenIsNotBound() external { + validOrder.buyToken = IERC20(makeAddr('unknown token')); + // it should revert + vm.expectRevert(IBPool.BPool_TokenNotBound.selector); + bCoWPool.verify(validOrder); + } + + function test_RevertWhen_SellTokenIsNotBound() external { + validOrder.sellToken = IERC20(makeAddr('unknown token')); + // it should revert + vm.expectRevert(IBPool.BPool_TokenNotBound.selector); + bCoWPool.verify(validOrder); + } + + function test_RevertWhen_OrderReceiverFlagIsNotSameAsOwner() external { + validOrder.receiver = makeAddr('somebodyElse'); + // it should revert + vm.expectRevert(IBCoWPool.BCoWPool_ReceiverIsNotBCoWPool.selector); + bCoWPool.verify(validOrder); + } + + function test_RevertWhen_OrderValidityIsTooLong(uint256 _timeOffset) external { + _timeOffset = bound(_timeOffset, MAX_ORDER_DURATION + 1, type(uint32).max - block.timestamp); + validOrder.validTo = uint32(block.timestamp + _timeOffset); + // it should revert + vm.expectRevert(IBCoWPool.BCoWPool_OrderValidityTooLong.selector); + bCoWPool.verify(validOrder); + } + + function test_RevertWhen_FeeAmountIsNotZero(uint256 _fee) external { + _fee = bound(_fee, 1, type(uint256).max); + validOrder.feeAmount = _fee; + // it should revert + vm.expectRevert(IBCoWPool.BCoWPool_FeeMustBeZero.selector); + bCoWPool.verify(validOrder); + } + + function test_RevertWhen_OrderKindIsNotKIND_SELL(bytes32 _orderKind) external { + vm.assume(_orderKind != GPv2Order.KIND_SELL); + validOrder.kind = _orderKind; + // it should revert + vm.expectRevert(IBCoWPool.BCoWPool_InvalidOperation.selector); + bCoWPool.verify(validOrder); + } + + function test_RevertWhen_BuyTokenBalanceFlagIsNotERC20Balances(bytes32 _balanceKind) external { + vm.assume(_balanceKind != GPv2Order.BALANCE_ERC20); + validOrder.buyTokenBalance = _balanceKind; + // it should revert + vm.expectRevert(IBCoWPool.BCoWPool_InvalidBalanceMarker.selector); + bCoWPool.verify(validOrder); + } + + function test_RevertWhen_SellTokenBalanceFlagIsNotERC20Balances(bytes32 _balanceKind) external { + vm.assume(_balanceKind != GPv2Order.BALANCE_ERC20); + validOrder.sellTokenBalance = _balanceKind; + // it should revert + vm.expectRevert(IBCoWPool.BCoWPool_InvalidBalanceMarker.selector); + bCoWPool.verify(validOrder); + } + + function test_RevertWhen_OrderBuyAmountExceedsMaxRatio(uint256 _buyAmount) external { + _buyAmount = bound(_buyAmount, bmul(tokenInBalance, MAX_IN_RATIO) + 1, type(uint256).max); + validOrder.buyAmount = _buyAmount; + // it should revert + vm.expectRevert(IBPool.BPool_TokenAmountInAboveMaxRatio.selector); + bCoWPool.verify(validOrder); + } + + function test_RevertWhen_CalculatedTokenAmountOutIsLessThanOrderSellAmount() external { + validOrder.sellAmount += 1; + // it should revert + vm.expectRevert(IBPool.BPool_TokenAmountOutBelowMinOut.selector); + bCoWPool.verify(validOrder); + } + + function test_WhenPreconditionsAreMet(uint256 _sellAmount) external { + _sellAmount = bound(_sellAmount, 0, validOrder.sellAmount); + validOrder.sellAmount = _sellAmount; + // it should query the balance of the buy token + vm.expectCall(tokenIn, abi.encodeCall(IERC20.balanceOf, (address(bCoWPool)))); + // it should query the balance of the sell token + vm.expectCall(tokenOut, abi.encodeCall(IERC20.balanceOf, (address(bCoWPool)))); + bCoWPool.verify(validOrder); + } +} diff --git a/test/unit/BCoWPool/BCoWPool_Verify.tree b/test/unit/BCoWPool/BCoWPool_Verify.tree new file mode 100644 index 00000000..56187a2a --- /dev/null +++ b/test/unit/BCoWPool/BCoWPool_Verify.tree @@ -0,0 +1,24 @@ +BCoWPool::Verify +├── when buyToken is not bound +│ └── it should revert +├── when sellToken is not bound +│ └── it should revert +├── when order receiver flag is not same as owner +│ └── it should revert +├── when order validity is too long +│ └── it should revert +├── when fee amount is not zero +│ └── it should revert +├── when order kind is not KIND_SELL +│ └── it should revert +├── when buy token balance flag is not ERC20 balances +│ └── it should revert +├── when sell token balance flag is not ERC20 balances +│ └── it should revert +├── when order buy amount exceeds max ratio +│ └── it should revert +├── when calculated token amount out is less than order sell amount +│ └── it should revert +└── when preconditions are met + ├── it should query the balance of the buy token + └── it should query the balance of the sell token diff --git a/test/unit/BPool.t.sol b/test/unit/BPool.t.sol index f9828571..7e80c886 100644 --- a/test/unit/BPool.t.sol +++ b/test/unit/BPool.t.sol @@ -34,9 +34,9 @@ abstract contract BasePoolTest is Test, BConst, Utils, BMath { function _setRandomTokens(uint256 _length) internal returns (address[] memory _tokensToAdd) { _tokensToAdd = _getDeterministicTokenArray(_length); for (uint256 i = 0; i < _length; i++) { - _setRecord(_tokensToAdd[i], IBPool.Record({bound: true, index: i, denorm: 0})); + bPool.set__records(_tokensToAdd[i], IBPool.Record({bound: true, index: i, denorm: 0})); } - _setTokens(_tokensToAdd); + bPool.set__tokens(_tokensToAdd); } function _mockTransfer(address _token) internal { @@ -53,14 +53,6 @@ abstract contract BasePoolTest is Test, BConst, Utils, BMath { vm.mockCall(_token, abi.encodeWithSelector(IERC20.balanceOf.selector, address(bPool)), abi.encode(_balance)); } - function _setTokens(address[] memory _tokens) internal { - bPool.set__tokens(_tokens); - } - - function _setRecord(address _token, IBPool.Record memory _record) internal { - bPool.set__records(_token, _record); - } - function _setSwapFee(uint256 _swapFee) internal { bPool.set__swapFee(_swapFee); } @@ -253,111 +245,6 @@ abstract contract BasePoolTest is Test, BConst, Utils, BMath { } } -abstract contract SwapExactAmountInUtils is BasePoolTest { - 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, - 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(SwapExactAmountIn_FuzzScenario memory _fuzz) internal view virtual { - // 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 - max - calcSpotPrice (spotPriceBefore) - _fuzz.tokenInBalance = bound(_fuzz.tokenInBalance, MIN_BALANCE, type(uint256).max / _fuzz.tokenInDenorm); - _fuzz.tokenOutBalance = bound(_fuzz.tokenOutBalance, MIN_BALANCE, 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 (spotPriceBefore) - _assumeCalcSpotPrice( - _fuzz.tokenInBalance, _fuzz.tokenInDenorm, _fuzz.tokenOutBalance, _fuzz.tokenOutDenorm, _fuzz.swapFee - ); - - // 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 - ); - - _assumeCalcOutGivenIn(_fuzz.tokenInBalance, _fuzz.tokenAmountIn, _fuzz.swapFee); - uint256 _tokenAmountOut = calcOutGivenIn( - _fuzz.tokenInBalance, - _fuzz.tokenInDenorm, - _fuzz.tokenOutBalance, - _fuzz.tokenOutDenorm, - _fuzz.tokenAmountIn, - _fuzz.swapFee - ); - vm.assume(_tokenAmountOut > BONE); - - // internal calculation for calcSpotPrice (spotPriceAfter) - _assumeCalcSpotPrice( - _fuzz.tokenInBalance + _fuzz.tokenAmountIn, - _fuzz.tokenInDenorm, - _fuzz.tokenOutBalance - _tokenAmountOut, - _fuzz.tokenOutDenorm, - _fuzz.swapFee - ); - - vm.assume(bmul(_spotPriceBefore, _tokenAmountOut) <= _fuzz.tokenAmountIn); - } - - modifier happyPath(SwapExactAmountIn_FuzzScenario memory _fuzz) { - _assumeHappyPath(_fuzz); - _setValues(_fuzz); - _; - } -} - contract BPool_Unit_GetCurrentTokens is BasePoolTest { function test_Returns_CurrentTokens(uint256 _length) public { vm.assume(_length > 0); @@ -435,7 +322,7 @@ contract BPool_Unit_GetNormalizedWeight is BasePoolTest { _weight = bound(_weight, MIN_WEIGHT, MAX_WEIGHT); _totalWeight = bound(_totalWeight, MIN_WEIGHT, MAX_TOTAL_WEIGHT); vm.assume(_weight < _totalWeight); - _setRecord(_token, IBPool.Record({bound: true, index: 0, denorm: _weight})); + bPool.set__records(_token, IBPool.Record({bound: true, index: 0, denorm: _weight})); _setTotalWeight(_totalWeight); assertEq(bPool.getNormalizedWeight(_token), bdiv(_weight, _totalWeight)); @@ -695,9 +582,9 @@ contract BPool_Unit_GetSpotPrice is BasePoolTest { } function _setValues(GetSpotPrice_FuzzScenario memory _fuzz) internal { - _setRecord(_fuzz.tokenIn, IBPool.Record({bound: true, index: 0, denorm: _fuzz.tokenInDenorm})); + bPool.set__records(_fuzz.tokenIn, IBPool.Record({bound: true, index: 0, denorm: _fuzz.tokenInDenorm})); _mockPoolBalance(_fuzz.tokenIn, _fuzz.tokenInBalance); - _setRecord(_fuzz.tokenOut, IBPool.Record({bound: true, index: 0, denorm: _fuzz.tokenOutDenorm})); + bPool.set__records(_fuzz.tokenOut, IBPool.Record({bound: true, index: 0, denorm: _fuzz.tokenOutDenorm})); _mockPoolBalance(_fuzz.tokenOut, _fuzz.tokenOutBalance); _setSwapFee(_fuzz.swapFee); } @@ -764,9 +651,9 @@ contract BPool_Unit_GetSpotPriceSansFee is BasePoolTest { } function _setValues(GetSpotPriceSansFee_FuzzScenario memory _fuzz) internal { - _setRecord(_fuzz.tokenIn, IBPool.Record({bound: true, index: 0, denorm: _fuzz.tokenInDenorm})); + bPool.set__records(_fuzz.tokenIn, IBPool.Record({bound: true, index: 0, denorm: _fuzz.tokenInDenorm})); _mockPoolBalance(_fuzz.tokenIn, _fuzz.tokenInBalance); - _setRecord(_fuzz.tokenOut, IBPool.Record({bound: true, index: 0, denorm: _fuzz.tokenOutDenorm})); + bPool.set__records(_fuzz.tokenOut, IBPool.Record({bound: true, index: 0, denorm: _fuzz.tokenOutDenorm})); _mockPoolBalance(_fuzz.tokenOut, _fuzz.tokenOutBalance); _setSwapFee(0); } @@ -838,7 +725,7 @@ contract BPool_Unit_JoinswapPoolAmountOut is BasePoolTest { _mockTransferFrom(tokenIn); // Set balances - _setRecord( + bPool.set__records( tokenIn, IBPool.Record({ bound: true, @@ -1070,7 +957,7 @@ contract BPool_Unit_ExitswapExternAmountOut is BasePoolTest { _mockTransfer(tokenOut); // Set balances - _setRecord( + bPool.set__records( tokenOut, IBPool.Record({ bound: true, diff --git a/test/unit/BPool/BPool.t.sol b/test/unit/BPool/BPool.t.sol index b3bebdee..264fef02 100644 --- a/test/unit/BPool/BPool.t.sol +++ b/test/unit/BPool/BPool.t.sol @@ -33,13 +33,13 @@ contract BPool is BPoolBase { } function test_IsBoundWhenTokenIsBound(address _token) external { - _setRecord(_token, IBPool.Record({bound: true, index: 0, denorm: 0})); + bPool.set__records(_token, IBPool.Record({bound: true, index: 0, denorm: 0})); // it returns true assertTrue(bPool.isBound(_token)); } function test_IsBoundWhenTokenIsNOTBound(address _token) external { - _setRecord(_token, IBPool.Record({bound: false, index: 0, denorm: 0})); + bPool.set__records(_token, IBPool.Record({bound: false, index: 0, denorm: 0})); // it returns false assertFalse(bPool.isBound(_token)); } diff --git a/test/unit/BPool/BPoolBase.sol b/test/unit/BPool/BPoolBase.sol index 113ddffd..36851e2c 100644 --- a/test/unit/BPool/BPoolBase.sol +++ b/test/unit/BPool/BPoolBase.sol @@ -21,16 +21,8 @@ contract BPoolBase is Test, BConst, Utils { function _setRandomTokens(uint256 _length) internal returns (address[] memory _tokensToAdd) { _tokensToAdd = _getDeterministicTokenArray(_length); for (uint256 i = 0; i < _length; i++) { - _setRecord(_tokensToAdd[i], IBPool.Record({bound: true, index: i, denorm: 0})); + bPool.set__records(_tokensToAdd[i], IBPool.Record({bound: true, index: i, denorm: 0})); } - _setTokens(_tokensToAdd); - } - - function _setTokens(address[] memory _tokens) internal { - bPool.set__tokens(_tokens); - } - - function _setRecord(address _token, IBPool.Record memory _record) internal { - bPool.set__records(_token, _record); + bPool.set__tokens(_tokensToAdd); } } diff --git a/test/unit/BPool/BPool_Bind.t.sol b/test/unit/BPool/BPool_Bind.t.sol index b3570b0b..c811dfb2 100644 --- a/test/unit/BPool/BPool_Bind.t.sol +++ b/test/unit/BPool/BPool_Bind.t.sol @@ -38,7 +38,7 @@ contract BPoolBind is BPoolBase { } function test_RevertWhen_TokenIsAlreadyBound() external whenCallerIsController { - _setRecord(tokens[0], IBPool.Record({bound: true, index: 0, denorm: tokenWeight})); + bPool.set__records(tokens[0], IBPool.Record({bound: true, index: 0, denorm: tokenWeight})); // it should revert vm.expectRevert(IBPool.BPool_TokenAlreadyBound.selector); bPool.bind(tokens[0], tokenBindBalance, tokenWeight); diff --git a/test/unit/BPool/BPool_ExitPool.t.sol b/test/unit/BPool/BPool_ExitPool.t.sol index fac066ce..858cbef1 100644 --- a/test/unit/BPool/BPool_ExitPool.t.sol +++ b/test/unit/BPool/BPool_ExitPool.t.sol @@ -31,8 +31,8 @@ contract BPoolExitPool is BPoolBase, BNum { bPool.call__mintPoolShare(INIT_POOL_SUPPLY); bPool.set__tokens(_tokensToMemory()); // token weights are not used for all-token exits - _setRecord(tokens[0], IBPool.Record({bound: true, index: 0, denorm: 0})); - _setRecord(tokens[1], IBPool.Record({bound: true, index: 1, denorm: 0})); + bPool.set__records(tokens[0], IBPool.Record({bound: true, index: 0, denorm: 0})); + bPool.set__records(tokens[1], IBPool.Record({bound: true, index: 1, denorm: 0})); // underlying balances are used instead vm.mockCall(tokens[0], abi.encodePacked(IERC20.balanceOf.selector), abi.encode(uint256(token0Balance))); vm.mockCall(tokens[1], abi.encodePacked(IERC20.balanceOf.selector), abi.encode(uint256(token1Balance))); diff --git a/test/unit/BPool/BPool_ExitswapPoolAmountIn.t.sol b/test/unit/BPool/BPool_ExitswapPoolAmountIn.t.sol index 2650fddf..8032cdf7 100644 --- a/test/unit/BPool/BPool_ExitswapPoolAmountIn.t.sol +++ b/test/unit/BPool/BPool_ExitswapPoolAmountIn.t.sol @@ -22,7 +22,7 @@ contract BPoolExitSwapPoolAmountIn is BPoolBase, BMath { function setUp() public virtual override { super.setUp(); tokenOut = tokens[1]; - _setRecord(tokenOut, IBPool.Record({bound: true, index: 0, denorm: tokenOutWeight})); + bPool.set__records(tokenOut, IBPool.Record({bound: true, index: 0, denorm: tokenOutWeight})); bPool.set__tokens(tokens); bPool.set__totalWeight(totalWeight); bPool.set__finalized(true); diff --git a/test/unit/BPool/BPool_JoinPool.t.sol b/test/unit/BPool/BPool_JoinPool.t.sol index 1f501275..eeb01efe 100644 --- a/test/unit/BPool/BPool_JoinPool.t.sol +++ b/test/unit/BPool/BPool_JoinPool.t.sol @@ -27,8 +27,8 @@ contract BPoolJoinPool is BPoolBase { bPool.call__mintPoolShare(INIT_POOL_SUPPLY); bPool.set__tokens(_tokensToMemory()); // token weights are not used for all-token joins - _setRecord(tokens[0], IBPool.Record({bound: true, index: 0, denorm: 0})); - _setRecord(tokens[1], IBPool.Record({bound: true, index: 1, denorm: 0})); + bPool.set__records(tokens[0], IBPool.Record({bound: true, index: 0, denorm: 0})); + bPool.set__records(tokens[1], IBPool.Record({bound: true, index: 1, denorm: 0})); // underlying balances are used instead vm.mockCall(tokens[0], abi.encodePacked(IERC20.balanceOf.selector), abi.encode(uint256(token0Balance))); vm.mockCall(tokens[1], abi.encodePacked(IERC20.balanceOf.selector), abi.encode(uint256(token1Balance))); diff --git a/test/unit/BPool/BPool_JoinswapExternAmountIn.t.sol b/test/unit/BPool/BPool_JoinswapExternAmountIn.t.sol index 04981553..afa67556 100644 --- a/test/unit/BPool/BPool_JoinswapExternAmountIn.t.sol +++ b/test/unit/BPool/BPool_JoinswapExternAmountIn.t.sol @@ -29,7 +29,7 @@ contract BPoolJoinswapExternAmountIn is BPoolBase, BNum { bPool.call__mintPoolShare(INIT_POOL_SUPPLY); bPool.set__tokens(_tokensToMemory()); bPool.set__totalWeight(totalWeight); - _setRecord(tokenIn, IBPool.Record({bound: true, index: 0, denorm: tokenInWeight})); + bPool.set__records(tokenIn, IBPool.Record({bound: true, index: 0, denorm: tokenInWeight})); vm.mockCall(tokenIn, abi.encodePacked(IERC20.balanceOf.selector), abi.encode(uint256(tokenInBalance))); } diff --git a/test/unit/BPool/BPool_SwapExactAmountIn.t.sol b/test/unit/BPool/BPool_SwapExactAmountIn.t.sol index 709b3461..9537bf7f 100644 --- a/test/unit/BPool/BPool_SwapExactAmountIn.t.sol +++ b/test/unit/BPool/BPool_SwapExactAmountIn.t.sol @@ -35,8 +35,8 @@ contract BPoolSwapExactAmountIn is BPoolBase, BNum { 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})); + bPool.set__records(tokenIn, IBPool.Record({bound: true, index: 0, denorm: tokenInWeight})); + bPool.set__records(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))); diff --git a/test/unit/BPool/BPool_SwapExactAmountOut.t.sol b/test/unit/BPool/BPool_SwapExactAmountOut.t.sol index f648b9a9..85e168b0 100644 --- a/test/unit/BPool/BPool_SwapExactAmountOut.t.sol +++ b/test/unit/BPool/BPool_SwapExactAmountOut.t.sol @@ -36,8 +36,8 @@ contract BPoolSwapExactAmountOut is BPoolBase, BNum { 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})); + bPool.set__records(tokenIn, IBPool.Record({bound: true, index: 0, denorm: tokenInWeight})); + bPool.set__records(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))); diff --git a/test/unit/BPool/BPool_Unbind.t.sol b/test/unit/BPool/BPool_Unbind.t.sol index 8741b776..c0c0f008 100644 --- a/test/unit/BPool/BPool_Unbind.t.sol +++ b/test/unit/BPool/BPool_Unbind.t.sol @@ -44,7 +44,7 @@ contract BPoolUnbind is BPoolBase { } function test_RevertWhen_PoolIsFinalized() external whenCallerIsController { - _setRecord(tokens[0], IBPool.Record({bound: true, index: 0, denorm: 0})); + bPool.set__records(tokens[0], IBPool.Record({bound: true, index: 0, denorm: 0})); bPool.set__finalized(true); // it should revert vm.expectRevert(IBPool.BPool_PoolIsFinalized.selector); @@ -52,7 +52,7 @@ contract BPoolUnbind is BPoolBase { } modifier whenTokenCanBeUnbound() { - _setRecord(tokens[0], IBPool.Record({bound: true, index: 0, denorm: tokenWeight})); + bPool.set__records(tokens[0], IBPool.Record({bound: true, index: 0, denorm: tokenWeight})); bPool.set__totalWeight(totalWeight); address[] memory tokens = new address[](1); tokens[0] = tokens[0]; @@ -83,7 +83,7 @@ contract BPoolUnbind is BPoolBase { } function test_WhenTokenIsNOTLastOnTheTokensArray() external whenCallerIsController whenTokenCanBeUnbound { - _setRecord(tokens[1], IBPool.Record({bound: true, index: 0, denorm: tokenWeight})); + bPool.set__records(tokens[1], IBPool.Record({bound: true, index: 0, denorm: tokenWeight})); bPool.set__tokens(_tokensToMemory()); bPool.unbind(tokens[0]); // it removes the token record From 2a0b42939f2b3e09207ea173eef67e34cdb17dca Mon Sep 17 00:00:00 2001 From: teddy Date: Mon, 22 Jul 2024 14:00:23 -0300 Subject: [PATCH 33/48] feat: add btt tests for finalize methods (#159) * 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 * refactor: get rid of _setRecord * test: btt tests for bcowpool.verify * chore: delete preexisting unit tests * chore: testcase renaming from review * chore: get rid of _setTokens altogether * test: fuzz all possible valid order.sellAmount values * chore: rename correctOrder -> validOrder * test: btt tests for bpool.finalize * test: btt tests for bcowpool.finalize * chore: remove preexisting unit tests replaced by ones in this pr * fix: feedback from review * refactor: make caller==controller default scenario * fix: reorganize .tree * fix: feedback from review * fix: feedback from review, calling internal method directly --- test/manual-smock/MockBCoWPool.sol | 39 +++++++++++++++ test/unit/BCoWPool.t.sol | 35 ------------- test/unit/BCoWPool/BCoWPool.t.sol | 47 ++++++++++++++++++ test/unit/BCoWPool/BCoWPool.tree | 8 +++ test/unit/BCoWPool/BCoWPoolBase.sol | 28 +++++++++++ test/unit/BPool/BPool.t.sol | 49 +++++++++++++++++++ test/unit/BPool/BPool.tree | 14 ++++++ test/unit/BPool/BPoolBase.sol | 2 - test/unit/BPool/BPool_Bind.t.sol | 27 +++++----- test/unit/BPool/BPool_Bind.tree | 45 +++++++++-------- test/unit/BPool/BPool_ExitPool.t.sol | 2 +- .../BPool/BPool_ExitswapPoolAmountIn.t.sol | 4 +- test/unit/BPool/BPool_Unbind.t.sol | 19 +++---- test/unit/BPool/BPool_Unbind.tree | 37 +++++++------- 14 files changed, 246 insertions(+), 110 deletions(-) create mode 100644 test/unit/BCoWPool/BCoWPool.t.sol create mode 100644 test/unit/BCoWPool/BCoWPool.tree create mode 100644 test/unit/BCoWPool/BCoWPoolBase.sol diff --git a/test/manual-smock/MockBCoWPool.sol b/test/manual-smock/MockBCoWPool.sol index 5ce2c703..c94a6bb1 100644 --- a/test/manual-smock/MockBCoWPool.sol +++ b/test/manual-smock/MockBCoWPool.sol @@ -359,6 +359,45 @@ contract MockBCoWPool is BCoWPool, Test { vm.expectCall(address(this), abi.encodeWithSignature('_pushUnderlying(address,address,uint256)', token, to, amount)); } + function mock_call__pushPoolShare(address to, uint256 amount) public { + vm.mockCall(address(this), abi.encodeWithSignature('_pushPoolShare(address,uint256)', to, amount), abi.encode()); + } + + function _pushPoolShare(address to, uint256 amount) internal override { + (bool _success, bytes memory _data) = + address(this).call(abi.encodeWithSignature('_pushPoolShare(address,uint256)', to, amount)); + + if (_success) return abi.decode(_data, ()); + else return super._pushPoolShare(to, amount); + } + + function call__pushPoolShare(address to, uint256 amount) public { + return _pushPoolShare(to, amount); + } + + function expectCall__pushPoolShare(address to, uint256 amount) public { + vm.expectCall(address(this), abi.encodeWithSignature('_pushPoolShare(address,uint256)', to, amount)); + } + + function mock_call__mintPoolShare(uint256 amount) public { + vm.mockCall(address(this), abi.encodeWithSignature('_mintPoolShare(uint256)', amount), abi.encode()); + } + + function _mintPoolShare(uint256 amount) internal override { + (bool _success, bytes memory _data) = address(this).call(abi.encodeWithSignature('_mintPoolShare(uint256)', amount)); + + if (_success) return abi.decode(_data, ()); + else return super._mintPoolShare(amount); + } + + function call__mintPoolShare(uint256 amount) public { + return _mintPoolShare(amount); + } + + function expectCall__mintPoolShare(uint256 amount) public { + vm.expectCall(address(this), abi.encodeWithSignature('_mintPoolShare(uint256)', amount)); + } + function call__afterFinalize() public { return _afterFinalize(); } diff --git a/test/unit/BCoWPool.t.sol b/test/unit/BCoWPool.t.sol index 0a73ea41..08b6673b 100644 --- a/test/unit/BCoWPool.t.sol +++ b/test/unit/BCoWPool.t.sol @@ -78,41 +78,6 @@ contract BCoWPool_Unit_Constructor is BaseCoWPoolTest { } } -contract BCoWPool_Unit_Finalize is BaseCoWPoolTest { - function setUp() public virtual override { - super.setUp(); - - for (uint256 i = 0; i < TOKENS_AMOUNT; i++) { - vm.mockCall(tokens[i], abi.encodePacked(IERC20.approve.selector), abi.encode(true)); - } - - vm.mockCall(address(bCoWPool.FACTORY()), abi.encodeWithSelector(IBCoWFactory.logBCoWPool.selector), abi.encode()); - } - - function test_Set_Approvals() public { - for (uint256 i = 0; i < TOKENS_AMOUNT; i++) { - vm.expectCall(tokens[i], abi.encodeCall(IERC20.approve, (vaultRelayer, type(uint256).max)), 1); - } - bCoWPool.finalize(); - } - - function test_Log_IfRevert() public { - vm.mockCallRevert( - address(bCoWPool.FACTORY()), abi.encodeWithSelector(IBCoWFactory.logBCoWPool.selector), abi.encode() - ); - - vm.expectEmit(address(bCoWPool)); - emit IBCoWFactory.COWAMMPoolCreated(address(bCoWPool)); - - bCoWPool.finalize(); - } - - function test_Call_LogBCoWPool() public { - vm.expectCall(address(bCoWPool.FACTORY()), abi.encodeWithSelector(IBCoWFactory.logBCoWPool.selector), 1); - bCoWPool.finalize(); - } -} - contract BCoWPool_Unit_Commit is BaseCoWPoolTest { function test_Revert_NonSolutionSettler(address sender, bytes32 orderHash) public { vm.assume(sender != cowSolutionSettler); diff --git a/test/unit/BCoWPool/BCoWPool.t.sol b/test/unit/BCoWPool/BCoWPool.t.sol new file mode 100644 index 00000000..b596a622 --- /dev/null +++ b/test/unit/BCoWPool/BCoWPool.t.sol @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.25; + +import {IERC20} from '@cowprotocol/interfaces/IERC20.sol'; + +import {BCoWPoolBase} from './BCoWPoolBase.sol'; + +import {IBCoWFactory} from 'interfaces/IBCoWFactory.sol'; +import {IBPool} from 'interfaces/IBPool.sol'; + +contract BCoWPool_afterFinalize is BCoWPoolBase { + uint256 public tokenWeight = 1e18; + + function setUp() public virtual override { + super.setUp(); + bCoWPool.set__tokens(tokens); + bCoWPool.set__records(tokens[0], IBPool.Record({bound: true, index: 0, denorm: tokenWeight})); + bCoWPool.set__records(tokens[1], IBPool.Record({bound: true, index: 1, denorm: tokenWeight})); + + vm.mockCall(address(this), abi.encodeCall(IBCoWFactory.logBCoWPool, ()), abi.encode()); + + vm.mockCall(tokens[0], abi.encodeCall(IERC20.approve, (vaultRelayer, type(uint256).max)), abi.encode(true)); + vm.mockCall(tokens[1], abi.encodeCall(IERC20.approve, (vaultRelayer, type(uint256).max)), abi.encode(true)); + } + + function test_WhenCalled() external { + // it calls approve on every bound token + vm.expectCall(tokens[0], abi.encodeCall(IERC20.approve, (vaultRelayer, type(uint256).max))); + vm.expectCall(tokens[1], abi.encodeCall(IERC20.approve, (vaultRelayer, type(uint256).max))); + // it calls logBCoWPool on the factory + vm.expectCall(address(this), abi.encodeCall(IBCoWFactory.logBCoWPool, ())); + bCoWPool.call__afterFinalize(); + } + + function test_WhenFactorysLogBCoWPoolDoesNotRevert() external { + // it returns + bCoWPool.call__afterFinalize(); + } + + function test_WhenFactorysLogBCoWPoolReverts(bytes memory revertData) external { + vm.mockCallRevert(address(this), abi.encodeCall(IBCoWFactory.logBCoWPool, ()), revertData); + // it emits a COWAMMPoolCreated event + vm.expectEmit(address(bCoWPool)); + emit IBCoWFactory.COWAMMPoolCreated(address(bCoWPool)); + bCoWPool.call__afterFinalize(); + } +} diff --git a/test/unit/BCoWPool/BCoWPool.tree b/test/unit/BCoWPool/BCoWPool.tree new file mode 100644 index 00000000..0dbbd1f0 --- /dev/null +++ b/test/unit/BCoWPool/BCoWPool.tree @@ -0,0 +1,8 @@ +BCoWPool::_afterFinalize +├── when called +│ ├── it calls approve on every bound token +│ └── it calls logBCoWPool on the factory +├── when factorys logBCoWPool does not revert +│ └── it returns +└── when factorys logBCoWPool reverts + └── it emits a COWAMMPoolCreated event diff --git a/test/unit/BCoWPool/BCoWPoolBase.sol b/test/unit/BCoWPool/BCoWPoolBase.sol new file mode 100644 index 00000000..3418e59e --- /dev/null +++ b/test/unit/BCoWPool/BCoWPoolBase.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.25; + +import {BPoolBase} from '../BPool/BPoolBase.sol'; +import {BCoWConst} from 'contracts/BCoWConst.sol'; +import {BNum} from 'contracts/BNum.sol'; + +import {ISettlement} from 'interfaces/ISettlement.sol'; +import {MockBCoWPool} from 'test/manual-smock/MockBCoWPool.sol'; + +contract BCoWPoolBase is BPoolBase, BCoWConst, BNum { + bytes32 public appData = bytes32('appData'); + address public cowSolutionSettler = makeAddr('cowSolutionSettler'); + bytes32 public domainSeparator = bytes32(bytes2(0xf00b)); + address public vaultRelayer = makeAddr('vaultRelayer'); + address public tokenIn; + address public tokenOut; + MockBCoWPool bCoWPool; + + function setUp() public virtual override { + super.setUp(); + tokenIn = tokens[0]; + tokenOut = tokens[1]; + vm.mockCall(cowSolutionSettler, abi.encodePacked(ISettlement.domainSeparator.selector), abi.encode(domainSeparator)); + vm.mockCall(cowSolutionSettler, abi.encodePacked(ISettlement.vaultRelayer.selector), abi.encode(vaultRelayer)); + bCoWPool = new MockBCoWPool(cowSolutionSettler, appData); + } +} diff --git a/test/unit/BPool/BPool.t.sol b/test/unit/BPool/BPool.t.sol index 264fef02..6bf2db80 100644 --- a/test/unit/BPool/BPool.t.sol +++ b/test/unit/BPool/BPool.t.sol @@ -6,6 +6,8 @@ import {IBPool} from 'interfaces/IBPool.sol'; import {MockBPool} from 'test/smock/MockBPool.sol'; contract BPool is BPoolBase { + uint256 public tokenWeight = 1e18; + function test_ConstructorWhenCalled(address _deployer) external { vm.prank(_deployer); MockBPool _newBPool = new MockBPool(); @@ -50,4 +52,51 @@ contract BPool is BPoolBase { // it returns number of tokens assertEq(bPool.getNumTokens(), _tokensToAdd); } + + function test_FinalizeRevertWhen_CallerIsNotController(address _caller) external { + vm.assume(_caller != address(this)); + vm.prank(_caller); + // it should revert + vm.expectRevert(IBPool.BPool_CallerIsNotController.selector); + bPool.finalize(); + } + + function test_FinalizeRevertWhen_PoolIsFinalized() external { + bPool.set__finalized(true); + // it should revert + vm.expectRevert(IBPool.BPool_PoolIsFinalized.selector); + bPool.finalize(); + } + + function test_FinalizeRevertWhen_ThereAreTooFewTokensBound() external { + address[] memory tokens_ = new address[](1); + tokens_[0] = tokens[0]; + bPool.set__tokens(tokens_); + // it should revert + vm.expectRevert(IBPool.BPool_TokensBelowMinimum.selector); + bPool.finalize(); + } + + function test_FinalizeWhenPreconditionsAreMet() external { + bPool.set__tokens(tokens); + bPool.set__records(tokens[0], IBPool.Record({bound: true, index: 0, denorm: tokenWeight})); + bPool.set__records(tokens[1], IBPool.Record({bound: true, index: 1, denorm: tokenWeight})); + bPool.mock_call__mintPoolShare(INIT_POOL_SUPPLY); + bPool.mock_call__pushPoolShare(address(this), INIT_POOL_SUPPLY); + + // it calls _afterFinalize hook + bPool.expectCall__afterFinalize(); + // it mints initial pool shares + bPool.expectCall__mintPoolShare(INIT_POOL_SUPPLY); + // it sends initial pool shares to controller + bPool.expectCall__pushPoolShare(address(this), INIT_POOL_SUPPLY); + // it emits a LOG_CALL event + bytes memory data = abi.encodeCall(IBPool.finalize, ()); + vm.expectEmit(address(bPool)); + emit IBPool.LOG_CALL(IBPool.finalize.selector, address(this), data); + + bPool.finalize(); + // it finalizes the pool + assertEq(bPool.call__finalized(), true); + } } diff --git a/test/unit/BPool/BPool.tree b/test/unit/BPool/BPool.tree index 379bec03..1f8f368e 100644 --- a/test/unit/BPool/BPool.tree +++ b/test/unit/BPool/BPool.tree @@ -20,3 +20,17 @@ BPool::isBound BPool::getNumTokens └── when called └── it returns number of tokens + +BPool::finalize +├── when caller is not controller +│ └── it should revert +├── when pool is finalized +│ └── it should revert +├── when there are too few tokens bound +│ └── it should revert +└── when preconditions are met + ├── it emits LOG_CALL event + ├── it finalizes the pool + ├── it mints initial pool shares + ├── it sends initial pool shares to controller + └── it calls _afterFinalize hook diff --git a/test/unit/BPool/BPoolBase.sol b/test/unit/BPool/BPoolBase.sol index 36851e2c..092d8147 100644 --- a/test/unit/BPool/BPoolBase.sol +++ b/test/unit/BPool/BPoolBase.sol @@ -9,10 +9,8 @@ import {Utils} from 'test/utils/Utils.sol'; contract BPoolBase is Test, BConst, Utils { MockBPool public bPool; - address public deployer = makeAddr('deployer'); function setUp() public virtual { - vm.prank(deployer); bPool = new MockBPool(); tokens.push(makeAddr('token0')); tokens.push(makeAddr('token1')); diff --git a/test/unit/BPool/BPool_Bind.t.sol b/test/unit/BPool/BPool_Bind.t.sol index c811dfb2..12fb07a4 100644 --- a/test/unit/BPool/BPool_Bind.t.sol +++ b/test/unit/BPool/BPool_Bind.t.sol @@ -26,76 +26,71 @@ contract BPoolBind is BPoolBase { function test_RevertWhen_CallerIsNOTController(address _caller) external { // it should revert - vm.assume(_caller != deployer); + vm.assume(_caller != address(this)); vm.prank(_caller); vm.expectRevert(IBPool.BPool_CallerIsNotController.selector); bPool.bind(tokens[0], tokenBindBalance, tokenWeight); } - modifier whenCallerIsController() { - vm.startPrank(deployer); - _; - } - - function test_RevertWhen_TokenIsAlreadyBound() external whenCallerIsController { + function test_RevertWhen_TokenIsAlreadyBound() external { bPool.set__records(tokens[0], IBPool.Record({bound: true, index: 0, denorm: tokenWeight})); // it should revert vm.expectRevert(IBPool.BPool_TokenAlreadyBound.selector); bPool.bind(tokens[0], tokenBindBalance, tokenWeight); } - function test_RevertWhen_PoolIsFinalized() external whenCallerIsController { + function test_RevertWhen_PoolIsFinalized() external { bPool.set__finalized(true); // it should revert vm.expectRevert(IBPool.BPool_PoolIsFinalized.selector); bPool.bind(tokens[0], tokenBindBalance, tokenWeight); } - function test_RevertWhen_MAX_BOUND_TOKENSTokensAreAlreadyBound() external whenCallerIsController { + function test_RevertWhen_MAX_BOUND_TOKENSTokensAreAlreadyBound() external { _setRandomTokens(MAX_BOUND_TOKENS); // it should revert vm.expectRevert(IBPool.BPool_TokensAboveMaximum.selector); bPool.bind(tokens[0], tokenBindBalance, tokenWeight); } - function test_RevertWhen_TokenWeightIsTooLow() external whenCallerIsController { + function test_RevertWhen_TokenWeightIsTooLow() external { // it should revert vm.expectRevert(IBPool.BPool_WeightBelowMinimum.selector); bPool.bind(tokens[0], tokenBindBalance, MIN_WEIGHT - 1); } - function test_RevertWhen_TokenWeightIsTooHigh() external whenCallerIsController { + function test_RevertWhen_TokenWeightIsTooHigh() external { // it should revert vm.expectRevert(IBPool.BPool_WeightAboveMaximum.selector); bPool.bind(tokens[0], tokenBindBalance, MAX_WEIGHT + 1); } - function test_RevertWhen_TooLittleBalanceIsProvided() external whenCallerIsController { + function test_RevertWhen_TooLittleBalanceIsProvided() external { // it should revert vm.expectRevert(IBPool.BPool_BalanceBelowMinimum.selector); bPool.bind(tokens[0], MIN_BALANCE - 1, tokenWeight); } - function test_RevertWhen_WeightSumExceedsMAX_TOTAL_WEIGHT() external whenCallerIsController { + function test_RevertWhen_WeightSumExceedsMAX_TOTAL_WEIGHT() external { bPool.set__totalWeight(2 * MAX_TOTAL_WEIGHT / 3); // it should revert vm.expectRevert(IBPool.BPool_TotalWeightAboveMaximum.selector); bPool.bind(tokens[0], tokenBindBalance, MAX_TOTAL_WEIGHT / 2); } - function test_WhenTokenCanBeBound(uint256 _existingTokens) external whenCallerIsController { + function test_WhenTokenCanBeBound(uint256 _existingTokens) external { _existingTokens = bound(_existingTokens, 0, MAX_BOUND_TOKENS - 1); bPool.set__tokens(_getDeterministicTokenArray(_existingTokens)); bPool.set__totalWeight(totalWeight); // it calls _pullUnderlying - bPool.expectCall__pullUnderlying(tokens[0], deployer, tokenBindBalance); + bPool.expectCall__pullUnderlying(tokens[0], address(this), tokenBindBalance); // it sets the reentrancy lock bPool.expectCall__setLock(_MUTEX_TAKEN); // it emits LOG_CALL event vm.expectEmit(); bytes memory _data = abi.encodeWithSelector(IBPool.bind.selector, tokens[0], tokenBindBalance, tokenWeight); - emit IBPool.LOG_CALL(IBPool.bind.selector, deployer, _data); + emit IBPool.LOG_CALL(IBPool.bind.selector, address(this), _data); bPool.bind(tokens[0], tokenBindBalance, tokenWeight); diff --git a/test/unit/BPool/BPool_Bind.tree b/test/unit/BPool/BPool_Bind.tree index f0ab3278..a7157745 100644 --- a/test/unit/BPool/BPool_Bind.tree +++ b/test/unit/BPool/BPool_Bind.tree @@ -3,26 +3,25 @@ BPool::Bind │ └── it should revert ├── when caller is NOT controller │ └── it should revert -└── when caller is controller - ├── when token is already bound - │ └── it should revert - ├── when pool is finalized - │ └── it should revert - ├── when MAX_BOUND_TOKENS tokens are already bound - │ └── it should revert - ├── when token weight is too low - │ └── it should revert - ├── when token weight is too high - │ └── it should revert - ├── when too little balance is provided - │ └── it should revert - ├── when weight sum exceeds MAX_TOTAL_WEIGHT - │ └── it should revert - └── when token can be bound - ├── it sets the reentrancy lock - ├── it emits LOG_CALL event - ├── it increments _totalWeight - ├── it calls _pullUnderlying - ├── it adds token to the tokens array - ├── it sets the token record - └── it clears the reentrancy lock +├── when token is already bound +│ └── it should revert +├── when pool is finalized +│ └── it should revert +├── when MAX_BOUND_TOKENS tokens are already bound +│ └── it should revert +├── when token weight is too low +│ └── it should revert +├── when token weight is too high +│ └── it should revert +├── when too little balance is provided +│ └── it should revert +├── when weight sum exceeds MAX_TOTAL_WEIGHT +│ └── it should revert +└── when token can be bound + ├── it sets the reentrancy lock + ├── it emits LOG_CALL event + ├── it increments _totalWeight + ├── it calls _pullUnderlying + ├── it adds token to the tokens array + ├── it sets the token record + └── it clears the reentrancy lock diff --git a/test/unit/BPool/BPool_ExitPool.t.sol b/test/unit/BPool/BPool_ExitPool.t.sol index 858cbef1..11259ba6 100644 --- a/test/unit/BPool/BPool_ExitPool.t.sol +++ b/test/unit/BPool/BPool_ExitPool.t.sol @@ -92,7 +92,7 @@ contract BPoolExitPool is BPoolBase, BNum { // it pulls poolAmountIn shares bPool.expectCall__pullPoolShare(address(this), poolAmountIn); // it sends exitFee to factory - bPool.expectCall__pushPoolShare(deployer, exitFee); + bPool.expectCall__pushPoolShare(address(this), exitFee); // it burns poolAmountIn - exitFee shares bPool.expectCall__burnPoolShare(poolAmountIn - exitFee); // it calls _pushUnderlying for every token diff --git a/test/unit/BPool/BPool_ExitswapPoolAmountIn.t.sol b/test/unit/BPool/BPool_ExitswapPoolAmountIn.t.sol index 8032cdf7..c9ecdff2 100644 --- a/test/unit/BPool/BPool_ExitswapPoolAmountIn.t.sol +++ b/test/unit/BPool/BPool_ExitswapPoolAmountIn.t.sol @@ -32,7 +32,7 @@ contract BPoolExitSwapPoolAmountIn is BPoolBase, BMath { bPool.mock_call__pullPoolShare(address(this), poolAmountIn); bPool.mock_call__burnPoolShare(poolAmountIn); - bPool.mock_call__pushPoolShare(deployer, 0); + bPool.mock_call__pushPoolShare(address(this), 0); bPool.mock_call__pushUnderlying(tokenOut, address(this), expectedAmountOut); } @@ -90,7 +90,7 @@ contract BPoolExitSwapPoolAmountIn is BPoolBase, BMath { // it burns poolAmountIn - exitFee shares bPool.expectCall__burnPoolShare(poolAmountIn); // it sends exitFee to factory - bPool.expectCall__pushPoolShare(deployer, 0); + bPool.expectCall__pushPoolShare(address(this), 0); // it calls _pushUnderlying for token out bPool.expectCall__pushUnderlying(tokenOut, address(this), expectedAmountOut); // it emits LOG_CALL event diff --git a/test/unit/BPool/BPool_Unbind.t.sol b/test/unit/BPool/BPool_Unbind.t.sol index c0c0f008..7034e85c 100644 --- a/test/unit/BPool/BPool_Unbind.t.sol +++ b/test/unit/BPool/BPool_Unbind.t.sol @@ -26,24 +26,19 @@ contract BPoolUnbind is BPoolBase { function test_RevertWhen_CallerIsNOTController(address _caller) external { // it should revert - vm.assume(_caller != deployer); + vm.assume(_caller != address(this)); vm.prank(_caller); vm.expectRevert(IBPool.BPool_CallerIsNotController.selector); bPool.unbind(tokens[0]); } - modifier whenCallerIsController() { - vm.startPrank(deployer); - _; - } - - function test_RevertWhen_TokenIsNotBound() external whenCallerIsController { + function test_RevertWhen_TokenIsNotBound() external { vm.expectRevert(IBPool.BPool_TokenNotBound.selector); // it should revert bPool.unbind(tokens[0]); } - function test_RevertWhen_PoolIsFinalized() external whenCallerIsController { + function test_RevertWhen_PoolIsFinalized() external { bPool.set__records(tokens[0], IBPool.Record({bound: true, index: 0, denorm: 0})); bPool.set__finalized(true); // it should revert @@ -60,16 +55,16 @@ contract BPoolUnbind is BPoolBase { _; } - function test_WhenTokenIsLastOnTheTokensArray() external whenCallerIsController whenTokenCanBeUnbound { + function test_WhenTokenIsLastOnTheTokensArray() external whenTokenCanBeUnbound { // it sets the reentrancy lock bPool.expectCall__setLock(_MUTEX_TAKEN); // it calls _pushUnderlying - bPool.expectCall__pushUnderlying(tokens[0], deployer, boundTokenAmount); + bPool.expectCall__pushUnderlying(tokens[0], address(this), boundTokenAmount); // it emits LOG_CALL event vm.expectEmit(); bytes memory _data = abi.encodeWithSelector(IBPool.unbind.selector, tokens[0]); - emit IBPool.LOG_CALL(IBPool.unbind.selector, deployer, _data); + emit IBPool.LOG_CALL(IBPool.unbind.selector, address(this), _data); bPool.unbind(tokens[0]); // it clears the reentrancy lock @@ -82,7 +77,7 @@ contract BPoolUnbind is BPoolBase { assertEq(bPool.call__totalWeight(), totalWeight - tokenWeight); } - function test_WhenTokenIsNOTLastOnTheTokensArray() external whenCallerIsController whenTokenCanBeUnbound { + function test_WhenTokenIsNOTLastOnTheTokensArray() external whenTokenCanBeUnbound { bPool.set__records(tokens[1], IBPool.Record({bound: true, index: 0, denorm: tokenWeight})); bPool.set__tokens(_tokensToMemory()); bPool.unbind(tokens[0]); diff --git a/test/unit/BPool/BPool_Unbind.tree b/test/unit/BPool/BPool_Unbind.tree index 0f8fd8e7..12ac3f5b 100644 --- a/test/unit/BPool/BPool_Unbind.tree +++ b/test/unit/BPool/BPool_Unbind.tree @@ -3,22 +3,21 @@ BPool::Unbind │ └── it should revert ├── when caller is NOT controller │ └── it should revert -└── when caller is controller - ├── when token is not bound - │ └── it should revert - ├── when pool is finalized - │ └── it should revert - └── when token can be unbound - ├── when token is last on the tokens array - │ ├── it sets the reentrancy lock - │ ├── it emits LOG_CALL event - │ ├── it calls _pushUnderlying - │ ├── it removes the token record - │ ├── it decreases the total weight - │ ├── it pops from the array - │ └── it clears the reentrancy lock - └── when token is NOT last on the tokens array - ├── it removes the token record - ├── it removes the token from the array - ├── it keeps other tokens in the array - └── it updates records to point to the correct indices +├── when token is not bound +│ └── it should revert +├── when pool is finalized +│ └── it should revert +└── when token can be unbound + ├── when token is last on the tokens array + │ ├── it sets the reentrancy lock + │ ├── it emits LOG_CALL event + │ ├── it calls _pushUnderlying + │ ├── it removes the token record + │ ├── it decreases the total weight + │ ├── it pops from the array + │ └── it clears the reentrancy lock + └── when token is NOT last on the tokens array + ├── it removes the token record + ├── it removes the token from the array + ├── it keeps other tokens in the array + └── it updates records to point to the correct indices From 49aba91fec0d670dc513b3a64c9480aa9b003078 Mon Sep 17 00:00:00 2001 From: teddy Date: Mon, 22 Jul 2024 15:02:09 -0300 Subject: [PATCH 34/48] test: btt tests for joinswapPoolAmountOut (#170) * test: btt tests for joinswapPoolAmountOut * fix: typos in tree * fix: missing stuff from dev mergeback --- .../BPool/BPool_JoinswapPoolAmountOut.t.sol | 95 +++++++++++++++++++ .../BPool/BPool_JoinswapPoolAmountOut.tree | 20 ++++ 2 files changed, 115 insertions(+) create mode 100644 test/unit/BPool/BPool_JoinswapPoolAmountOut.t.sol create mode 100644 test/unit/BPool/BPool_JoinswapPoolAmountOut.tree diff --git a/test/unit/BPool/BPool_JoinswapPoolAmountOut.t.sol b/test/unit/BPool/BPool_JoinswapPoolAmountOut.t.sol new file mode 100644 index 00000000..98c22e0d --- /dev/null +++ b/test/unit/BPool/BPool_JoinswapPoolAmountOut.t.sol @@ -0,0 +1,95 @@ +// 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 BPoolJoinswapPoolAmountOut is BPoolBase, BNum { + address public tokenIn; + + // Valid scenario + uint256 public poolAmountOut = 1e18; + uint256 public tokenInWeight = 8e18; + uint256 public totalWeight = 10e18; + uint256 public tokenInBalance = 300e18; + // ((((INIT_POOL_SUPPLY+poolAmountOut)/INIT_POOL_SUPPLY)^(1/(tokenInWeight/totalWeight)))*tokenInBalance-tokenInBalance)/(1-((1-(tokenInWeight/totalWeight))*MIN_FEE)) + // ((((100+1)/100)^(1/(8/10)))*300-300)/(1-((1-(8/10))*(10^-6))) + // 3.754676583174615979425132956656691 + uint256 public maxTokenIn = 3.754676583181324836e18; + + function setUp() public virtual override { + super.setUp(); + tokenIn = tokens[0]; + bPool.set__finalized(true); + // mint an initial amount of pool shares (expected to happen at _finalize) + bPool.call__mintPoolShare(INIT_POOL_SUPPLY); + bPool.set__tokens(_tokensToMemory()); + bPool.set__totalWeight(totalWeight); + bPool.set__records(tokenIn, IBPool.Record({bound: true, index: 0, denorm: tokenInWeight})); + vm.mockCall(tokenIn, abi.encodePacked(IERC20.balanceOf.selector), abi.encode(uint256(tokenInBalance))); + } + + function test_RevertWhen_ReentrancyLockIsSet() external { + bPool.call__setLock(_MUTEX_TAKEN); + // it should revert + vm.expectRevert(IBPool.BPool_Reentrancy.selector); + bPool.joinswapPoolAmountOut(tokenIn, poolAmountOut, maxTokenIn); + } + + function test_RevertWhen_PoolIsNotFinalized() external { + bPool.call__setLock(_MUTEX_TAKEN); + // it should revert + vm.expectRevert(IBPool.BPool_Reentrancy.selector); + bPool.joinswapPoolAmountOut(tokenIn, poolAmountOut, maxTokenIn); + } + + function test_RevertWhen_TokenInIsNotBound() external { + // it should revert + vm.expectRevert(IBPool.BPool_TokenNotBound.selector); + bPool.joinswapPoolAmountOut(makeAddr('unknown token'), poolAmountOut, maxTokenIn); + } + + function test_RevertWhen_TokenAmountInExceedsMaxRatio() external { + // it should revert + vm.expectRevert(IBPool.BPool_TokenAmountInAboveMaxRatio.selector); + // growing pool supply by 50% -> user has to provide over half of the + // pool's tokenIn (198 in this case, consistent with weight=0.8), while + // MAX_IN_RATIO=0.5 + bPool.joinswapPoolAmountOut(tokenIn, 50e18, type(uint256).max); + } + + function test_RevertWhen_CalculatedTokenAmountInIsMoreThanExpected() external { + // it should revert + vm.expectRevert(IBPool.BPool_TokenAmountInAboveMaxAmountIn.selector); + bPool.joinswapPoolAmountOut(tokenIn, poolAmountOut, maxTokenIn - 1); + } + + function test_WhenPreconditionsAreMet() external { + // it sets reentrancy lock + bPool.expectCall__setLock(_MUTEX_TAKEN); + // it queries token in balance + vm.expectCall(tokenIn, abi.encodeCall(IERC20.balanceOf, (address(bPool)))); + // it calls _pullUnderlying for token in + bPool.mock_call__pullUnderlying(tokenIn, address(this), maxTokenIn); + bPool.expectCall__pullUnderlying(tokenIn, address(this), maxTokenIn); + // it mints the pool shares + bPool.expectCall__mintPoolShare(poolAmountOut); + // it sends pool shares to caller + bPool.expectCall__pushPoolShare(address(this), poolAmountOut); + // it emits LOG_CALL event + bytes memory _data = + abi.encodeWithSelector(IBPool.joinswapPoolAmountOut.selector, tokenIn, poolAmountOut, maxTokenIn); + vm.expectEmit(); + emit IBPool.LOG_CALL(IBPool.joinswapPoolAmountOut.selector, address(this), _data); + // it emits LOG_JOIN event for token in + vm.expectEmit(); + emit IBPool.LOG_JOIN(address(this), tokenIn, maxTokenIn); + bPool.joinswapPoolAmountOut(tokenIn, poolAmountOut, maxTokenIn); + + // it clears the reentrancy lock + assertEq(_MUTEX_FREE, bPool.call__getLock()); + } +} diff --git a/test/unit/BPool/BPool_JoinswapPoolAmountOut.tree b/test/unit/BPool/BPool_JoinswapPoolAmountOut.tree new file mode 100644 index 00000000..ec19a013 --- /dev/null +++ b/test/unit/BPool/BPool_JoinswapPoolAmountOut.tree @@ -0,0 +1,20 @@ +BPool::JoinswapPoolAmountOut +├── 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 amount in exceeds max ratio +│ └── it should revert +├── when calculated token amount in is more than expected +│ └── it should revert +└── when preconditions are met + ├── it emits LOG_CALL event + ├── it sets the reentrancy lock + ├── it queries token in balance + ├── it emits LOG_JOIN event for token in + ├── it mints the pool shares + ├── it sends pool shares to caller + ├── it calls _pullUnderlying for token in + └── it clears the reentrancy lock From 76ea1ef9107a25ea8ab8daf407291f7adcd33c1f Mon Sep 17 00:00:00 2001 From: teddy Date: Mon, 22 Jul 2024 16:14:45 -0300 Subject: [PATCH 35/48] feat: add btt tests for BCoWPool commit (#162) * test: btt tests for bcowpool.commit * chore: remove preexisting unit tests replaced by ones in this pr * test: reorganize commit tree * fix: make bulloak happy --- test/unit/BCoWPool.t.sol | 29 ------------------------- test/unit/BCoWPool/BCoWPool.t.sol | 35 +++++++++++++++++++++++++++---- test/unit/BCoWPool/BCoWPool.tree | 8 +++++++ 3 files changed, 39 insertions(+), 33 deletions(-) diff --git a/test/unit/BCoWPool.t.sol b/test/unit/BCoWPool.t.sol index 08b6673b..722780ce 100644 --- a/test/unit/BCoWPool.t.sol +++ b/test/unit/BCoWPool.t.sol @@ -78,35 +78,6 @@ contract BCoWPool_Unit_Constructor is BaseCoWPoolTest { } } -contract BCoWPool_Unit_Commit is BaseCoWPoolTest { - function test_Revert_NonSolutionSettler(address sender, bytes32 orderHash) public { - vm.assume(sender != cowSolutionSettler); - vm.prank(sender); - vm.expectRevert(IBCoWPool.CommitOutsideOfSettlement.selector); - bCoWPool.commit(orderHash); - } - - function test_Revert_CommitmentAlreadySet(bytes32 _existingCommitment, bytes32 _newCommitment) public { - vm.assume(_existingCommitment != bytes32(0)); - bCoWPool.call__setLock(_existingCommitment); - vm.prank(cowSolutionSettler); - vm.expectRevert(IBPool.BPool_Reentrancy.selector); - bCoWPool.commit(_newCommitment); - } - - function test_Call_SetLock(bytes32 orderHash) public { - bCoWPool.expectCall__setLock(orderHash); - vm.prank(cowSolutionSettler); - bCoWPool.commit(orderHash); - } - - function test_Set_ReentrancyLock(bytes32 orderHash) public { - vm.prank(cowSolutionSettler); - bCoWPool.commit(orderHash); - assertEq(bCoWPool.call__getLock(), orderHash); - } -} - contract BCoWPool_Unit_IsValidSignature is BaseCoWPoolTest { function setUp() public virtual override { super.setUp(); diff --git a/test/unit/BCoWPool/BCoWPool.t.sol b/test/unit/BCoWPool/BCoWPool.t.sol index b596a622..2318bd16 100644 --- a/test/unit/BCoWPool/BCoWPool.t.sol +++ b/test/unit/BCoWPool/BCoWPool.t.sol @@ -6,13 +6,17 @@ import {IERC20} from '@cowprotocol/interfaces/IERC20.sol'; import {BCoWPoolBase} from './BCoWPoolBase.sol'; import {IBCoWFactory} from 'interfaces/IBCoWFactory.sol'; + +import {IBCoWPool} from 'interfaces/IBCoWPool.sol'; import {IBPool} from 'interfaces/IBPool.sol'; -contract BCoWPool_afterFinalize is BCoWPoolBase { +contract BCoWPool is BCoWPoolBase { + bytes32 public commitmentValue = bytes32(uint256(0xf00ba5)); uint256 public tokenWeight = 1e18; function setUp() public virtual override { super.setUp(); + bCoWPool.set__tokens(tokens); bCoWPool.set__records(tokens[0], IBPool.Record({bound: true, index: 0, denorm: tokenWeight})); bCoWPool.set__records(tokens[1], IBPool.Record({bound: true, index: 1, denorm: tokenWeight})); @@ -23,7 +27,7 @@ contract BCoWPool_afterFinalize is BCoWPoolBase { vm.mockCall(tokens[1], abi.encodeCall(IERC20.approve, (vaultRelayer, type(uint256).max)), abi.encode(true)); } - function test_WhenCalled() external { + function test__afterFinalizeWhenCalled() external { // it calls approve on every bound token vm.expectCall(tokens[0], abi.encodeCall(IERC20.approve, (vaultRelayer, type(uint256).max))); vm.expectCall(tokens[1], abi.encodeCall(IERC20.approve, (vaultRelayer, type(uint256).max))); @@ -32,16 +36,39 @@ contract BCoWPool_afterFinalize is BCoWPoolBase { bCoWPool.call__afterFinalize(); } - function test_WhenFactorysLogBCoWPoolDoesNotRevert() external { + function test__afterFinalizeWhenFactorysLogBCoWPoolDoesNotRevert() external { // it returns bCoWPool.call__afterFinalize(); } - function test_WhenFactorysLogBCoWPoolReverts(bytes memory revertData) external { + function test__afterFinalizeWhenFactorysLogBCoWPoolReverts(bytes memory revertData) external { vm.mockCallRevert(address(this), abi.encodeCall(IBCoWFactory.logBCoWPool, ()), revertData); // it emits a COWAMMPoolCreated event vm.expectEmit(address(bCoWPool)); emit IBCoWFactory.COWAMMPoolCreated(address(bCoWPool)); bCoWPool.call__afterFinalize(); } + + function test_CommitRevertWhen_ReentrancyLockIsSet(bytes32 lockValue) external { + vm.assume(lockValue != _MUTEX_FREE); + bCoWPool.call__setLock(lockValue); + // it should revert + vm.expectRevert(IBPool.BPool_Reentrancy.selector); + bCoWPool.commit(commitmentValue); + } + + function test_CommitRevertWhen_SenderIsNotSolutionSettler(address caller) external { + vm.assume(caller != cowSolutionSettler); + vm.prank(caller); + // it should revert + vm.expectRevert(abi.encodeWithSelector(IBCoWPool.CommitOutsideOfSettlement.selector)); + bCoWPool.commit(commitmentValue); + } + + function test_CommitWhenPreconditionsAreMet(bytes32 commitmentValue_) external { + vm.prank(cowSolutionSettler); + bCoWPool.commit(commitmentValue_); + // it should set the transient reentrancy lock + assertEq(bCoWPool.call__getLock(), commitmentValue_); + } } diff --git a/test/unit/BCoWPool/BCoWPool.tree b/test/unit/BCoWPool/BCoWPool.tree index 0dbbd1f0..abf9b3bb 100644 --- a/test/unit/BCoWPool/BCoWPool.tree +++ b/test/unit/BCoWPool/BCoWPool.tree @@ -6,3 +6,11 @@ BCoWPool::_afterFinalize │ └── it returns └── when factorys logBCoWPool reverts └── it emits a COWAMMPoolCreated event + +BCoWPool::Commit +├── when reentrancy lock is set +│ └──it should revert +├── when sender is not solution settler +│ └──it should revert +└── when preconditions are met + └── it should set the transient reentrancy lock From 0e30c85a8cdbc3952c8793df0a4093703d3d1d04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wei=C3=9Fer=20Hase?= Date: Mon, 22 Jul 2024 21:59:53 +0200 Subject: [PATCH 36/48] feat: adding BTT tests for BPool getters and setters (#165) * feat: adding BTT tests for BPool getters * feat: adding tests for spotPrice getters * feat: adding btt tests for bPool setters (#166) * fix: rm legacy bPool tests * fix: manually calculating spot price * fix: test issues on merge conflicts * fix: using funny spot prices --------- Co-authored-by: teddy --- test/unit/BPool.t.sol | 461 ------------------------------------ test/unit/BPool/BPool.t.sol | 338 +++++++++++++++++++++++++- test/unit/BPool/BPool.tree | 107 +++++++++ 3 files changed, 436 insertions(+), 470 deletions(-) diff --git a/test/unit/BPool.t.sol b/test/unit/BPool.t.sol index 7e80c886..9010ae93 100644 --- a/test/unit/BPool.t.sol +++ b/test/unit/BPool.t.sol @@ -245,467 +245,6 @@ abstract contract BasePoolTest is Test, BConst, Utils, BMath { } } -contract BPool_Unit_GetCurrentTokens is BasePoolTest { - function test_Returns_CurrentTokens(uint256 _length) public { - vm.assume(_length > 0); - vm.assume(_length <= MAX_BOUND_TOKENS); - address[] memory _tokensToAdd = _setRandomTokens(_length); - - assertEq(bPool.getCurrentTokens(), _tokensToAdd); - } - - function test_Revert_Reentrancy() public { - _expectRevertByReentrancy(); - bPool.getCurrentTokens(); - } -} - -contract BPool_Unit_GetFinalTokens is BasePoolTest { - function test_Returns_FinalTokens(uint256 _length) public { - vm.assume(_length > 0); - vm.assume(_length <= MAX_BOUND_TOKENS); - address[] memory _tokensToAdd = _setRandomTokens(_length); - _setFinalize(true); - - assertEq(bPool.getFinalTokens(), _tokensToAdd); - } - - function test_Revert_Reentrancy() public { - _expectRevertByReentrancy(); - bPool.getFinalTokens(); - } - - function test_Revert_NotFinalized(uint256 _length) public { - vm.assume(_length > 0); - vm.assume(_length <= MAX_BOUND_TOKENS); - _setRandomTokens(_length); - _setFinalize(false); - - vm.expectRevert(IBPool.BPool_PoolNotFinalized.selector); - bPool.getFinalTokens(); - } -} - -contract BPool_Unit_GetDenormalizedWeight is BasePoolTest { - function test_Returns_DenormalizedWeight(address _token, uint256 _weight) public { - bPool.set__records(_token, IBPool.Record({bound: true, index: 0, denorm: _weight})); - - assertEq(bPool.getDenormalizedWeight(_token), _weight); - } - - function test_Revert_Reentrancy() public { - _expectRevertByReentrancy(); - bPool.getDenormalizedWeight(address(0)); - } - - function test_Revert_NotBound(address _token) public { - vm.expectRevert(IBPool.BPool_TokenNotBound.selector); - bPool.getDenormalizedWeight(_token); - } -} - -contract BPool_Unit_GetTotalDenormalizedWeight is BasePoolTest { - function test_Returns_TotalDenormalizedWeight(uint256 _totalWeight) public { - _setTotalWeight(_totalWeight); - - assertEq(bPool.getTotalDenormalizedWeight(), _totalWeight); - } - - function test_Revert_Reentrancy() public { - _expectRevertByReentrancy(); - bPool.getTotalDenormalizedWeight(); - } -} - -contract BPool_Unit_GetNormalizedWeight is BasePoolTest { - function test_Returns_NormalizedWeight(address _token, uint256 _weight, uint256 _totalWeight) public { - _weight = bound(_weight, MIN_WEIGHT, MAX_WEIGHT); - _totalWeight = bound(_totalWeight, MIN_WEIGHT, MAX_TOTAL_WEIGHT); - vm.assume(_weight < _totalWeight); - bPool.set__records(_token, IBPool.Record({bound: true, index: 0, denorm: _weight})); - _setTotalWeight(_totalWeight); - - assertEq(bPool.getNormalizedWeight(_token), bdiv(_weight, _totalWeight)); - } - - function test_Revert_Reentrancy() public { - _expectRevertByReentrancy(); - bPool.getNormalizedWeight(address(0)); - } - - function test_Revert_NotBound(address _token) public { - vm.expectRevert(IBPool.BPool_TokenNotBound.selector); - bPool.getNormalizedWeight(_token); - } -} - -contract BPool_Unit_GetBalance is BasePoolTest { - function test_Returns_Balance(address _token, uint256 _balance) public { - assumeNotForgeAddress(_token); - - bPool.set__records(_token, IBPool.Record({bound: true, index: 0, denorm: 0})); - _mockPoolBalance(_token, _balance); - - assertEq(bPool.getBalance(_token), _balance); - } - - function test_Revert_Reentrancy() public { - _expectRevertByReentrancy(); - bPool.getBalance(address(0)); - } - - function test_Revert_NotBound(address _token) public { - vm.expectRevert(IBPool.BPool_TokenNotBound.selector); - bPool.getBalance(_token); - } -} - -contract BPool_Unit_GetSwapFee is BasePoolTest { - function test_Returns_SwapFee(uint256 _swapFee) public { - _setSwapFee(_swapFee); - - assertEq(bPool.getSwapFee(), _swapFee); - } - - function test_Revert_Reentrancy() public { - _expectRevertByReentrancy(); - bPool.getSwapFee(); - } -} - -contract BPool_Unit_GetController is BasePoolTest { - function test_Returns_Controller(address _controller) public { - bPool.set__controller(_controller); - - assertEq(bPool.getController(), _controller); - } - - function test_Revert_Reentrancy() public { - _expectRevertByReentrancy(); - bPool.getController(); - } -} - -contract BPool_Unit_SetSwapFee is BasePoolTest { - modifier happyPath(uint256 _fee) { - vm.assume(_fee >= MIN_FEE); - vm.assume(_fee <= MAX_FEE); - _; - } - - function test_Revert_Finalized(uint256 _fee) public happyPath(_fee) { - _setFinalize(true); - - vm.expectRevert(IBPool.BPool_PoolIsFinalized.selector); - bPool.setSwapFee(_fee); - } - - function test_Revert_NotController(address _controller, address _caller, uint256 _fee) public happyPath(_fee) { - vm.assume(_controller != _caller); - bPool.set__controller(_controller); - - vm.expectRevert(IBPool.BPool_CallerIsNotController.selector); - vm.prank(_caller); - bPool.setSwapFee(_fee); - } - - function test_Revert_MinFee(uint256 _fee) public { - vm.assume(_fee < MIN_FEE); - - vm.expectRevert(IBPool.BPool_FeeBelowMinimum.selector); - bPool.setSwapFee(_fee); - } - - function test_Revert_MaxFee(uint256 _fee) public { - vm.assume(_fee > MAX_FEE); - - vm.expectRevert(IBPool.BPool_FeeAboveMaximum.selector); - bPool.setSwapFee(_fee); - } - - function test_Revert_Reentrancy(uint256 _fee) public happyPath(_fee) { - _expectRevertByReentrancy(); - bPool.setSwapFee(_fee); - } - - function test_Set_SwapFee(uint256 _fee) public happyPath(_fee) { - bPool.setSwapFee(_fee); - - assertEq(bPool.call__swapFee(), _fee); - } - - function test_Set_ReentrancyLock(uint256 _fee) public happyPath(_fee) { - _expectSetReentrancyLock(); - bPool.setSwapFee(_fee); - } - - function test_Emit_LogCall(uint256 _fee) public happyPath(_fee) { - vm.expectEmit(); - bytes memory _data = abi.encodeWithSelector(BPool.setSwapFee.selector, _fee); - emit IBPool.LOG_CALL(BPool.setSwapFee.selector, address(this), _data); - - bPool.setSwapFee(_fee); - } -} - -contract BPool_Unit_SetController is BasePoolTest { - function test_Revert_NotController(address _controller, address _caller, address _newController) public { - vm.assume(_newController != address(0)); - vm.assume(_controller != _caller); - bPool.set__controller(_controller); - - vm.expectRevert(IBPool.BPool_CallerIsNotController.selector); - vm.prank(_caller); - bPool.setController(_newController); - } - - function test_Revert_Reentrancy(address _controller) public { - _expectRevertByReentrancy(); - bPool.setController(_controller); - } - - function test_Revert_AddressZero() public { - vm.expectRevert(IBPool.BPool_AddressZero.selector); - - bPool.setController(address(0)); - } - - function test_Set_Controller(address _controller) public { - vm.assume(_controller != address(0)); - bPool.setController(_controller); - - assertEq(bPool.call__controller(), _controller); - } - - function test_Emit_LogCall(address _controller) public { - vm.assume(_controller != address(0)); - vm.expectEmit(); - bytes memory _data = abi.encodeWithSelector(BPool.setController.selector, _controller); - emit IBPool.LOG_CALL(BPool.setController.selector, address(this), _data); - - bPool.setController(_controller); - } - - function test_Set_ReentrancyLock(address _controller) public { - vm.assume(_controller != address(0)); - _expectSetReentrancyLock(); - bPool.setController(_controller); - } -} - -contract BPool_Unit_Finalize is BasePoolTest { - modifier happyPath(uint256 _tokensLength) { - _tokensLength = bound(_tokensLength, MIN_BOUND_TOKENS, MAX_BOUND_TOKENS); - _setRandomTokens(_tokensLength); - _; - } - - function test_Revert_NotController( - address _controller, - address _caller, - uint256 _tokensLength - ) public happyPath(_tokensLength) { - vm.assume(_controller != _caller); - bPool.set__controller(_controller); - - vm.prank(_caller); - vm.expectRevert(IBPool.BPool_CallerIsNotController.selector); - bPool.finalize(); - } - - function test_Revert_Finalized(uint256 _tokensLength) public happyPath(_tokensLength) { - _setFinalize(true); - - vm.expectRevert(IBPool.BPool_PoolIsFinalized.selector); - bPool.finalize(); - } - - function test_Revert_MinTokens(uint256 _tokensLength) public { - _tokensLength = bound(_tokensLength, 0, MIN_BOUND_TOKENS - 1); - _setRandomTokens(_tokensLength); - - vm.expectRevert(IBPool.BPool_TokensBelowMinimum.selector); - bPool.finalize(); - } - - function test_Revert_Reentrancy(uint256 _tokensLength) public happyPath(_tokensLength) { - _expectRevertByReentrancy(); - bPool.finalize(); - } - - function test_Set_Finalize(uint256 _tokensLength) public happyPath(_tokensLength) { - bPool.finalize(); - - assertEq(bPool.call__finalized(), true); - } - - function test_Set_ReentrancyLock(uint256 _tokensLength) public happyPath(_tokensLength) { - _expectSetReentrancyLock(); - bPool.finalize(); - } - - function test_Call_AfterFinalizeHook(uint256 _tokensLength) public happyPath(_tokensLength) { - bPool.expectCall__afterFinalize(); - bPool.finalize(); - } - - function test_Mint_InitPoolSupply(uint256 _tokensLength) public happyPath(_tokensLength) { - bPool.finalize(); - - assertEq(bPool.totalSupply(), INIT_POOL_SUPPLY); - } - - function test_Push_InitPoolSupply(uint256 _tokensLength) public happyPath(_tokensLength) { - bPool.finalize(); - - assertEq(bPool.balanceOf(address(this)), INIT_POOL_SUPPLY); - } - - function test_Emit_LogCall(uint256 _tokensLength) public happyPath(_tokensLength) { - vm.expectEmit(); - bytes memory _data = abi.encodeWithSelector(BPool.finalize.selector); - emit IBPool.LOG_CALL(BPool.finalize.selector, address(this), _data); - - bPool.finalize(); - } -} - -contract BPool_Unit_GetSpotPrice is BasePoolTest { - struct GetSpotPrice_FuzzScenario { - address tokenIn; - address tokenOut; - uint256 tokenInBalance; - uint256 tokenInDenorm; - uint256 tokenOutBalance; - uint256 tokenOutDenorm; - uint256 swapFee; - } - - function _setValues(GetSpotPrice_FuzzScenario memory _fuzz) internal { - bPool.set__records(_fuzz.tokenIn, IBPool.Record({bound: true, index: 0, denorm: _fuzz.tokenInDenorm})); - _mockPoolBalance(_fuzz.tokenIn, _fuzz.tokenInBalance); - bPool.set__records(_fuzz.tokenOut, IBPool.Record({bound: true, index: 0, denorm: _fuzz.tokenOutDenorm})); - _mockPoolBalance(_fuzz.tokenOut, _fuzz.tokenOutBalance); - _setSwapFee(_fuzz.swapFee); - } - - function _assumeHappyPath(GetSpotPrice_FuzzScenario memory _fuzz) internal pure { - assumeNotForgeAddress(_fuzz.tokenIn); - assumeNotForgeAddress(_fuzz.tokenOut); - vm.assume(_fuzz.tokenIn != _fuzz.tokenOut); - _assumeCalcSpotPrice( - _fuzz.tokenInBalance, _fuzz.tokenInDenorm, _fuzz.tokenOutBalance, _fuzz.tokenOutDenorm, _fuzz.swapFee - ); - } - - modifier happyPath(GetSpotPrice_FuzzScenario memory _fuzz) { - _assumeHappyPath(_fuzz); - _setValues(_fuzz); - _; - } - - function test_Revert_NotBoundTokenIn( - GetSpotPrice_FuzzScenario memory _fuzz, - address _tokenIn - ) public happyPath(_fuzz) { - vm.assume(_tokenIn != _fuzz.tokenIn); - vm.assume(_tokenIn != _fuzz.tokenOut); - - vm.expectRevert(IBPool.BPool_TokenNotBound.selector); - bPool.getSpotPrice(_tokenIn, _fuzz.tokenOut); - } - - function test_Revert_NotBoundTokenOut( - GetSpotPrice_FuzzScenario memory _fuzz, - address _tokenOut - ) public happyPath(_fuzz) { - vm.assume(_tokenOut != _fuzz.tokenIn); - vm.assume(_tokenOut != _fuzz.tokenOut); - - vm.expectRevert(IBPool.BPool_TokenNotBound.selector); - bPool.getSpotPrice(_fuzz.tokenIn, _tokenOut); - } - - function test_Returns_SpotPrice(GetSpotPrice_FuzzScenario memory _fuzz) public happyPath(_fuzz) { - uint256 _expectedSpotPrice = calcSpotPrice( - _fuzz.tokenInBalance, _fuzz.tokenInDenorm, _fuzz.tokenOutBalance, _fuzz.tokenOutDenorm, _fuzz.swapFee - ); - uint256 _spotPrice = bPool.getSpotPrice(_fuzz.tokenIn, _fuzz.tokenOut); - assertEq(_spotPrice, _expectedSpotPrice); - } - - function test_Revert_Reentrancy(GetSpotPrice_FuzzScenario memory _fuzz) public happyPath(_fuzz) { - _expectRevertByReentrancy(); - bPool.getSpotPrice(_fuzz.tokenIn, _fuzz.tokenOut); - } -} - -contract BPool_Unit_GetSpotPriceSansFee is BasePoolTest { - struct GetSpotPriceSansFee_FuzzScenario { - address tokenIn; - address tokenOut; - uint256 tokenInBalance; - uint256 tokenInDenorm; - uint256 tokenOutBalance; - uint256 tokenOutDenorm; - } - - function _setValues(GetSpotPriceSansFee_FuzzScenario memory _fuzz) internal { - bPool.set__records(_fuzz.tokenIn, IBPool.Record({bound: true, index: 0, denorm: _fuzz.tokenInDenorm})); - _mockPoolBalance(_fuzz.tokenIn, _fuzz.tokenInBalance); - bPool.set__records(_fuzz.tokenOut, IBPool.Record({bound: true, index: 0, denorm: _fuzz.tokenOutDenorm})); - _mockPoolBalance(_fuzz.tokenOut, _fuzz.tokenOutBalance); - _setSwapFee(0); - } - - function _assumeHappyPath(GetSpotPriceSansFee_FuzzScenario memory _fuzz) internal pure { - assumeNotForgeAddress(_fuzz.tokenIn); - assumeNotForgeAddress(_fuzz.tokenOut); - vm.assume(_fuzz.tokenIn != _fuzz.tokenOut); - _assumeCalcSpotPrice(_fuzz.tokenInBalance, _fuzz.tokenInDenorm, _fuzz.tokenOutBalance, _fuzz.tokenOutDenorm, 0); - } - - modifier happyPath(GetSpotPriceSansFee_FuzzScenario memory _fuzz) { - _assumeHappyPath(_fuzz); - _setValues(_fuzz); - _; - } - - function test_Revert_NotBoundTokenIn( - GetSpotPriceSansFee_FuzzScenario memory _fuzz, - address _tokenIn - ) public happyPath(_fuzz) { - vm.assume(_tokenIn != _fuzz.tokenIn); - vm.assume(_tokenIn != _fuzz.tokenOut); - - vm.expectRevert(IBPool.BPool_TokenNotBound.selector); - bPool.getSpotPriceSansFee(_tokenIn, _fuzz.tokenOut); - } - - function test_Revert_NotBoundTokenOut( - GetSpotPriceSansFee_FuzzScenario memory _fuzz, - address _tokenOut - ) public happyPath(_fuzz) { - vm.assume(_tokenOut != _fuzz.tokenIn); - vm.assume(_tokenOut != _fuzz.tokenOut); - - vm.expectRevert(IBPool.BPool_TokenNotBound.selector); - bPool.getSpotPriceSansFee(_fuzz.tokenIn, _tokenOut); - } - - function test_Returns_SpotPrice(GetSpotPriceSansFee_FuzzScenario memory _fuzz) public happyPath(_fuzz) { - uint256 _expectedSpotPrice = - calcSpotPrice(_fuzz.tokenInBalance, _fuzz.tokenInDenorm, _fuzz.tokenOutBalance, _fuzz.tokenOutDenorm, 0); - uint256 _spotPrice = bPool.getSpotPriceSansFee(_fuzz.tokenIn, _fuzz.tokenOut); - assertEq(_spotPrice, _expectedSpotPrice); - } - - function test_Revert_Reentrancy(GetSpotPriceSansFee_FuzzScenario memory _fuzz) public happyPath(_fuzz) { - _expectRevertByReentrancy(); - bPool.getSpotPriceSansFee(_fuzz.tokenIn, _fuzz.tokenOut); - } -} - contract BPool_Unit_JoinswapPoolAmountOut is BasePoolTest { address tokenIn; diff --git a/test/unit/BPool/BPool.t.sol b/test/unit/BPool/BPool.t.sol index 6bf2db80..7b1f2ec3 100644 --- a/test/unit/BPool/BPool.t.sol +++ b/test/unit/BPool/BPool.t.sol @@ -2,14 +2,45 @@ 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 {IBPool} from 'interfaces/IBPool.sol'; import {MockBPool} from 'test/smock/MockBPool.sol'; -contract BPool is BPoolBase { +contract BPool is BPoolBase, BMath { + address controller = makeAddr('controller'); + address randomCaller = makeAddr('random caller'); + address unknownToken = makeAddr('unknown token'); + uint256 swapFee = 0.1e18; + uint256 public tokenWeight = 1e18; + uint256 public totalWeight = 10e18; + uint256 public balanceTokenIn = 10e18; + uint256 public balanceTokenOut = 20e18; + + // sP = (tokenInBalance / tokenInWeight) / (tokenOutBalance/ tokenOutWeight) * (1 / (1 - swapFee)) + // tokenInWeight == tokenOutWeight + // sP = 10 / 20 = 0.5e18 + // sPf = (10 / 20) * (1 / (1-0.1)) = 0.555...e18 (round-up) + uint256 public spotPriceWithoutFee = 0.5e18; + uint256 public spotPrice = 0.555555555555555556e18; + + function setUp() public virtual override { + super.setUp(); + + bPool.set__finalized(true); + bPool.set__tokens(tokens); + bPool.set__records(tokens[0], IBPool.Record({bound: true, index: 0, denorm: tokenWeight})); + bPool.set__records(tokens[1], IBPool.Record({bound: true, index: 1, denorm: tokenWeight})); + bPool.set__totalWeight(totalWeight); + bPool.set__swapFee(swapFee); + bPool.set__controller(controller); + } function test_ConstructorWhenCalled(address _deployer) external { - vm.prank(_deployer); + vm.startPrank(_deployer); MockBPool _newBPool = new MockBPool(); // it sets caller as controller @@ -22,8 +53,96 @@ contract BPool is BPoolBase { assertEq(_newBPool.call__finalized(), false); } - function test_IsFinalizedWhenPoolIsFinalized() external { - bPool.set__finalized(true); + function test_SetSwapFeeRevertWhen_ReentrancyLockIsSet() external { + bPool.call__setLock(_MUTEX_TAKEN); + // it should revert + vm.expectRevert(IBPool.BPool_Reentrancy.selector); + bPool.setSwapFee(0); + } + + function test_SetSwapFeeRevertWhen_CallerIsNotController() external { + vm.prank(randomCaller); + // it should revert + vm.expectRevert(IBPool.BPool_CallerIsNotController.selector); + bPool.setSwapFee(0); + } + + function test_SetSwapFeeRevertWhen_PoolIsFinalized() external { + vm.prank(controller); + // it should revert + vm.expectRevert(IBPool.BPool_PoolIsFinalized.selector); + bPool.setSwapFee(0); + } + + function test_SetSwapFeeRevertWhen_SwapFeeIsBelowMIN_FEE() external { + bPool.set__finalized(false); + vm.prank(controller); + // it should revert + vm.expectRevert(IBPool.BPool_FeeBelowMinimum.selector); + bPool.setSwapFee(MIN_FEE - 1); + } + + function test_SetSwapFeeRevertWhen_SwapFeeIsAboveMAX_FEE() external { + bPool.set__finalized(false); + vm.prank(controller); + // it should revert + vm.expectRevert(IBPool.BPool_FeeAboveMaximum.selector); + bPool.setSwapFee(MAX_FEE + 1); + } + + function test_SetSwapFeeWhenPreconditionsAreMet(uint256 _swapFee) external { + bPool.set__finalized(false); + vm.prank(controller); + _swapFee = bound(_swapFee, MIN_FEE, MAX_FEE); + + // it emits LOG_CALL event + vm.expectEmit(); + bytes memory _data = abi.encodeWithSelector(IBPool.setSwapFee.selector, _swapFee); + emit IBPool.LOG_CALL(IBPool.setSwapFee.selector, controller, _data); + + bPool.setSwapFee(_swapFee); + + // it sets swap fee + assertEq(bPool.getSwapFee(), _swapFee); + } + + function test_SetControllerRevertWhen_ReentrancyLockIsSet() external { + bPool.call__setLock(_MUTEX_TAKEN); + // it should revert + vm.expectRevert(IBPool.BPool_Reentrancy.selector); + bPool.setController(controller); + } + + function test_SetControllerRevertWhen_CallerIsNotController() external { + vm.prank(randomCaller); + // it should revert + vm.expectRevert(IBPool.BPool_CallerIsNotController.selector); + bPool.setController(controller); + } + + function test_SetControllerRevertWhen_NewControllerIsZeroAddress() external { + vm.prank(controller); + // it should revert + vm.expectRevert(IBPool.BPool_AddressZero.selector); + bPool.setController(address(0)); + } + + function test_SetControllerWhenPreconditionsAreMet(address _controller) external { + vm.prank(controller); + vm.assume(_controller != address(0)); + + // it emits LOG_CALL event + vm.expectEmit(); + bytes memory _data = abi.encodeWithSelector(IBPool.setController.selector, _controller); + emit IBPool.LOG_CALL(IBPool.setController.selector, controller, _data); + + bPool.setController(_controller); + + // it sets new controller + assertEq(bPool.getController(), _controller); + } + + function test_IsFinalizedWhenPoolIsFinalized() external view { // it returns true assertTrue(bPool.isFinalized()); } @@ -53,22 +172,221 @@ contract BPool is BPoolBase { assertEq(bPool.getNumTokens(), _tokensToAdd); } + function test_GetFinalTokensRevertWhen_ReentrancyLockIsSet() external { + bPool.call__setLock(_MUTEX_TAKEN); + // it should revert + vm.expectRevert(IBPool.BPool_Reentrancy.selector); + bPool.getFinalTokens(); + } + + function test_GetFinalTokensRevertWhen_PoolIsNotFinalized() external { + bPool.set__finalized(false); + // it should revert + vm.expectRevert(IBPool.BPool_PoolNotFinalized.selector); + bPool.getFinalTokens(); + } + + function test_GetFinalTokensWhenPreconditionsAreMet() external view { + // it returns pool tokens + address[] memory _tokens = bPool.getFinalTokens(); + assertEq(_tokens.length, tokens.length); + assertEq(_tokens[0], tokens[0]); + assertEq(_tokens[1], tokens[1]); + } + + function test_GetCurrentTokensRevertWhen_ReentrancyLockIsSet() external { + bPool.call__setLock(_MUTEX_TAKEN); + // it should revert + vm.expectRevert(IBPool.BPool_Reentrancy.selector); + bPool.getCurrentTokens(); + } + + function test_GetCurrentTokensWhenPreconditionsAreMet() external view { + // it returns pool tokens + address[] memory _tokens = bPool.getCurrentTokens(); + assertEq(_tokens.length, tokens.length); + assertEq(_tokens[0], tokens[0]); + assertEq(_tokens[1], tokens[1]); + } + + function test_GetDenormalizedWeightRevertWhen_ReentrancyLockIsSet() external { + bPool.call__setLock(_MUTEX_TAKEN); + // it should revert + vm.expectRevert(IBPool.BPool_Reentrancy.selector); + bPool.getDenormalizedWeight(tokens[0]); + } + + function test_GetDenormalizedWeightRevertWhen_TokenIsNotBound() external { + // it should revert + vm.expectRevert(IBPool.BPool_TokenNotBound.selector); + bPool.getDenormalizedWeight(unknownToken); + } + + function test_GetDenormalizedWeightWhenPreconditionsAreMet() external view { + // it returns token weight + uint256 _tokenWeight = bPool.getDenormalizedWeight(tokens[0]); + assertEq(_tokenWeight, tokenWeight); + } + + function test_GetTotalDenormalizedWeightRevertWhen_ReentrancyLockIsSet() external { + bPool.call__setLock(_MUTEX_TAKEN); + // it should revert + vm.expectRevert(IBPool.BPool_Reentrancy.selector); + bPool.getTotalDenormalizedWeight(); + } + + function test_GetTotalDenormalizedWeightWhenPreconditionsAreMet() external view { + // it returns total weight + uint256 _totalWeight = bPool.getTotalDenormalizedWeight(); + assertEq(_totalWeight, totalWeight); + } + + function test_GetNormalizedWeightRevertWhen_ReentrancyLockIsSet() external { + bPool.call__setLock(_MUTEX_TAKEN); + // it should revert + vm.expectRevert(IBPool.BPool_Reentrancy.selector); + bPool.getNormalizedWeight(tokens[0]); + } + + function test_GetNormalizedWeightRevertWhen_TokenIsNotBound() external { + // it should revert + vm.expectRevert(IBPool.BPool_TokenNotBound.selector); + bPool.getNormalizedWeight(unknownToken); + } + + function test_GetNormalizedWeightWhenPreconditionsAreMet() external view { + // it returns normalized weight + // normalizedWeight = tokenWeight / totalWeight + uint256 _normalizedWeight = bPool.getNormalizedWeight(tokens[0]); + assertEq(_normalizedWeight, 0.1e18); + } + + function test_GetBalanceRevertWhen_ReentrancyLockIsSet() external { + bPool.call__setLock(_MUTEX_TAKEN); + // it should revert + vm.expectRevert(IBPool.BPool_Reentrancy.selector); + bPool.getBalance(tokens[0]); + } + + function test_GetBalanceRevertWhen_TokenIsNotBound() external { + // it should revert + vm.expectRevert(IBPool.BPool_TokenNotBound.selector); + bPool.getBalance(unknownToken); + } + + function test_GetBalanceWhenPreconditionsAreMet(uint256 tokenBalance) external { + vm.mockCall(tokens[0], abi.encodePacked(IERC20.balanceOf.selector), abi.encode(tokenBalance)); + // it queries token balance + vm.expectCall(tokens[0], abi.encodeWithSelector(IERC20.balanceOf.selector)); + // it returns token balance + uint256 _balance = bPool.getBalance(tokens[0]); + assertEq(_balance, tokenBalance); + } + + function test_GetSwapFeeRevertWhen_ReentrancyLockIsSet() external { + bPool.call__setLock(_MUTEX_TAKEN); + // it should revert + vm.expectRevert(IBPool.BPool_Reentrancy.selector); + bPool.getSwapFee(); + } + + function test_GetSwapFeeWhenPreconditionsAreMet() external view { + // it returns swap fee + uint256 _swapFee = bPool.getSwapFee(); + assertEq(_swapFee, swapFee); + } + + function test_GetControllerRevertWhen_ReentrancyLockIsSet() external { + bPool.call__setLock(_MUTEX_TAKEN); + // it should revert + vm.expectRevert(IBPool.BPool_Reentrancy.selector); + bPool.getController(); + } + + function test_GetControllerWhenPreconditionsAreMet() external view { + // it returns controller + address _controller = bPool.getController(); + assertEq(_controller, controller); + } + + function test_GetSpotPriceRevertWhen_ReentrancyLockIsSet() external { + bPool.call__setLock(_MUTEX_TAKEN); + // it should revert + vm.expectRevert(IBPool.BPool_Reentrancy.selector); + bPool.getSpotPrice(tokens[0], tokens[1]); + } + + function test_GetSpotPriceRevertWhen_TokenInIsNotBound() external { + // it should revert + vm.expectRevert(IBPool.BPool_TokenNotBound.selector); + bPool.getSpotPrice(unknownToken, tokens[1]); + } + + function test_GetSpotPriceRevertWhen_TokenOutIsNotBound() external { + // it should revert + vm.expectRevert(IBPool.BPool_TokenNotBound.selector); + bPool.getSpotPrice(tokens[0], unknownToken); + } + + function test_GetSpotPriceWhenPreconditionsAreMet() external { + // it queries token in balance + vm.mockCall(tokens[0], abi.encodePacked(IERC20.balanceOf.selector), abi.encode(balanceTokenIn)); + vm.expectCall(tokens[0], abi.encodeWithSelector(IERC20.balanceOf.selector)); + // it queries token out balance + vm.mockCall(tokens[1], abi.encodePacked(IERC20.balanceOf.selector), abi.encode(balanceTokenOut)); + vm.expectCall(tokens[1], abi.encodeWithSelector(IERC20.balanceOf.selector)); + // it returns spot price + assertEq(bPool.getSpotPrice(tokens[0], tokens[1]), spotPrice); + } + + function test_GetSpotPriceSansFeeRevertWhen_ReentrancyLockIsSet() external { + bPool.call__setLock(_MUTEX_TAKEN); + // it should revert + vm.expectRevert(IBPool.BPool_Reentrancy.selector); + bPool.getSpotPriceSansFee(tokens[0], tokens[1]); + } + + function test_GetSpotPriceSansFeeRevertWhen_TokenInIsNotBound() external { + // it should revert + vm.expectRevert(IBPool.BPool_TokenNotBound.selector); + bPool.getSpotPriceSansFee(unknownToken, tokens[1]); + } + + function test_GetSpotPriceSansFeeRevertWhen_TokenOutIsNotBound() external { + // it should revert + vm.expectRevert(IBPool.BPool_TokenNotBound.selector); + bPool.getSpotPriceSansFee(tokens[0], unknownToken); + } + + function test_GetSpotPriceSansFeeWhenPreconditionsAreMet() external { + // it queries token in balance + vm.mockCall(tokens[0], abi.encodePacked(IERC20.balanceOf.selector), abi.encode(balanceTokenIn)); + vm.expectCall(tokens[0], abi.encodeWithSelector(IERC20.balanceOf.selector)); + // it queries token out balance + vm.mockCall(tokens[1], abi.encodePacked(IERC20.balanceOf.selector), abi.encode(balanceTokenOut)); + vm.expectCall(tokens[1], abi.encodeWithSelector(IERC20.balanceOf.selector)); + // it returns spot price + assertEq(bPool.getSpotPriceSansFee(tokens[0], tokens[1]), spotPriceWithoutFee); + } + function test_FinalizeRevertWhen_CallerIsNotController(address _caller) external { vm.assume(_caller != address(this)); - vm.prank(_caller); + vm.startPrank(_caller); // it should revert vm.expectRevert(IBPool.BPool_CallerIsNotController.selector); bPool.finalize(); } function test_FinalizeRevertWhen_PoolIsFinalized() external { - bPool.set__finalized(true); + vm.startPrank(controller); // it should revert vm.expectRevert(IBPool.BPool_PoolIsFinalized.selector); bPool.finalize(); } function test_FinalizeRevertWhen_ThereAreTooFewTokensBound() external { + vm.startPrank(controller); + bPool.set__finalized(false); address[] memory tokens_ = new address[](1); tokens_[0] = tokens[0]; bPool.set__tokens(tokens_); @@ -78,22 +396,24 @@ contract BPool is BPoolBase { } function test_FinalizeWhenPreconditionsAreMet() external { + vm.startPrank(controller); + bPool.set__finalized(false); bPool.set__tokens(tokens); bPool.set__records(tokens[0], IBPool.Record({bound: true, index: 0, denorm: tokenWeight})); bPool.set__records(tokens[1], IBPool.Record({bound: true, index: 1, denorm: tokenWeight})); bPool.mock_call__mintPoolShare(INIT_POOL_SUPPLY); - bPool.mock_call__pushPoolShare(address(this), INIT_POOL_SUPPLY); + bPool.mock_call__pushPoolShare(controller, INIT_POOL_SUPPLY); // it calls _afterFinalize hook bPool.expectCall__afterFinalize(); // it mints initial pool shares bPool.expectCall__mintPoolShare(INIT_POOL_SUPPLY); // it sends initial pool shares to controller - bPool.expectCall__pushPoolShare(address(this), INIT_POOL_SUPPLY); + bPool.expectCall__pushPoolShare(controller, INIT_POOL_SUPPLY); // it emits a LOG_CALL event bytes memory data = abi.encodeCall(IBPool.finalize, ()); vm.expectEmit(address(bPool)); - emit IBPool.LOG_CALL(IBPool.finalize.selector, address(this), data); + emit IBPool.LOG_CALL(IBPool.finalize.selector, controller, data); bPool.finalize(); // it finalizes the pool diff --git a/test/unit/BPool/BPool.tree b/test/unit/BPool/BPool.tree index 1f8f368e..7a034e3a 100644 --- a/test/unit/BPool/BPool.tree +++ b/test/unit/BPool/BPool.tree @@ -5,6 +5,32 @@ BPool::constructor ├── it sets swap fee to MIN_FEE └── it does NOT finalize the pool +BPool::setSwapFee +├── when reentrancy lock is set +│ └── it should revert +├── when caller is not controller +│ └── it should revert +├── when pool is finalized +│ └── it should revert +├── when swap fee is below MIN_FEE +│ └── it should revert +├── when swap fee is above MAX_FEE +│ └── it should revert +└── when preconditions are met + ├── it emits LOG_CALL event + └── it sets swap fee + +BPool::setController +├── when reentrancy lock is set +│ └── it should revert +├── when caller is not controller +│ └── it should revert +├── when new controller is zero address +│ └── it should revert +└── when preconditions are met + ├── it emits LOG_CALL event + └── it sets new controller + BPool::isFinalized ├── when pool is finalized │ └── it returns true @@ -21,6 +47,87 @@ BPool::getNumTokens └── when called └── it returns number of tokens +BPool::getFinalTokens +├── when reentrancy lock is set +│ └── it should revert +├── when pool is not finalized +│ └── it should revert +└── when preconditions are met + └── it returns pool tokens + +BPool::getCurrentTokens +├── when reentrancy lock is set +│ └── it should revert +└── when preconditions are met + └── it returns pool tokens + +BPool::getDenormalizedWeight +├── when reentrancy lock is set +│ └── it should revert +├── when token is not bound +│ └── it should revert +└── when preconditions are met + └── it returns token weight + +BPool::getTotalDenormalizedWeight +├── when reentrancy lock is set +│ └── it should revert +└── when preconditions are met + └── it returns total weight + +BPool::getNormalizedWeight +├── when reentrancy lock is set +│ └── it should revert +├── when token is not bound +│ └── it should revert +└── when preconditions are met + └── it returns normalized weight + +BPool::getBalance +├── when reentrancy lock is set +│ └── it should revert +├── when token is not bound +│ └── it should revert +└── when preconditions are met + ├── it queries token balance + └── it returns token balance + +BPool::getSwapFee +├── when reentrancy lock is set +│ └── it should revert +└── when preconditions are met + └── it returns swap fee + +BPool::getController +├── when reentrancy lock is set +│ └── it should revert +└── when preconditions are met + └── it returns controller + +BPool::getSpotPrice +├── when reentrancy lock is set +│ └── it should revert +├── when token in is not bound +│ └── it should revert +├── when token out is not bound +│ └── it should revert +└── when preconditions are met + ├── it queries token in balance + ├── it queries token out balance + └── it returns spot price + +BPool::getSpotPriceSansFee +├── when reentrancy lock is set +│ └── it should revert +├── when token in is not bound +│ └── it should revert +├── when token out is not bound +│ └── it should revert +└── when preconditions are met + ├── it queries token in balance + ├── it queries token out balance + └── it returns spot price sans fee + BPool::finalize ├── when caller is not controller │ └── it should revert From c9a38494380c4f8ede4edfaf24d3307465f04dfc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wei=C3=9Fer=20Hase?= Date: Tue, 23 Jul 2024 11:26:10 +0200 Subject: [PATCH 37/48] feat: adding CoW helper MVP (#121) * feat: non-weighted tradeable order test * fix: adding GetTradeableOrder library * feat: adding weights to the math * feat: variable weights * feat: adding test for helper * feat: adding support for weights * fix: fmt * fix: sending correct price vector * fix: making helper return valid signature * feat: deprecated weights in helper * feat: updated buyAmount as per aaf44a3 * refactor: mv GetTradeableOrder to libraries dir * fix: update comment on BCoWHelper * fix: rm unused variable warning * feat: adding BTT test for BCoWHelper * fix: linter warnings * fix: rm remanent line change * fix: rm unused lines * fix: missing natspec * fix: uncommenting assertion in test * fix: missing natspec * fix: safety checks comments * feat: vendoring interface from cow-amm * fix: lint run * chore: merge dev * fix: gas snapshot * refactor: reordered helper flow and cleanup * feat: vendoring GetTradeableOrder from cow-amm * feat: improving commitment expectation * fix: typos in comments * fix: overwriting sellAmount in order to avoid rounding issues (#154) * fix: typos in comments * fix: overwriting sellAmount in order to avoid rounding issues * feat: improving the test case to cover both cases * chore: addressing contract changes from PR comments * chore: addressing comments in tests * fix: adding helper mock * fix: addressing comments from PR * refactor: deprecate fuzzed integration valid order test in favour of unit test * feat: simplifying helper integration test * feat: improving unit test for valid order * feat: adding call tokens expectation * fix: branching different behaviours given skewness * fix: adding comments on the skewness sign --- package.json | 1 + remappings.txt | 3 + src/contracts/BCoWHelper.sol | 125 +++++++++++++++ test/integration/BCoWHelper.t.sol | 181 +++++++++++++++++++++ test/integration/BCowPool.t.sol | 1 + test/manual-smock/MockBCoWFactory.sol | 43 ++++- test/manual-smock/MockBCoWHelper.sol | 57 +++++++ test/manual-smock/MockBCoWPool.sol | 92 ++++++++++- test/unit/BCoWHelper.t.sol | 222 ++++++++++++++++++++++++++ test/unit/BCoWHelper.tree | 33 ++++ yarn.lock | 4 + 11 files changed, 759 insertions(+), 3 deletions(-) create mode 100644 src/contracts/BCoWHelper.sol create mode 100644 test/integration/BCoWHelper.t.sol create mode 100644 test/manual-smock/MockBCoWHelper.sol create mode 100644 test/unit/BCoWHelper.t.sol create mode 100644 test/unit/BCoWHelper.tree diff --git a/package.json b/package.json index 836757e0..90eb4343 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "@cowprotocol/contracts": "github:cowprotocol/contracts.git#a10f40788a", "@openzeppelin/contracts": "5.0.2", "composable-cow": "github:cowprotocol/composable-cow.git#24d556b", + "cow-amm": "github:cowprotocol/cow-amm.git#6566128", "solmate": "github:transmissions11/solmate#c892309" }, "devDependencies": { diff --git a/remappings.txt b/remappings.txt index 5b011a43..08fc002a 100644 --- a/remappings.txt +++ b/remappings.txt @@ -5,6 +5,9 @@ solmate/=node_modules/solmate/src @cowprotocol/=node_modules/@cowprotocol/contracts/src/contracts cowprotocol/=node_modules/@cowprotocol/contracts/src/ @composable-cow/=node_modules/composable-cow/ +@cow-amm/=node_modules/cow-amm/src +lib/openzeppelin/=node_modules/@openzeppelin contracts/=src/contracts interfaces/=src/interfaces +libraries/=src/libraries diff --git a/src/contracts/BCoWHelper.sol b/src/contracts/BCoWHelper.sol new file mode 100644 index 00000000..4562bcda --- /dev/null +++ b/src/contracts/BCoWHelper.sol @@ -0,0 +1,125 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.25; + +import {IBCoWFactory} from 'interfaces/IBCoWFactory.sol'; +import {IBCoWPool} from 'interfaces/IBCoWPool.sol'; + +import {ICOWAMMPoolHelper} from '@cow-amm/interfaces/ICOWAMMPoolHelper.sol'; +import {GetTradeableOrder} from '@cow-amm/libraries/GetTradeableOrder.sol'; + +import {IERC20} from '@cowprotocol/interfaces/IERC20.sol'; +import {GPv2Interaction} from '@cowprotocol/libraries/GPv2Interaction.sol'; +import {GPv2Order} from '@cowprotocol/libraries/GPv2Order.sol'; + +import {BMath} from 'contracts/BMath.sol'; + +/** + * @title BCoWHelper + * @notice Helper contract that allows to trade on CoW Swap Protocol. + * @dev This contract supports only 2-token equal-weights pools. + */ +contract BCoWHelper is ICOWAMMPoolHelper, BMath { + using GPv2Order for GPv2Order.Data; + + /// @notice The app data used by this helper's factory. + bytes32 internal immutable _APP_DATA; + + /// @inheritdoc ICOWAMMPoolHelper + // solhint-disable-next-line style-guide-casing + address public immutable factory; + + constructor(address factory_) { + factory = factory_; + _APP_DATA = IBCoWFactory(factory_).APP_DATA(); + } + + /// @inheritdoc ICOWAMMPoolHelper + function order( + address pool, + uint256[] calldata prices + ) + external + view + returns ( + GPv2Order.Data memory order_, + GPv2Interaction.Data[] memory preInteractions, + GPv2Interaction.Data[] memory postInteractions, + bytes memory sig + ) + { + address[] memory tokens_ = tokens(pool); + + GetTradeableOrder.GetTradeableOrderParams memory params = GetTradeableOrder.GetTradeableOrderParams({ + pool: pool, + token0: IERC20(tokens_[0]), + token1: IERC20(tokens_[1]), + // The price of this function is expressed as amount of + // token1 per amount of token0. The `prices` vector is + // expressed the other way around. + priceNumerator: prices[1], + priceDenominator: prices[0], + appData: _APP_DATA + }); + + order_ = GetTradeableOrder.getTradeableOrder(params); + + { + // NOTE: Using calcOutGivenIn for the sell amount in order to avoid possible rounding + // issues that may cause invalid orders. This prevents CoW Protocol back-end from generating + // orders that may be ignored due to rounding-induced reverts. + + uint256 balanceToken0 = IERC20(tokens_[0]).balanceOf(pool); + uint256 balanceToken1 = IERC20(tokens_[1]).balanceOf(pool); + (uint256 balanceIn, uint256 balanceOut) = + address(order_.buyToken) == tokens_[0] ? (balanceToken0, balanceToken1) : (balanceToken1, balanceToken0); + + order_.sellAmount = calcOutGivenIn({ + tokenBalanceIn: balanceIn, + tokenWeightIn: 1e18, + tokenBalanceOut: balanceOut, + tokenWeightOut: 1e18, + tokenAmountIn: order_.buyAmount, + swapFee: 0 + }); + } + + // A ERC-1271 signature on CoW Protocol is composed of two parts: the + // signer address and the valid ERC-1271 signature data for that signer. + bytes memory eip1271sig; + eip1271sig = abi.encode(order_); + sig = abi.encodePacked(pool, eip1271sig); + + // Generate the order commitment pre-interaction + bytes32 domainSeparator = IBCoWPool(pool).SOLUTION_SETTLER_DOMAIN_SEPARATOR(); + bytes32 orderCommitment = order_.hash(domainSeparator); + + preInteractions = new GPv2Interaction.Data[](1); + preInteractions[0] = GPv2Interaction.Data({ + target: pool, + value: 0, + callData: abi.encodeWithSelector(IBCoWPool.commit.selector, orderCommitment) + }); + + return (order_, preInteractions, postInteractions, sig); + } + + /// @inheritdoc ICOWAMMPoolHelper + function tokens(address pool) public view virtual returns (address[] memory tokens_) { + // reverts in case pool is not deployed by the helper's factory + if (!IBCoWFactory(factory).isBPool(pool)) { + revert PoolDoesNotExist(); + } + + // call reverts with `BPool_PoolNotFinalized()` in case pool is not finalized + tokens_ = IBCoWPool(pool).getFinalTokens(); + + // reverts in case pool is not supported (non-2-token pool) + if (tokens_.length != 2) { + revert PoolDoesNotExist(); + } + // reverts in case pool is not supported (non-equal weights) + if (IBCoWPool(pool).getNormalizedWeight(tokens_[0]) != IBCoWPool(pool).getNormalizedWeight(tokens_[1])) { + revert PoolDoesNotExist(); + } + } +} diff --git a/test/integration/BCoWHelper.t.sol b/test/integration/BCoWHelper.t.sol new file mode 100644 index 00000000..7c1fc536 --- /dev/null +++ b/test/integration/BCoWHelper.t.sol @@ -0,0 +1,181 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.24; + +import {Test} from 'forge-std/Test.sol'; + +import {IERC20} from '@cowprotocol/interfaces/IERC20.sol'; + +import {IBCoWPool} from 'interfaces/IBCoWPool.sol'; +import {IBPool} from 'interfaces/IBPool.sol'; +import {ISettlement} from 'interfaces/ISettlement.sol'; + +import {ICOWAMMPoolHelper} from '@cow-amm/interfaces/ICOWAMMPoolHelper.sol'; +import {GPv2Interaction} from '@cowprotocol/libraries/GPv2Interaction.sol'; +import {GPv2Order} from '@cowprotocol/libraries/GPv2Order.sol'; +import {GPv2Trade} from '@cowprotocol/libraries/GPv2Trade.sol'; +import {GPv2Signing} from '@cowprotocol/mixins/GPv2Signing.sol'; + +import {GPv2TradeEncoder} from '@composable-cow/test/vendored/GPv2TradeEncoder.sol'; + +import {BCoWFactory} from 'contracts/BCoWFactory.sol'; +import {BCoWHelper} from 'contracts/BCoWHelper.sol'; + +contract BCoWHelperIntegrationTest is Test { + using GPv2Order for GPv2Order.Data; + + BCoWHelper private helper; + + // All hardcoded addresses are mainnet addresses + address public lp = makeAddr('lp'); + + ISettlement private settlement = ISettlement(0x9008D19f58AAbD9eD0D60971565AA8510560ab41); + address private vaultRelayer; + + address private solver = 0x423cEc87f19F0778f549846e0801ee267a917935; + + BCoWFactory private ammFactory; + IBPool private weightedPool; + IBPool private basicPool; + + IERC20 private constant DAI = IERC20(0x6B175474E89094C44Da98b954EedeAC495271d0F); + IERC20 private constant WETH = IERC20(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); + + uint256 constant TEN_PERCENT = 0.1 ether; + // NOTE: 1 ETH = 1000 DAI + uint256 constant INITIAL_DAI_BALANCE = 1000 ether; + uint256 constant INITIAL_WETH_BALANCE = 1 ether; + uint256 constant INITIAL_SPOT_PRICE = 0.001e18; + + uint256 constant SKEWENESS_RATIO = 95; // -5% skewness + uint256 constant EXPECTED_FINAL_SPOT_PRICE = INITIAL_SPOT_PRICE * 100 / SKEWENESS_RATIO; + + function setUp() public { + vm.createSelectFork('mainnet', 20_012_063); + + vaultRelayer = address(settlement.vaultRelayer()); + + ammFactory = new BCoWFactory(address(settlement), bytes32('appData')); + helper = new BCoWHelper(address(ammFactory)); + + deal(address(DAI), lp, type(uint256).max, false); + deal(address(WETH), lp, type(uint256).max, false); + + vm.startPrank(lp); + basicPool = ammFactory.newBPool(); + weightedPool = ammFactory.newBPool(); + + DAI.approve(address(basicPool), type(uint256).max); + WETH.approve(address(basicPool), type(uint256).max); + basicPool.bind(address(DAI), INITIAL_DAI_BALANCE, 4.2e18); // no weight + basicPool.bind(address(WETH), INITIAL_WETH_BALANCE, 4.2e18); // no weight + + DAI.approve(address(weightedPool), type(uint256).max); + WETH.approve(address(weightedPool), type(uint256).max); + // NOTE: pool is 80-20 DAI-WETH, has 4xDAI balance than basic, same spot price + weightedPool.bind(address(DAI), 4 * INITIAL_DAI_BALANCE, 8e18); // 80% weight + weightedPool.bind(address(WETH), INITIAL_WETH_BALANCE, 2e18); // 20% weight + + // finalize + basicPool.finalize(); + weightedPool.finalize(); + + vm.stopPrank(); + } + + function testBasicOrder() public { + IBCoWPool pool = IBCoWPool(address(basicPool)); + + uint256 spotPrice = pool.getSpotPriceSansFee(address(WETH), address(DAI)); + assertEq(spotPrice, INITIAL_SPOT_PRICE); + + _executeHelperOrder(pool); + + uint256 postSpotPrice = pool.getSpotPriceSansFee(address(WETH), address(DAI)); + assertEq(postSpotPrice, EXPECTED_FINAL_SPOT_PRICE); + } + + // NOTE: reverting test, weighted pools are not supported + function testWeightedOrder() public { + IBCoWPool pool = IBCoWPool(address(weightedPool)); + + uint256 spotPrice = pool.getSpotPriceSansFee(address(WETH), address(DAI)); + assertEq(spotPrice, INITIAL_SPOT_PRICE); + + vm.expectRevert(ICOWAMMPoolHelper.PoolDoesNotExist.selector); + helper.order(address(pool), new uint256[](2)); + } + + function _executeHelperOrder(IBPool pool) internal { + address[] memory tokens = helper.tokens(address(pool)); + uint256 daiIndex = 0; + uint256 wethIndex = 1; + assertEq(tokens.length, 2); + assertEq(tokens[daiIndex], address(DAI)); + assertEq(tokens[wethIndex], address(WETH)); + + // Prepare the price vector used in the execution of the settlement in + // CoW Protocol. We skew the price by ~5% towards a cheaper WETH, so + // that the AMM wants to buy WETH. + uint256[] memory prices = new uint256[](2); + // Note: oracle price are expressed in the same format as prices in + // a call to `settle`, where the price vector is expressed so that + // if the first token is DAI and the second WETH then a price of 3000 + // DAI per WETH means a price vector of [1, 3000] (if the decimals are + // different, as in WETH/USDC, then the atom amount is what counts). + prices[daiIndex] = INITIAL_WETH_BALANCE; + prices[wethIndex] = INITIAL_DAI_BALANCE * SKEWENESS_RATIO / 100; + + // The helper generates the AMM order + GPv2Order.Data memory ammOrder; + GPv2Interaction.Data[] memory preInteractions; + GPv2Interaction.Data[] memory postInteractions; + bytes memory sig; + (ammOrder, preInteractions, postInteractions, sig) = helper.order(address(pool), prices); + + // We expect a commit interaction in pre interactions + assertEq(preInteractions.length, 1); + assertEq(postInteractions.length, 0); + + // Because of how we changed the price, we expect to buy WETH + assertEq(address(ammOrder.sellToken), address(DAI)); + assertEq(address(ammOrder.buyToken), address(WETH)); + + // Check that the amounts and price aren't unreasonable. We changed the + // price by about 5%, so the amounts aren't expected to change + // significantly more (say, about 2.5% of the original balance). + assertApproxEqRel(ammOrder.sellAmount, INITIAL_DAI_BALANCE * 25 / 1000, TEN_PERCENT); + assertApproxEqRel(ammOrder.buyAmount, INITIAL_WETH_BALANCE * 25 / 1000, TEN_PERCENT); + + GPv2Trade.Data[] memory trades = new GPv2Trade.Data[](1); + + // pool's trade + trades[0] = GPv2Trade.Data({ + sellTokenIndex: 0, + buyTokenIndex: 1, + receiver: ammOrder.receiver, + sellAmount: ammOrder.sellAmount, + buyAmount: ammOrder.buyAmount, + validTo: ammOrder.validTo, + appData: ammOrder.appData, + feeAmount: ammOrder.feeAmount, + flags: GPv2TradeEncoder.encodeFlags(ammOrder, GPv2Signing.Scheme.Eip1271), + executedAmount: ammOrder.sellAmount, + signature: sig + }); + + GPv2Interaction.Data[][3] memory interactions = + [new GPv2Interaction.Data[](1), new GPv2Interaction.Data[](0), new GPv2Interaction.Data[](0)]; + + interactions[0][0] = preInteractions[0]; + + // cast tokens array to IERC20 array + IERC20[] memory ierc20vec; + assembly { + ierc20vec := tokens + } + + // finally, settle + vm.prank(solver); + settlement.settle(ierc20vec, prices, trades, interactions); + } +} diff --git a/test/integration/BCowPool.t.sol b/test/integration/BCowPool.t.sol index 83a16922..da711035 100644 --- a/test/integration/BCowPool.t.sol +++ b/test/integration/BCowPool.t.sol @@ -8,6 +8,7 @@ import {GPv2Interaction} from '@cowprotocol/libraries/GPv2Interaction.sol'; import {GPv2Order} from '@cowprotocol/libraries/GPv2Order.sol'; import {GPv2Trade} from '@cowprotocol/libraries/GPv2Trade.sol'; import {GPv2Signing} from '@cowprotocol/mixins/GPv2Signing.sol'; + import {BCoWConst} from 'contracts/BCoWConst.sol'; import {BCoWFactory} from 'contracts/BCoWFactory.sol'; diff --git a/test/manual-smock/MockBCoWFactory.sol b/test/manual-smock/MockBCoWFactory.sol index 5e7baa5a..c4b2e427 100644 --- a/test/manual-smock/MockBCoWFactory.sol +++ b/test/manual-smock/MockBCoWFactory.sol @@ -5,6 +5,16 @@ import {BCoWFactory, BCoWPool, BFactory, IBCoWFactory, IBPool} from '../../src/c import {Test} from 'forge-std/Test.sol'; contract MockBCoWFactory is BCoWFactory, Test { + // NOTE: manually added methods (immutable overrides not supported in smock) + function mock_call_APP_DATA(bytes32 _appData) public { + vm.mockCall(address(this), abi.encodeWithSignature('APP_DATA()'), abi.encode(_appData)); + } + + function expectCall_APP_DATA() public { + vm.expectCall(address(this), abi.encodeWithSignature('APP_DATA()')); + } + + // BCoWFactory methods constructor(address solutionSettler, bytes32 appData) BCoWFactory(solutionSettler, appData) {} function mock_call_logBCoWPool() public { @@ -31,8 +41,39 @@ contract MockBCoWFactory is BCoWFactory, Test { } // MockBFactory methods - function set__isBPool(address _key0, bool _value) public { _isBPool[_key0] = _value; } + + function call__isBPool(address _key0) public view returns (bool) { + return _isBPool[_key0]; + } + + function set__bDao(address __bDao) public { + _bDao = __bDao; + } + + function call__bDao() public view returns (address) { + return _bDao; + } + + function mock_call_newBPool(IBPool bPool) public { + vm.mockCall(address(this), abi.encodeWithSignature('newBPool()'), abi.encode(bPool)); + } + + function mock_call_setBDao(address bDao) public { + vm.mockCall(address(this), abi.encodeWithSignature('setBDao(address)', bDao), abi.encode()); + } + + function mock_call_collect(IBPool bPool) public { + vm.mockCall(address(this), abi.encodeWithSignature('collect(IBPool)', bPool), abi.encode()); + } + + function mock_call_isBPool(address bPool, bool _returnParam0) public { + vm.mockCall(address(this), abi.encodeWithSignature('isBPool(address)', bPool), abi.encode(_returnParam0)); + } + + function mock_call_getBDao(address _returnParam0) public { + vm.mockCall(address(this), abi.encodeWithSignature('getBDao()'), abi.encode(_returnParam0)); + } } diff --git a/test/manual-smock/MockBCoWHelper.sol b/test/manual-smock/MockBCoWHelper.sol new file mode 100644 index 00000000..003b4ce2 --- /dev/null +++ b/test/manual-smock/MockBCoWHelper.sol @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.0; + +import { + BCoWHelper, + BMath, + GPv2Interaction, + GPv2Order, + GetTradeableOrder, + IBCoWFactory, + IBCoWPool, + ICOWAMMPoolHelper, + IERC20 +} from '../../src/contracts/BCoWHelper.sol'; +import {Test} from 'forge-std/Test.sol'; + +contract MockBCoWHelper is BCoWHelper, Test { + // NOTE: manually added methods (internal immutable exposers not supported in smock) + function call__APP_DATA() external view returns (bytes32) { + return _APP_DATA; + } + + // NOTE: manually added method (public overrides not supported in smock) + function tokens(address pool) public view override returns (address[] memory tokens_) { + (bool _success, bytes memory _data) = address(this).staticcall(abi.encodeWithSignature('tokens(address)', pool)); + + if (_success) return abi.decode(_data, (address[])); + else return super.tokens(pool); + } + + // NOTE: manually added method (public overrides not supported in smock) + function expectCall_tokens(address pool) public { + vm.expectCall(address(this), abi.encodeWithSignature('tokens(address)', pool)); + } + + // BCoWHelper methods + constructor(address factory_) BCoWHelper(factory_) {} + + function mock_call_order( + address pool, + uint256[] calldata prices, + GPv2Order.Data memory order_, + GPv2Interaction.Data[] memory preInteractions, + GPv2Interaction.Data[] memory postInteractions, + bytes memory sig + ) public { + vm.mockCall( + address(this), + abi.encodeWithSignature('order(address,uint256[])', pool, prices), + abi.encode(order_, preInteractions, postInteractions, sig) + ); + } + + function mock_call_tokens(address pool, address[] memory tokens_) public { + vm.mockCall(address(this), abi.encodeWithSignature('tokens(address)', pool), abi.encode(tokens_)); + } +} diff --git a/test/manual-smock/MockBCoWPool.sol b/test/manual-smock/MockBCoWPool.sol index c94a6bb1..98b19d1f 100644 --- a/test/manual-smock/MockBCoWPool.sol +++ b/test/manual-smock/MockBCoWPool.sol @@ -21,8 +21,18 @@ contract MockBCoWPool is BCoWPool, Test { vm.expectCall(address(this), abi.encodeWithSignature('verify(GPv2Order.Data)', order)); } - /// MockBCoWPool mock methods + // NOTE: manually added methods (immutable overrides not supported in smock) + function mock_call_SOLUTION_SETTLER_DOMAIN_SEPARATOR(bytes32 domainSeparator) public { + vm.mockCall( + address(this), abi.encodeWithSignature('SOLUTION_SETTLER_DOMAIN_SEPARATOR()'), abi.encode(domainSeparator) + ); + } + function expectCall_SOLUTION_SETTLER_DOMAIN_SEPARATOR() public { + vm.expectCall(address(this), abi.encodeWithSignature('SOLUTION_SETTLER_DOMAIN_SEPARATOR()')); + } + + /// MockBCoWPool mock methods constructor(address cowSolutionSettler, bytes32 appData) BCoWPool(cowSolutionSettler, appData) {} function mock_call_commit(bytes32 orderHash) public { @@ -406,6 +416,84 @@ contract MockBCoWPool is BCoWPool, Test { vm.expectCall(address(this), abi.encodeWithSignature('_afterFinalize()')); } + function mock_call__pullPoolShare(address from, uint256 amount) public { + vm.mockCall(address(this), abi.encodeWithSignature('_pullPoolShare(address,uint256)', from, amount), abi.encode()); + } + + function _pullPoolShare(address from, uint256 amount) internal override { + (bool _success, bytes memory _data) = + address(this).call(abi.encodeWithSignature('_pullPoolShare(address,uint256)', from, amount)); + + if (_success) return abi.decode(_data, ()); + else return super._pullPoolShare(from, amount); + } + + function call__pullPoolShare(address from, uint256 amount) public { + return _pullPoolShare(from, amount); + } + + function expectCall__pullPoolShare(address from, uint256 amount) public { + vm.expectCall(address(this), abi.encodeWithSignature('_pullPoolShare(address,uint256)', from, amount)); + } + + function mock_call__pushPoolShare(address to, uint256 amount) public { + vm.mockCall(address(this), abi.encodeWithSignature('_pushPoolShare(address,uint256)', to, amount), abi.encode()); + } + + function _pushPoolShare(address to, uint256 amount) internal override { + (bool _success, bytes memory _data) = + address(this).call(abi.encodeWithSignature('_pushPoolShare(address,uint256)', to, amount)); + + if (_success) return abi.decode(_data, ()); + else return super._pushPoolShare(to, amount); + } + + function call__pushPoolShare(address to, uint256 amount) public { + return _pushPoolShare(to, amount); + } + + function expectCall__pushPoolShare(address to, uint256 amount) public { + vm.expectCall(address(this), abi.encodeWithSignature('_pushPoolShare(address,uint256)', to, amount)); + } + + function mock_call__mintPoolShare(uint256 amount) public { + vm.mockCall(address(this), abi.encodeWithSignature('_mintPoolShare(uint256)', amount), abi.encode()); + } + + function _mintPoolShare(uint256 amount) internal override { + (bool _success, bytes memory _data) = address(this).call(abi.encodeWithSignature('_mintPoolShare(uint256)', amount)); + + if (_success) return abi.decode(_data, ()); + else return super._mintPoolShare(amount); + } + + function call__mintPoolShare(uint256 amount) public { + return _mintPoolShare(amount); + } + + function expectCall__mintPoolShare(uint256 amount) public { + vm.expectCall(address(this), abi.encodeWithSignature('_mintPoolShare(uint256)', amount)); + } + + function mock_call__burnPoolShare(uint256 amount) public { + vm.mockCall(address(this), abi.encodeWithSignature('_burnPoolShare(uint256)', amount), abi.encode()); + } + + function _burnPoolShare(uint256 amount) internal override { + (bool _success, bytes memory _data) = address(this).call(abi.encodeWithSignature('_burnPoolShare(uint256)', amount)); + + if (_success) return abi.decode(_data, ()); + else return super._burnPoolShare(amount); + } + + function call__burnPoolShare(uint256 amount) public { + return _burnPoolShare(amount); + } + + function expectCall__burnPoolShare(uint256 amount) public { + vm.expectCall(address(this), abi.encodeWithSignature('_burnPoolShare(uint256)', amount)); + } + function mock_call__getLock(bytes32 value) public { vm.mockCall(address(this), abi.encodeWithSignature('_getLock()'), abi.encode(value)); } @@ -417,7 +505,7 @@ contract MockBCoWPool is BCoWPool, Test { else return super._getLock(); } - function call__getLock() public returns (bytes32 value) { + function call__getLock() public view returns (bytes32 value) { return _getLock(); } diff --git a/test/unit/BCoWHelper.t.sol b/test/unit/BCoWHelper.t.sol new file mode 100644 index 00000000..ef6d03b5 --- /dev/null +++ b/test/unit/BCoWHelper.t.sol @@ -0,0 +1,222 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.25; + +import {Test} from 'forge-std/Test.sol'; +import {MockBCoWHelper} from 'test/manual-smock/MockBCoWHelper.sol'; + +import {IBCoWPool} from 'interfaces/IBCoWPool.sol'; +import {IBPool} from 'interfaces/IBPool.sol'; +import {ISettlement} from 'interfaces/ISettlement.sol'; + +import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; + +import {ICOWAMMPoolHelper} from '@cow-amm/interfaces/ICOWAMMPoolHelper.sol'; +import {GPv2Interaction} from '@cowprotocol/libraries/GPv2Interaction.sol'; +import {GPv2Order} from '@cowprotocol/libraries/GPv2Order.sol'; + +import {MockBCoWFactory} from 'test/manual-smock/MockBCoWFactory.sol'; +import {MockBCoWPool} from 'test/manual-smock/MockBCoWPool.sol'; + +contract BCoWHelperTest is Test { + MockBCoWHelper helper; + + MockBCoWFactory factory; + MockBCoWPool pool; + address invalidPool = makeAddr('invalidPool'); + address[] tokens = new address[](2); + uint256[] priceVector = new uint256[](2); + + uint256 constant VALID_WEIGHT = 1e18; + uint256 constant BASE = 1e18; + + function setUp() external { + factory = new MockBCoWFactory(address(0), bytes32(0)); + + address solutionSettler = makeAddr('solutionSettler'); + vm.mockCall( + solutionSettler, abi.encodePacked(ISettlement.domainSeparator.selector), abi.encode(bytes32('domainSeparator')) + ); + vm.mockCall( + solutionSettler, abi.encodePacked(ISettlement.vaultRelayer.selector), abi.encode(makeAddr('vaultRelayer')) + ); + pool = new MockBCoWPool(makeAddr('solutionSettler'), bytes32(0)); + + // creating a valid pool setup + factory.mock_call_isBPool(address(pool), true); + tokens[0] = makeAddr('token0'); + tokens[1] = makeAddr('token1'); + pool.set__tokens(tokens); + pool.set__records(tokens[0], IBPool.Record({bound: true, index: 0, denorm: VALID_WEIGHT})); + pool.set__records(tokens[1], IBPool.Record({bound: true, index: 1, denorm: VALID_WEIGHT})); + pool.set__totalWeight(2 * VALID_WEIGHT); + pool.set__finalized(true); + + priceVector[0] = 1e18; + priceVector[1] = 1.05e18; + + vm.mockCall(tokens[0], abi.encodePacked(IERC20.balanceOf.selector), abi.encode(priceVector[0])); + vm.mockCall(tokens[1], abi.encodePacked(IERC20.balanceOf.selector), abi.encode(priceVector[1])); + + factory.mock_call_APP_DATA(bytes32('appData')); + helper = new MockBCoWHelper(address(factory)); + } + + function test_ConstructorWhenCalled(bytes32 _appData) external { + factory.expectCall_APP_DATA(); + factory.mock_call_APP_DATA(_appData); + helper = new MockBCoWHelper(address(factory)); + // it should set factory + assertEq(helper.factory(), address(factory)); + // it should set app data from factory + assertEq(helper.call__APP_DATA(), _appData); + } + + function test_TokensRevertWhen_PoolIsNotRegisteredInFactory() external { + factory.mock_call_isBPool(address(pool), false); + // it should revert + vm.expectRevert(ICOWAMMPoolHelper.PoolDoesNotExist.selector); + helper.tokens(address(pool)); + } + + function test_TokensRevertWhen_PoolHasLessThan2Tokens() external { + address[] memory invalidTokens = new address[](1); + invalidTokens[0] = makeAddr('token0'); + pool.set__tokens(invalidTokens); + // it should revert + vm.expectRevert(ICOWAMMPoolHelper.PoolDoesNotExist.selector); + helper.tokens(address(pool)); + } + + function test_TokensRevertWhen_PoolHasMoreThan2Tokens() external { + address[] memory invalidTokens = new address[](3); + invalidTokens[0] = makeAddr('token0'); + invalidTokens[1] = makeAddr('token1'); + invalidTokens[2] = makeAddr('token2'); + pool.set__tokens(invalidTokens); + // it should revert + vm.expectRevert(ICOWAMMPoolHelper.PoolDoesNotExist.selector); + helper.tokens(address(pool)); + } + + function test_TokensRevertWhen_PoolTokensHaveDifferentWeights() external { + pool.mock_call_getNormalizedWeight(tokens[0], VALID_WEIGHT); + pool.mock_call_getNormalizedWeight(tokens[1], VALID_WEIGHT + 1); + + vm.expectRevert(ICOWAMMPoolHelper.PoolDoesNotExist.selector); + // it should revert + helper.tokens(address(pool)); + } + + function test_TokensWhenPoolIsSupported() external view { + // it should return pool tokens + address[] memory returned = helper.tokens(address(pool)); + assertEq(returned[0], tokens[0]); + assertEq(returned[1], tokens[1]); + } + + function test_OrderRevertWhen_ThePoolIsNotSupported() external { + // it should revert + vm.expectRevert(ICOWAMMPoolHelper.PoolDoesNotExist.selector); + helper.order(invalidPool, priceVector); + } + + function test_OrderWhenThePoolIsSupported(bytes32 domainSeparator) external { + // it should call tokens + helper.mock_call_tokens(address(pool), tokens); + helper.expectCall_tokens(address(pool)); + + // it should query the domain separator from the pool + pool.expectCall_SOLUTION_SETTLER_DOMAIN_SEPARATOR(); + pool.mock_call_SOLUTION_SETTLER_DOMAIN_SEPARATOR(domainSeparator); + + ( + GPv2Order.Data memory order_, + GPv2Interaction.Data[] memory preInteractions, + GPv2Interaction.Data[] memory postInteractions, + bytes memory sig + ) = helper.order(address(pool), priceVector); + + // it should return a valid pool order + assertEq(order_.receiver, GPv2Order.RECEIVER_SAME_AS_OWNER); + assertLe(order_.validTo, block.timestamp + 5 minutes); + assertEq(order_.feeAmount, 0); + assertEq(order_.appData, factory.APP_DATA()); + assertEq(order_.kind, GPv2Order.KIND_SELL); + assertEq(order_.buyTokenBalance, GPv2Order.BALANCE_ERC20); + assertEq(order_.sellTokenBalance, GPv2Order.BALANCE_ERC20); + + // it should return a commit pre-interaction + assertEq(preInteractions.length, 1); + assertEq(preInteractions[0].target, address(pool)); + assertEq(preInteractions[0].value, 0); + bytes memory commitment = abi.encodeCall(IBCoWPool.commit, GPv2Order.hash(order_, domainSeparator)); + assertEq(keccak256(preInteractions[0].callData), keccak256(commitment)); + + // it should return an empty post-interaction + assertTrue(postInteractions.length == 0); + + // it should return a valid signature + bytes memory validSig = abi.encodePacked(pool, abi.encode(order_)); + assertEq(keccak256(validSig), keccak256(sig)); + } + + function test_OrderGivenAPriceSkewenessToToken1( + uint256 priceSkewness, + uint256 balanceToken0, + uint256 balanceToken1 + ) external { + // skew the price by max 50% (more could result in reverts bc of max swap ratio) + // avoids no-skewness revert + priceSkewness = bound(priceSkewness, BASE + 0.0001e18, 1.5e18); + + balanceToken0 = bound(balanceToken0, 1e18, 1e27); + balanceToken1 = bound(balanceToken1, 1e18, 1e27); + vm.mockCall(tokens[0], abi.encodePacked(IERC20.balanceOf.selector), abi.encode(balanceToken0)); + vm.mockCall(tokens[1], abi.encodePacked(IERC20.balanceOf.selector), abi.encode(balanceToken1)); + + // NOTE: the price of token 1 is increased by the skeweness + uint256[] memory prices = new uint256[](2); + prices[0] = balanceToken1; + prices[1] = balanceToken0 * priceSkewness / BASE; + + // it should return a valid pool order + (GPv2Order.Data memory ammOrder,,,) = helper.order(address(pool), prices); + + // it should buy token0 + assertEq(address(ammOrder.buyToken), tokens[0]); + + // it should return a valid pool order + // this call should not revert + pool.verify(ammOrder); + } + + function test_OrderGivenAPriceSkewenessToToken0( + uint256 priceSkewness, + uint256 balanceToken0, + uint256 balanceToken1 + ) external { + // skew the price by max 50% (more could result in reverts bc of max swap ratio) + // avoids no-skewness revert + priceSkewness = bound(priceSkewness, 0.5e18, BASE - 0.0001e18); + + balanceToken0 = bound(balanceToken0, 1e18, 1e27); + balanceToken1 = bound(balanceToken1, 1e18, 1e27); + vm.mockCall(tokens[0], abi.encodePacked(IERC20.balanceOf.selector), abi.encode(balanceToken0)); + vm.mockCall(tokens[1], abi.encodePacked(IERC20.balanceOf.selector), abi.encode(balanceToken1)); + + // NOTE: the price of token 1 is decrease by the skeweness + uint256[] memory prices = new uint256[](2); + prices[0] = balanceToken1; + prices[1] = balanceToken0 * priceSkewness / BASE; + + // it should return a valid pool order + (GPv2Order.Data memory ammOrder,,,) = helper.order(address(pool), prices); + + // it should buy token1 + assertEq(address(ammOrder.buyToken), tokens[1]); + + // it should return a valid pool order + // this call should not revert + pool.verify(ammOrder); + } +} diff --git a/test/unit/BCoWHelper.tree b/test/unit/BCoWHelper.tree new file mode 100644 index 00000000..e27a5207 --- /dev/null +++ b/test/unit/BCoWHelper.tree @@ -0,0 +1,33 @@ +BCoWHelperTest::constructor +└── when called + ├── it should set factory + └── it should set app data from factory + +BCoWHelperTest::tokens +├── when pool is not registered in factory +│ └── it should revert +├── when pool has less than 2 tokens +│ └── it should revert +├── when pool has more than 2 tokens +│ └── it should revert +├── when pool tokens have different weights +│ └── it should revert +└── when pool is supported + └── it should return pool tokens + +BCoWHelperTest::order +├── when the pool is not supported +│ └── it should revert +├── when the pool is supported +│ ├── it should call tokens +│ ├── it should query the domain separator from the pool +│ ├── it should return a valid pool order +│ ├── it should return a commit pre-interaction +│ ├── it should return an empty post-interaction +│ └── it should return a valid signature +├── given a price skeweness to token1 +│ ├── it should buy token0 +│ └── it should return a valid pool order +└── given a price skeweness to token0 + ├── it should buy token1 + └── it should return a valid pool order diff --git a/yarn.lock b/yarn.lock index 27b5429f..bba6419e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -603,6 +603,10 @@ cosmiconfig@^9.0.0: js-yaml "^4.1.0" parse-json "^5.2.0" +"cow-amm@github:cowprotocol/cow-amm.git#6566128": + version "0.0.0" + resolved "https://codeload.github.com/cowprotocol/cow-amm/tar.gz/6566128b6c73008062cf4a6d1957db602409b719" + cross-spawn@^7.0.3: version "7.0.3" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" From eef9f2a62e3b508f6ec1015514b7e5f6272c830e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wei=C3=9Fer=20Hase?= Date: Tue, 23 Jul 2024 11:29:02 +0200 Subject: [PATCH 38/48] feat: adding btt tests for bToken (#167) * feat: adding btt tests for bToken * feat: testing address zero checks on increase/decrease approvals * feat: adding more checks to bToken tests --- test/unit/BToken.t.sol | 217 ++++++++++++++++++----------------------- test/unit/BToken.tree | 38 ++++++++ 2 files changed, 131 insertions(+), 124 deletions(-) create mode 100644 test/unit/BToken.tree diff --git a/test/unit/BToken.t.sol b/test/unit/BToken.t.sol index ac16f760..1207a294 100644 --- a/test/unit/BToken.t.sol +++ b/test/unit/BToken.t.sol @@ -1,154 +1,123 @@ -// SPDX-License-Identifier: GPL-3 -pragma solidity ^0.8.25; +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.25; +import {IERC20} from '@openzeppelin/contracts/interfaces/IERC20.sol'; import {IERC20Errors} from '@openzeppelin/contracts/interfaces/draft-IERC6093.sol'; import {Test} from 'forge-std/Test.sol'; import {MockBToken} from 'test/smock/MockBToken.sol'; -contract BToken_Unit_Constructor is Test { - function test_ConstructorParams() public { - MockBToken btoken = new MockBToken(); - assertEq(btoken.name(), 'Balancer Pool Token'); - assertEq(btoken.symbol(), 'BPT'); - assertEq(btoken.decimals(), 18); +contract BToken is Test { + MockBToken public bToken; + uint256 public initialApproval = 100e18; + uint256 public initialBalance = 100e18; + address public caller = makeAddr('caller'); + address public spender = makeAddr('spender'); + address public target = makeAddr('target'); + + function setUp() external { + bToken = new MockBToken(); + + vm.startPrank(caller); + // sets initial approval (cannot be mocked) + bToken.approve(spender, initialApproval); } -} -abstract contract BToken_Unit_base is Test { - MockBToken internal bToken; + function test_ConstructorWhenCalled() external { + MockBToken _bToken = new MockBToken(); + // it sets token name + assertEq(_bToken.name(), 'Balancer Pool Token'); + // it sets token symbol + assertEq(_bToken.symbol(), 'BPT'); + } - modifier assumeNonZeroAddresses(address addr1, address addr2) { - vm.assume(addr1 != address(0)); - vm.assume(addr2 != address(0)); - _; + function test_IncreaseApprovalRevertWhen_SenderIsAddressZero() external { + vm.startPrank(address(0)); + // it should revert + vm.expectRevert(abi.encodeWithSelector(IERC20Errors.ERC20InvalidApprover.selector, address(0))); + + bToken.increaseApproval(spender, 100e18); } - modifier assumeNonZeroAddress(address addr) { - vm.assume(addr != address(0)); - _; + function test_IncreaseApprovalRevertWhen_SpenderIsAddressZero() external { + // it should revert + vm.expectRevert(abi.encodeWithSelector(IERC20Errors.ERC20InvalidSpender.selector, address(0))); + bToken.increaseApproval(address(0), 100e18); } - function setUp() public virtual { - bToken = new MockBToken(); + function test_IncreaseApprovalWhenCalled() external { + // it emits Approval event + vm.expectEmit(); + emit IERC20.Approval(caller, spender, 200e18); + + bToken.increaseApproval(spender, 100e18); + // it increases spender approval + assertEq(bToken.allowance(caller, spender), 200e18); } -} -contract BToken_Unit_IncreaseApproval is BToken_Unit_base { - function test_increasesApprovalFromZero( - address sender, - address spender, - uint256 amount - ) public assumeNonZeroAddresses(sender, spender) { - vm.prank(sender); - bToken.increaseApproval(spender, amount); - assertEq(bToken.allowance(sender, spender), amount); + function test_DecreaseApprovalRevertWhen_SenderIsAddressZero() external { + vm.startPrank(address(0)); + // it should revert + vm.expectRevert(abi.encodeWithSelector(IERC20Errors.ERC20InvalidApprover.selector, address(0))); + bToken.decreaseApproval(spender, 50e18); } - function test_increasesApprovalFromNonZero( - address sender, - address spender, - uint128 existingAllowance, - uint128 amount - ) public assumeNonZeroAddresses(sender, spender) { - vm.assume(existingAllowance > 0); - vm.startPrank(sender); - bToken.approve(spender, existingAllowance); - bToken.increaseApproval(spender, amount); - vm.stopPrank(); - assertEq(bToken.allowance(sender, spender), uint256(amount) + existingAllowance); + function test_DecreaseApprovalRevertWhen_SpenderIsAddressZero() external { + // it should revert + vm.expectRevert(abi.encodeWithSelector(IERC20Errors.ERC20InvalidSpender.selector, address(0))); + bToken.decreaseApproval(address(0), 50e18); } -} -contract BToken_Unit_DecreaseApproval is BToken_Unit_base { - function test_decreaseApprovalToNonZero( - address sender, - address spender, - uint256 existingAllowance, - uint256 amount - ) public assumeNonZeroAddresses(sender, spender) { - existingAllowance = bound(existingAllowance, 1, type(uint256).max); - amount = bound(amount, 0, existingAllowance - 1); - vm.startPrank(sender); - bToken.approve(spender, existingAllowance); - bToken.decreaseApproval(spender, amount); - vm.stopPrank(); - assertEq(bToken.allowance(sender, spender), existingAllowance - amount); + function test_DecreaseApprovalWhenDecrementIsBiggerThanCurrentApproval() external { + bToken.decreaseApproval(spender, 200e18); + // it decreases spender approval to 0 + assertEq(bToken.allowance(caller, spender), 0); } - function test_decreaseApprovalToZero( - address sender, - address spender, - uint256 existingAllowance, - uint256 amount - ) public assumeNonZeroAddresses(sender, spender) { - amount = bound(amount, existingAllowance, type(uint256).max); - vm.startPrank(sender); - bToken.approve(spender, existingAllowance); - bToken.decreaseApproval(spender, amount); - vm.stopPrank(); - assertEq(bToken.allowance(sender, spender), 0); + function test_DecreaseApprovalWhenCalled() external { + // it emits Approval event + vm.expectEmit(); + emit IERC20.Approval(caller, spender, 50e18); + + bToken.decreaseApproval(spender, 50e18); + // it decreases spender approval + assertEq(bToken.allowance(caller, spender), 50e18); } -} -contract BToken_Unit__push is BToken_Unit_base { - function test_revertsOnInsufficientSelfBalance( - address to, - uint128 existingBalance, - uint128 offset - ) public assumeNonZeroAddress(to) { - vm.assume(offset > 1); - deal(address(bToken), address(bToken), existingBalance); - vm.expectRevert( - abi.encodeWithSelector( - IERC20Errors.ERC20InsufficientBalance.selector, - address(bToken), - existingBalance, - uint256(existingBalance) + offset - ) - ); - bToken.call__push(to, uint256(existingBalance) + offset); + function test__pushRevertWhen_ContractDoesNotHaveEnoughBalance() external { + // it should revert + vm.expectRevert(abi.encodeWithSelector(IERC20Errors.ERC20InsufficientBalance.selector, address(bToken), 0, 50e18)); + bToken.call__push(target, 50e18); } - function test_sendsTokens( - address to, - uint128 existingBalance, - uint256 transferAmount - ) public assumeNonZeroAddress(to) { - vm.assume(to != address(bToken)); - transferAmount = bound(transferAmount, 0, existingBalance); - deal(address(bToken), address(bToken), existingBalance); - bToken.call__push(to, transferAmount); - assertEq(bToken.balanceOf(to), transferAmount); - assertEq(bToken.balanceOf(address(bToken)), existingBalance - transferAmount); + function test__pushWhenCalled() external { + deal(address(bToken), address(bToken), initialBalance); + // it emits Transfer event + vm.expectEmit(); + emit IERC20.Transfer(address(bToken), target, 50e18); + + bToken.call__push(target, 50e18); + + // it transfers tokens to recipient + assertEq(bToken.balanceOf(address(bToken)), 50e18); + assertEq(bToken.balanceOf(target), 50e18); } -} -contract BToken_Unit__pull is BToken_Unit_base { - function test_revertsOnInsufficientFromBalance( - address from, - uint128 existingBalance, - uint128 offset - ) public assumeNonZeroAddress(from) { - vm.assume(offset > 1); - deal(address(bToken), from, existingBalance); - vm.expectRevert( - abi.encodeWithSelector( - IERC20Errors.ERC20InsufficientBalance.selector, from, existingBalance, uint256(existingBalance) + offset - ) - ); - bToken.call__pull(from, uint256(existingBalance) + offset); + function test__pullRevertWhen_TargetDoesNotHaveEnoughBalance() external { + // it should revert + vm.expectRevert(abi.encodeWithSelector(IERC20Errors.ERC20InsufficientBalance.selector, target, 0, 50e18)); + bToken.call__pull(target, 50e18); } - function test_getsTokens( - address from, - uint128 existingBalance, - uint256 transferAmount - ) public assumeNonZeroAddress(from) { - vm.assume(from != address(bToken)); - transferAmount = bound(transferAmount, 0, existingBalance); - deal(address(bToken), address(from), existingBalance); - bToken.call__pull(from, transferAmount); - assertEq(bToken.balanceOf(address(bToken)), transferAmount); - assertEq(bToken.balanceOf(from), existingBalance - transferAmount); + function test__pullWhenCalled() external { + deal(address(bToken), address(target), initialBalance); + // it emits Transfer event + vm.expectEmit(); + emit IERC20.Transfer(target, address(bToken), 50e18); + + bToken.call__pull(target, 50e18); + + // it transfers tokens from sender + assertEq(bToken.balanceOf(target), 50e18); + assertEq(bToken.balanceOf(address(bToken)), 50e18); } } diff --git a/test/unit/BToken.tree b/test/unit/BToken.tree new file mode 100644 index 00000000..35f85bec --- /dev/null +++ b/test/unit/BToken.tree @@ -0,0 +1,38 @@ +BToken::constructor +└── when called + ├── it sets token name + └── it sets token symbol + +BToken::increaseApproval +├── when sender is address zero +│ └── it should revert +├── when spender is address zero +│ └── it should revert +└── when called + ├── it emits Approval event + └── it increases spender approval + +BToken::decreaseApproval +├── when sender is address zero +│ └── it should revert +├── when spender is address zero +│ └── it should revert +├── when decrement is bigger than current approval +│ └── it decreases spender approval to 0 +└── when called + ├── it emits Approval event + └── it decreases spender approval + +BToken::_push +├── when contract does not have enough balance +│ └── it should revert +└── when called + ├── it emits Transfer event + └── it transfers tokens to recipient + +BToken::_pull +├── when target does not have enough balance +│ └── it should revert +└── when called + ├── it emits Transfer event + └── it transfers tokens from sender From 4b5fab70ea25bfee329389218aaaea7de81b728e Mon Sep 17 00:00:00 2001 From: teddy Date: Tue, 23 Jul 2024 06:31:48 -0300 Subject: [PATCH 39/48] feat: add btt for isValidSignature (#161) * 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 * refactor: get rid of _setRecord * test: btt tests for bcowpool.verify * chore: delete preexisting unit tests * chore: testcase renaming from review * chore: get rid of _setTokens altogether * test: fuzz all possible valid order.sellAmount values * chore: rename correctOrder -> validOrder * test: btt tests for bcowpool.isValidSignature * chore: remove preexisting unit tests replaced by ones in this pr * fix: more descriptive tree --- test/unit/BCoWPool.t.sol | 70 ------------------- .../BCoWPool/BCoWPool_IsValidSignature.t.sol | 56 +++++++++++++++ .../BCoWPool/BCoWPool_IsValidSignature.tree | 10 +++ 3 files changed, 66 insertions(+), 70 deletions(-) create mode 100644 test/unit/BCoWPool/BCoWPool_IsValidSignature.t.sol create mode 100644 test/unit/BCoWPool/BCoWPool_IsValidSignature.tree diff --git a/test/unit/BCoWPool.t.sol b/test/unit/BCoWPool.t.sol index 722780ce..7d448ae6 100644 --- a/test/unit/BCoWPool.t.sol +++ b/test/unit/BCoWPool.t.sol @@ -77,73 +77,3 @@ contract BCoWPool_Unit_Constructor is BaseCoWPoolTest { assertEq(pool.APP_DATA(), _appData); } } - -contract BCoWPool_Unit_IsValidSignature is BaseCoWPoolTest { - function setUp() public virtual override { - super.setUp(); - for (uint256 i = 0; i < TOKENS_AMOUNT; i++) { - vm.mockCall(tokens[i], abi.encodePacked(IERC20.approve.selector), abi.encode(true)); - } - vm.mockCall(address(bCoWPool.FACTORY()), abi.encodeWithSelector(IBCoWFactory.logBCoWPool.selector), abi.encode()); - bCoWPool.finalize(); - } - - modifier happyPath(GPv2Order.Data memory _order) { - // sets the order appData to the one defined at deployment (setUp) - _order.appData = appData; - - // stores the order hash in the transient storage slot - bytes32 _orderHash = GPv2Order.hash(_order, domainSeparator); - bCoWPool.call__setLock(_orderHash); - _; - } - - function test_Revert_OrderWithWrongAppdata(GPv2Order.Data memory _order, bytes32 _appData) public { - vm.assume(_appData != appData); - _order.appData = _appData; - bytes32 _orderHash = GPv2Order.hash(_order, domainSeparator); - vm.expectRevert(IBCoWPool.AppDataDoesNotMatch.selector); - bCoWPool.isValidSignature(_orderHash, abi.encode(_order)); - } - - function test_Revert_OrderSignedWithWrongDomainSeparator( - GPv2Order.Data memory _order, - bytes32 _differentDomainSeparator - ) public happyPath(_order) { - vm.assume(_differentDomainSeparator != domainSeparator); - bytes32 _orderHash = GPv2Order.hash(_order, _differentDomainSeparator); - vm.expectRevert(IBCoWPool.OrderDoesNotMatchMessageHash.selector); - bCoWPool.isValidSignature(_orderHash, abi.encode(_order)); - } - - function test_Revert_OrderWithUnrelatedSignature( - GPv2Order.Data memory _order, - bytes32 _orderHash - ) public happyPath(_order) { - vm.expectRevert(IBCoWPool.OrderDoesNotMatchMessageHash.selector); - bCoWPool.isValidSignature(_orderHash, abi.encode(_order)); - } - - function test_Revert_OrderHashDifferentFromCommitment( - GPv2Order.Data memory _order, - bytes32 _differentCommitment - ) public happyPath(_order) { - bCoWPool.call__setLock(_differentCommitment); - bytes32 _orderHash = GPv2Order.hash(_order, domainSeparator); - vm.expectRevert(IBCoWPool.OrderDoesNotMatchCommitmentHash.selector); - bCoWPool.isValidSignature(_orderHash, abi.encode(_order)); - } - - function test_Call_Verify(GPv2Order.Data memory _order) public happyPath(_order) { - bytes32 _orderHash = GPv2Order.hash(_order, domainSeparator); - bCoWPool.mock_call_verify(_order); - bCoWPool.expectCall_verify(_order); - bCoWPool.isValidSignature(_orderHash, abi.encode(_order)); - } - - function test_Return_MagicValue(GPv2Order.Data memory _order) public happyPath(_order) { - bytes32 _orderHash = GPv2Order.hash(_order, domainSeparator); - bCoWPool.mock_call_verify(_order); - assertEq(bCoWPool.isValidSignature(_orderHash, abi.encode(_order)), IERC1271.isValidSignature.selector); - } -} diff --git a/test/unit/BCoWPool/BCoWPool_IsValidSignature.t.sol b/test/unit/BCoWPool/BCoWPool_IsValidSignature.t.sol new file mode 100644 index 00000000..aeb8086b --- /dev/null +++ b/test/unit/BCoWPool/BCoWPool_IsValidSignature.t.sol @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.25; + +import {IERC20} from '@cowprotocol/interfaces/IERC20.sol'; + +import {GPv2Order} from '@cowprotocol/libraries/GPv2Order.sol'; +import {IERC1271} from '@openzeppelin/contracts/interfaces/IERC1271.sol'; + +import {BCoWPoolBase} from './BCoWPoolBase.sol'; +import {IBCoWPool} from 'interfaces/IBCoWPool.sol'; + +contract BCoWPoolIsValidSignature is BCoWPoolBase { + GPv2Order.Data validOrder; + bytes32 validHash; + + function setUp() public virtual override { + super.setUp(); + // only set up the values that are checked in this method + validOrder.appData = appData; + validHash = GPv2Order.hash(validOrder, domainSeparator); + + bCoWPool.mock_call_verify(validOrder); + } + + function test_RevertWhen_OrdersAppdataIsDifferentThanOneSetAtConstruction(bytes32 appData_) external { + vm.assume(appData != appData_); + validOrder.appData = appData_; + // it should revert + vm.expectRevert(IBCoWPool.AppDataDoesNotMatch.selector); + bCoWPool.isValidSignature(validHash, abi.encode(validOrder)); + } + + function test_RevertWhen_OrderHashDoesNotMatchHashedOrder(bytes32 orderHash) external { + vm.assume(orderHash != validHash); + // it should revert + vm.expectRevert(IBCoWPool.OrderDoesNotMatchMessageHash.selector); + bCoWPool.isValidSignature(orderHash, abi.encode(validOrder)); + } + + function test_RevertWhen_HashedOrderDoesNotMatchCommitment(bytes32 commitment) external { + vm.assume(validHash != commitment); + bCoWPool.call__setLock(commitment); + // it should revert + vm.expectRevert(IBCoWPool.OrderDoesNotMatchCommitmentHash.selector); + bCoWPool.isValidSignature(validHash, abi.encode(validOrder)); + } + + function test_WhenPreconditionsAreMet() external { + // can't do it in setUp because transient storage is wiped in between + bCoWPool.call__setLock(validHash); + // it calls verify + bCoWPool.expectCall_verify(validOrder); + // it returns EIP-1271 magic value + assertEq(bCoWPool.isValidSignature(validHash, abi.encode(validOrder)), IERC1271.isValidSignature.selector); + } +} diff --git a/test/unit/BCoWPool/BCoWPool_IsValidSignature.tree b/test/unit/BCoWPool/BCoWPool_IsValidSignature.tree new file mode 100644 index 00000000..524e8290 --- /dev/null +++ b/test/unit/BCoWPool/BCoWPool_IsValidSignature.tree @@ -0,0 +1,10 @@ +BCoWPool::IsValidSignature +├── when orders appdata is different than one set at construction +│ └── it should revert +├── when orderHash does not match hashed order +│ └── it should revert +├── when hashed order does not match commitment +│ └── it should revert +└── when preconditions are met + ├── it calls verify + └── it returns EIP-1271 magic value From dad92aaa019754e4abebf42ec8d229589e5c01d2 Mon Sep 17 00:00:00 2001 From: teddy Date: Tue, 23 Jul 2024 06:38:07 -0300 Subject: [PATCH 40/48] test: btt tests for bcowpool constructor (#163) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 * refactor: get rid of _setRecord * test: btt tests for bcowpool.verify * chore: delete preexisting unit tests * chore: testcase renaming from review * chore: get rid of _setTokens altogether * test: fuzz all possible valid order.sellAmount values * chore: rename correctOrder -> validOrder * test: btt tests for bpool.finalize * test: btt tests for bcowpool.finalize * chore: remove preexisting unit tests replaced by ones in this pr * fix: feedback from review * refactor: make caller==controller default scenario * test: btt tests for bcowpool constructor * chore: remove preexisting unit tests replaced by ones in this pr * fix: feedback from review * fix: make bulloak happy * fix: mergeback mistake --------- Co-authored-by: Weißer Hase --- test/manual-smock/MockBCoWPool.sol | 39 ------------------------------ test/unit/BCoWPool.t.sol | 31 ------------------------ test/unit/BCoWPool/BCoWPool.t.sol | 26 ++++++++++++++++++++ test/unit/BCoWPool/BCoWPool.tree | 9 +++++++ 4 files changed, 35 insertions(+), 70 deletions(-) diff --git a/test/manual-smock/MockBCoWPool.sol b/test/manual-smock/MockBCoWPool.sol index 98b19d1f..a0f1176c 100644 --- a/test/manual-smock/MockBCoWPool.sol +++ b/test/manual-smock/MockBCoWPool.sol @@ -369,45 +369,6 @@ contract MockBCoWPool is BCoWPool, Test { vm.expectCall(address(this), abi.encodeWithSignature('_pushUnderlying(address,address,uint256)', token, to, amount)); } - function mock_call__pushPoolShare(address to, uint256 amount) public { - vm.mockCall(address(this), abi.encodeWithSignature('_pushPoolShare(address,uint256)', to, amount), abi.encode()); - } - - function _pushPoolShare(address to, uint256 amount) internal override { - (bool _success, bytes memory _data) = - address(this).call(abi.encodeWithSignature('_pushPoolShare(address,uint256)', to, amount)); - - if (_success) return abi.decode(_data, ()); - else return super._pushPoolShare(to, amount); - } - - function call__pushPoolShare(address to, uint256 amount) public { - return _pushPoolShare(to, amount); - } - - function expectCall__pushPoolShare(address to, uint256 amount) public { - vm.expectCall(address(this), abi.encodeWithSignature('_pushPoolShare(address,uint256)', to, amount)); - } - - function mock_call__mintPoolShare(uint256 amount) public { - vm.mockCall(address(this), abi.encodeWithSignature('_mintPoolShare(uint256)', amount), abi.encode()); - } - - function _mintPoolShare(uint256 amount) internal override { - (bool _success, bytes memory _data) = address(this).call(abi.encodeWithSignature('_mintPoolShare(uint256)', amount)); - - if (_success) return abi.decode(_data, ()); - else return super._mintPoolShare(amount); - } - - function call__mintPoolShare(uint256 amount) public { - return _mintPoolShare(amount); - } - - function expectCall__mintPoolShare(uint256 amount) public { - vm.expectCall(address(this), abi.encodeWithSignature('_mintPoolShare(uint256)', amount)); - } - function call__afterFinalize() public { return _afterFinalize(); } diff --git a/test/unit/BCoWPool.t.sol b/test/unit/BCoWPool.t.sol index 7d448ae6..eb8acad5 100644 --- a/test/unit/BCoWPool.t.sol +++ b/test/unit/BCoWPool.t.sol @@ -46,34 +46,3 @@ abstract contract BaseCoWPoolTest is BasePoolTest, BCoWConst { }); } } - -contract BCoWPool_Unit_Constructor is BaseCoWPoolTest { - function test_Set_SolutionSettler(address _settler) public { - assumeNotForgeAddress(_settler); - vm.mockCall(_settler, abi.encodePacked(ISettlement.domainSeparator.selector), abi.encode(domainSeparator)); - vm.mockCall(_settler, abi.encodePacked(ISettlement.vaultRelayer.selector), abi.encode(vaultRelayer)); - MockBCoWPool pool = new MockBCoWPool(_settler, appData); - assertEq(address(pool.SOLUTION_SETTLER()), _settler); - } - - function test_Set_DomainSeparator(address _settler, bytes32 _separator) public { - assumeNotForgeAddress(_settler); - vm.mockCall(_settler, abi.encodePacked(ISettlement.domainSeparator.selector), abi.encode(_separator)); - vm.mockCall(_settler, abi.encodePacked(ISettlement.vaultRelayer.selector), abi.encode(vaultRelayer)); - MockBCoWPool pool = new MockBCoWPool(_settler, appData); - assertEq(pool.SOLUTION_SETTLER_DOMAIN_SEPARATOR(), _separator); - } - - function test_Set_VaultRelayer(address _settler, address _relayer) public { - assumeNotForgeAddress(_settler); - vm.mockCall(_settler, abi.encodePacked(ISettlement.domainSeparator.selector), abi.encode(domainSeparator)); - vm.mockCall(_settler, abi.encodePacked(ISettlement.vaultRelayer.selector), abi.encode(_relayer)); - MockBCoWPool pool = new MockBCoWPool(_settler, appData); - assertEq(pool.VAULT_RELAYER(), _relayer); - } - - function test_Set_AppData(bytes32 _appData) public { - MockBCoWPool pool = new MockBCoWPool(cowSolutionSettler, _appData); - assertEq(pool.APP_DATA(), _appData); - } -} diff --git a/test/unit/BCoWPool/BCoWPool.t.sol b/test/unit/BCoWPool/BCoWPool.t.sol index 2318bd16..66d4a825 100644 --- a/test/unit/BCoWPool/BCoWPool.t.sol +++ b/test/unit/BCoWPool/BCoWPool.t.sol @@ -9,6 +9,8 @@ import {IBCoWFactory} from 'interfaces/IBCoWFactory.sol'; import {IBCoWPool} from 'interfaces/IBCoWPool.sol'; import {IBPool} from 'interfaces/IBPool.sol'; +import {ISettlement} from 'interfaces/ISettlement.sol'; +import {MockBCoWPool} from 'test/manual-smock/MockBCoWPool.sol'; contract BCoWPool is BCoWPoolBase { bytes32 public commitmentValue = bytes32(uint256(0xf00ba5)); @@ -27,6 +29,30 @@ contract BCoWPool is BCoWPoolBase { vm.mockCall(tokens[1], abi.encodeCall(IERC20.approve, (vaultRelayer, type(uint256).max)), abi.encode(true)); } + function test_ConstructorWhenCalled( + address _settler, + bytes32 _separator, + address _relayer, + bytes32 _appData + ) external { + assumeNotForgeAddress(_settler); + vm.mockCall(_settler, abi.encodePacked(ISettlement.domainSeparator.selector), abi.encode(_separator)); + vm.mockCall(_settler, abi.encodePacked(ISettlement.vaultRelayer.selector), abi.encode(_relayer)); + // it should query the solution settler for the domain separator + vm.expectCall(_settler, abi.encodePacked(ISettlement.domainSeparator.selector)); + // it should query the solution settler for the vault relayer + vm.expectCall(_settler, abi.encodePacked(ISettlement.vaultRelayer.selector)); + MockBCoWPool pool = new MockBCoWPool(_settler, _appData); + // it should set the solution settler + assertEq(address(pool.SOLUTION_SETTLER()), _settler); + // it should set the domain separator + assertEq(pool.SOLUTION_SETTLER_DOMAIN_SEPARATOR(), _separator); + // it should set the vault relayer + assertEq(pool.VAULT_RELAYER(), _relayer); + // it should set the app data + assertEq(pool.APP_DATA(), _appData); + } + function test__afterFinalizeWhenCalled() external { // it calls approve on every bound token vm.expectCall(tokens[0], abi.encodeCall(IERC20.approve, (vaultRelayer, type(uint256).max))); diff --git a/test/unit/BCoWPool/BCoWPool.tree b/test/unit/BCoWPool/BCoWPool.tree index abf9b3bb..9331eb1f 100644 --- a/test/unit/BCoWPool/BCoWPool.tree +++ b/test/unit/BCoWPool/BCoWPool.tree @@ -1,3 +1,12 @@ +BCoWPool::Constructor +└── when called + ├── it should set the solution settler + ├── it should query the solution settler for the domain separator + ├── it should set the domain separator + ├── it should query the solution settler for the vault relayer + ├── it should set the vault relayer + └── it should set the app data + BCoWPool::_afterFinalize ├── when called │ ├── it calls approve on every bound token From 585d3d2d6a8c81348fbc701aebf6dda1c41ef8db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wei=C3=9Fer=20Hase?= Date: Tue, 23 Jul 2024 17:28:41 +0200 Subject: [PATCH 41/48] chore: cleaning up remanent test code (#171) --- test/unit/BCoWPool.t.sol | 48 -------- test/unit/BPool.t.sol | 232 --------------------------------------- 2 files changed, 280 deletions(-) delete mode 100644 test/unit/BCoWPool.t.sol diff --git a/test/unit/BCoWPool.t.sol b/test/unit/BCoWPool.t.sol deleted file mode 100644 index eb8acad5..00000000 --- a/test/unit/BCoWPool.t.sol +++ /dev/null @@ -1,48 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.25; - -import {BasePoolTest} from './BPool.t.sol'; -import {IERC20} from '@cowprotocol/interfaces/IERC20.sol'; -import {GPv2Order} from '@cowprotocol/libraries/GPv2Order.sol'; -import {IERC1271} from '@openzeppelin/contracts/interfaces/IERC1271.sol'; -import {BCoWConst} from 'contracts/BCoWConst.sol'; -import {IBCoWFactory} from 'interfaces/IBCoWFactory.sol'; -import {IBCoWPool} from 'interfaces/IBCoWPool.sol'; -import {IBPool} from 'interfaces/IBPool.sol'; -import {ISettlement} from 'interfaces/ISettlement.sol'; -import {MockBCoWPool} from 'test/manual-smock/MockBCoWPool.sol'; -import {MockBPool} from 'test/smock/MockBPool.sol'; - -abstract contract BaseCoWPoolTest is BasePoolTest, BCoWConst { - address public cowSolutionSettler = makeAddr('cowSolutionSettler'); - bytes32 public domainSeparator = bytes32(bytes2(0xf00b)); - address public vaultRelayer = makeAddr('vaultRelayer'); - bytes32 public appData = bytes32('appData'); - - GPv2Order.Data correctOrder; - - MockBCoWPool bCoWPool; - - function setUp() public virtual override { - super.setUp(); - vm.mockCall(cowSolutionSettler, abi.encodePacked(ISettlement.domainSeparator.selector), abi.encode(domainSeparator)); - vm.mockCall(cowSolutionSettler, abi.encodePacked(ISettlement.vaultRelayer.selector), abi.encode(vaultRelayer)); - bCoWPool = new MockBCoWPool(cowSolutionSettler, appData); - bPool = MockBPool(address(bCoWPool)); - _setRandomTokens(TOKENS_AMOUNT); - correctOrder = GPv2Order.Data({ - sellToken: IERC20(tokens[1]), - buyToken: IERC20(tokens[0]), - receiver: GPv2Order.RECEIVER_SAME_AS_OWNER, - sellAmount: 0, - buyAmount: 0, - validTo: uint32(block.timestamp + 1 minutes), - appData: appData, - feeAmount: 0, - kind: GPv2Order.KIND_SELL, - partiallyFillable: false, - sellTokenBalance: GPv2Order.BALANCE_ERC20, - buyTokenBalance: GPv2Order.BALANCE_ERC20 - }); - } -} diff --git a/test/unit/BPool.t.sol b/test/unit/BPool.t.sol index 9010ae93..5e1f0abc 100644 --- a/test/unit/BPool.t.sol +++ b/test/unit/BPool.t.sol @@ -245,238 +245,6 @@ abstract contract BasePoolTest is Test, BConst, Utils, BMath { } } -contract BPool_Unit_JoinswapPoolAmountOut is BasePoolTest { - address tokenIn; - - struct JoinswapPoolAmountOut_FuzzScenario { - uint256 poolAmountOut; - uint256 tokenInBalance; - uint256 tokenInDenorm; - uint256 totalSupply; - uint256 totalWeight; - uint256 swapFee; - } - - function _setValues(JoinswapPoolAmountOut_FuzzScenario memory _fuzz) internal { - tokenIn = tokens[0]; - - // Create mocks for tokenIn - _mockTransferFrom(tokenIn); - - // Set balances - bPool.set__records( - tokenIn, - IBPool.Record({ - bound: true, - index: 0, // NOTE: irrelevant for this method - denorm: _fuzz.tokenInDenorm - }) - ); - _mockPoolBalance(tokenIn, _fuzz.tokenInBalance); - - // Set swapFee - _setSwapFee(_fuzz.swapFee); - // Set finalize - _setFinalize(true); - // Set totalSupply - _setTotalSupply(_fuzz.totalSupply); - // Set totalWeight - _setTotalWeight(_fuzz.totalWeight); - } - - function _assumeHappyPath(JoinswapPoolAmountOut_FuzzScenario memory _fuzz) internal view { - // safe bound assumptions - _fuzz.tokenInDenorm = bound(_fuzz.tokenInDenorm, 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.poolAmountOut = bound(_fuzz.poolAmountOut, INIT_POOL_SUPPLY, type(uint256).max - INIT_POOL_SUPPLY); - _fuzz.totalSupply = bound(_fuzz.totalSupply, INIT_POOL_SUPPLY, type(uint256).max - _fuzz.poolAmountOut); - - // min - vm.assume(_fuzz.tokenInBalance >= MIN_BALANCE); - - // internal calculation for calcSingleInGivenPoolOut - _assumeCalcSingleInGivenPoolOut( - _fuzz.tokenInBalance, _fuzz.tokenInDenorm, _fuzz.totalSupply, _fuzz.totalWeight, _fuzz.poolAmountOut - ); - - uint256 _tokenAmountIn = calcSingleInGivenPoolOut( - _fuzz.tokenInBalance, - _fuzz.tokenInDenorm, - _fuzz.totalSupply, - _fuzz.totalWeight, - _fuzz.poolAmountOut, - _fuzz.swapFee - ); - - // L428 BPool.sol - vm.assume(_tokenAmountIn > 0); - - // max - vm.assume(_fuzz.tokenInBalance < type(uint256).max - _tokenAmountIn); - - // MAX_IN_RATIO - vm.assume(_fuzz.tokenInBalance < type(uint256).max / MAX_IN_RATIO); - vm.assume(_tokenAmountIn <= bmul(_fuzz.tokenInBalance, MAX_IN_RATIO)); - } - - modifier happyPath(JoinswapPoolAmountOut_FuzzScenario memory _fuzz) { - _assumeHappyPath(_fuzz); - _setValues(_fuzz); - _; - } - - function test_Revert_NotFinalized(JoinswapPoolAmountOut_FuzzScenario memory _fuzz) public happyPath(_fuzz) { - _setFinalize(false); - - vm.expectRevert(IBPool.BPool_PoolNotFinalized.selector); - bPool.joinswapPoolAmountOut(tokenIn, _fuzz.poolAmountOut, type(uint256).max); - } - - function test_Revert_NotBound( - JoinswapPoolAmountOut_FuzzScenario memory _fuzz, - address _tokenIn - ) public happyPath(_fuzz) { - assumeNotForgeAddress(_tokenIn); - - vm.expectRevert(IBPool.BPool_TokenNotBound.selector); - bPool.joinswapPoolAmountOut(_tokenIn, _fuzz.poolAmountOut, type(uint256).max); - } - - function test_Revert_InvalidTokenAmountIn(JoinswapPoolAmountOut_FuzzScenario memory _fuzz) public happyPath(_fuzz) { - _fuzz.poolAmountOut = 0; - - vm.expectRevert(IBPool.BPool_InvalidTokenAmountIn.selector); - bPool.joinswapPoolAmountOut(tokenIn, _fuzz.poolAmountOut, type(uint256).max); - } - - function test_Revert_TokenAmountInAboveMaxAmountIn( - JoinswapPoolAmountOut_FuzzScenario memory _fuzz, - uint256 _maxAmountIn - ) public happyPath(_fuzz) { - uint256 _tokenAmountIn = calcSingleInGivenPoolOut( - _fuzz.tokenInBalance, - _fuzz.tokenInDenorm, - _fuzz.totalSupply, - _fuzz.totalWeight, - _fuzz.poolAmountOut, - _fuzz.swapFee - ); - _maxAmountIn = bound(_maxAmountIn, 0, _tokenAmountIn - 1); - - vm.expectRevert(IBPool.BPool_TokenAmountInAboveMaxAmountIn.selector); - bPool.joinswapPoolAmountOut(tokenIn, _fuzz.poolAmountOut, _maxAmountIn); - } - - function test_Revert_TokenAmountInAboveMaxIn(JoinswapPoolAmountOut_FuzzScenario memory _fuzz) public { - // Replicating _assumeHappyPath, but removing irrelevant assumptions and conditioning the revert - _fuzz.tokenInDenorm = bound(_fuzz.tokenInDenorm, 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.tokenInBalance = bound(_fuzz.tokenInBalance, MIN_BALANCE, type(uint256).max / MAX_IN_RATIO); - _fuzz.poolAmountOut = bound(_fuzz.poolAmountOut, INIT_POOL_SUPPLY, type(uint256).max - INIT_POOL_SUPPLY); - _fuzz.totalSupply = bound(_fuzz.totalSupply, INIT_POOL_SUPPLY, type(uint256).max - _fuzz.poolAmountOut); - _assumeCalcSingleInGivenPoolOut( - _fuzz.tokenInBalance, _fuzz.tokenInDenorm, _fuzz.totalSupply, _fuzz.totalWeight, _fuzz.poolAmountOut - ); - uint256 _tokenAmountIn = calcSingleInGivenPoolOut( - _fuzz.tokenInBalance, - _fuzz.tokenInDenorm, - _fuzz.totalSupply, - _fuzz.totalWeight, - _fuzz.poolAmountOut, - _fuzz.swapFee - ); - vm.assume(_tokenAmountIn > bmul(_fuzz.tokenInBalance, MAX_IN_RATIO)); - - _setValues(_fuzz); - - vm.expectRevert(IBPool.BPool_TokenAmountInAboveMaxRatio.selector); - bPool.joinswapPoolAmountOut(tokenIn, _fuzz.poolAmountOut, type(uint256).max); - } - - function test_Revert_Reentrancy(JoinswapPoolAmountOut_FuzzScenario memory _fuzz) public { - _expectRevertByReentrancy(); - bPool.joinswapPoolAmountOut(tokenIn, _fuzz.poolAmountOut, type(uint256).max); - } - - function test_Emit_LogJoin(JoinswapPoolAmountOut_FuzzScenario memory _fuzz) public happyPath(_fuzz) { - uint256 _tokenAmountIn = calcSingleInGivenPoolOut( - _fuzz.tokenInBalance, - _fuzz.tokenInDenorm, - _fuzz.totalSupply, - _fuzz.totalWeight, - _fuzz.poolAmountOut, - _fuzz.swapFee - ); - - vm.expectEmit(); - emit IBPool.LOG_JOIN(address(this), tokenIn, _tokenAmountIn); - bPool.joinswapPoolAmountOut(tokenIn, _fuzz.poolAmountOut, type(uint256).max); - } - - function test_Set_ReentrancyLock(JoinswapPoolAmountOut_FuzzScenario memory _fuzz) public happyPath(_fuzz) { - _expectSetReentrancyLock(); - bPool.joinswapPoolAmountOut(tokenIn, _fuzz.poolAmountOut, type(uint256).max); - } - - function test_Mint_PoolShare(JoinswapPoolAmountOut_FuzzScenario memory _fuzz) public happyPath(_fuzz) { - bPool.joinswapPoolAmountOut(tokenIn, _fuzz.poolAmountOut, type(uint256).max); - - assertEq(bPool.totalSupply(), _fuzz.totalSupply + _fuzz.poolAmountOut); - } - - function test_Push_PoolShare(JoinswapPoolAmountOut_FuzzScenario memory _fuzz) public happyPath(_fuzz) { - uint256 _balanceBefore = bPool.balanceOf(address(this)); - - bPool.joinswapPoolAmountOut(tokenIn, _fuzz.poolAmountOut, type(uint256).max); - - assertEq(bPool.balanceOf(address(this)), _balanceBefore + _fuzz.poolAmountOut); - } - - function test_Pull_Underlying(JoinswapPoolAmountOut_FuzzScenario memory _fuzz) public happyPath(_fuzz) { - uint256 _tokenAmountIn = calcSingleInGivenPoolOut( - _fuzz.tokenInBalance, - _fuzz.tokenInDenorm, - _fuzz.totalSupply, - _fuzz.totalWeight, - _fuzz.poolAmountOut, - _fuzz.swapFee - ); - - vm.expectCall( - address(tokenIn), - abi.encodeWithSelector(IERC20.transferFrom.selector, address(this), address(bPool), _tokenAmountIn) - ); - bPool.joinswapPoolAmountOut(tokenIn, _fuzz.poolAmountOut, type(uint256).max); - } - - function test_Returns_TokenAmountIn(JoinswapPoolAmountOut_FuzzScenario memory _fuzz) public happyPath(_fuzz) { - uint256 _expectedTokenAmountIn = calcSingleInGivenPoolOut( - _fuzz.tokenInBalance, - _fuzz.tokenInDenorm, - _fuzz.totalSupply, - _fuzz.totalWeight, - _fuzz.poolAmountOut, - _fuzz.swapFee - ); - - (uint256 _tokenAmountIn) = bPool.joinswapPoolAmountOut(tokenIn, _fuzz.poolAmountOut, type(uint256).max); - - assertEq(_expectedTokenAmountIn, _tokenAmountIn); - } - - function test_Emit_LogCall(JoinswapPoolAmountOut_FuzzScenario memory _fuzz) public happyPath(_fuzz) { - vm.expectEmit(); - bytes memory _data = - abi.encodeWithSelector(BPool.joinswapPoolAmountOut.selector, tokenIn, _fuzz.poolAmountOut, type(uint256).max); - emit IBPool.LOG_CALL(BPool.joinswapPoolAmountOut.selector, address(this), _data); - - bPool.joinswapPoolAmountOut(tokenIn, _fuzz.poolAmountOut, type(uint256).max); - } -} - contract BPool_Unit_ExitswapExternAmountOut is BasePoolTest { address tokenOut; From 85dd939f19376045a99dc3ede6dd18287240f338 Mon Sep 17 00:00:00 2001 From: teddy Date: Tue, 23 Jul 2024 15:20:48 -0300 Subject: [PATCH 42/48] feat: btt tests for push/pull underlying (#173) * test: btt tests for push/pull underlying * chore: remove preexisting unit tests replaced by ones in this pr * fix: safeERC20 returns different errors with vs without return data * fix: finalize test that was broken on main * fix: feedback from review --- test/unit/BPool.t.sol | 58 ----------------- test/unit/BPool/BPool.t.sol | 124 +++++++++++++++++++++++++++++++++++- test/unit/BPool/BPool.tree | 24 +++++++ 3 files changed, 147 insertions(+), 59 deletions(-) diff --git a/test/unit/BPool.t.sol b/test/unit/BPool.t.sol index 5e1f0abc..a3d752d8 100644 --- a/test/unit/BPool.t.sol +++ b/test/unit/BPool.t.sol @@ -495,61 +495,3 @@ contract BPool_Unit_ExitswapExternAmountOut is BasePoolTest { bPool.exitswapExternAmountOut(tokenOut, _fuzz.tokenAmountOut, type(uint256).max); } } - -contract BPool_Unit__PullUnderlying is BasePoolTest { - function test_Call_TransferFrom(address _erc20, address _from, uint256 _amount) public { - assumeNotForgeAddress(_erc20); - - vm.mockCall( - _erc20, abi.encodeWithSelector(IERC20.transferFrom.selector, _from, address(bPool), _amount), abi.encode(true) - ); - - vm.expectCall(address(_erc20), abi.encodeWithSelector(IERC20.transferFrom.selector, _from, address(bPool), _amount)); - bPool.call__pullUnderlying(_erc20, _from, _amount); - } - - function test_Revert_ERC20False(address _erc20, address _from, uint256 _amount) public { - assumeNotForgeAddress(_erc20); - vm.mockCall( - _erc20, abi.encodeWithSelector(IERC20.transferFrom.selector, _from, address(bPool), _amount), abi.encode(false) - ); - - vm.expectRevert(abi.encodeWithSelector(SafeERC20.SafeERC20FailedOperation.selector, _erc20)); - bPool.call__pullUnderlying(_erc20, _from, _amount); - } - - function test_Success_NoReturnValueERC20(address _erc20, address _from, uint256 _amount) public { - assumeNotForgeAddress(_erc20); - vm.mockCall( - _erc20, abi.encodeWithSelector(IERC20.transferFrom.selector, _from, address(bPool), _amount), abi.encode() - ); - - bPool.call__pullUnderlying(_erc20, _from, _amount); - } -} - -contract BPool_Unit__PushUnderlying is BasePoolTest { - function test_Call_Transfer(address _erc20, address _to, uint256 _amount) public { - assumeNotForgeAddress(_erc20); - - vm.mockCall(_erc20, abi.encodeWithSelector(IERC20.transfer.selector, _to, _amount), abi.encode(true)); - - vm.expectCall(address(_erc20), abi.encodeWithSelector(IERC20.transfer.selector, _to, _amount)); - bPool.call__pushUnderlying(_erc20, _to, _amount); - } - - function test_Revert_ERC20False(address _erc20, address _to, uint256 _amount) public { - assumeNotForgeAddress(_erc20); - vm.mockCall(_erc20, abi.encodeWithSelector(IERC20.transfer.selector, _to, _amount), abi.encode(false)); - - vm.expectRevert(abi.encodeWithSelector(SafeERC20.SafeERC20FailedOperation.selector, _erc20)); - bPool.call__pushUnderlying(_erc20, _to, _amount); - } - - function test_Success_NoReturnValueERC20(address _erc20, address _to, uint256 _amount) public { - assumeNotForgeAddress(_erc20); - vm.mockCall(_erc20, abi.encodeWithSelector(IERC20.transfer.selector, _to, _amount), abi.encode()); - - bPool.call__pushUnderlying(_erc20, _to, _amount); - } -} diff --git a/test/unit/BPool/BPool.t.sol b/test/unit/BPool/BPool.t.sol index 7b1f2ec3..07954497 100644 --- a/test/unit/BPool/BPool.t.sol +++ b/test/unit/BPool/BPool.t.sol @@ -4,6 +4,8 @@ pragma solidity 0.8.25; import {BPoolBase} from './BPoolBase.sol'; import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; +import {SafeERC20} from '@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol'; +import {Address} from '@openzeppelin/contracts/utils/Address.sol'; import {BMath} from 'contracts/BMath.sol'; import {IBPool} from 'interfaces/IBPool.sol'; @@ -26,6 +28,10 @@ contract BPool is BPoolBase, BMath { // sPf = (10 / 20) * (1 / (1-0.1)) = 0.555...e18 (round-up) uint256 public spotPriceWithoutFee = 0.5e18; uint256 public spotPrice = 0.555555555555555556e18; + address public transferRecipient = makeAddr('transfer recipient'); + address public transferFromSpender = makeAddr('transfer from spender'); + address public transferredToken = makeAddr('underlying token'); + uint256 public transferredAmount = 10e18; function setUp() public virtual override { super.setUp(); @@ -370,7 +376,7 @@ contract BPool is BPoolBase, BMath { } function test_FinalizeRevertWhen_CallerIsNotController(address _caller) external { - vm.assume(_caller != address(this)); + vm.assume(_caller != controller); vm.startPrank(_caller); // it should revert vm.expectRevert(IBPool.BPool_CallerIsNotController.selector); @@ -419,4 +425,120 @@ contract BPool is BPoolBase, BMath { // it finalizes the pool assertEq(bPool.call__finalized(), true); } + + function test__pushUnderlyingRevertWhen_UnderlyingTokenReturnsFalse() external { + vm.mockCall( + transferredToken, + abi.encodeWithSelector(IERC20.transfer.selector, transferRecipient, transferredAmount), + abi.encode(false) + ); + // it should revert + vm.expectRevert(abi.encodeWithSelector(SafeERC20.SafeERC20FailedOperation.selector, transferredToken)); + bPool.call__pushUnderlying(transferredToken, transferRecipient, transferredAmount); + } + + function test__pushUnderlyingWhenUnderlyingTokenDoesntReturnAValue() external { + vm.mockCall( + transferredToken, + abi.encodeWithSelector(IERC20.transfer.selector, transferRecipient, transferredAmount), + abi.encode() + ); + // it assumes transfer success + bPool.call__pushUnderlying(transferredToken, transferRecipient, transferredAmount); + } + + function test__pushUnderlyingRevertWhen_UnderlyingTokenRevertsWithoutData() external { + // it should revert with FailedInnerCall + vm.mockCallRevert( + transferredToken, + abi.encodeWithSelector(IERC20.transfer.selector, transferRecipient, transferredAmount), + abi.encode() + ); + vm.expectRevert(Address.FailedInnerCall.selector); + bPool.call__pushUnderlying(transferredToken, transferRecipient, transferredAmount); + } + + function test__pushUnderlyingRevertWhen_UnderlyingTokenRevertsWithData(bytes memory errorData) external { + vm.assume(keccak256(errorData) != keccak256(bytes(''))); + vm.mockCallRevert( + transferredToken, + abi.encodeWithSelector(IERC20.transfer.selector, transferRecipient, transferredAmount), + errorData + ); + // it should revert with same error data + vm.expectRevert(errorData); + bPool.call__pushUnderlying(transferredToken, transferRecipient, transferredAmount); + } + + function test__pushUnderlyingWhenUnderlyingTokenReturnsTrue() external { + vm.mockCall( + transferredToken, + abi.encodeWithSelector(IERC20.transfer.selector, transferRecipient, transferredAmount), + abi.encode(true) + ); + // it calls underlying transfer + vm.expectCall( + transferredToken, abi.encodeWithSelector(IERC20.transfer.selector, transferRecipient, transferredAmount) + ); + bPool.call__pushUnderlying(transferredToken, transferRecipient, transferredAmount); + } + + function test__pullUnderlyingRevertWhen_UnderlyingTokenReturnsFalse() external { + vm.mockCall( + transferredToken, + abi.encodeWithSelector(IERC20.transferFrom.selector, transferFromSpender, address(bPool), transferredAmount), + abi.encode(false) + ); + // it should revert + vm.expectRevert(abi.encodeWithSelector(SafeERC20.SafeERC20FailedOperation.selector, transferredToken)); + bPool.call__pullUnderlying(transferredToken, transferFromSpender, transferredAmount); + } + + function test__pullUnderlyingWhenUnderlyingTokenDoesntReturnAValue() external { + // it assumes transferFrom success + vm.mockCall( + transferredToken, + abi.encodeWithSelector(IERC20.transferFrom.selector, transferFromSpender, address(bPool), transferredAmount), + abi.encode() + ); + // it assumes transferFrom success + bPool.call__pullUnderlying(transferredToken, transferFromSpender, transferredAmount); + } + + function test__pullUnderlyingRevertWhen_UnderlyingTokenRevertsWithoutData() external { + vm.mockCallRevert( + transferredToken, + abi.encodeWithSelector(IERC20.transferFrom.selector, transferFromSpender, address(bPool), transferredAmount), + abi.encode() + ); + // it should revert with FailedInnerCall + vm.expectRevert(Address.FailedInnerCall.selector); + bPool.call__pullUnderlying(transferredToken, transferFromSpender, transferredAmount); + } + + function test__pullUnderlyingRevertWhen_UnderlyingTokenRevertsWithData(bytes memory errorData) external { + vm.assume(keccak256(errorData) != keccak256(bytes(''))); + vm.mockCallRevert( + transferredToken, + abi.encodeWithSelector(IERC20.transferFrom.selector, transferFromSpender, address(bPool), transferredAmount), + errorData + ); + // it should revert with same error data + vm.expectRevert(errorData); + bPool.call__pullUnderlying(transferredToken, transferFromSpender, transferredAmount); + } + + function test__pullUnderlyingWhenUnderlyingTokenReturnsTrue() external { + vm.mockCall( + transferredToken, + abi.encodeWithSelector(IERC20.transferFrom.selector, transferFromSpender, address(bPool), transferredAmount), + abi.encode(true) + ); + // it calls underlying transferFrom + vm.expectCall( + transferredToken, + abi.encodeWithSelector(IERC20.transferFrom.selector, transferFromSpender, address(bPool), transferredAmount) + ); + bPool.call__pullUnderlying(transferredToken, transferFromSpender, transferredAmount); + } } diff --git a/test/unit/BPool/BPool.tree b/test/unit/BPool/BPool.tree index 7a034e3a..85659d4c 100644 --- a/test/unit/BPool/BPool.tree +++ b/test/unit/BPool/BPool.tree @@ -141,3 +141,27 @@ BPool::finalize ├── it mints initial pool shares ├── it sends initial pool shares to controller └── it calls _afterFinalize hook + +BPool::_pushUnderlying +├── when underlying token returns false +│ └── it should revert +├── when underlying token doesnt return a value +│ └── it assumes transfer success +├── when underlying token reverts without data +│ └── it should revert // FailedInnerCall +├── when underlying token reverts with data +│ └── it should revert +└── when underlying token returns true + └── it calls underlying transfer + +BPool::_pullUnderlying +├── when underlying token returns false +│ └── it should revert +├── when underlying token doesnt return a value +│ └── it assumes transferFrom success +├── when underlying token reverts without data +│ └── it should revert // FailedInnerCall +├── when underlying token reverts with data +│ └── it should revert +└── when underlying token returns true + └── it calls underlying transferFrom From e7099e3c67667f1621eb91f0d96de45946bceae2 Mon Sep 17 00:00:00 2001 From: teddy Date: Tue, 23 Jul 2024 15:28:35 -0300 Subject: [PATCH 43/48] feat: btt tests for exitswapExternAmountOut (#172) * test: btt tests for exitswapExternAmountOut * chore: remove preexisting unit tests replaced by ones in this pr * fix: dont call setup twice * chore: finally remove bpool.t.sol --- test/unit/BPool.t.sol | 497 ------------------ .../BPool/BPool_ExitswapExternAmountOut.t.sol | 103 ++++ .../BPool/BPool_ExitswapExternAmountOut.tree | 24 + 3 files changed, 127 insertions(+), 497 deletions(-) delete mode 100644 test/unit/BPool.t.sol create mode 100644 test/unit/BPool/BPool_ExitswapExternAmountOut.t.sol create mode 100644 test/unit/BPool/BPool_ExitswapExternAmountOut.tree diff --git a/test/unit/BPool.t.sol b/test/unit/BPool.t.sol deleted file mode 100644 index a3d752d8..00000000 --- a/test/unit/BPool.t.sol +++ /dev/null @@ -1,497 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.25; - -import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; -import {SafeERC20} from '@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol'; - -import {BPool} from 'contracts/BPool.sol'; -import {IBPool} from 'interfaces/IBPool.sol'; -import {MockBPool} from 'test/smock/MockBPool.sol'; - -import {BConst} from 'contracts/BConst.sol'; -import {BMath} from 'contracts/BMath.sol'; -import {Test} from 'forge-std/Test.sol'; -import {Pow} from 'test/utils/Pow.sol'; -import {Utils} from 'test/utils/Utils.sol'; - -abstract contract BasePoolTest is Test, BConst, Utils, BMath { - MockBPool public bPool; - - // Deploy this external contract to perform a try-catch when calling bpow. - // If the call fails, it means that the function overflowed, then we reject the fuzzed inputs - Pow public pow = new Pow(); - - function setUp() public virtual { - bPool = new MockBPool(); - - // Create fake tokens - address[] memory _tokensToAdd = _getDeterministicTokenArray(TOKENS_AMOUNT); - for (uint256 i = 0; i < _tokensToAdd.length; i++) { - tokens.push(_tokensToAdd[i]); - } - } - - function _setRandomTokens(uint256 _length) internal returns (address[] memory _tokensToAdd) { - _tokensToAdd = _getDeterministicTokenArray(_length); - for (uint256 i = 0; i < _length; i++) { - bPool.set__records(_tokensToAdd[i], IBPool.Record({bound: true, index: i, denorm: 0})); - } - bPool.set__tokens(_tokensToAdd); - } - - function _mockTransfer(address _token) internal { - // TODO: add amount to transfer to check that it's called with the right amount - vm.mockCall(_token, abi.encodeWithSelector(IERC20.transfer.selector), abi.encode(true)); - } - - function _mockTransferFrom(address _token) internal { - // TODO: add from and amount to transfer to check that it's called with the right params - vm.mockCall(_token, abi.encodeWithSelector(IERC20.transferFrom.selector), abi.encode(true)); - } - - function _mockPoolBalance(address _token, uint256 _balance) internal { - vm.mockCall(_token, abi.encodeWithSelector(IERC20.balanceOf.selector, address(bPool)), abi.encode(_balance)); - } - - function _setSwapFee(uint256 _swapFee) internal { - bPool.set__swapFee(_swapFee); - } - - function _setFinalize(bool _isFinalized) internal { - bPool.set__finalized(_isFinalized); - } - - function _setPoolBalance(address _user, uint256 _balance) internal { - deal(address(bPool), _user, _balance, true); - } - - function _setTotalSupply(uint256 _totalSupply) internal { - _setPoolBalance(address(0), _totalSupply); - } - - function _setTotalWeight(uint256 _totalWeight) internal { - bPool.set__totalWeight(_totalWeight); - } - - function _expectRevertByReentrancy() internal { - // Assert that the contract is accessible - assertEq(bPool.call__getLock(), _MUTEX_FREE); - // Simulate ongoing call to the contract - bPool.call__setLock(_MUTEX_TAKEN); - - vm.expectRevert(IBPool.BPool_Reentrancy.selector); - } - - function _expectSetReentrancyLock() internal { - // Assert that the contract is accessible - assertEq(bPool.call__getLock(), _MUTEX_FREE); - // Expect reentrancy lock to be set - bPool.expectCall__setLock(_MUTEX_TAKEN); - } - - function _assumeCalcSpotPrice( - uint256 _tokenInBalance, - uint256 _tokenInDenorm, - uint256 _tokenOutBalance, - uint256 _tokenOutDenorm, - uint256 _swapFee - ) internal pure { - vm.assume(_tokenInDenorm > 0); - vm.assume(_tokenInBalance < type(uint256).max / BONE); - vm.assume(_tokenInBalance * BONE < type(uint256).max - (_tokenInDenorm / 2)); - - uint256 _numer = bdiv(_tokenInBalance, _tokenInDenorm); - vm.assume(_tokenOutDenorm > 0); - vm.assume(_tokenOutBalance < type(uint256).max / BONE); - vm.assume(_tokenOutBalance * BONE < type(uint256).max - (_tokenOutDenorm / 2)); - - uint256 _denom = bdiv(_tokenOutBalance, _tokenOutDenorm); - vm.assume(_denom > 0); - vm.assume(_numer < type(uint256).max / BONE); - vm.assume(_numer * BONE < type(uint256).max - (_denom / 2)); - vm.assume(_swapFee <= BONE); - - uint256 _ratio = bdiv(_numer, _denom); - vm.assume(bsub(BONE, _swapFee) > 0); - - 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); - } - - function _assumeCalcOutGivenIn(uint256 _tokenInBalance, uint256 _tokenAmountIn, uint256 _swapFee) internal pure { - uint256 _adjustedIn = bsub(BONE, _swapFee); - _adjustedIn = bmul(_tokenAmountIn, _adjustedIn); - vm.assume(_tokenInBalance < type(uint256).max / BONE); - vm.assume(_tokenInBalance * BONE < type(uint256).max - (badd(_tokenInBalance, _adjustedIn) / 2)); - } - - function _assumeCalcPoolOutGivenSingleIn( - uint256 _tokenInDenorm, - uint256 _tokenInBalance, - uint256 _tokenAmountIn, - uint256 _swapFee, - uint256 _totalWeight, - uint256 _totalSupply - ) internal pure { - uint256 _normalizedWeight = bdiv(_tokenInDenorm, _totalWeight); - vm.assume(_normalizedWeight < bdiv(MAX_WEIGHT, MAX_TOTAL_WEIGHT)); - - uint256 _zaz = bmul(bsub(BONE, _normalizedWeight), _swapFee); - uint256 _tokenAmountInAfterFee = bmul(_tokenAmountIn, bsub(BONE, _zaz)); - uint256 _newTokenBalanceIn = badd(_tokenInBalance, _tokenAmountInAfterFee); - vm.assume(_newTokenBalanceIn < type(uint256).max / BONE); - vm.assume(_newTokenBalanceIn > _tokenInBalance); - - uint256 _tokenInRatio = bdiv(_newTokenBalanceIn, _tokenInBalance); - uint256 _poolRatio = bpow(_tokenInRatio, _normalizedWeight); - vm.assume(_poolRatio < type(uint256).max / _totalSupply); - } - - function _assumeCalcSingleInGivenPoolOut( - uint256 _tokenInBalance, - uint256 _tokenInDenorm, - uint256 _poolSupply, - uint256 _totalWeight, - uint256 _poolAmountOut - ) internal view { - uint256 _normalizedWeight = bdiv(_tokenInDenorm, _totalWeight); - uint256 _newPoolSupply = badd(_poolSupply, _poolAmountOut); - vm.assume(_newPoolSupply < type(uint256).max / BONE); - vm.assume(_newPoolSupply * BONE < type(uint256).max - (_poolSupply / 2)); // bdiv require - - uint256 _poolRatio = bdiv(_newPoolSupply, _poolSupply); - vm.assume(_poolRatio < MAX_BPOW_BASE); - vm.assume(BONE > _normalizedWeight); - - uint256 _boo = bdiv(BONE, _normalizedWeight); - uint256 _tokenRatio; - try pow.pow(_poolRatio, _boo) returns (uint256 _result) { - // pow didn't overflow - _tokenRatio = _result; - } catch { - // pow did an overflow. Reject this inputs - vm.assume(false); - } - - vm.assume(_tokenRatio < type(uint256).max / _tokenInBalance); - } - - function _assumeCalcSingleOutGivenPoolIn( - uint256 _tokenOutBalance, - uint256 _tokenOutDenorm, - uint256 _poolSupply, - uint256 _totalWeight, - uint256 _poolAmountIn, - uint256 _swapFee - ) internal pure { - uint256 _normalizedWeight = bdiv(_tokenOutDenorm, _totalWeight); - uint256 _exitFee = bsub(BONE, EXIT_FEE); - vm.assume(_poolAmountIn < type(uint256).max / _exitFee); - - uint256 _poolAmountInAfterExitFee = bmul(_poolAmountIn, _exitFee); - uint256 _newPoolSupply = bsub(_poolSupply, _poolAmountInAfterExitFee); - vm.assume(_newPoolSupply < type(uint256).max / BONE); - vm.assume(_newPoolSupply * BONE < type(uint256).max - (_poolSupply / 2)); // bdiv require - - uint256 _poolRatio = bdiv(_newPoolSupply, _poolSupply); - vm.assume(_poolRatio < MAX_BPOW_BASE); - vm.assume(_poolRatio > MIN_BPOW_BASE); - vm.assume(BONE > _normalizedWeight); - - uint256 _tokenOutRatio = bpow(_poolRatio, bdiv(BONE, _normalizedWeight)); - vm.assume(_tokenOutRatio < type(uint256).max / _tokenOutBalance); - - uint256 _newTokenOutBalance = bmul(_tokenOutRatio, _tokenOutBalance); - uint256 _tokenAmountOutBeforeSwapFee = bsub(_tokenOutBalance, _newTokenOutBalance); - uint256 _zaz = bmul(bsub(BONE, _normalizedWeight), _swapFee); - vm.assume(_tokenAmountOutBeforeSwapFee < type(uint256).max / bsub(BONE, _zaz)); - } - - function _assumeCalcPoolInGivenSingleOut( - uint256 _tokenOutBalance, - uint256 _tokenOutDenorm, - uint256 _poolSupply, - uint256 _totalWeight, - uint256 _tokenAmountOut, - uint256 _swapFee - ) internal pure { - uint256 _normalizedWeight = bdiv(_tokenOutDenorm, _totalWeight); - vm.assume(BONE > _normalizedWeight); - - 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); - - uint256 _tokenOutRatio = bdiv(_newTokenOutBalance, _tokenOutBalance); - uint256 _poolRatio = bpow(_tokenOutRatio, _normalizedWeight); - vm.assume(_poolRatio < type(uint256).max / _poolSupply); - } -} - -contract BPool_Unit_ExitswapExternAmountOut is BasePoolTest { - address tokenOut; - - struct ExitswapExternAmountOut_FuzzScenario { - uint256 tokenAmountOut; - uint256 tokenOutBalance; - uint256 tokenOutDenorm; - uint256 totalSupply; - uint256 totalWeight; - uint256 swapFee; - } - - function _setValues(ExitswapExternAmountOut_FuzzScenario memory _fuzz, uint256 _poolAmountIn) internal { - tokenOut = tokens[0]; - - // Create mocks for tokenOut - _mockTransfer(tokenOut); - - // Set balances - bPool.set__records( - 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), _poolAmountIn); // give LP tokens to fn caller - // Set totalSupply - _setTotalSupply(_fuzz.totalSupply - _poolAmountIn); - // Set totalWeight - _setTotalWeight(_fuzz.totalWeight); - } - - function _assumeHappyPath(ExitswapExternAmountOut_FuzzScenario memory _fuzz) - internal - pure - returns (uint256 _poolAmountIn) - { - // 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); - - // min - _fuzz.totalSupply = bound(_fuzz.totalSupply, INIT_POOL_SUPPLY, type(uint256).max / BONE); - - // MAX_OUT_RATIO - vm.assume(_fuzz.tokenOutBalance < type(uint256).max / MAX_OUT_RATIO); - vm.assume(_fuzz.tokenAmountOut <= bmul(_fuzz.tokenOutBalance, MAX_OUT_RATIO)); - - // min - vm.assume(_fuzz.tokenOutBalance >= MIN_BALANCE); - - // max - vm.assume(_fuzz.tokenOutBalance < type(uint256).max - _fuzz.tokenAmountOut); - - // internal calculation for calcPoolInGivenSingleOut - _assumeCalcPoolInGivenSingleOut( - _fuzz.tokenOutBalance, - _fuzz.tokenOutDenorm, - _fuzz.totalSupply, - _fuzz.totalWeight, - _fuzz.tokenAmountOut, - _fuzz.swapFee - ); - - _poolAmountIn = calcPoolInGivenSingleOut( - _fuzz.tokenOutBalance, - _fuzz.tokenOutDenorm, - _fuzz.totalSupply, - _fuzz.totalWeight, - _fuzz.tokenAmountOut, - _fuzz.swapFee - ); - - // min - vm.assume(_poolAmountIn > 0); - - // max - vm.assume(_poolAmountIn < _fuzz.totalSupply); - vm.assume(_fuzz.totalSupply < type(uint256).max - _poolAmountIn); - } - - modifier happyPath(ExitswapExternAmountOut_FuzzScenario memory _fuzz) { - uint256 _poolAmountIn = _assumeHappyPath(_fuzz); - _setValues(_fuzz, _poolAmountIn); - _; - } - - function test_Revert_NotFinalized(ExitswapExternAmountOut_FuzzScenario memory _fuzz) public { - _setFinalize(false); - - vm.expectRevert(IBPool.BPool_PoolNotFinalized.selector); - bPool.exitswapExternAmountOut(tokenOut, _fuzz.tokenAmountOut, type(uint256).max); - } - - function test_Revert_NotBound( - ExitswapExternAmountOut_FuzzScenario memory _fuzz, - address _tokenOut - ) public happyPath(_fuzz) { - vm.assume(_tokenOut != tokenOut); - assumeNotForgeAddress(_tokenOut); - - vm.expectRevert(IBPool.BPool_TokenNotBound.selector); - bPool.exitswapExternAmountOut(_tokenOut, _fuzz.tokenAmountOut, type(uint256).max); - } - - function test_Revert_TokenAmountOutAboveMaxOut(ExitswapExternAmountOut_FuzzScenario memory _fuzz) - public - happyPath(_fuzz) - { - uint256 _maxTokenAmountOut = bmul(_fuzz.tokenOutBalance, MAX_OUT_RATIO); - - vm.expectRevert(IBPool.BPool_TokenAmountOutAboveMaxOut.selector); - bPool.exitswapExternAmountOut(tokenOut, _maxTokenAmountOut + 1, type(uint256).max); - } - - function test_Revert_InvalidPoolAmountIn(ExitswapExternAmountOut_FuzzScenario memory _fuzz) public happyPath(_fuzz) { - _fuzz.tokenAmountOut = 0; - - vm.expectRevert(IBPool.BPool_InvalidPoolAmountIn.selector); - bPool.exitswapExternAmountOut(tokenOut, _fuzz.tokenAmountOut, type(uint256).max); - } - - function test_Revert_PoolAmountInAboveMaxPoolAmountIn( - ExitswapExternAmountOut_FuzzScenario memory _fuzz, - uint256 _maxPoolAmountIn - ) public happyPath(_fuzz) { - uint256 _poolAmountIn = calcPoolInGivenSingleOut( - _fuzz.tokenOutBalance, - _fuzz.tokenOutDenorm, - _fuzz.totalSupply, - _fuzz.totalWeight, - _fuzz.tokenAmountOut, - _fuzz.swapFee - ); - _maxPoolAmountIn = bound(_maxPoolAmountIn, 0, _poolAmountIn - 1); - - vm.expectRevert(IBPool.BPool_PoolAmountInAboveMaxPoolAmountIn.selector); - bPool.exitswapExternAmountOut(tokenOut, _fuzz.tokenAmountOut, _maxPoolAmountIn); - } - - function test_Revert_Reentrancy(ExitswapExternAmountOut_FuzzScenario memory _fuzz) public { - _expectRevertByReentrancy(); - bPool.exitswapExternAmountOut(tokenOut, _fuzz.tokenAmountOut, type(uint256).max); - } - - function test_Emit_LogExit(ExitswapExternAmountOut_FuzzScenario memory _fuzz) public happyPath(_fuzz) { - vm.expectEmit(); - emit IBPool.LOG_EXIT(address(this), tokenOut, _fuzz.tokenAmountOut); - - bPool.exitswapExternAmountOut(tokenOut, _fuzz.tokenAmountOut, type(uint256).max); - } - - function test_Set_ReentrancyLock(ExitswapExternAmountOut_FuzzScenario memory _fuzz) public happyPath(_fuzz) { - _expectSetReentrancyLock(); - bPool.exitswapExternAmountOut(tokenOut, _fuzz.tokenAmountOut, type(uint256).max); - } - - function test_Pull_PoolShare(ExitswapExternAmountOut_FuzzScenario memory _fuzz) public happyPath(_fuzz) { - uint256 _balanceBefore = bPool.balanceOf(address(this)); - uint256 _poolAmountIn = calcPoolInGivenSingleOut( - _fuzz.tokenOutBalance, - _fuzz.tokenOutDenorm, - _fuzz.totalSupply, - _fuzz.totalWeight, - _fuzz.tokenAmountOut, - _fuzz.swapFee - ); - uint256 _exitFee = bmul(_poolAmountIn, EXIT_FEE); - - bPool.exitswapExternAmountOut(tokenOut, _fuzz.tokenAmountOut, type(uint256).max); - - assertEq(bPool.balanceOf(address(this)), _balanceBefore - bsub(_poolAmountIn, _exitFee)); - } - - function test_Burn_PoolShare(ExitswapExternAmountOut_FuzzScenario memory _fuzz) public happyPath(_fuzz) { - uint256 _totalSupplyBefore = bPool.totalSupply(); - uint256 _poolAmountIn = calcPoolInGivenSingleOut( - _fuzz.tokenOutBalance, - _fuzz.tokenOutDenorm, - _fuzz.totalSupply, - _fuzz.totalWeight, - _fuzz.tokenAmountOut, - _fuzz.swapFee - ); - uint256 _exitFee = bmul(_poolAmountIn, EXIT_FEE); - - bPool.exitswapExternAmountOut(tokenOut, _fuzz.tokenAmountOut, type(uint256).max); - - assertEq(bPool.totalSupply(), _totalSupplyBefore - bsub(_poolAmountIn, _exitFee)); - } - - function test_Push_PoolShare(ExitswapExternAmountOut_FuzzScenario memory _fuzz) public happyPath(_fuzz) { - address _factoryAddress = bPool.FACTORY(); - uint256 _balanceBefore = bPool.balanceOf(_factoryAddress); - uint256 _poolAmountIn = calcPoolInGivenSingleOut( - _fuzz.tokenOutBalance, - _fuzz.tokenOutDenorm, - _fuzz.totalSupply, - _fuzz.totalWeight, - _fuzz.tokenAmountOut, - _fuzz.swapFee - ); - uint256 _exitFee = bmul(_poolAmountIn, EXIT_FEE); - - bPool.exitswapExternAmountOut(tokenOut, _fuzz.tokenAmountOut, type(uint256).max); - - assertEq(bPool.balanceOf(_factoryAddress), _balanceBefore - _poolAmountIn + _exitFee); - } - - function test_Push_Underlying(ExitswapExternAmountOut_FuzzScenario memory _fuzz) public happyPath(_fuzz) { - vm.expectCall( - address(tokenOut), abi.encodeWithSelector(IERC20.transfer.selector, address(this), _fuzz.tokenAmountOut) - ); - bPool.exitswapExternAmountOut(tokenOut, _fuzz.tokenAmountOut, type(uint256).max); - } - - function test_Returns_PoolAmountIn(ExitswapExternAmountOut_FuzzScenario memory _fuzz) public happyPath(_fuzz) { - uint256 _expectedPoolAmountIn = calcPoolInGivenSingleOut( - _fuzz.tokenOutBalance, - _fuzz.tokenOutDenorm, - _fuzz.totalSupply, - _fuzz.totalWeight, - _fuzz.tokenAmountOut, - _fuzz.swapFee - ); - - (uint256 _poolAmountIn) = bPool.exitswapExternAmountOut(tokenOut, _fuzz.tokenAmountOut, type(uint256).max); - - assertEq(_expectedPoolAmountIn, _poolAmountIn); - } - - function test_Emit_LogCall(ExitswapExternAmountOut_FuzzScenario memory _fuzz) public happyPath(_fuzz) { - vm.expectEmit(); - bytes memory _data = - abi.encodeWithSelector(BPool.exitswapExternAmountOut.selector, tokenOut, _fuzz.tokenAmountOut, type(uint256).max); - emit IBPool.LOG_CALL(BPool.exitswapExternAmountOut.selector, address(this), _data); - - bPool.exitswapExternAmountOut(tokenOut, _fuzz.tokenAmountOut, type(uint256).max); - } -} diff --git a/test/unit/BPool/BPool_ExitswapExternAmountOut.t.sol b/test/unit/BPool/BPool_ExitswapExternAmountOut.t.sol new file mode 100644 index 00000000..74c64e0d --- /dev/null +++ b/test/unit/BPool/BPool_ExitswapExternAmountOut.t.sol @@ -0,0 +1,103 @@ +// 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 BPoolExitSwapExternAmountOut is BPoolBase, BNum { + // Valid scenario: + address public tokenOut; + uint256 public tokenOutWeight = 5e18; + uint256 public tokenOutBalance = 200e18; + uint256 public totalWeight = 10e18; + uint256 public tokenAmountOut = 20e18; + // calcPoolInGivenSingleOut(200,5,100,10,20,0) + uint256 public expectedPoolIn = 5.1316728300798443e18; + uint256 public exitFee = 0; + + function setUp() public virtual override { + super.setUp(); + tokenOut = tokens[1]; + bPool.set__records(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), expectedPoolIn); + bPool.mock_call__burnPoolShare(expectedPoolIn); + bPool.mock_call__pushPoolShare(address(this), exitFee); + bPool.mock_call__pushUnderlying(tokenOut, address(this), tokenAmountOut); + } + + function test_RevertWhen_ReentrancyLockIsSet() external { + bPool.call__setLock(_MUTEX_TAKEN); + // it should revert + vm.expectRevert(IBPool.BPool_Reentrancy.selector); + bPool.exitswapExternAmountOut(tokenOut, tokenAmountOut, expectedPoolIn); + } + + function test_RevertWhen_PoolIsNotFinalized() external { + bPool.set__finalized(false); + // it should revert + vm.expectRevert(IBPool.BPool_PoolNotFinalized.selector); + bPool.exitswapExternAmountOut(tokenOut, tokenAmountOut, expectedPoolIn); + } + + function test_RevertWhen_TokenIsNotBound() external { + // it should revert + vm.expectRevert(IBPool.BPool_TokenNotBound.selector); + bPool.exitswapExternAmountOut(makeAddr('unknown token'), tokenAmountOut, expectedPoolIn); + } + + function test_RevertWhen_TokenAmountOutExceedsMaxAllowedRatio() external { + // it should revert + vm.expectRevert(IBPool.BPool_TokenAmountOutAboveMaxOut.selector); + // just barely above 1/3rd of tokenOut.balanceOf(bPool) + bPool.exitswapExternAmountOut(tokenOut, bmul(tokenOutBalance, MAX_OUT_RATIO) + 1, expectedPoolIn); + } + + function test_RevertWhen_ComputedPoolAmountInIsZero() external { + // it should revert + vm.expectRevert(abi.encodeWithSelector(IBPool.BPool_InvalidPoolAmountIn.selector)); + bPool.exitswapExternAmountOut(tokenOut, 0, expectedPoolIn); + } + + function test_RevertWhen_ComputedPoolAmountInIsMoreThanMaxPoolAmountIn() external { + // it should revert + vm.expectRevert(abi.encodeWithSelector(IBPool.BPool_PoolAmountInAboveMaxPoolAmountIn.selector)); + bPool.exitswapExternAmountOut(tokenOut, tokenAmountOut, expectedPoolIn - 1); + } + + 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), expectedPoolIn); + // it burns poolAmountIn - exitFee shares + bPool.expectCall__burnPoolShare(expectedPoolIn); + // it sends exitFee to factory + bPool.expectCall__pushPoolShare(address(this), exitFee); + // it calls _pushUnderlying for token out + bPool.expectCall__pushUnderlying(tokenOut, address(this), tokenAmountOut); + // it emits LOG_CALL event + bytes memory _data = abi.encodeCall(IBPool.exitswapExternAmountOut, (tokenOut, tokenAmountOut, expectedPoolIn)); + vm.expectEmit(); + emit IBPool.LOG_CALL(IBPool.exitswapExternAmountOut.selector, address(this), _data); + // it emits LOG_EXIT event for token out + emit IBPool.LOG_EXIT(address(this), tokenOut, expectedPoolIn); + // it returns pool amount in + uint256 poolIn = bPool.exitswapExternAmountOut(tokenOut, tokenAmountOut, expectedPoolIn); + assertEq(expectedPoolIn, poolIn); + // it clears the reentrancy lock + assertEq(bPool.call__getLock(), _MUTEX_FREE); + } +} diff --git a/test/unit/BPool/BPool_ExitswapExternAmountOut.tree b/test/unit/BPool/BPool_ExitswapExternAmountOut.tree new file mode 100644 index 00000000..7511505e --- /dev/null +++ b/test/unit/BPool/BPool_ExitswapExternAmountOut.tree @@ -0,0 +1,24 @@ +BPool::ExitSwapExternAmountOut +├── 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 token amount out exceeds max allowed ratio +│ └── it should revert +├── when computed pool amount in is zero +│ └── it should revert +├── when computed pool amount in is more than maxPoolAmountIn +│ └── 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 pool amount in + └── it clears the reentrancy lock From 5920cd1bad68eacb671aeeca870591a2786204eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wei=C3=9Fer=20Hase?= Date: Wed, 24 Jul 2024 20:23:16 +0200 Subject: [PATCH 44/48] chore: preparing package for release (#157) * chore: preparing package for release * fix: broken links --- package.json | 14 +++++++------- src/interfaces/ISettlement.sol | 2 +- test/integration/BCoWHelper.t.sol | 2 +- test/unit/BCoWHelper.t.sol | 2 +- test/unit/BFactory.t.sol | 2 +- test/unit/BMath.t.sol | 2 +- test/unit/BNum.t.sol | 2 +- test/unit/BToken.t.sol | 2 +- 8 files changed, 14 insertions(+), 14 deletions(-) diff --git a/package.json b/package.json index 90eb4343..3501dc8d 100644 --- a/package.json +++ b/package.json @@ -1,17 +1,17 @@ { - "name": "balancer-core", - "version": "0.0.7", + "name": "balancer-cow-amm", + "version": "1.0.0", "private": true, - "description": "Balancer Core Contracts and ABI", - "homepage": "https://github.com/balancer-labs/balancer-core#readme", + "description": "Balancer CoW AMM", + "homepage": "https://github.com/balancer/cow-amm#readme", "bugs": { - "url": "https://github.com/balancer-labs/balancer-core/issues" + "url": "https://github.com/balancer/cow-amm/issues" }, "repository": { "type": "git", - "url": "git+https://github.com/balancer-labs/balancer-core.git" + "url": "git+https://github.com/balancer/cow-amm.git" }, - "license": "GPL-3.0-only", + "license": "GPL-3.0-or-later", "scripts": { "build": "forge build", "build:optimized": "FOUNDRY_PROFILE=optimized forge build", diff --git a/src/interfaces/ISettlement.sol b/src/interfaces/ISettlement.sol index 1b176cc1..1bcb793a 100644 --- a/src/interfaces/ISettlement.sol +++ b/src/interfaces/ISettlement.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: GPL-3.0 +// SPDX-License-Identifier: GPL-3.0-or-later pragma solidity 0.8.25; import {IERC20} from '@cowprotocol/interfaces/IERC20.sol'; diff --git a/test/integration/BCoWHelper.t.sol b/test/integration/BCoWHelper.t.sol index 7c1fc536..2bd63ab3 100644 --- a/test/integration/BCoWHelper.t.sol +++ b/test/integration/BCoWHelper.t.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: GPL-3.0 +// SPDX-License-Identifier: MIT pragma solidity ^0.8.24; import {Test} from 'forge-std/Test.sol'; diff --git a/test/unit/BCoWHelper.t.sol b/test/unit/BCoWHelper.t.sol index ef6d03b5..23964575 100644 --- a/test/unit/BCoWHelper.t.sol +++ b/test/unit/BCoWHelper.t.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: UNLICENSED +// SPDX-License-Identifier: MIT pragma solidity 0.8.25; import {Test} from 'forge-std/Test.sol'; diff --git a/test/unit/BFactory.t.sol b/test/unit/BFactory.t.sol index 24e0ed78..698f8823 100644 --- a/test/unit/BFactory.t.sol +++ b/test/unit/BFactory.t.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: UNLICENSED +// SPDX-License-Identifier: MIT pragma solidity 0.8.25; import {IERC20} from '@openzeppelin/contracts/token/ERC20/ERC20.sol'; diff --git a/test/unit/BMath.t.sol b/test/unit/BMath.t.sol index ad91456b..96f6b957 100644 --- a/test/unit/BMath.t.sol +++ b/test/unit/BMath.t.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: UNLICENSED +// SPDX-License-Identifier: MIT pragma solidity 0.8.25; import {BConst} from 'contracts/BConst.sol'; diff --git a/test/unit/BNum.t.sol b/test/unit/BNum.t.sol index 58041f73..81b6f375 100644 --- a/test/unit/BNum.t.sol +++ b/test/unit/BNum.t.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: UNLICENSED +// SPDX-License-Identifier: MIT pragma solidity 0.8.25; import {BConst} from 'contracts/BConst.sol'; diff --git a/test/unit/BToken.t.sol b/test/unit/BToken.t.sol index 1207a294..ce2f54f2 100644 --- a/test/unit/BToken.t.sol +++ b/test/unit/BToken.t.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: UNLICENSED +// SPDX-License-Identifier: MIT pragma solidity 0.8.25; import {IERC20} from '@openzeppelin/contracts/interfaces/IERC20.sol'; From c25075436f831b219ebe3cf62b04dd6fc16d459a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wei=C3=9Fer=20Hase?= Date: Wed, 24 Jul 2024 20:43:40 +0200 Subject: [PATCH 45/48] feat: adding deployment script with faucet erc20s (#114) * feat: adding deployment script with faucet erc20s * fix: missing files * feat: update appData to a valid one * fix: natspec in IFaucet * fix: final tweaks * refactor: deployment script * chore: testnet deployment (updated registry) * fix: rm solhint 1-contract-per-file rule in script dir * fix: update bDao address * fix: gas snapshots * fix: refresh snapshots * fix: extra line * chore: testnet redeployment * chore: add grafiti to factory * chore: deployment addresses * chore: rm grafiti from factory * refactor: reorganize deployment and scripting architecture * fix: linter warnings * fix: deprecate mainnet and testnet id constants * fix: contract comments * feat: adding explicit revert on unknown chains * feat: adding BCoWHelper to deployment script and registry --- README.md | 8 +++++ package.json | 10 +++--- script/.solhint.json | 6 ++++ script/Deploy.s.sol | 43 +++++++++++++++++++++++++ script/DeployBCoWFactory.s.sol | 17 ---------- script/DeployBFactory.s.sol | 17 ---------- script/Params.s.sol | 48 +++++++++++++++------------ script/Registry.s.sol | 36 +++++++++++++++++++++ script/Script.s.sol | 59 ++++++++++++++++++++++++++++++++++ src/interfaces/IFaucet.sol | 14 ++++++++ 10 files changed, 200 insertions(+), 58 deletions(-) create mode 100644 script/.solhint.json create mode 100644 script/Deploy.s.sol delete mode 100644 script/DeployBCoWFactory.s.sol delete mode 100644 script/DeployBFactory.s.sol create mode 100644 script/Registry.s.sol create mode 100644 script/Script.s.sol create mode 100644 src/interfaces/IFaucet.sol diff --git a/README.md b/README.md index 5a1eac83..1245d520 100644 --- a/README.md +++ b/README.md @@ -57,3 +57,11 @@ yarn test # run the tests - The weight represents the intended distribution of value between the tokens in the pool - Modify the pool's swap fee by calling `IBPool.setSwapFee(fee)` - Finalize the pool by calling `IBPool.finalize()` + +# Deployments +Ethereum Mainnet: + - BCoWFactory: (0x21Cd97D70f8475DF3d62917880aF9f41D9a9dCeF)[https://etherscan.io/address/0x21Cd97D70f8475DF3d62917880aF9f41D9a9dCeF#code] + +Ethereum Sepolia: + - BCoWFactory: (0xe8587525430fFC9193831e1113a672f3133C1B8A)[https://sepolia.etherscan.io/address/0xe8587525430fFC9193831e1113a672f3133C1B8A#code] + - BCoWPool: (0xFe1ce255D68B3Bff95E71DDef1c8fc55459aaCd7)[https://sepolia.etherscan.io/address/0xFe1ce255D68B3Bff95E71DDef1c8fc55459aaCd7#code] \ No newline at end of file diff --git a/package.json b/package.json index 3501dc8d..a704b568 100644 --- a/package.json +++ b/package.json @@ -16,15 +16,17 @@ "build": "forge build", "build:optimized": "FOUNDRY_PROFILE=optimized forge build", "coverage": "forge coverage --match-path 'test/unit/**'", - "deploy:bcowfactory:mainnet": "bash -c 'source .env && forge script DeployBCoWFactory -vvvvv --rpc-url $MAINNET_RPC --broadcast --chain mainnet --private-key $MAINNET_DEPLOYER_PK --verify --etherscan-api-key $ETHERSCAN_API_KEY'", - "deploy:bcowfactory:testnet": "bash -c 'source .env && forge script DeployBCoWFactory -vvvvv --rpc-url $SEPOLIA_RPC --broadcast --chain sepolia --private-key $SEPOLIA_DEPLOYER_PK --verify --etherscan-api-key $ETHERSCAN_API_KEY'", - "deploy:bfactory:mainnet": "bash -c 'source .env && forge script DeployBFactory -vvvvv --rpc-url $MAINNET_RPC --broadcast --chain mainnet --private-key $MAINNET_DEPLOYER_PK --verify --etherscan-api-key $ETHERSCAN_API_KEY'", - "deploy:bfactory:testnet": "bash -c 'source .env && forge script DeployBFactory -vvvvv --rpc-url $SEPOLIA_RPC --broadcast --chain sepolia --private-key $SEPOLIA_DEPLOYER_PK --verify --etherscan-api-key $ETHERSCAN_API_KEY'", + "deploy:bcowfactory:mainnet": "forge script DeployBCoWFactory -vvvvv --rpc-url $MAINNET_RPC --broadcast --chain mainnet --private-key $MAINNET_DEPLOYER_PK --verify", + "deploy:bcowfactory:testnet": "forge script DeployBCoWFactory -vvvvv --rpc-url $SEPOLIA_RPC --broadcast --chain sepolia --private-key $SEPOLIA_DEPLOYER_PK --verify", + "deploy:bfactory:mainnet": "forge script DeployBFactory -vvvvv --rpc-url $MAINNET_RPC --broadcast --chain mainnet --private-key $MAINNET_DEPLOYER_PK --verify", + "deploy:bfactory:testnet": "forge script DeployBFactory -vvvvv --rpc-url $SEPOLIA_RPC --broadcast --chain sepolia --private-key $SEPOLIA_DEPLOYER_PK --verify", "lint:bulloak": "find test/unit -name '*.tree' | xargs bulloak check", "lint:check": "solhint 'src/**/*.sol' 'test/**/*.sol' 'script/**/*.sol' && forge fmt --check", "lint:fix": "solhint --fix 'src/**/*.sol' 'test/**/*.sol' 'script/**/*.sol' && sort-package-json && forge fmt", "lint:natspec": "npx @defi-wonderland/natspec-smells --config natspec-smells.config.js", "prepare": "husky install", + "script:mainnet": "forge script MainnetScript -vvvvv --rpc-url $MAINNET_RPC --broadcast --chain mainnet --private-key $MAINNET_DEPLOYER_PK --verify", + "script:testnet": "forge script TestnetScript -vvvvv --rpc-url $SEPOLIA_RPC --broadcast --chain sepolia --private-key $SEPOLIA_DEPLOYER_PK --verify", "smock": "smock-foundry --contracts src/contracts", "test": "yarn test:integration && yarn test:unit", "test:integration": "forge test --ffi --match-path 'test/integration/**' -vvv --isolate", diff --git a/script/.solhint.json b/script/.solhint.json new file mode 100644 index 00000000..c82a6256 --- /dev/null +++ b/script/.solhint.json @@ -0,0 +1,6 @@ +{ + "rules": { + "one-contract-per-file": "off", + "custom-errors": "off" + } +} diff --git a/script/Deploy.s.sol b/script/Deploy.s.sol new file mode 100644 index 00000000..c697f26a --- /dev/null +++ b/script/Deploy.s.sol @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.25; + +import {BCoWFactory} from 'contracts/BCoWFactory.sol'; +import {BCoWHelper} from 'contracts/BCoWHelper.sol'; +import {BFactory} from 'contracts/BFactory.sol'; +import {IBFactory} from 'interfaces/IBFactory.sol'; + +import {Script} from 'forge-std/Script.sol'; +import {Params} from 'script/Params.s.sol'; + +/// @notice This base script is shared across `yarn script:{b|bcow}factory:{mainnet|testnet}` +abstract contract DeployBaseFactory is Script, Params { + constructor() Params(block.chainid) {} + + function run() public { + vm.startBroadcast(); + IBFactory bFactory = _deployFactory(); + bFactory.setBDao(_bFactoryDeploymentParams.bDao); + vm.stopBroadcast(); + } + + function _deployFactory() internal virtual returns (IBFactory); +} + +/// @notice This script will be executed by `yarn script:bfactory:{mainnet|testnet}` +contract DeployBFactory is DeployBaseFactory { + function _deployFactory() internal override returns (IBFactory bFactory) { + bFactory = new BFactory(); + } +} + +/// @notice This script will be executed by `yarn script:bcowfactory:{mainnet|testnet}` +contract DeployBCoWFactory is DeployBaseFactory { + function _deployFactory() internal override returns (IBFactory bFactory) { + bFactory = new BCoWFactory({ + solutionSettler: _bCoWFactoryDeploymentParams.settlement, + appData: _bCoWFactoryDeploymentParams.appData + }); + + new BCoWHelper(address(bFactory)); + } +} diff --git a/script/DeployBCoWFactory.s.sol b/script/DeployBCoWFactory.s.sol deleted file mode 100644 index 57905392..00000000 --- a/script/DeployBCoWFactory.s.sol +++ /dev/null @@ -1,17 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.25; - -import {BCoWFactory} from 'contracts/BCoWFactory.sol'; -import {Script} from 'forge-std/Script.sol'; -import {Params} from 'script/Params.s.sol'; - -contract DeployBCoWFactory is Script, Params { - function run() public { - BCoWFactoryDeploymentParams memory params = _bCoWFactoryDeploymentParams[block.chainid]; - - vm.startBroadcast(); - BCoWFactory bCoWFactory = new BCoWFactory(params.settlement, params.appData); - bCoWFactory.setBDao(params.bDao); - vm.stopBroadcast(); - } -} diff --git a/script/DeployBFactory.s.sol b/script/DeployBFactory.s.sol deleted file mode 100644 index 1d7a0157..00000000 --- a/script/DeployBFactory.s.sol +++ /dev/null @@ -1,17 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.25; - -import {BFactory} from 'contracts/BFactory.sol'; -import {Script} from 'forge-std/Script.sol'; -import {Params} from 'script/Params.s.sol'; - -contract DeployBFactory is Script, Params { - function run() public { - BFactoryDeploymentParams memory params = _bFactoryDeploymentParams[block.chainid]; - - vm.startBroadcast(); - BFactory bFactory = new BFactory(); - bFactory.setBDao(params.bDao); - vm.stopBroadcast(); - } -} diff --git a/script/Params.s.sol b/script/Params.s.sol index 9df9a8f0..330f60e1 100644 --- a/script/Params.s.sol +++ b/script/Params.s.sol @@ -1,36 +1,44 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.25; -contract Params { +/// @notice Deployment parameters +abstract contract Params { struct BFactoryDeploymentParams { address bDao; } struct BCoWFactoryDeploymentParams { - address bDao; address settlement; bytes32 appData; } /// @notice Settlement address address internal constant _GPV2_SETTLEMENT = 0x9008D19f58AAbD9eD0D60971565AA8510560ab41; - - /// @notice AppData identifier - bytes32 internal constant _APP_DATA = bytes32('appData'); - - /// @notice BFactory deployment parameters for each chain - mapping(uint256 _chainId => BFactoryDeploymentParams _params) internal _bFactoryDeploymentParams; - - /// @notice BCoWFactory deployment parameters for each chain - mapping(uint256 _chainId => BCoWFactoryDeploymentParams _params) internal _bCoWFactoryDeploymentParams; - - constructor() { - // Mainnet - _bFactoryDeploymentParams[1] = BFactoryDeploymentParams(address(this)); - _bCoWFactoryDeploymentParams[1] = BCoWFactoryDeploymentParams(address(this), _GPV2_SETTLEMENT, _APP_DATA); - - // Sepolia - _bFactoryDeploymentParams[11_155_111] = BFactoryDeploymentParams(address(this)); - _bCoWFactoryDeploymentParams[11_155_111] = BCoWFactoryDeploymentParams(address(this), _GPV2_SETTLEMENT, _APP_DATA); + /// @notice Balancer DAO address (has controller permission to collect fees from BFactory pools) + address internal constant _B_DAO = 0xce88686553686DA562CE7Cea497CE749DA109f9F; + + /** + * @notice AppData identifier + * @dev Value obtained from https://explorer.cow.fi/appdata?tab=encode + * - appCode: "CoW AMM Balancer" + * - metadata:hooks:version: 0.1.0 + * - version: 1.1.0 + */ + bytes32 internal constant _APP_DATA = 0x362e5182440b52aa8fffe70a251550fbbcbca424740fe5a14f59bf0c1b06fe1d; + + /// @notice BFactory deployment parameters + BFactoryDeploymentParams internal _bFactoryDeploymentParams; + + /// @notice BCoWFactory deployment parameters + BCoWFactoryDeploymentParams internal _bCoWFactoryDeploymentParams; + + constructor(uint256 chainId) { + if (chainId == 1 || chainId == 11_155_111) { + // Ethereum Mainnet & Ethereum Sepolia [Testnet] + _bFactoryDeploymentParams = BFactoryDeploymentParams({bDao: _B_DAO}); + _bCoWFactoryDeploymentParams = BCoWFactoryDeploymentParams({settlement: _GPV2_SETTLEMENT, appData: _APP_DATA}); + } else { + revert('Params: unknown chain ID'); + } } } diff --git a/script/Registry.s.sol b/script/Registry.s.sol new file mode 100644 index 00000000..16334c1c --- /dev/null +++ b/script/Registry.s.sol @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.25; + +import {BCoWFactory} from 'contracts/BCoWFactory.sol'; +import {BCoWHelper} from 'contracts/BCoWHelper.sol'; +import {BFactory} from 'contracts/BFactory.sol'; + +import {Params} from 'script/Params.s.sol'; + +/// @notice Registry of deployed contracts +abstract contract Registry is Params { + /// @notice Balancer Pool Factory + BFactory public bFactory; + /// @notice Balancer CoW Pool Factory + BCoWFactory public bCoWFactory; + /// @notice Balancer CoW Helper + BCoWHelper public bCoWHelper; + + constructor(uint256 chainId) Params(chainId) { + // TODO: redeploy + if (chainId == 1) { + // Ethereum Mainnet + bFactory = BFactory(0xaD0447be7BDC80cf2e6DA20B13599E5dc859b667); + bCoWFactory = BCoWFactory(0x21Cd97D70f8475DF3d62917880aF9f41D9a9dCeF); + bCoWHelper = BCoWHelper(0xE50481D88f147B8b4aaCdf9a1B7b7bA44F87823f); + } else if (chainId == 11_155_111) { + // Ethereum Sepolia [Testnet] + bFactory = BFactory(0x2bfA24B26B85DD812b2C69E3B1cb4C85C886C8E2); + bCoWFactory = BCoWFactory(0xe8587525430fFC9193831e1113a672f3133C1B8A); + bCoWHelper = BCoWHelper(0x0fd365F9Ed185512536E7dbfc7a8DaE43cD3CA09); + } else { + // TODO: add Gnosis chain + revert('Registry: unknown chain ID'); + } + } +} diff --git a/script/Script.s.sol b/script/Script.s.sol new file mode 100644 index 00000000..d21a54cc --- /dev/null +++ b/script/Script.s.sol @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.25; + +import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; +import {IBPool} from 'contracts/BPool.sol'; +import {IFaucet} from 'interfaces/IFaucet.sol'; + +import {Script} from 'forge-std/Script.sol'; +import {Registry} from 'script/Registry.s.sol'; + +/// @notice This base script is shared across `yarn script:{mainnet|testnet}` +abstract contract BaseScript is Registry, Script { + constructor() Registry(block.chainid) {} +} + +/// @notice This script will be executed by `yarn script:mainnet` +contract MainnetScript is BaseScript { + function run() public { + assert(block.chainid == 1); + vm.startBroadcast(); + + // script logic here + + vm.stopBroadcast(); + } +} + +/// @notice This script will be executed by `yarn script:testnet` +contract TestnetScript is BaseScript { + /// @notice ERC20 and Faucet addresses + address internal constant _SEPOLIA_FAUCET = 0x26bfAecAe4D5fa93eE1737ce1Ce7D53F2a0E9b2d; + address internal constant _SEPOLIA_BAL_TOKEN = 0xb19382073c7A0aDdbb56Ac6AF1808Fa49e377B75; + address internal constant _SEPOLIA_DAI_TOKEN = 0xB77EB1A70A96fDAAeB31DB1b42F2b8b5846b2613; + address internal constant _SEPOLIA_USDC_TOKEN = 0x80D6d3946ed8A1Da4E226aa21CCdDc32bd127d1A; + + /// @dev The following is an example of a script that deploys a Balancer CoW pool + function run() public { + assert(block.chainid == 11_155_111); + vm.startBroadcast(); + + // NOTE: dripping can be called by anyone but only once a day (per address) + IFaucet(_SEPOLIA_FAUCET).drip(_SEPOLIA_BAL_TOKEN); + IFaucet(_SEPOLIA_FAUCET).drip(_SEPOLIA_DAI_TOKEN); + IFaucet(_SEPOLIA_FAUCET).drip(_SEPOLIA_USDC_TOKEN); + + IBPool bPool = bCoWFactory.newBPool(); + + IERC20(_SEPOLIA_BAL_TOKEN).approve(address(bPool), type(uint256).max); + IERC20(_SEPOLIA_DAI_TOKEN).approve(address(bPool), type(uint256).max); + IERC20(_SEPOLIA_USDC_TOKEN).approve(address(bPool), type(uint256).max); + + bPool.bind(_SEPOLIA_BAL_TOKEN, 40e18, 1e18); + bPool.bind(_SEPOLIA_DAI_TOKEN, 10e18, 1e18); + bPool.bind(_SEPOLIA_USDC_TOKEN, 10e6, 1e18); + + bPool.finalize(); + vm.stopBroadcast(); + } +} diff --git a/src/interfaces/IFaucet.sol b/src/interfaces/IFaucet.sol new file mode 100644 index 00000000..84d507b3 --- /dev/null +++ b/src/interfaces/IFaucet.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity 0.8.25; + +/** + * @title IFaucet + * @notice External interface of Sepolia's Faucet contract. + */ +interface IFaucet { + /** + * @notice Drips an amount of tokens to the caller. + * @param token The address of the token to drip. + */ + function drip(address token) external; +} From 318eab9af0203782b1a793a8d9e7b811cc62a100 Mon Sep 17 00:00:00 2001 From: teddy Date: Thu, 25 Jul 2024 11:32:37 -0300 Subject: [PATCH 46/48] Chore: post btt cleanup (#174) * chore: remove unused utils directory * test: missing case for joinswapPoolAmountOut * test: cover different bmul branches explicitly * test: cover {push,pull}PoolShares * test: token ratio > spot price before on swapExactAmountOut * test: cover bpowApprox with a few valid scenarios --- test/unit/BNum.t.sol | 43 ++++-- test/unit/BPool/BPool.t.sol | 30 ++++ test/unit/BPool/BPool.tree | 10 ++ test/unit/BPool/BPoolBase.sol | 23 ++- .../BPool/BPool_JoinswapPoolAmountOut.t.sol | 7 + .../BPool/BPool_JoinswapPoolAmountOut.tree | 2 + .../unit/BPool/BPool_SwapExactAmountOut.t.sol | 21 ++- test/utils/Pow.sol | 10 -- test/utils/Utils.sol | 132 ------------------ 9 files changed, 115 insertions(+), 163 deletions(-) delete mode 100644 test/utils/Pow.sol delete mode 100644 test/utils/Utils.sol diff --git a/test/unit/BNum.t.sol b/test/unit/BNum.t.sol index 81b6f375..097b275a 100644 --- a/test/unit/BNum.t.sol +++ b/test/unit/BNum.t.sol @@ -165,8 +165,8 @@ contract BNumTest is Test, BConst { } function test_BmulRevertWhen_PassingAAndBTooBig(uint256 _a, uint256 _b) external { - _a = bound(_a, 1, type(uint256).max); - _b = bound(_b, _a == 1 ? type(uint256).max : type(uint256).max / _a + 1, type(uint256).max); + _a = bound(_a, type(uint256).max / 2, type(uint256).max); + _b = bound(_b, type(uint256).max / 2, type(uint256).max); // it should revert // a * b > uint256 max @@ -175,14 +175,14 @@ contract BNumTest is Test, BConst { bNum.call_bmul(_a, _b); } - function test_BmulRevertWhen_PassingAMulBTooBig(uint256 _a, uint256 _b) external { - _a = bound(_a, 1, type(uint256).max); - _b = bound(_b, (type(uint256).max - (BONE / 2)) / _a + 1, type(uint256).max); + function test_BmulRevertWhen_PassingAMulBTooBig() external { + // type(uint256).max - BONE/2 < (2^248 - 1)*2^8 < type(uint256).max + uint256 _a = 2 ** 248 - 1; + uint256 _b = 2 ** 8; // it should revert // a * b + BONE / 2 > uint256 max vm.expectRevert(BNum.BNum_MulOverflow.selector); - bNum.call_bmul(_a, _b); } @@ -334,12 +334,29 @@ contract BNumTest is Test, BConst { } function test_BpowWhenPassingKnownValues() external { - // it should return correct value - // 1.01 ^ 3 = 1.030301 - uint256 _a = 1.01e18; - uint256 _b = 3e18; - - uint256 _result = bNum.call_bpow(_a, _b); - assertEq(_result, 1.030301e18); + uint256 testcasesCount = 5; + uint256[] memory bases = new uint256[](testcasesCount); + bases[0] = 1.01e18; + bases[1] = 0.03e18; + bases[2] = 0.4e18; + bases[3] = 1.5e18; + bases[4] = 1.2e18; + uint256[] memory exponents = new uint256[](testcasesCount); + exponents[0] = 3e18; + exponents[1] = 1.01e18; + exponents[2] = 4.1e18; + exponents[3] = 9e18; + exponents[4] = 0.003e18; + + uint256[] memory results = new uint256[](testcasesCount); + results[0] = 1.030301e18; + results[1] = 0.02896626284766446e18; + results[2] = 0.02335855453582031e18; + results[3] = 38.443359375e18; + results[4] = 1.000547114282833518e18; + for (uint256 i = 0; i < testcasesCount; i++) { + uint256 _result = bNum.call_bpow(bases[i], exponents[i]); + assertApproxEqAbs(_result, results[i], BPOW_PRECISION); + } } } diff --git a/test/unit/BPool/BPool.t.sol b/test/unit/BPool/BPool.t.sol index 07954497..0a8264da 100644 --- a/test/unit/BPool/BPool.t.sol +++ b/test/unit/BPool/BPool.t.sol @@ -3,6 +3,7 @@ pragma solidity 0.8.25; import {BPoolBase} from './BPoolBase.sol'; +import {IERC20Errors} from '@openzeppelin/contracts/interfaces/draft-IERC6093.sol'; import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; import {SafeERC20} from '@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol'; import {Address} from '@openzeppelin/contracts/utils/Address.sol'; @@ -541,4 +542,33 @@ contract BPool is BPoolBase, BMath { ); bPool.call__pullUnderlying(transferredToken, transferFromSpender, transferredAmount); } + + function test__mintPoolShareWhenCalled() external { + uint256 mintAmount = 100e18; + // it mints shares to the pool's own balance + bPool.call__mintPoolShare(mintAmount); + assertEq(bPool.balanceOf(address(bPool)), mintAmount); + } + + function test__burnPoolShareRevertWhen_PoolHasLessBalanceThanAmountToBurn() external { + uint256 existingBalance = 40e18; + deal(address(bPool), address(bPool), existingBalance); + uint256 burnAmount = 100e18; + // it should revert + vm.expectRevert( + abi.encodeWithSelector( + IERC20Errors.ERC20InsufficientBalance.selector, address(bPool), existingBalance, burnAmount + ) + ); + bPool.call__burnPoolShare(burnAmount); + } + + function test__burnPoolShareWhenPoolHasEnoughBalance() external { + uint256 existingBalance = 100e18; + uint256 burnAmount = 30e18; + deal(address(bPool), address(bPool), existingBalance); + bPool.call__burnPoolShare(burnAmount); + // it mints shares to the pool's own balance + assertEq(bPool.balanceOf(address(bPool)), existingBalance - burnAmount); + } } diff --git a/test/unit/BPool/BPool.tree b/test/unit/BPool/BPool.tree index 85659d4c..0405f003 100644 --- a/test/unit/BPool/BPool.tree +++ b/test/unit/BPool/BPool.tree @@ -165,3 +165,13 @@ BPool::_pullUnderlying │ └── it should revert └── when underlying token returns true └── it calls underlying transferFrom + +BPool::_mintPoolShare +└── when called + └── it mints shares to the pool's own balance + +BPool::_burnPoolShare +├── when pool has less balance than amount to burn +│ └── it should revert +└── when pool has enough balance + └── it mints shares to the pool's own balance diff --git a/test/unit/BPool/BPoolBase.sol b/test/unit/BPool/BPoolBase.sol index 092d8147..1495f9d1 100644 --- a/test/unit/BPool/BPoolBase.sol +++ b/test/unit/BPool/BPoolBase.sol @@ -4,10 +4,15 @@ pragma solidity 0.8.25; import {BConst} from 'contracts/BConst.sol'; import {Test} from 'forge-std/Test.sol'; import {IBPool} from 'interfaces/IBPool.sol'; + +import {LibString} from 'solmate/utils/LibString.sol'; import {MockBPool} from 'test/smock/MockBPool.sol'; -import {Utils} from 'test/utils/Utils.sol'; -contract BPoolBase is Test, BConst, Utils { +contract BPoolBase is Test, BConst { + using LibString for uint256; + + address[] public tokens; + MockBPool public bPool; function setUp() public virtual { @@ -16,6 +21,20 @@ contract BPoolBase is Test, BConst, Utils { tokens.push(makeAddr('token1')); } + function _getDeterministicTokenArray(uint256 _length) internal returns (address[] memory _tokenArray) { + _tokenArray = new address[](_length); + for (uint256 i = 0; i < _length; i++) { + _tokenArray[i] = makeAddr(i.toString()); + } + } + + function _tokensToMemory() internal view returns (address[] memory _tokens) { + _tokens = new address[](tokens.length); + for (uint256 i = 0; i < tokens.length; i++) { + _tokens[i] = tokens[i]; + } + } + function _setRandomTokens(uint256 _length) internal returns (address[] memory _tokensToAdd) { _tokensToAdd = _getDeterministicTokenArray(_length); for (uint256 i = 0; i < _length; i++) { diff --git a/test/unit/BPool/BPool_JoinswapPoolAmountOut.t.sol b/test/unit/BPool/BPool_JoinswapPoolAmountOut.t.sol index 98c22e0d..78ef1fbb 100644 --- a/test/unit/BPool/BPool_JoinswapPoolAmountOut.t.sol +++ b/test/unit/BPool/BPool_JoinswapPoolAmountOut.t.sol @@ -52,6 +52,13 @@ contract BPoolJoinswapPoolAmountOut is BPoolBase, BNum { bPool.joinswapPoolAmountOut(makeAddr('unknown token'), poolAmountOut, maxTokenIn); } + function test_RevertWhen_TokenAmountInIsZero() external { + // it should revert + vm.expectRevert(IBPool.BPool_InvalidTokenAmountIn.selector); + // using a small amount that rounds to zero + bPool.joinswapPoolAmountOut(tokenIn, 20, type(uint256).max); + } + function test_RevertWhen_TokenAmountInExceedsMaxRatio() external { // it should revert vm.expectRevert(IBPool.BPool_TokenAmountInAboveMaxRatio.selector); diff --git a/test/unit/BPool/BPool_JoinswapPoolAmountOut.tree b/test/unit/BPool/BPool_JoinswapPoolAmountOut.tree index ec19a013..b795f4b7 100644 --- a/test/unit/BPool/BPool_JoinswapPoolAmountOut.tree +++ b/test/unit/BPool/BPool_JoinswapPoolAmountOut.tree @@ -5,6 +5,8 @@ BPool::JoinswapPoolAmountOut │ └── it should revert ├── when token in is not bound │ └── it should revert +├── when token amount in is zero +│ └── it should revert ├── when token amount in exceeds max ratio │ └── it should revert ├── when calculated token amount in is more than expected diff --git a/test/unit/BPool/BPool_SwapExactAmountOut.t.sol b/test/unit/BPool/BPool_SwapExactAmountOut.t.sol index 85e168b0..0792a6d2 100644 --- a/test/unit/BPool/BPool_SwapExactAmountOut.t.sol +++ b/test/unit/BPool/BPool_SwapExactAmountOut.t.sol @@ -95,13 +95,22 @@ contract BPoolSwapExactAmountOut is BPoolBase, BNum { } function test_RevertWhen_TokenRatioAfterSwapExceedsSpotPriceBeforeSwap() external { + // params obtained from legacy fuzz tests: + uint256 tokenAmountOut_ = 621_143_522_536_167_460_787_693_100_883_186_780; + uint256 tokenInBalance_ = 1_020_504_230_788_863_581_113_405_134_266_627; + uint256 tokenInDenorm_ = 49_062_504_624_460_684_226; + uint256 tokenOutBalance_ = 15_332_515_003_530_544_593_793_307_770_397_516_084_212_022_325; + uint256 tokenOutDenorm_ = 19_469_010_750_289_341_034; + uint256 swapFee_ = 894_812_326_421_000_610; + + vm.mockCall(tokenIn, abi.encodePacked(IERC20.balanceOf.selector), abi.encode(uint256(tokenInBalance_))); + vm.mockCall(tokenOut, abi.encodePacked(IERC20.balanceOf.selector), abi.encode(uint256(tokenOutBalance_))); + bPool.set__records(tokenIn, IBPool.Record({bound: true, index: 0, denorm: tokenInDenorm_})); + bPool.set__records(tokenOut, IBPool.Record({bound: true, index: 1, denorm: tokenOutDenorm_})); + bPool.set__swapFee(swapFee_); // 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); + vm.expectRevert(IBPool.BPool_SpotPriceBeforeAboveTokenRatio.selector); + bPool.swapExactAmountOut(tokenIn, type(uint256).max, tokenOut, tokenAmountOut_, type(uint256).max); } function test_WhenPreconditionsAreMet() external { diff --git a/test/utils/Pow.sol b/test/utils/Pow.sol deleted file mode 100644 index e9142c57..00000000 --- a/test/utils/Pow.sol +++ /dev/null @@ -1,10 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.25; - -import {BNum} from 'contracts/BNum.sol'; - -contract Pow is BNum { - function pow(uint256 _base, uint256 _exp) public pure returns (uint256 _result) { - _result = bpow(_base, _exp); - } -} diff --git a/test/utils/Utils.sol b/test/utils/Utils.sol deleted file mode 100644 index 59a4a135..00000000 --- a/test/utils/Utils.sol +++ /dev/null @@ -1,132 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.25; - -import {Test} from 'forge-std/Test.sol'; -import {LibString} from 'solmate/utils/LibString.sol'; - -contract Utils is Test { - using LibString for uint256; - - uint256 public constant TOKENS_AMOUNT = 3; - - address[] public tokens; - - function _getDeterministicTokenArray(uint256 _length) internal returns (address[] memory _tokenArray) { - _tokenArray = new address[](_length); - for (uint256 i = 0; i < _length; i++) { - _tokenArray[i] = makeAddr(i.toString()); - } - } - - function _tokensToMemory() internal view returns (address[] memory _tokens) { - _tokens = new address[](tokens.length); - for (uint256 i = 0; i < tokens.length; i++) { - _tokens[i] = tokens[i]; - } - } - - function _staticToDynamicUintArray(uint256[TOKENS_AMOUNT] memory _fixedUintArray) - internal - pure - returns (uint256[] memory _memoryUintArray) - { - _memoryUintArray = new uint256[](_fixedUintArray.length); - for (uint256 i = 0; i < _fixedUintArray.length; i++) { - _memoryUintArray[i] = _fixedUintArray[i]; - } - } - - /** - * @dev Write a uint256 value to a storage slot. - * @param _target The address of the contract. - * @param _slotNumber The slot number to write to. - * @param _value The value to write. - */ - function _writeUintToStorage(address _target, uint256 _slotNumber, uint256 _value) internal { - vm.store(_target, bytes32(_slotNumber), bytes32(_value)); - } - - /** - * @dev Write the length of an array in storage. - * @dev This must be performed before writing any items to the array. - * @param _target The address of the contract. - * @param _arraySlotNumber The slot number of the array. - * @param _arrayLength The length of the array. - */ - function _writeArrayLengthToStorage(address _target, uint256 _arraySlotNumber, uint256 _arrayLength) internal { - _writeUintToStorage(_target, _arraySlotNumber, _arrayLength); - } - - /** - * @dev Write an address array item to a storage slot. - * @param _target The address of the contract. - * @param _arraySlotNumber The slot number of the array. - * @param _index The index of the item in the array. - * @param _value The address value to write. - */ - function _writeAddressArrayItemToStorage( - address _target, - uint256 _arraySlotNumber, - uint256 _index, - address _value - ) internal { - bytes memory _arraySlot = abi.encode(_arraySlotNumber); - bytes32 _hashArraySlot = keccak256(_arraySlot); - vm.store(_target, bytes32(uint256(_hashArraySlot) + _index), bytes32(abi.encode(_value))); - } - - /** - * @dev Write a struct property to a mapping in storage. - * @param _target The address of the contract. - * @param _mappingSlotNumber The slot number of the mapping. - * @param _mappingKey The address key of the mapping. - * @param _propertySlotNumber The slot number of the property in the struct. - * @param _value The value to write. - */ - function _writeStructPropertyAtAddressMapping( - address _target, - uint256 _mappingSlotNumber, - address _mappingKey, - uint256 _propertySlotNumber, - uint256 _value - ) internal { - bytes32 _slot = keccak256(abi.encode(_mappingKey, _mappingSlotNumber)); - _writeUintToStorage(_target, uint256(_slot) + _propertySlotNumber, _value); - } - - /** - * @dev Write a uint256 value to an address mapping in storage. - * @param _target The address of the contract. - * @param _mappingSlotNumber The slot number of the mapping. - * @param _mappingKey The address key of the mapping. - * @param _value The value to write. - */ - function _writeUintAtAddressMapping( - address _target, - uint256 _mappingSlotNumber, - address _mappingKey, - uint256 _value - ) internal { - bytes32 _slot = keccak256(abi.encode(_mappingKey, _mappingSlotNumber)); - _writeUintToStorage(_target, uint256(_slot), _value); - } - - /** - * @dev Load an array of type(uint256).max values into memory. - * @param _length The length of the array. - */ - function _maxArray(uint256 _length) internal pure returns (uint256[] memory _maxUintArray) { - _maxUintArray = new uint256[](_length); - for (uint256 i = 0; i < TOKENS_AMOUNT; i++) { - _maxUintArray[i] = type(uint256).max; - } - } - - /** - * @dev Load an array of 0 values into memory. - * @param _length The length of the array. - */ - function _zeroArray(uint256 _length) internal pure returns (uint256[] memory _zeroUintArray) { - _zeroUintArray = new uint256[](_length); - } -} From 521b10c24f462a32e5f1ddf5b4f09c6b69f4d996 Mon Sep 17 00:00:00 2001 From: teddy Date: Thu, 25 Jul 2024 14:48:41 -0300 Subject: [PATCH 47/48] fix: test small fixes (#176) * chore: rename BPoolBase.sol BPoolBase.t.sol * chore: remove BCoWPoolBase.sol * chore: remove token{In,Out} from base setup * chore: give token to bind its own variable --- test/unit/BCoWPool/BCoWPool.t.sol | 2 +- test/unit/BCoWPool/BCoWPoolBase.sol | 28 ------------- test/unit/BCoWPool/BCoWPoolBase.t.sol | 6 +-- .../BCoWPool/BCoWPool_IsValidSignature.t.sol | 2 +- test/unit/BCoWPool/BCoWPool_Verify.t.sol | 4 ++ test/unit/BPool/BPool.t.sol | 2 +- .../BPool/{BPoolBase.sol => BPoolBase.t.sol} | 0 test/unit/BPool/BPool_Bind.t.sol | 42 ++++++++++--------- test/unit/BPool/BPool_ExitPool.t.sol | 2 +- .../BPool/BPool_ExitswapExternAmountOut.t.sol | 2 +- .../BPool/BPool_ExitswapPoolAmountIn.t.sol | 2 +- test/unit/BPool/BPool_JoinPool.t.sol | 2 +- .../BPool/BPool_JoinswapExternAmountIn.t.sol | 2 +- .../BPool/BPool_JoinswapPoolAmountOut.t.sol | 2 +- test/unit/BPool/BPool_SwapExactAmountIn.t.sol | 2 +- .../unit/BPool/BPool_SwapExactAmountOut.t.sol | 2 +- test/unit/BPool/BPool_Unbind.t.sol | 2 +- 17 files changed, 39 insertions(+), 65 deletions(-) delete mode 100644 test/unit/BCoWPool/BCoWPoolBase.sol rename test/unit/BPool/{BPoolBase.sol => BPoolBase.t.sol} (100%) diff --git a/test/unit/BCoWPool/BCoWPool.t.sol b/test/unit/BCoWPool/BCoWPool.t.sol index 66d4a825..5c8c79f3 100644 --- a/test/unit/BCoWPool/BCoWPool.t.sol +++ b/test/unit/BCoWPool/BCoWPool.t.sol @@ -3,7 +3,7 @@ pragma solidity 0.8.25; import {IERC20} from '@cowprotocol/interfaces/IERC20.sol'; -import {BCoWPoolBase} from './BCoWPoolBase.sol'; +import {BCoWPoolBase} from './BCoWPoolBase.t.sol'; import {IBCoWFactory} from 'interfaces/IBCoWFactory.sol'; diff --git a/test/unit/BCoWPool/BCoWPoolBase.sol b/test/unit/BCoWPool/BCoWPoolBase.sol deleted file mode 100644 index 3418e59e..00000000 --- a/test/unit/BCoWPool/BCoWPoolBase.sol +++ /dev/null @@ -1,28 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.25; - -import {BPoolBase} from '../BPool/BPoolBase.sol'; -import {BCoWConst} from 'contracts/BCoWConst.sol'; -import {BNum} from 'contracts/BNum.sol'; - -import {ISettlement} from 'interfaces/ISettlement.sol'; -import {MockBCoWPool} from 'test/manual-smock/MockBCoWPool.sol'; - -contract BCoWPoolBase is BPoolBase, BCoWConst, BNum { - bytes32 public appData = bytes32('appData'); - address public cowSolutionSettler = makeAddr('cowSolutionSettler'); - bytes32 public domainSeparator = bytes32(bytes2(0xf00b)); - address public vaultRelayer = makeAddr('vaultRelayer'); - address public tokenIn; - address public tokenOut; - MockBCoWPool bCoWPool; - - function setUp() public virtual override { - super.setUp(); - tokenIn = tokens[0]; - tokenOut = tokens[1]; - vm.mockCall(cowSolutionSettler, abi.encodePacked(ISettlement.domainSeparator.selector), abi.encode(domainSeparator)); - vm.mockCall(cowSolutionSettler, abi.encodePacked(ISettlement.vaultRelayer.selector), abi.encode(vaultRelayer)); - bCoWPool = new MockBCoWPool(cowSolutionSettler, appData); - } -} diff --git a/test/unit/BCoWPool/BCoWPoolBase.t.sol b/test/unit/BCoWPool/BCoWPoolBase.t.sol index 3418e59e..db328d1d 100644 --- a/test/unit/BCoWPool/BCoWPoolBase.t.sol +++ b/test/unit/BCoWPool/BCoWPoolBase.t.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.25; -import {BPoolBase} from '../BPool/BPoolBase.sol'; +import {BPoolBase} from '../BPool/BPoolBase.t.sol'; import {BCoWConst} from 'contracts/BCoWConst.sol'; import {BNum} from 'contracts/BNum.sol'; @@ -13,14 +13,10 @@ contract BCoWPoolBase is BPoolBase, BCoWConst, BNum { address public cowSolutionSettler = makeAddr('cowSolutionSettler'); bytes32 public domainSeparator = bytes32(bytes2(0xf00b)); address public vaultRelayer = makeAddr('vaultRelayer'); - address public tokenIn; - address public tokenOut; MockBCoWPool bCoWPool; function setUp() public virtual override { super.setUp(); - tokenIn = tokens[0]; - tokenOut = tokens[1]; vm.mockCall(cowSolutionSettler, abi.encodePacked(ISettlement.domainSeparator.selector), abi.encode(domainSeparator)); vm.mockCall(cowSolutionSettler, abi.encodePacked(ISettlement.vaultRelayer.selector), abi.encode(vaultRelayer)); bCoWPool = new MockBCoWPool(cowSolutionSettler, appData); diff --git a/test/unit/BCoWPool/BCoWPool_IsValidSignature.t.sol b/test/unit/BCoWPool/BCoWPool_IsValidSignature.t.sol index aeb8086b..ba1a58bb 100644 --- a/test/unit/BCoWPool/BCoWPool_IsValidSignature.t.sol +++ b/test/unit/BCoWPool/BCoWPool_IsValidSignature.t.sol @@ -6,7 +6,7 @@ import {IERC20} from '@cowprotocol/interfaces/IERC20.sol'; import {GPv2Order} from '@cowprotocol/libraries/GPv2Order.sol'; import {IERC1271} from '@openzeppelin/contracts/interfaces/IERC1271.sol'; -import {BCoWPoolBase} from './BCoWPoolBase.sol'; +import {BCoWPoolBase} from './BCoWPoolBase.t.sol'; import {IBCoWPool} from 'interfaces/IBCoWPool.sol'; contract BCoWPoolIsValidSignature is BCoWPoolBase { diff --git a/test/unit/BCoWPool/BCoWPool_Verify.t.sol b/test/unit/BCoWPool/BCoWPool_Verify.t.sol index 3af73f50..fce924be 100644 --- a/test/unit/BCoWPool/BCoWPool_Verify.t.sol +++ b/test/unit/BCoWPool/BCoWPool_Verify.t.sol @@ -10,6 +10,8 @@ import {IBPool} from 'interfaces/IBPool.sol'; contract BCoWPoolVerify is BCoWPoolBase { // Valid scenario: + address public tokenIn; + address public tokenOut; uint256 public tokenAmountIn = 1e18; uint256 public tokenInBalance = 100e18; uint256 public tokenOutBalance = 80e18; @@ -22,6 +24,8 @@ contract BCoWPoolVerify is BCoWPoolBase { function setUp() public virtual override { super.setUp(); + tokenIn = tokens[0]; + tokenOut = tokens[1]; bCoWPool.set__tokens(tokens); bCoWPool.set__records(tokenIn, IBPool.Record({bound: true, index: 0, denorm: tokenInWeight})); bCoWPool.set__records(tokenOut, IBPool.Record({bound: true, index: 1, denorm: tokenOutWeight})); diff --git a/test/unit/BPool/BPool.t.sol b/test/unit/BPool/BPool.t.sol index 0a8264da..a14f3d9f 100644 --- a/test/unit/BPool/BPool.t.sol +++ b/test/unit/BPool/BPool.t.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.25; -import {BPoolBase} from './BPoolBase.sol'; +import {BPoolBase} from './BPoolBase.t.sol'; import {IERC20Errors} from '@openzeppelin/contracts/interfaces/draft-IERC6093.sol'; import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; diff --git a/test/unit/BPool/BPoolBase.sol b/test/unit/BPool/BPoolBase.t.sol similarity index 100% rename from test/unit/BPool/BPoolBase.sol rename to test/unit/BPool/BPoolBase.t.sol diff --git a/test/unit/BPool/BPool_Bind.t.sol b/test/unit/BPool/BPool_Bind.t.sol index 12fb07a4..8e8c0bc5 100644 --- a/test/unit/BPool/BPool_Bind.t.sol +++ b/test/unit/BPool/BPool_Bind.t.sol @@ -1,27 +1,29 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.25; -import {BPoolBase} from './BPoolBase.sol'; +import {BPoolBase} from './BPoolBase.t.sol'; import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; import {IBPool} from 'interfaces/IBPool.sol'; contract BPoolBind is BPoolBase { + address public token; uint256 public tokenBindBalance = 100e18; uint256 public tokenWeight = 1e18; uint256 public totalWeight = 10e18; function setUp() public virtual override { super.setUp(); + token = tokens[0]; - vm.mockCall(tokens[0], abi.encodePacked(IERC20.transferFrom.selector), abi.encode()); - vm.mockCall(tokens[0], abi.encodePacked(IERC20.transfer.selector), abi.encode()); + vm.mockCall(token, abi.encodePacked(IERC20.transferFrom.selector), abi.encode()); + vm.mockCall(token, abi.encodePacked(IERC20.transfer.selector), abi.encode()); } function test_RevertWhen_ReentrancyLockIsSet() external { bPool.call__setLock(_MUTEX_TAKEN); vm.expectRevert(IBPool.BPool_Reentrancy.selector); // it should revert - bPool.bind(tokens[0], tokenBindBalance, tokenWeight); + bPool.bind(token, tokenBindBalance, tokenWeight); } function test_RevertWhen_CallerIsNOTController(address _caller) external { @@ -29,53 +31,53 @@ contract BPoolBind is BPoolBase { vm.assume(_caller != address(this)); vm.prank(_caller); vm.expectRevert(IBPool.BPool_CallerIsNotController.selector); - bPool.bind(tokens[0], tokenBindBalance, tokenWeight); + bPool.bind(token, tokenBindBalance, tokenWeight); } function test_RevertWhen_TokenIsAlreadyBound() external { - bPool.set__records(tokens[0], IBPool.Record({bound: true, index: 0, denorm: tokenWeight})); + bPool.set__records(token, IBPool.Record({bound: true, index: 0, denorm: tokenWeight})); // it should revert vm.expectRevert(IBPool.BPool_TokenAlreadyBound.selector); - bPool.bind(tokens[0], tokenBindBalance, tokenWeight); + bPool.bind(token, tokenBindBalance, tokenWeight); } function test_RevertWhen_PoolIsFinalized() external { bPool.set__finalized(true); // it should revert vm.expectRevert(IBPool.BPool_PoolIsFinalized.selector); - bPool.bind(tokens[0], tokenBindBalance, tokenWeight); + bPool.bind(token, tokenBindBalance, tokenWeight); } function test_RevertWhen_MAX_BOUND_TOKENSTokensAreAlreadyBound() external { _setRandomTokens(MAX_BOUND_TOKENS); // it should revert vm.expectRevert(IBPool.BPool_TokensAboveMaximum.selector); - bPool.bind(tokens[0], tokenBindBalance, tokenWeight); + bPool.bind(token, tokenBindBalance, tokenWeight); } function test_RevertWhen_TokenWeightIsTooLow() external { // it should revert vm.expectRevert(IBPool.BPool_WeightBelowMinimum.selector); - bPool.bind(tokens[0], tokenBindBalance, MIN_WEIGHT - 1); + bPool.bind(token, tokenBindBalance, MIN_WEIGHT - 1); } function test_RevertWhen_TokenWeightIsTooHigh() external { // it should revert vm.expectRevert(IBPool.BPool_WeightAboveMaximum.selector); - bPool.bind(tokens[0], tokenBindBalance, MAX_WEIGHT + 1); + bPool.bind(token, tokenBindBalance, MAX_WEIGHT + 1); } function test_RevertWhen_TooLittleBalanceIsProvided() external { // it should revert vm.expectRevert(IBPool.BPool_BalanceBelowMinimum.selector); - bPool.bind(tokens[0], MIN_BALANCE - 1, tokenWeight); + bPool.bind(token, MIN_BALANCE - 1, tokenWeight); } function test_RevertWhen_WeightSumExceedsMAX_TOTAL_WEIGHT() external { bPool.set__totalWeight(2 * MAX_TOTAL_WEIGHT / 3); // it should revert vm.expectRevert(IBPool.BPool_TotalWeightAboveMaximum.selector); - bPool.bind(tokens[0], tokenBindBalance, MAX_TOTAL_WEIGHT / 2); + bPool.bind(token, tokenBindBalance, MAX_TOTAL_WEIGHT / 2); } function test_WhenTokenCanBeBound(uint256 _existingTokens) external { @@ -84,24 +86,24 @@ contract BPoolBind is BPoolBase { bPool.set__totalWeight(totalWeight); // it calls _pullUnderlying - bPool.expectCall__pullUnderlying(tokens[0], address(this), tokenBindBalance); + bPool.expectCall__pullUnderlying(token, address(this), tokenBindBalance); // it sets the reentrancy lock bPool.expectCall__setLock(_MUTEX_TAKEN); // it emits LOG_CALL event vm.expectEmit(); - bytes memory _data = abi.encodeWithSelector(IBPool.bind.selector, tokens[0], tokenBindBalance, tokenWeight); + bytes memory _data = abi.encodeWithSelector(IBPool.bind.selector, token, tokenBindBalance, tokenWeight); emit IBPool.LOG_CALL(IBPool.bind.selector, address(this), _data); - bPool.bind(tokens[0], tokenBindBalance, tokenWeight); + bPool.bind(token, tokenBindBalance, tokenWeight); // it clears the reentrancy lock assertEq(bPool.call__getLock(), _MUTEX_FREE); // it adds token to the tokens array - assertEq(bPool.call__tokens()[_existingTokens], tokens[0]); + assertEq(bPool.call__tokens()[_existingTokens], token); // it sets the token record - assertEq(bPool.call__records(tokens[0]).bound, true); - assertEq(bPool.call__records(tokens[0]).denorm, tokenWeight); - assertEq(bPool.call__records(tokens[0]).index, _existingTokens); + assertEq(bPool.call__records(token).bound, true); + assertEq(bPool.call__records(token).denorm, tokenWeight); + assertEq(bPool.call__records(token).index, _existingTokens); // it sets total weight assertEq(bPool.call__totalWeight(), totalWeight + tokenWeight); } diff --git a/test/unit/BPool/BPool_ExitPool.t.sol b/test/unit/BPool/BPool_ExitPool.t.sol index 11259ba6..143fe7ba 100644 --- a/test/unit/BPool/BPool_ExitPool.t.sol +++ b/test/unit/BPool/BPool_ExitPool.t.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.25; -import {BPoolBase} from './BPoolBase.sol'; +import {BPoolBase} from './BPoolBase.t.sol'; import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; diff --git a/test/unit/BPool/BPool_ExitswapExternAmountOut.t.sol b/test/unit/BPool/BPool_ExitswapExternAmountOut.t.sol index 74c64e0d..48f7aeb7 100644 --- a/test/unit/BPool/BPool_ExitswapExternAmountOut.t.sol +++ b/test/unit/BPool/BPool_ExitswapExternAmountOut.t.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.25; -import {BPoolBase} from './BPoolBase.sol'; +import {BPoolBase} from './BPoolBase.t.sol'; import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; diff --git a/test/unit/BPool/BPool_ExitswapPoolAmountIn.t.sol b/test/unit/BPool/BPool_ExitswapPoolAmountIn.t.sol index c9ecdff2..e856630c 100644 --- a/test/unit/BPool/BPool_ExitswapPoolAmountIn.t.sol +++ b/test/unit/BPool/BPool_ExitswapPoolAmountIn.t.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.25; -import {BPoolBase} from './BPoolBase.sol'; +import {BPoolBase} from './BPoolBase.t.sol'; import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; diff --git a/test/unit/BPool/BPool_JoinPool.t.sol b/test/unit/BPool/BPool_JoinPool.t.sol index eeb01efe..1b954745 100644 --- a/test/unit/BPool/BPool_JoinPool.t.sol +++ b/test/unit/BPool/BPool_JoinPool.t.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.25; -import {BPoolBase} from './BPoolBase.sol'; +import {BPoolBase} from './BPoolBase.t.sol'; import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; import {BNum} from 'contracts/BNum.sol'; diff --git a/test/unit/BPool/BPool_JoinswapExternAmountIn.t.sol b/test/unit/BPool/BPool_JoinswapExternAmountIn.t.sol index afa67556..1ac173e2 100644 --- a/test/unit/BPool/BPool_JoinswapExternAmountIn.t.sol +++ b/test/unit/BPool/BPool_JoinswapExternAmountIn.t.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.25; -import {BPoolBase} from './BPoolBase.sol'; +import {BPoolBase} from './BPoolBase.t.sol'; import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; import {BNum} from 'contracts/BNum.sol'; diff --git a/test/unit/BPool/BPool_JoinswapPoolAmountOut.t.sol b/test/unit/BPool/BPool_JoinswapPoolAmountOut.t.sol index 78ef1fbb..7707fdb0 100644 --- a/test/unit/BPool/BPool_JoinswapPoolAmountOut.t.sol +++ b/test/unit/BPool/BPool_JoinswapPoolAmountOut.t.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.25; -import {BPoolBase} from './BPoolBase.sol'; +import {BPoolBase} from './BPoolBase.t.sol'; import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; import {BNum} from 'contracts/BNum.sol'; diff --git a/test/unit/BPool/BPool_SwapExactAmountIn.t.sol b/test/unit/BPool/BPool_SwapExactAmountIn.t.sol index 9537bf7f..51af608d 100644 --- a/test/unit/BPool/BPool_SwapExactAmountIn.t.sol +++ b/test/unit/BPool/BPool_SwapExactAmountIn.t.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.25; -import {BPoolBase} from './BPoolBase.sol'; +import {BPoolBase} from './BPoolBase.t.sol'; import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; import {BNum} from 'contracts/BNum.sol'; diff --git a/test/unit/BPool/BPool_SwapExactAmountOut.t.sol b/test/unit/BPool/BPool_SwapExactAmountOut.t.sol index 0792a6d2..1f379b5b 100644 --- a/test/unit/BPool/BPool_SwapExactAmountOut.t.sol +++ b/test/unit/BPool/BPool_SwapExactAmountOut.t.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.25; -import {BPoolBase} from './BPoolBase.sol'; +import {BPoolBase} from './BPoolBase.t.sol'; import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; import {BNum} from 'contracts/BNum.sol'; diff --git a/test/unit/BPool/BPool_Unbind.t.sol b/test/unit/BPool/BPool_Unbind.t.sol index 7034e85c..5bcbece2 100644 --- a/test/unit/BPool/BPool_Unbind.t.sol +++ b/test/unit/BPool/BPool_Unbind.t.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.25; -import {BPoolBase} from './BPoolBase.sol'; +import {BPoolBase} from './BPoolBase.t.sol'; import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; import {IBPool} from 'interfaces/IBPool.sol'; From 0fe77504cfbcd8ffea4577b880e7d2143334348e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wei=C3=9Fer=20Hase?= Date: Thu, 25 Jul 2024 20:25:23 +0200 Subject: [PATCH 48/48] chore: v1.0.0 deployment addresses (#175) * chore: adding deployment addresses * fix: addressing comments in PR * fix: ouch --- .env.example | 3 +++ README.md | 12 +++++++++--- foundry.toml | 2 ++ package.json | 3 +++ script/Params.s.sol | 2 +- script/Registry.s.sol | 19 ++++++++----------- script/Script.s.sol | 2 +- 7 files changed, 27 insertions(+), 16 deletions(-) diff --git a/.env.example b/.env.example index fe295b73..ed757eec 100644 --- a/.env.example +++ b/.env.example @@ -1,6 +1,9 @@ MAINNET_RPC= MAINNET_DEPLOYER_PK= +GNOSIS_RPC= +GNOSIS_DEPLOYER_PK= + SEPOLIA_RPC= SEPOLIA_DEPLOYER_PK= diff --git a/README.md b/README.md index 1245d520..74567edd 100644 --- a/README.md +++ b/README.md @@ -60,8 +60,14 @@ yarn test # run the tests # Deployments Ethereum Mainnet: - - BCoWFactory: (0x21Cd97D70f8475DF3d62917880aF9f41D9a9dCeF)[https://etherscan.io/address/0x21Cd97D70f8475DF3d62917880aF9f41D9a9dCeF#code] + - BCoWFactory: [0x5AC134DAC7070eFeE8b1C5e3fD0B353922ceD843](https://etherscan.io/address/0x5AC134DAC7070eFeE8b1C5e3fD0B353922ceD843) + - BCoWHelper: [0x703Bd8115E6F21a37BB5Df97f78614ca72Ad7624](https://etherscan.io/address/0x703Bd8115E6F21a37BB5Df97f78614ca72Ad7624) Ethereum Sepolia: - - BCoWFactory: (0xe8587525430fFC9193831e1113a672f3133C1B8A)[https://sepolia.etherscan.io/address/0xe8587525430fFC9193831e1113a672f3133C1B8A#code] - - BCoWPool: (0xFe1ce255D68B3Bff95E71DDef1c8fc55459aaCd7)[https://sepolia.etherscan.io/address/0xFe1ce255D68B3Bff95E71DDef1c8fc55459aaCd7#code] \ No newline at end of file + - BCoWFactory: [0xf3916A8567DdC51a60208B35AC542F5226f46773](https://sepolia.etherscan.io/address/0xf3916A8567DdC51a60208B35AC542F5226f46773) + - BCoWHelper: [0x55DDf396886C85e443E0B5A8E42CAA3939E4Cf50](https://sepolia.etherscan.io/address/0x55DDf396886C85e443E0B5A8E42CAA3939E4Cf50) + - BCoWPool: [0x60048091401F27117C3DFb8136c1ec550D949B12](https://sepolia.etherscan.io/address/0x60048091401F27117C3DFb8136c1ec550D949B12) + + Gnosis Mainnet: + - BCoWFactory: [0xaD0447be7BDC80cf2e6DA20B13599E5dc859b667](https://gnosisscan.io/address/0xaD0447be7BDC80cf2e6DA20B13599E5dc859b667) + - BCoWHelper: [0x21Ac2E4115429EcE4b5FE79409fCC48EB6315Ccc](https://gnosisscan.io/address/0x21Ac2E4115429EcE4b5FE79409fCC48EB6315Ccc) \ No newline at end of file diff --git a/foundry.toml b/foundry.toml index 9fd206be..1a2224e0 100644 --- a/foundry.toml +++ b/foundry.toml @@ -37,8 +37,10 @@ max_test_rejects = 2_500_000 [rpc_endpoints] mainnet = "${MAINNET_RPC}" +gnosis = "${GNOSIS_RPC}" sepolia = "${SEPOLIA_RPC}" [etherscan] mainnet = { key = "${ETHERSCAN_API_KEY}", chain = "mainnet" } +gnosis = { key = "${ETHERSCAN_API_KEY}", chain = "gnosis" } sepolia = { key = "${ETHERSCAN_API_KEY}", chain = "sepolia" } diff --git a/package.json b/package.json index a704b568..54e6f4d6 100644 --- a/package.json +++ b/package.json @@ -16,8 +16,10 @@ "build": "forge build", "build:optimized": "FOUNDRY_PROFILE=optimized forge build", "coverage": "forge coverage --match-path 'test/unit/**'", + "deploy:bcowfactory:gnosis": "forge script DeployBCoWFactory -vvvvv --rpc-url $GNOSIS_RPC --broadcast --chain gnosis --private-key $GNOSIS_DEPLOYER_PK --verify", "deploy:bcowfactory:mainnet": "forge script DeployBCoWFactory -vvvvv --rpc-url $MAINNET_RPC --broadcast --chain mainnet --private-key $MAINNET_DEPLOYER_PK --verify", "deploy:bcowfactory:testnet": "forge script DeployBCoWFactory -vvvvv --rpc-url $SEPOLIA_RPC --broadcast --chain sepolia --private-key $SEPOLIA_DEPLOYER_PK --verify", + "deploy:bfactory:gnosis": "forge script DeployBFactory -vvvvv --rpc-url $GNOSIS_RPC --broadcast --chain gnosis --private-key $GNOSIS_DEPLOYER_PK --verify", "deploy:bfactory:mainnet": "forge script DeployBFactory -vvvvv --rpc-url $MAINNET_RPC --broadcast --chain mainnet --private-key $MAINNET_DEPLOYER_PK --verify", "deploy:bfactory:testnet": "forge script DeployBFactory -vvvvv --rpc-url $SEPOLIA_RPC --broadcast --chain sepolia --private-key $SEPOLIA_DEPLOYER_PK --verify", "lint:bulloak": "find test/unit -name '*.tree' | xargs bulloak check", @@ -25,6 +27,7 @@ "lint:fix": "solhint --fix 'src/**/*.sol' 'test/**/*.sol' 'script/**/*.sol' && sort-package-json && forge fmt", "lint:natspec": "npx @defi-wonderland/natspec-smells --config natspec-smells.config.js", "prepare": "husky install", + "script:gnosis": "forge script MainnetScript -vvvvv --rpc-url $GNOSIS_RPC --broadcast --chain gnosis --private-key $GNOSIS_DEPLOYER_PK --verify", "script:mainnet": "forge script MainnetScript -vvvvv --rpc-url $MAINNET_RPC --broadcast --chain mainnet --private-key $MAINNET_DEPLOYER_PK --verify", "script:testnet": "forge script TestnetScript -vvvvv --rpc-url $SEPOLIA_RPC --broadcast --chain sepolia --private-key $SEPOLIA_DEPLOYER_PK --verify", "smock": "smock-foundry --contracts src/contracts", diff --git a/script/Params.s.sol b/script/Params.s.sol index 330f60e1..f60e1750 100644 --- a/script/Params.s.sol +++ b/script/Params.s.sol @@ -33,7 +33,7 @@ abstract contract Params { BCoWFactoryDeploymentParams internal _bCoWFactoryDeploymentParams; constructor(uint256 chainId) { - if (chainId == 1 || chainId == 11_155_111) { + if (chainId == 1 || chainId == 100 || chainId == 11_155_111) { // Ethereum Mainnet & Ethereum Sepolia [Testnet] _bFactoryDeploymentParams = BFactoryDeploymentParams({bDao: _B_DAO}); _bCoWFactoryDeploymentParams = BCoWFactoryDeploymentParams({settlement: _GPV2_SETTLEMENT, appData: _APP_DATA}); diff --git a/script/Registry.s.sol b/script/Registry.s.sol index 16334c1c..7360f051 100644 --- a/script/Registry.s.sol +++ b/script/Registry.s.sol @@ -3,33 +3,30 @@ pragma solidity 0.8.25; import {BCoWFactory} from 'contracts/BCoWFactory.sol'; import {BCoWHelper} from 'contracts/BCoWHelper.sol'; -import {BFactory} from 'contracts/BFactory.sol'; import {Params} from 'script/Params.s.sol'; /// @notice Registry of deployed contracts abstract contract Registry is Params { - /// @notice Balancer Pool Factory - BFactory public bFactory; /// @notice Balancer CoW Pool Factory BCoWFactory public bCoWFactory; /// @notice Balancer CoW Helper BCoWHelper public bCoWHelper; constructor(uint256 chainId) Params(chainId) { - // TODO: redeploy if (chainId == 1) { // Ethereum Mainnet - bFactory = BFactory(0xaD0447be7BDC80cf2e6DA20B13599E5dc859b667); - bCoWFactory = BCoWFactory(0x21Cd97D70f8475DF3d62917880aF9f41D9a9dCeF); - bCoWHelper = BCoWHelper(0xE50481D88f147B8b4aaCdf9a1B7b7bA44F87823f); + bCoWFactory = BCoWFactory(0x5AC134DAC7070eFeE8b1C5e3fD0B353922ceD843); + bCoWHelper = BCoWHelper(0x703Bd8115E6F21a37BB5Df97f78614ca72Ad7624); + } else if (chainId == 100) { + // Gnosis Mainnet + bCoWFactory = BCoWFactory(0xaD0447be7BDC80cf2e6DA20B13599E5dc859b667); + bCoWHelper = BCoWHelper(0x21Ac2E4115429EcE4b5FE79409fCC48EB6315Ccc); } else if (chainId == 11_155_111) { // Ethereum Sepolia [Testnet] - bFactory = BFactory(0x2bfA24B26B85DD812b2C69E3B1cb4C85C886C8E2); - bCoWFactory = BCoWFactory(0xe8587525430fFC9193831e1113a672f3133C1B8A); - bCoWHelper = BCoWHelper(0x0fd365F9Ed185512536E7dbfc7a8DaE43cD3CA09); + bCoWFactory = BCoWFactory(0xf3916A8567DdC51a60208B35AC542F5226f46773); + bCoWHelper = BCoWHelper(0x55DDf396886C85e443E0B5A8E42CAA3939E4Cf50); } else { - // TODO: add Gnosis chain revert('Registry: unknown chain ID'); } } diff --git a/script/Script.s.sol b/script/Script.s.sol index d21a54cc..6d86aa49 100644 --- a/script/Script.s.sol +++ b/script/Script.s.sol @@ -16,7 +16,7 @@ abstract contract BaseScript is Registry, Script { /// @notice This script will be executed by `yarn script:mainnet` contract MainnetScript is BaseScript { function run() public { - assert(block.chainid == 1); + assert(block.chainid == 1 || block.chainid == 100); vm.startBroadcast(); // script logic here