Skip to content

Commit

Permalink
feat: adding bcowpool verify btt tests (#155)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
0xteddybear authored Jul 22, 2024
1 parent cb92f9d commit 6c13de8
Show file tree
Hide file tree
Showing 15 changed files with 212 additions and 290 deletions.
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.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];
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);
}
}
132 changes: 132 additions & 0 deletions test/unit/BCoWPool/BCoWPool_Verify.t.sol
Original file line number Diff line number Diff line change
@@ -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);
}
}
24 changes: 24 additions & 0 deletions test/unit/BCoWPool/BCoWPool_Verify.tree
Original file line number Diff line number Diff line change
@@ -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
Loading

0 comments on commit 6c13de8

Please sign in to comment.