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 01/14] 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 02/14] 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 03/14] 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 04/14] 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 05/14] 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 06/14] 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 07/14] 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 08/14] 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 09/14] 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 10/14] 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 11/14] 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 12/14] 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 13/14] 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 14/14] 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