Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: adding bcowpool verify btt tests #155

Merged
merged 21 commits into from
Jul 22, 2024
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
143 changes: 1 addition & 142 deletions test/unit/BCoWPool.t.sol
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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();
Expand Down
28 changes: 28 additions & 0 deletions test/unit/BCoWPool/BCoWPoolBase.sol
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

watch out this contract and BPoolBase don't have the .t.sol ending, is this intended?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, the idea being files where testcases are defined end in .t.sol, while all others don't, I tried to be consistent with test/utils/Utils.sol not ending in .t.sol

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmmm i understand that the coverage for example skips all .t.sol tests alltogether, so perhaps we should name them all as .t.sol

Original file line number Diff line number Diff line change
@@ -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];
0xteddybear marked this conversation as resolved.
Show resolved Hide resolved
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);
}
}
127 changes: 127 additions & 0 deletions test/unit/BCoWPool/BCoWPool_Verify.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
// 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.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 correctOrder;

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)));

correctOrder = GPv2Order.Data({
0xteddybear marked this conversation as resolved.
Show resolved Hide resolved
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 {
correctOrder.buyToken = IERC20(makeAddr('unknown token'));
// it should revert
vm.expectRevert(IBPool.BPool_TokenNotBound.selector);
bCoWPool.verify(correctOrder);
}

function test_RevertWhen_SellTokenIsNotBound() external {
correctOrder.sellToken = IERC20(makeAddr('unknown token'));
// it should revert
vm.expectRevert(IBPool.BPool_TokenNotBound.selector);
bCoWPool.verify(correctOrder);
}

function test_RevertWhen_OrderReceiverIsNotRECEIVER_SAME_AS_OWNERFlag() external {
correctOrder.receiver = makeAddr('somebodyElse');
// it should revert
vm.expectRevert(IBCoWPool.BCoWPool_ReceiverIsNotBCoWPool.selector);
bCoWPool.verify(correctOrder);
}

function test_RevertWhen_OrderIsValidForMoreThanMAX_ORDER_DURATION(uint256 _timeOffset) external {
_timeOffset = bound(_timeOffset, MAX_ORDER_DURATION + 1, type(uint32).max - block.timestamp);
correctOrder.validTo = uint32(block.timestamp + _timeOffset);
// it should revert
vm.expectRevert(IBCoWPool.BCoWPool_OrderValidityTooLong.selector);
bCoWPool.verify(correctOrder);
}

function test_RevertWhen_FeeAmountIsNotZero(uint256 _fee) external {
_fee = bound(_fee, 1, type(uint256).max);
correctOrder.feeAmount = _fee;
// it should revert
vm.expectRevert(IBCoWPool.BCoWPool_FeeMustBeZero.selector);
bCoWPool.verify(correctOrder);
}

function test_RevertWhen_OrderKindIsNotKIND_SELL(bytes32 _orderKind) external {
vm.assume(_orderKind != GPv2Order.KIND_SELL);
correctOrder.kind = _orderKind;
// it should revert
vm.expectRevert(IBCoWPool.BCoWPool_InvalidOperation.selector);
bCoWPool.verify(correctOrder);
}

function test_RevertWhen_TokenBalanceMarkerForBuyTokenIsNotDirectERC20Balances(bytes32 _balanceKind) external {
vm.assume(_balanceKind != GPv2Order.BALANCE_ERC20);
correctOrder.buyTokenBalance = _balanceKind;
// it should revert
vm.expectRevert(IBCoWPool.BCoWPool_InvalidBalanceMarker.selector);
bCoWPool.verify(correctOrder);
}

function test_RevertWhen_TokenBalanceMarkerForSellTokenIsNotDirectERC20Balances(bytes32 _balanceKind) external {
vm.assume(_balanceKind != GPv2Order.BALANCE_ERC20);
correctOrder.sellTokenBalance = _balanceKind;
// it should revert
vm.expectRevert(IBCoWPool.BCoWPool_InvalidBalanceMarker.selector);
bCoWPool.verify(correctOrder);
}

function test_RevertWhen_OrderBuyAmountExceedsMAX_IN_RATIO(uint256 _buyAmount) external {
_buyAmount = bound(_buyAmount, bmul(tokenInBalance, MAX_IN_RATIO) + 1, type(uint256).max);
correctOrder.buyAmount = _buyAmount;
// it should revert
vm.expectRevert(IBPool.BPool_TokenAmountInAboveMaxRatio.selector);
bCoWPool.verify(correctOrder);
}

function test_RevertWhen_ComputedTokenAmountOutIsLessThanOrderMinSellAmount() external {
correctOrder.sellAmount += 1;
// it should revert
vm.expectRevert(IBPool.BPool_TokenAmountOutBelowMinOut.selector);
bCoWPool.verify(correctOrder);
}

function test_WhenPreconditionsAreMet() external {
// it should return
bCoWPool.verify(correctOrder);
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

since we checked for order sellAmount + 1, how would you feel here fuzzing from 0 -> calculatedOut to not lose the branch where sellAmount is lesser than calculated (that is the same as when it's ==, but we're not showing that behaviour)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

seems reasonable, I don't think it's a different branch since it covers the exact same code. As far as I understood, we can use a single testcase for different values if they don't cover different lines/branches of code.

CCing @drgorillamd in case he considers we should use a separate testcase

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This makes sense to me, you have a doubt about it? I'm missing something?

}
23 changes: 23 additions & 0 deletions test/unit/BCoWPool/BCoWPool_Verify.tree
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
BCoWPool::Verify
├── when buyToken is not bound
│ └── it should revert
├── when sellToken is not bound
│ └── it should revert
├── when order receiver is not RECEIVER_SAME_AS_OWNER flag
0xteddybear marked this conversation as resolved.
Show resolved Hide resolved
│ └── it should revert
├── when order is valid for more than MAX_ORDER_DURATION
0xteddybear marked this conversation as resolved.
Show resolved Hide resolved
│ └── it should revert
├── when fee amount is not zero
│ └── it should revert
├── when order kind is not KIND_SELL
│ └── it should revert
├── when TokenBalance marker for buy token is not direct ERC20 balances
0xteddybear marked this conversation as resolved.
Show resolved Hide resolved
│ └── it should revert
├── when TokenBalance marker for sell token is not direct ERC20 balances
│ └── it should revert
├── when order buy amount exceeds MAX_IN_RATIO
0xteddybear marked this conversation as resolved.
Show resolved Hide resolved
│ └── it should revert
├── when computed token amount out is less than order min sell amount
0xteddybear marked this conversation as resolved.
Show resolved Hide resolved
│ └── it should revert
└── when preconditions are met
└── it should return
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i feel like we're forgetting about querying the balances, are we not?

Loading
Loading