From 699d9d711534465772201063346f87b5772f64bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wei=C3=9Fer=20Hase?= Date: Thu, 27 Jun 2024 15:33:33 +0200 Subject: [PATCH 01/41] feat: non-weighted tradeable order test --- test/integration/BCowPool.t.sol | 56 +++++++++++++++++++++++++++++++++ test/integration/PoolSwap.t.sol | 4 +-- 2 files changed, 58 insertions(+), 2 deletions(-) diff --git a/test/integration/BCowPool.t.sol b/test/integration/BCowPool.t.sol index cd34318b..5e49ab41 100644 --- a/test/integration/BCowPool.t.sol +++ b/test/integration/BCowPool.t.sol @@ -15,6 +15,11 @@ import {IBCoWPool} from 'interfaces/IBCoWPool.sol'; import {IBFactory} from 'interfaces/IBFactory.sol'; import {ISettlement} from 'interfaces/ISettlement.sol'; +import {BPool} from 'contracts/BPool.sol'; +// TODO: add interface of BMath to IBPool +import {GetTradeableOrder} from 'contracts/GetTradeableOrder.sol'; +import {console} from 'forge-std/console.sol'; + contract BCowPoolIntegrationTest is PoolSwapIntegrationTest, BCoWConst { using GPv2Order for GPv2Order.Data; @@ -28,6 +33,57 @@ contract BCowPoolIntegrationTest is PoolSwapIntegrationTest, BCoWConst { return new BCoWFactory(address(settlement), APP_DATA); } + function testGetTradeableOrder() public { + uint256 initialSpotPrice = BPool(address(pool)).calcSpotPrice({ + tokenBalanceIn: weth.balanceOf(address(pool)), // 1 WETH + tokenWeightIn: 1e18, + tokenBalanceOut: dai.balanceOf(address(pool)), // 4000 DAI + tokenWeightOut: 1e18, + swapFee: 0 + }); + console.log(initialSpotPrice); + // 0.00025 (1 DAI in WETH terms) + + GPv2Order.Data memory order = GetTradeableOrder.getTradeableOrder( + GetTradeableOrder.GetTradeableOrderParams({ + pool: address(pool), + token0: weth, + token1: dai, + priceNumerator: 1e18, + priceDenominator: 8000e18, // reach 0.000125 + appData: APP_DATA + }) + ); + + console.log(order.sellAmount); + // sell 0.25 WETH + console.log(order.buyAmount); + // buy 1500 DAI + + // execute the transfers + vm.startPrank(settlement.vaultRelayer()); + order.sellToken.transferFrom(address(pool), address(this), order.sellAmount); + deal(address(order.buyToken), address(settlement.vaultRelayer()), order.buyAmount); + order.buyToken.transfer(address(pool), order.buyAmount); + vm.stopPrank(); + + // final balances: + // 0.75 WETH + // 5500 DAI + + uint256 finalSpotPrice = BPool(address(pool)).calcSpotPrice({ + tokenBalanceIn: weth.balanceOf(address(pool)), + tokenWeightIn: 1e18, + tokenBalanceOut: dai.balanceOf(address(pool)), + tokenWeightOut: 1e18, + swapFee: 0 + }); + + console.log(finalSpotPrice); + // 0.000136 (1 DAI in WETH terms) + // doesn't reach 0.000125 but doesn't overshoot + } + function _makeSwap() internal override { uint32 latestValidTimestamp = uint32(block.timestamp) + MAX_ORDER_DURATION - 1; diff --git a/test/integration/PoolSwap.t.sol b/test/integration/PoolSwap.t.sol index 625069a5..513fb8f4 100644 --- a/test/integration/PoolSwap.t.sol +++ b/test/integration/PoolSwap.t.sol @@ -64,8 +64,8 @@ abstract contract PoolSwapIntegrationTest is Test, GasSnapshot { dai.approve(address(pool), type(uint256).max); weth.approve(address(pool), type(uint256).max); - pool.bind(address(dai), DAI_LP_AMOUNT, 8e18); // 80% weight - pool.bind(address(weth), WETH_LP_AMOUNT, 2e18); // 20% weight + pool.bind(address(dai), DAI_LP_AMOUNT, 1e18); // 80% weight + pool.bind(address(weth), WETH_LP_AMOUNT, 1e18); // 20% weight // finalize pool.finalize(); } From 271e00a0761335bdddd2d027440a478789fae7ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wei=C3=9Fer=20Hase?= Date: Thu, 27 Jun 2024 15:33:57 +0200 Subject: [PATCH 02/41] fix: adding GetTradeableOrder library --- src/contracts/GetTradeableOrder.sol | 89 +++++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 src/contracts/GetTradeableOrder.sol diff --git a/src/contracts/GetTradeableOrder.sol b/src/contracts/GetTradeableOrder.sol new file mode 100644 index 00000000..eb6fc68b --- /dev/null +++ b/src/contracts/GetTradeableOrder.sol @@ -0,0 +1,89 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity ^0.8.24; + +import {IERC20} from '@cowprotocol/interfaces/IERC20.sol'; +import {GPv2Order} from '@cowprotocol/libraries/GPv2Order.sol'; +import {Math} from '@openzeppelin/contracts/utils/math/Math.sol'; + +library GetTradeableOrder { + /// @dev Avoid stack too deep errors with `getTradeableOrder`. + struct GetTradeableOrderParams { + address pool; + IERC20 token0; + IERC20 token1; + uint256 priceNumerator; + uint256 priceDenominator; + bytes32 appData; + } + + /// @notice The largest possible duration of any AMM order, starting from the current block timestamp. + uint32 public constant MAX_ORDER_DURATION = 5 * 60; + + function getTradeableOrder(GetTradeableOrderParams memory params) + internal + view + returns (GPv2Order.Data memory order_) + { + (uint256 selfReserve0, uint256 selfReserve1) = + (params.token0.balanceOf(params.pool), params.token1.balanceOf(params.pool)); + + IERC20 sellToken; + IERC20 buyToken; + uint256 sellAmount; + uint256 buyAmount; + // Note on rounding: we want to round down the sell amount and up the + // buy amount. This is because the math for the order makes it lie + // precisely on the AMM curve, and a rounding error to the other way + // could cause a valid order to become invalid. + // Note on the if condition: it guarantees that sellAmount is positive + // in the corresponding branch (it would be negative in the other). This + // excludes rounding errors: in this case, the function could revert but + // the amounts involved would be just a few atoms, so we accept that no + // order will be available. + // Note on the order price: The buy amount is not optimal for the AMM + // given the sell amount. This is intended because we want to force + // solvers to maximize the surplus for this order with the price that + // isn't the AMM best price. + uint256 selfReserve0TimesPriceDenominator = selfReserve0 * params.priceDenominator; + uint256 selfReserve1TimesPriceNumerator = selfReserve1 * params.priceNumerator; + uint256 tradedAmountToken0; + if (selfReserve1TimesPriceNumerator < selfReserve0TimesPriceDenominator) { + sellToken = params.token0; + buyToken = params.token1; + sellAmount = selfReserve0 / 2 - Math.ceilDiv(selfReserve1TimesPriceNumerator, 2 * params.priceDenominator); + buyAmount = Math.mulDiv( + sellAmount, + selfReserve1TimesPriceNumerator + (params.priceDenominator * sellAmount), + params.priceNumerator * selfReserve0, + Math.Rounding.Ceil + ); + tradedAmountToken0 = sellAmount; + } else { + sellToken = params.token1; + buyToken = params.token0; + sellAmount = selfReserve1 / 2 - Math.ceilDiv(selfReserve0TimesPriceDenominator, 2 * params.priceNumerator); + buyAmount = Math.mulDiv( + sellAmount, + selfReserve0TimesPriceDenominator + (params.priceNumerator * sellAmount), + params.priceDenominator * selfReserve1, + Math.Rounding.Ceil + ); + tradedAmountToken0 = buyAmount; + } + + order_ = GPv2Order.Data( + sellToken, + buyToken, + GPv2Order.RECEIVER_SAME_AS_OWNER, + sellAmount, + buyAmount, + uint32(block.timestamp) + MAX_ORDER_DURATION, + params.appData, + 0, + GPv2Order.KIND_SELL, + true, + GPv2Order.BALANCE_ERC20, + GPv2Order.BALANCE_ERC20 + ); + } +} From 48888e33c5202d2a9c53d42969c50721ea22d8ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wei=C3=9Fer=20Hase?= Date: Thu, 27 Jun 2024 16:11:59 +0200 Subject: [PATCH 03/41] feat: adding weights to the math --- src/contracts/GetTradeableOrder.sol | 23 ++++++++++------------- test/integration/BCowPool.t.sol | 21 ++++++++++++--------- test/integration/PoolSwap.t.sol | 4 ++-- 3 files changed, 24 insertions(+), 24 deletions(-) diff --git a/src/contracts/GetTradeableOrder.sol b/src/contracts/GetTradeableOrder.sol index eb6fc68b..a355f6be 100644 --- a/src/contracts/GetTradeableOrder.sol +++ b/src/contracts/GetTradeableOrder.sol @@ -11,6 +11,8 @@ library GetTradeableOrder { address pool; IERC20 token0; IERC20 token1; + uint256 token0Weight; + uint256 token1Weight; uint256 priceNumerator; uint256 priceDenominator; bytes32 appData; @@ -27,23 +29,14 @@ library GetTradeableOrder { (uint256 selfReserve0, uint256 selfReserve1) = (params.token0.balanceOf(params.pool), params.token1.balanceOf(params.pool)); + selfReserve0 = Math.mulDiv(selfReserve0, 1e18, params.token0Weight); + selfReserve1 = Math.mulDiv(selfReserve1, 1e18, params.token1Weight); + IERC20 sellToken; IERC20 buyToken; uint256 sellAmount; uint256 buyAmount; - // Note on rounding: we want to round down the sell amount and up the - // buy amount. This is because the math for the order makes it lie - // precisely on the AMM curve, and a rounding error to the other way - // could cause a valid order to become invalid. - // Note on the if condition: it guarantees that sellAmount is positive - // in the corresponding branch (it would be negative in the other). This - // excludes rounding errors: in this case, the function could revert but - // the amounts involved would be just a few atoms, so we accept that no - // order will be available. - // Note on the order price: The buy amount is not optimal for the AMM - // given the sell amount. This is intended because we want to force - // solvers to maximize the surplus for this order with the price that - // isn't the AMM best price. + uint256 selfReserve0TimesPriceDenominator = selfReserve0 * params.priceDenominator; uint256 selfReserve1TimesPriceNumerator = selfReserve1 * params.priceNumerator; uint256 tradedAmountToken0; @@ -58,7 +51,11 @@ library GetTradeableOrder { Math.Rounding.Ceil ); tradedAmountToken0 = sellAmount; + + sellAmount = Math.mulDiv(sellAmount, params.token0Weight, 1e18); + buyAmount = Math.mulDiv(buyAmount, params.token1Weight, 1e18); } else { + revert('avoiding this branch in PoC'); sellToken = params.token1; buyToken = params.token0; sellAmount = selfReserve1 / 2 - Math.ceilDiv(selfReserve0TimesPriceDenominator, 2 * params.priceNumerator); diff --git a/test/integration/BCowPool.t.sol b/test/integration/BCowPool.t.sol index 5e49ab41..15e11682 100644 --- a/test/integration/BCowPool.t.sol +++ b/test/integration/BCowPool.t.sol @@ -36,21 +36,24 @@ contract BCowPoolIntegrationTest is PoolSwapIntegrationTest, BCoWConst { function testGetTradeableOrder() public { uint256 initialSpotPrice = BPool(address(pool)).calcSpotPrice({ tokenBalanceIn: weth.balanceOf(address(pool)), // 1 WETH - tokenWeightIn: 1e18, + tokenWeightIn: 2e18, tokenBalanceOut: dai.balanceOf(address(pool)), // 4000 DAI - tokenWeightOut: 1e18, + tokenWeightOut: 8e18, swapFee: 0 }); console.log(initialSpotPrice); - // 0.00025 (1 DAI in WETH terms) + // 0.0001 (1 DAI in WETH terms) + // notice the weight distribution of 80% DAI and 20% WETH GPv2Order.Data memory order = GetTradeableOrder.getTradeableOrder( GetTradeableOrder.GetTradeableOrderParams({ pool: address(pool), token0: weth, token1: dai, + token0Weight: 2e18, + token1Weight: 8e18, priceNumerator: 1e18, - priceDenominator: 8000e18, // reach 0.000125 + priceDenominator: 2000e18, // reach 0.0005 appData: APP_DATA }) ); @@ -67,21 +70,21 @@ contract BCowPoolIntegrationTest is PoolSwapIntegrationTest, BCoWConst { order.buyToken.transfer(address(pool), order.buyAmount); vm.stopPrank(); - // final balances: + // final balances // 0.75 WETH // 5500 DAI uint256 finalSpotPrice = BPool(address(pool)).calcSpotPrice({ tokenBalanceIn: weth.balanceOf(address(pool)), - tokenWeightIn: 1e18, + tokenWeightIn: 2e18, tokenBalanceOut: dai.balanceOf(address(pool)), - tokenWeightOut: 1e18, + tokenWeightOut: 8e18, swapFee: 0 }); console.log(finalSpotPrice); - // 0.000136 (1 DAI in WETH terms) - // doesn't reach 0.000125 but doesn't overshoot + // 0.00054 (1 DAI in WETH terms) + // doesn't reach 0.0005 but doesn't overshoot } function _makeSwap() internal override { diff --git a/test/integration/PoolSwap.t.sol b/test/integration/PoolSwap.t.sol index 513fb8f4..625069a5 100644 --- a/test/integration/PoolSwap.t.sol +++ b/test/integration/PoolSwap.t.sol @@ -64,8 +64,8 @@ abstract contract PoolSwapIntegrationTest is Test, GasSnapshot { dai.approve(address(pool), type(uint256).max); weth.approve(address(pool), type(uint256).max); - pool.bind(address(dai), DAI_LP_AMOUNT, 1e18); // 80% weight - pool.bind(address(weth), WETH_LP_AMOUNT, 1e18); // 20% weight + pool.bind(address(dai), DAI_LP_AMOUNT, 8e18); // 80% weight + pool.bind(address(weth), WETH_LP_AMOUNT, 2e18); // 20% weight // finalize pool.finalize(); } From 28565f7f755c93d6e1df8b48c96ee934fcb1553d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wei=C3=9Fer=20Hase?= Date: Fri, 28 Jun 2024 00:52:22 +0200 Subject: [PATCH 04/41] feat: variable weights --- test/integration/BCowPool.t.sol | 12 ++++++------ test/integration/PoolSwap.t.sol | 3 +++ 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/test/integration/BCowPool.t.sol b/test/integration/BCowPool.t.sol index 15e11682..c9058c55 100644 --- a/test/integration/BCowPool.t.sol +++ b/test/integration/BCowPool.t.sol @@ -36,9 +36,9 @@ contract BCowPoolIntegrationTest is PoolSwapIntegrationTest, BCoWConst { function testGetTradeableOrder() public { uint256 initialSpotPrice = BPool(address(pool)).calcSpotPrice({ tokenBalanceIn: weth.balanceOf(address(pool)), // 1 WETH - tokenWeightIn: 2e18, + tokenWeightIn: WETH_WEIGHT, tokenBalanceOut: dai.balanceOf(address(pool)), // 4000 DAI - tokenWeightOut: 8e18, + tokenWeightOut: DAI_WEIGHT, swapFee: 0 }); console.log(initialSpotPrice); @@ -50,8 +50,8 @@ contract BCowPoolIntegrationTest is PoolSwapIntegrationTest, BCoWConst { pool: address(pool), token0: weth, token1: dai, - token0Weight: 2e18, - token1Weight: 8e18, + token0Weight: WETH_WEIGHT, + token1Weight: DAI_WEIGHT, priceNumerator: 1e18, priceDenominator: 2000e18, // reach 0.0005 appData: APP_DATA @@ -76,9 +76,9 @@ contract BCowPoolIntegrationTest is PoolSwapIntegrationTest, BCoWConst { uint256 finalSpotPrice = BPool(address(pool)).calcSpotPrice({ tokenBalanceIn: weth.balanceOf(address(pool)), - tokenWeightIn: 2e18, + tokenWeightIn: WETH_WEIGHT, tokenBalanceOut: dai.balanceOf(address(pool)), - tokenWeightOut: 8e18, + tokenWeightOut: DAI_WEIGHT, swapFee: 0 }); diff --git a/test/integration/PoolSwap.t.sol b/test/integration/PoolSwap.t.sol index 625069a5..7d6c4b7d 100644 --- a/test/integration/PoolSwap.t.sol +++ b/test/integration/PoolSwap.t.sol @@ -43,6 +43,9 @@ abstract contract PoolSwapIntegrationTest is Test, GasSnapshot { uint256 public constant DAI_AMOUNT = HUNDRED_UNITS; uint256 public constant WETH_AMOUNT_INVERSE = ONE_TENTH_UNIT; + uint256 public constant DAI_WEIGHT = 8e18; + uint256 public constant WETH_WEIGHT = 2e18; + // swap amounts OUT // NOTE: amounts OUT are hardcoded from test result uint256 public constant WETH_OUT_AMOUNT = 94_049_266_814_811_022; // 0.094 ETH From 1a44f5e7823a9e50a27de08adcc6910494a04005 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wei=C3=9Fer=20Hase?= Date: Mon, 8 Jul 2024 12:59:56 +0200 Subject: [PATCH 05/41] feat: adding test for helper --- src/contracts/BCoWPool.sol | 2 +- src/contracts/GetTradeableOrder.sol | 31 +++++++++++++++++++---------- test/integration/BCowPool.t.sol | 4 ++-- 3 files changed, 24 insertions(+), 13 deletions(-) diff --git a/src/contracts/BCoWPool.sol b/src/contracts/BCoWPool.sol index d5af3a28..819dbc69 100644 --- a/src/contracts/BCoWPool.sol +++ b/src/contracts/BCoWPool.sol @@ -99,7 +99,7 @@ contract BCoWPool is IERC1271, IBCoWPool, BPool, BCoWConst { if (order.receiver != GPv2Order.RECEIVER_SAME_AS_OWNER) { revert BCoWPool_ReceiverIsNotBCoWPool(); } - if (order.validTo >= block.timestamp + MAX_ORDER_DURATION) { + if (order.validTo > block.timestamp + MAX_ORDER_DURATION) { revert BCoWPool_OrderValidityTooLong(); } if (order.feeAmount != 0) { diff --git a/src/contracts/GetTradeableOrder.sol b/src/contracts/GetTradeableOrder.sol index a355f6be..d839b970 100644 --- a/src/contracts/GetTradeableOrder.sol +++ b/src/contracts/GetTradeableOrder.sol @@ -11,9 +11,15 @@ library GetTradeableOrder { address pool; IERC20 token0; IERC20 token1; - uint256 token0Weight; - uint256 token1Weight; + /// @dev The numerator of the price, expressed in amount of token1 per + /// amount of token0. For example, if token0 is DAI and the price is + /// 1 WETH (token1) for 3000 DAI, then this could be 1 (and the + /// denominator would be 3000). uint256 priceNumerator; + /// @dev The denominator of the price, expressed in amount of token1 per + /// amount of token0. For example, if token0 is DAI and the price is + /// 1 WETH (token1) for 3000 DAI, then this could be 3000 (and the + /// denominator would be 1). uint256 priceDenominator; bytes32 appData; } @@ -29,14 +35,23 @@ library GetTradeableOrder { (uint256 selfReserve0, uint256 selfReserve1) = (params.token0.balanceOf(params.pool), params.token1.balanceOf(params.pool)); - selfReserve0 = Math.mulDiv(selfReserve0, 1e18, params.token0Weight); - selfReserve1 = Math.mulDiv(selfReserve1, 1e18, params.token1Weight); - IERC20 sellToken; IERC20 buyToken; uint256 sellAmount; uint256 buyAmount; - + // Note on rounding: we want to round down the sell amount and up the + // buy amount. This is because the math for the order makes it lie + // precisely on the AMM curve, and a rounding error to the other way + // could cause a valid order to become invalid. + // Note on the if condition: it guarantees that sellAmount is positive + // in the corresponding branch (it would be negative in the other). This + // excludes rounding errors: in this case, the function could revert but + // the amounts involved would be just a few atoms, so we accept that no + // order will be available. + // Note on the order price: The buy amount is not optimal for the AMM + // given the sell amount. This is intended because we want to force + // solvers to maximize the surplus for this order with the price that + // isn't the AMM best price. uint256 selfReserve0TimesPriceDenominator = selfReserve0 * params.priceDenominator; uint256 selfReserve1TimesPriceNumerator = selfReserve1 * params.priceNumerator; uint256 tradedAmountToken0; @@ -51,11 +66,7 @@ library GetTradeableOrder { Math.Rounding.Ceil ); tradedAmountToken0 = sellAmount; - - sellAmount = Math.mulDiv(sellAmount, params.token0Weight, 1e18); - buyAmount = Math.mulDiv(buyAmount, params.token1Weight, 1e18); } else { - revert('avoiding this branch in PoC'); sellToken = params.token1; buyToken = params.token0; sellAmount = selfReserve1 / 2 - Math.ceilDiv(selfReserve0TimesPriceDenominator, 2 * params.priceNumerator); diff --git a/test/integration/BCowPool.t.sol b/test/integration/BCowPool.t.sol index c9058c55..a080b95e 100644 --- a/test/integration/BCowPool.t.sol +++ b/test/integration/BCowPool.t.sol @@ -50,8 +50,8 @@ contract BCowPoolIntegrationTest is PoolSwapIntegrationTest, BCoWConst { pool: address(pool), token0: weth, token1: dai, - token0Weight: WETH_WEIGHT, - token1Weight: DAI_WEIGHT, + // token0Weight: WETH_WEIGHT, + // token1Weight: DAI_WEIGHT, priceNumerator: 1e18, priceDenominator: 2000e18, // reach 0.0005 appData: APP_DATA From 8bf22ddc0fd3f2cd53cf12cb6d7ee497b16a0fa9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wei=C3=9Fer=20Hase?= Date: Mon, 8 Jul 2024 14:39:42 +0200 Subject: [PATCH 06/41] feat: adding support for weights --- src/contracts/BCoWHelper.sol | 76 +++++++++++ src/contracts/BConst.sol | 2 +- src/contracts/GetTradeableOrder.sol | 20 ++- src/interfaces/ICOWAMMPoolHelper.sol | 87 ++++++++++++ test/integration/BCoWHelper.t.sol | 192 +++++++++++++++++++++++++++ test/integration/BCowPool.t.sol | 4 +- 6 files changed, 375 insertions(+), 6 deletions(-) create mode 100644 src/contracts/BCoWHelper.sol create mode 100644 src/interfaces/ICOWAMMPoolHelper.sol create mode 100644 test/integration/BCoWHelper.t.sol diff --git a/src/contracts/BCoWHelper.sol b/src/contracts/BCoWHelper.sol new file mode 100644 index 00000000..fb104fc8 --- /dev/null +++ b/src/contracts/BCoWHelper.sol @@ -0,0 +1,76 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.25; + +import {IERC20} from '@cowprotocol/interfaces/IERC20.sol'; +import {GetTradeableOrder} from 'contracts/GetTradeableOrder.sol'; +import {IBCoWFactory} from 'interfaces/IBCoWFactory.sol'; +import {IBCoWPool} from 'interfaces/IBCoWPool.sol'; +import {IBFactory} from 'interfaces/IBFactory.sol'; +import {GPv2Interaction, GPv2Order, ICOWAMMPoolHelper} from 'interfaces/ICOWAMMPoolHelper.sol'; + +/** + * @title BCoWHelper + * @notice Helper contract that allows to trade on CoW Swap Protocol. + * @dev This contract supports only 2-token pools. + */ +contract BCoWHelper is ICOWAMMPoolHelper { + using GPv2Order for GPv2Order.Data; + + address public factory; + bytes32 public immutable APP_DATA; + + constructor(address factory_) { + factory = factory_; + APP_DATA = IBCoWFactory(factory_).APP_DATA(); + } + + function tokens(address pool) public view returns (address[] memory tokens_) { + if (!IBFactory(factory).isBPool(pool)) revert PoolDoesNotExist(); + // NOTE: reverts in case pool is not finalized + tokens_ = IBCoWPool(pool).getFinalTokens(); + if (tokens_.length != 2) revert PoolDoesNotExist(); + + return tokens_; + } + + 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]), + weightToken0: IBCoWPool(pool).getNormalizedWeight(tokens_[0]), + weightToken1: IBCoWPool(pool).getNormalizedWeight(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); + + bytes32 domainSeparator = IBCoWPool(pool).SOLUTION_SETTLER_DOMAIN_SEPARATOR(); + + preInteractions = new GPv2Interaction.Data[](1); + preInteractions[0] = GPv2Interaction.Data({ + target: pool, + value: 0, + callData: abi.encodeWithSelector(IBCoWPool.commit.selector, order_.hash(domainSeparator)) + }); + } +} diff --git a/src/contracts/BConst.sol b/src/contracts/BConst.sol index c6f8cd30..0f3546e2 100644 --- a/src/contracts/BConst.sol +++ b/src/contracts/BConst.sol @@ -22,7 +22,7 @@ contract BConst { uint256 public constant EXIT_FEE = 0; /// @notice The minimum weight that a token can have. - uint256 public constant MIN_WEIGHT = BONE; + uint256 public constant MIN_WEIGHT = 0.01e18; /// @notice The maximum weight that a token can have. uint256 public constant MAX_WEIGHT = BONE * 50; /// @notice The maximum sum of weights of all tokens in a pool. diff --git a/src/contracts/GetTradeableOrder.sol b/src/contracts/GetTradeableOrder.sol index d839b970..364803ad 100644 --- a/src/contracts/GetTradeableOrder.sol +++ b/src/contracts/GetTradeableOrder.sol @@ -5,12 +5,16 @@ import {IERC20} from '@cowprotocol/interfaces/IERC20.sol'; import {GPv2Order} from '@cowprotocol/libraries/GPv2Order.sol'; import {Math} from '@openzeppelin/contracts/utils/math/Math.sol'; +import {console} from 'forge-std/console.sol'; + library GetTradeableOrder { /// @dev Avoid stack too deep errors with `getTradeableOrder`. struct GetTradeableOrderParams { address pool; IERC20 token0; IERC20 token1; + uint256 weightToken0; + uint256 weightToken1; /// @dev The numerator of the price, expressed in amount of token1 per /// amount of token0. For example, if token0 is DAI and the price is /// 1 WETH (token1) for 3000 DAI, then this could be 1 (and the @@ -35,6 +39,10 @@ library GetTradeableOrder { (uint256 selfReserve0, uint256 selfReserve1) = (params.token0.balanceOf(params.pool), params.token1.balanceOf(params.pool)); + console.log('selfReserve0', selfReserve0); + console.log('selfReserve1', selfReserve1); + + IERC20 sellToken; IERC20 buyToken; uint256 sellAmount; @@ -54,8 +62,12 @@ library GetTradeableOrder { // isn't the AMM best price. uint256 selfReserve0TimesPriceDenominator = selfReserve0 * params.priceDenominator; uint256 selfReserve1TimesPriceNumerator = selfReserve1 * params.priceNumerator; - uint256 tradedAmountToken0; + + console.log('selfReserve0TimesPriceDenominator', selfReserve0TimesPriceDenominator); + console.log('selfReserve1TimesPriceNumerator', selfReserve1TimesPriceNumerator); + if (selfReserve1TimesPriceNumerator < selfReserve0TimesPriceDenominator) { + console.log('sell token 0, buy token1'); sellToken = params.token0; buyToken = params.token1; sellAmount = selfReserve0 / 2 - Math.ceilDiv(selfReserve1TimesPriceNumerator, 2 * params.priceDenominator); @@ -65,8 +77,8 @@ library GetTradeableOrder { params.priceNumerator * selfReserve0, Math.Rounding.Ceil ); - tradedAmountToken0 = sellAmount; } else { + console.log('sell token 1, buy token0'); sellToken = params.token1; buyToken = params.token0; sellAmount = selfReserve1 / 2 - Math.ceilDiv(selfReserve0TimesPriceDenominator, 2 * params.priceNumerator); @@ -76,9 +88,11 @@ library GetTradeableOrder { params.priceDenominator * selfReserve1, Math.Rounding.Ceil ); - tradedAmountToken0 = buyAmount; } + console.log('sellAmount', sellAmount); + console.log('buyAmount', buyAmount); + order_ = GPv2Order.Data( sellToken, buyToken, diff --git a/src/interfaces/ICOWAMMPoolHelper.sol b/src/interfaces/ICOWAMMPoolHelper.sol new file mode 100644 index 00000000..a6d11382 --- /dev/null +++ b/src/interfaces/ICOWAMMPoolHelper.sol @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity ^0.8.24; + +import {GPv2Interaction} from 'cowprotocol/contracts/libraries/GPv2Interaction.sol'; +import {GPv2Order} from 'cowprotocol/contracts/libraries/GPv2Order.sol'; + +/** + * @notice Pool-specific helper interface for AMM's operating in CoW Protocol. + */ +interface ICOWAMMPoolHelper { + /** + * All functions that take `pool` as an argument MUST revert with this error + * if the `pool` does not exist. + * @dev Indexers monitoring CoW AMM pools MAY use this as a signal to purge the + * pool from their index. + */ + error PoolDoesNotExist(); + + /** + * All functions that take `pool` as an argument MUST revert with this error + * in the event that the pool is paused (ONLY applicable if the pool is pausable). + * @dev Indexers monitoring CoW AMM pools SHOULD use this as a signal to retain + * the pool in the index with back-off on polling for orders. + */ + error PoolIsPaused(); + + /** + * All functions that take `pool` as an argument MUST revert with this error + * in the event that the pool is closed (ONLY applicable if the pool can be + * closed). + * @dev Indexers monitoring CoW AMM pools MAY use this as a signal to purge the + * pool from their index. + */ + error PoolIsClosed(); + + /** + * Returned by the `order` function if there is no order matching the supplied + * parameters. + */ + error NoOrder(); + + /** + * AMM Pool helpers MUST return the factory target for indexing of CoW AMM pools. + */ + function factory() external view returns (address); + + /** + * AMM Pool helpers MUST return all tokens that may be traded on this pool. + * The order of the tokens is expected to be consistent and must be the same + * as that used for the input price vector in the `order` function. + */ + function tokens(address pool) external view returns (address[] memory); + + /** + * AMM Pool helpers MUST provide a method for returning the canonical order + * required to satisfy the pool's invariants, given a pricing vector. + * @dev Reverts with `NoOrder` if the `pool` has no canonical order matching the + * given price vector. + * @param pool to calculate the order / signature for + * @param prices supplied for determining the order, assumed to be in the + * same order as returned from `tokens(pool)`. Tokens prices are + * expressed relative to each other: for example, if tokens[0] is + * WETH, tokens[2] is DAI, and the price is 1 WETH per 3000 DAI, then + * a valid price vector is [3000, *, 1, ...]. If tokens[1] is another + * stablecoin with 18 decimals, then a valid price vector could be + * [3000, 3000, 1, ...]. + * This price vector is compatible with the price vector used in a + * call to `settle`, assuming the traded token array is in the same + * order as in `tokens(pool)`. + * @return order The CoW Protocol JIT order + * @return preInteractions The array array for any **PRE** interactions (empty if none) + * @return postInteractions The array array for any **POST** interactions (empty if none) + * @return sig The ERC-1271 signature for the order + */ + 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 + ); +} diff --git a/test/integration/BCoWHelper.t.sol b/test/integration/BCoWHelper.t.sol new file mode 100644 index 00000000..1ff95af8 --- /dev/null +++ b/test/integration/BCoWHelper.t.sol @@ -0,0 +1,192 @@ +// 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 {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 ConstantProductHelperForkedTest 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 DAI = IERC20(0x6B175474E89094C44Da98b954EedeAC495271d0F); + IERC20 private USDC = IERC20(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48); + IERC20 private WETH = IERC20(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); + + uint256 constant VALID_AMOUNT = 1e6; + uint256 constant TEN_PERCENT = 0.1 ether; + + 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, 2 * VALID_AMOUNT); + deal(address(WETH), lp, 2 * VALID_AMOUNT); + + vm.startPrank(lp); + weightedPool = ammFactory.newBPool(); + basicPool = ammFactory.newBPool(); + + DAI.approve(address(weightedPool), type(uint256).max); + WETH.approve(address(weightedPool), type(uint256).max); + weightedPool.bind(address(DAI), VALID_AMOUNT, 0.16e18); // 80% weight + weightedPool.bind(address(WETH), VALID_AMOUNT, 0.04e18); // 20% weight + + DAI.approve(address(basicPool), type(uint256).max); + WETH.approve(address(basicPool), type(uint256).max); + basicPool.bind(address(DAI), VALID_AMOUNT, 1e18); // no weight + basicPool.bind(address(WETH), VALID_AMOUNT, 1e18); // no weight + + // finalize + weightedPool.finalize(); + basicPool.finalize(); + + vm.stopPrank(); + } + + // NOTE: 1 ETH = 1000e6 DAI + uint256 constant INITIAL_SPOT_PRICE = 0.001e18; + + function _testHelper(IBPool pool, uint256 ammWethInitialBalance, uint256 ammDaiInitialBalance) internal { + IERC20[] memory tokens = addressVecToIerc20Vec(helper.tokens(address(pool))); + uint256 daiIndex = 0; + uint256 wethIndex = 1; + assertEq(tokens.length, 2); + assertEq(address(tokens[daiIndex]), address(DAI)); + assertEq(address(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] = ammWethInitialBalance; + prices[wethIndex] = ammDaiInitialBalance * 95 / 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 DAI + 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, ammDaiInitialBalance * 25 / 1000, TEN_PERCENT); + // assertApproxEqRel(ammOrder.buyAmount, ammWethInitialBalance * 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: abi.encodePacked(address(pool), abi.encode(ammOrder)) + }); + + GPv2Interaction.Data[][3] memory interactions = + [new GPv2Interaction.Data[](1), new GPv2Interaction.Data[](0), new GPv2Interaction.Data[](0)]; + + interactions[0][0] = preInteractions[0]; + + // finally, settle + vm.prank(solver); + settlement.settle(tokens, prices, trades, interactions); + } + + function testBasicOrder() public { + IBCoWPool pool = IBCoWPool(address(basicPool)); + + uint256 ammWethInitialBalance = 1 ether; + uint256 ammDaiInitialBalance = 1000 ether; + + deal(address(WETH), address(pool), ammWethInitialBalance); + deal(address(DAI), address(pool), ammDaiInitialBalance); + + uint256 spotPrice = pool.getSpotPriceSansFee(address(WETH), address(DAI)); + assertEq(spotPrice, INITIAL_SPOT_PRICE); + + _testHelper(pool, ammWethInitialBalance, ammDaiInitialBalance); + + uint256 postSpotPrice = pool.getSpotPriceSansFee(address(WETH), address(DAI)); + assertEq(postSpotPrice, 1_052_631_578_947_368); + } + + // NOTE: failing test + function testWeightedOrder() public { + IBCoWPool pool = IBCoWPool(address(weightedPool)); + + uint256 ammWethInitialBalance = 1 ether; + uint256 ammDaiInitialBalance = 4000 ether; + + deal(address(WETH), address(pool), ammWethInitialBalance); + deal(address(DAI), address(pool), ammDaiInitialBalance); + + // NOTE: pool is 80-20 DAI-WETH, has 4xDAI balance than basic, same spot price + uint256 spotPrice = pool.getSpotPriceSansFee(address(WETH), address(DAI)); + assertEq(spotPrice, INITIAL_SPOT_PRICE); + + _testHelper(pool, ammWethInitialBalance, ammDaiInitialBalance); + + uint256 postSpotPrice = pool.getSpotPriceSansFee(address(WETH), address(DAI)); + assertEq(postSpotPrice, 1_052_631_578_947_368); + } + + function addressVecToIerc20Vec(address[] memory addrVec) private pure returns (IERC20[] memory ierc20vec) { + assembly { + ierc20vec := addrVec + } + } +} diff --git a/test/integration/BCowPool.t.sol b/test/integration/BCowPool.t.sol index a080b95e..5d47aa3d 100644 --- a/test/integration/BCowPool.t.sol +++ b/test/integration/BCowPool.t.sol @@ -50,8 +50,8 @@ contract BCowPoolIntegrationTest is PoolSwapIntegrationTest, BCoWConst { pool: address(pool), token0: weth, token1: dai, - // token0Weight: WETH_WEIGHT, - // token1Weight: DAI_WEIGHT, + weightToken0: WETH_WEIGHT, + weightToken1: DAI_WEIGHT, priceNumerator: 1e18, priceDenominator: 2000e18, // reach 0.0005 appData: APP_DATA From 368394352f4aa6d99a2b445debcc9ca5f9629aa5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wei=C3=9Fer=20Hase?= Date: Mon, 8 Jul 2024 14:39:55 +0200 Subject: [PATCH 07/41] fix: fmt --- src/contracts/GetTradeableOrder.sol | 1 - 1 file changed, 1 deletion(-) diff --git a/src/contracts/GetTradeableOrder.sol b/src/contracts/GetTradeableOrder.sol index 364803ad..f826c556 100644 --- a/src/contracts/GetTradeableOrder.sol +++ b/src/contracts/GetTradeableOrder.sol @@ -42,7 +42,6 @@ library GetTradeableOrder { console.log('selfReserve0', selfReserve0); console.log('selfReserve1', selfReserve1); - IERC20 sellToken; IERC20 buyToken; uint256 sellAmount; From 9422865e2a30396db2f2e185bb3da102c794978d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wei=C3=9Fer=20Hase?= Date: Mon, 8 Jul 2024 15:20:16 +0200 Subject: [PATCH 08/41] fix: sending correct price vector --- src/contracts/GetTradeableOrder.sol | 13 ------------- test/integration/BCoWHelper.t.sol | 6 +++--- test/integration/BCowPool.t.sol | 5 ----- 3 files changed, 3 insertions(+), 21 deletions(-) diff --git a/src/contracts/GetTradeableOrder.sol b/src/contracts/GetTradeableOrder.sol index f826c556..ec51bd8b 100644 --- a/src/contracts/GetTradeableOrder.sol +++ b/src/contracts/GetTradeableOrder.sol @@ -5,8 +5,6 @@ import {IERC20} from '@cowprotocol/interfaces/IERC20.sol'; import {GPv2Order} from '@cowprotocol/libraries/GPv2Order.sol'; import {Math} from '@openzeppelin/contracts/utils/math/Math.sol'; -import {console} from 'forge-std/console.sol'; - library GetTradeableOrder { /// @dev Avoid stack too deep errors with `getTradeableOrder`. struct GetTradeableOrderParams { @@ -39,9 +37,6 @@ library GetTradeableOrder { (uint256 selfReserve0, uint256 selfReserve1) = (params.token0.balanceOf(params.pool), params.token1.balanceOf(params.pool)); - console.log('selfReserve0', selfReserve0); - console.log('selfReserve1', selfReserve1); - IERC20 sellToken; IERC20 buyToken; uint256 sellAmount; @@ -62,11 +57,7 @@ library GetTradeableOrder { uint256 selfReserve0TimesPriceDenominator = selfReserve0 * params.priceDenominator; uint256 selfReserve1TimesPriceNumerator = selfReserve1 * params.priceNumerator; - console.log('selfReserve0TimesPriceDenominator', selfReserve0TimesPriceDenominator); - console.log('selfReserve1TimesPriceNumerator', selfReserve1TimesPriceNumerator); - if (selfReserve1TimesPriceNumerator < selfReserve0TimesPriceDenominator) { - console.log('sell token 0, buy token1'); sellToken = params.token0; buyToken = params.token1; sellAmount = selfReserve0 / 2 - Math.ceilDiv(selfReserve1TimesPriceNumerator, 2 * params.priceDenominator); @@ -77,7 +68,6 @@ library GetTradeableOrder { Math.Rounding.Ceil ); } else { - console.log('sell token 1, buy token0'); sellToken = params.token1; buyToken = params.token0; sellAmount = selfReserve1 / 2 - Math.ceilDiv(selfReserve0TimesPriceDenominator, 2 * params.priceNumerator); @@ -89,9 +79,6 @@ library GetTradeableOrder { ); } - console.log('sellAmount', sellAmount); - console.log('buyAmount', buyAmount); - order_ = GPv2Order.Data( sellToken, buyToken, diff --git a/test/integration/BCoWHelper.t.sol b/test/integration/BCoWHelper.t.sol index 1ff95af8..61bc4531 100644 --- a/test/integration/BCoWHelper.t.sol +++ b/test/integration/BCoWHelper.t.sol @@ -169,12 +169,12 @@ contract ConstantProductHelperForkedTest is Test { IBCoWPool pool = IBCoWPool(address(weightedPool)); uint256 ammWethInitialBalance = 1 ether; - uint256 ammDaiInitialBalance = 4000 ether; + uint256 ammDaiInitialBalance = 1000 ether; deal(address(WETH), address(pool), ammWethInitialBalance); - deal(address(DAI), address(pool), ammDaiInitialBalance); - // NOTE: pool is 80-20 DAI-WETH, has 4xDAI balance than basic, same spot price + deal(address(DAI), address(pool), 4 * ammDaiInitialBalance); + uint256 spotPrice = pool.getSpotPriceSansFee(address(WETH), address(DAI)); assertEq(spotPrice, INITIAL_SPOT_PRICE); diff --git a/test/integration/BCowPool.t.sol b/test/integration/BCowPool.t.sol index 5d47aa3d..fc718815 100644 --- a/test/integration/BCowPool.t.sol +++ b/test/integration/BCowPool.t.sol @@ -18,7 +18,6 @@ import {ISettlement} from 'interfaces/ISettlement.sol'; import {BPool} from 'contracts/BPool.sol'; // TODO: add interface of BMath to IBPool import {GetTradeableOrder} from 'contracts/GetTradeableOrder.sol'; -import {console} from 'forge-std/console.sol'; contract BCowPoolIntegrationTest is PoolSwapIntegrationTest, BCoWConst { using GPv2Order for GPv2Order.Data; @@ -41,7 +40,6 @@ contract BCowPoolIntegrationTest is PoolSwapIntegrationTest, BCoWConst { tokenWeightOut: DAI_WEIGHT, swapFee: 0 }); - console.log(initialSpotPrice); // 0.0001 (1 DAI in WETH terms) // notice the weight distribution of 80% DAI and 20% WETH @@ -58,9 +56,7 @@ contract BCowPoolIntegrationTest is PoolSwapIntegrationTest, BCoWConst { }) ); - console.log(order.sellAmount); // sell 0.25 WETH - console.log(order.buyAmount); // buy 1500 DAI // execute the transfers @@ -82,7 +78,6 @@ contract BCowPoolIntegrationTest is PoolSwapIntegrationTest, BCoWConst { swapFee: 0 }); - console.log(finalSpotPrice); // 0.00054 (1 DAI in WETH terms) // doesn't reach 0.0005 but doesn't overshoot } From be1701d93bbb23fa2da93c63d2a5d5b53df9c4ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wei=C3=9Fer=20Hase?= Date: Mon, 8 Jul 2024 15:52:57 +0200 Subject: [PATCH 09/41] fix: making helper return valid signature --- src/contracts/BCoWHelper.sol | 9 ++- src/interfaces/ICOWAMMPoolHelper.sol | 2 +- test/integration/BCoWHelper.t.sol | 92 ++++++++++++++-------------- 3 files changed, 55 insertions(+), 48 deletions(-) diff --git a/src/contracts/BCoWHelper.sol b/src/contracts/BCoWHelper.sol index fb104fc8..bd175400 100644 --- a/src/contracts/BCoWHelper.sol +++ b/src/contracts/BCoWHelper.sol @@ -64,13 +64,20 @@ contract BCoWHelper is ICOWAMMPoolHelper { order_ = GetTradeableOrder.getTradeableOrder(params); + bytes memory eip1271sig; + eip1271sig = abi.encode(order_); bytes32 domainSeparator = IBCoWPool(pool).SOLUTION_SETTLER_DOMAIN_SEPARATOR(); + bytes32 orderCommitment = order_.hash(domainSeparator); + + // 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. + sig = abi.encodePacked(pool, eip1271sig); preInteractions = new GPv2Interaction.Data[](1); preInteractions[0] = GPv2Interaction.Data({ target: pool, value: 0, - callData: abi.encodeWithSelector(IBCoWPool.commit.selector, order_.hash(domainSeparator)) + callData: abi.encodeWithSelector(IBCoWPool.commit.selector, orderCommitment) }); } } diff --git a/src/interfaces/ICOWAMMPoolHelper.sol b/src/interfaces/ICOWAMMPoolHelper.sol index a6d11382..c7b7345c 100644 --- a/src/interfaces/ICOWAMMPoolHelper.sol +++ b/src/interfaces/ICOWAMMPoolHelper.sol @@ -70,7 +70,7 @@ interface ICOWAMMPoolHelper { * @return order The CoW Protocol JIT order * @return preInteractions The array array for any **PRE** interactions (empty if none) * @return postInteractions The array array for any **POST** interactions (empty if none) - * @return sig The ERC-1271 signature for the order + * @return sig A valid CoW-Protocol signature for the resulting order using the ERC-1271 signature scheme. */ function order( address pool, diff --git a/test/integration/BCoWHelper.t.sol b/test/integration/BCoWHelper.t.sol index 61bc4531..24e2a439 100644 --- a/test/integration/BCoWHelper.t.sol +++ b/test/integration/BCoWHelper.t.sol @@ -78,7 +78,51 @@ contract ConstantProductHelperForkedTest is Test { // NOTE: 1 ETH = 1000e6 DAI uint256 constant INITIAL_SPOT_PRICE = 0.001e18; - function _testHelper(IBPool pool, uint256 ammWethInitialBalance, uint256 ammDaiInitialBalance) internal { + function testBasicOrder() public { + IBCoWPool pool = IBCoWPool(address(basicPool)); + + uint256 ammWethInitialBalance = 1 ether; + uint256 ammDaiInitialBalance = 1000 ether; + + deal(address(WETH), address(pool), ammWethInitialBalance); + deal(address(DAI), address(pool), ammDaiInitialBalance); + + uint256 spotPrice = pool.getSpotPriceSansFee(address(WETH), address(DAI)); + assertEq(spotPrice, INITIAL_SPOT_PRICE); + + _executeHelperOrder(pool, ammWethInitialBalance, ammDaiInitialBalance); + + uint256 postSpotPrice = pool.getSpotPriceSansFee(address(WETH), address(DAI)); + assertEq(postSpotPrice, 1_052_631_578_947_368); + } + + // NOTE: failing test + function testWeightedOrder() public { + IBCoWPool pool = IBCoWPool(address(weightedPool)); + + uint256 ammWethInitialBalance = 1 ether; + uint256 ammDaiInitialBalance = 1000 ether; + + deal(address(WETH), address(pool), ammWethInitialBalance); + // NOTE: pool is 80-20 DAI-WETH, has 4xDAI balance than basic, same spot price + deal(address(DAI), address(pool), 4 * ammDaiInitialBalance); + + uint256 spotPrice = pool.getSpotPriceSansFee(address(WETH), address(DAI)); + assertEq(spotPrice, INITIAL_SPOT_PRICE); + + _executeHelperOrder(pool, ammWethInitialBalance, ammDaiInitialBalance); + + uint256 postSpotPrice = pool.getSpotPriceSansFee(address(WETH), address(DAI)); + assertEq(postSpotPrice, 1_052_631_578_947_368); + } + + function addressVecToIerc20Vec(address[] memory addrVec) private pure returns (IERC20[] memory ierc20vec) { + assembly { + ierc20vec := addrVec + } + } + + function _executeHelperOrder(IBPool pool, uint256 ammWethInitialBalance, uint256 ammDaiInitialBalance) internal { IERC20[] memory tokens = addressVecToIerc20Vec(helper.tokens(address(pool))); uint256 daiIndex = 0; uint256 wethIndex = 1; @@ -133,7 +177,7 @@ contract ConstantProductHelperForkedTest is Test { feeAmount: ammOrder.feeAmount, flags: GPv2TradeEncoder.encodeFlags(ammOrder, GPv2Signing.Scheme.Eip1271), executedAmount: ammOrder.sellAmount, - signature: abi.encodePacked(address(pool), abi.encode(ammOrder)) + signature: sig }); GPv2Interaction.Data[][3] memory interactions = @@ -145,48 +189,4 @@ contract ConstantProductHelperForkedTest is Test { vm.prank(solver); settlement.settle(tokens, prices, trades, interactions); } - - function testBasicOrder() public { - IBCoWPool pool = IBCoWPool(address(basicPool)); - - uint256 ammWethInitialBalance = 1 ether; - uint256 ammDaiInitialBalance = 1000 ether; - - deal(address(WETH), address(pool), ammWethInitialBalance); - deal(address(DAI), address(pool), ammDaiInitialBalance); - - uint256 spotPrice = pool.getSpotPriceSansFee(address(WETH), address(DAI)); - assertEq(spotPrice, INITIAL_SPOT_PRICE); - - _testHelper(pool, ammWethInitialBalance, ammDaiInitialBalance); - - uint256 postSpotPrice = pool.getSpotPriceSansFee(address(WETH), address(DAI)); - assertEq(postSpotPrice, 1_052_631_578_947_368); - } - - // NOTE: failing test - function testWeightedOrder() public { - IBCoWPool pool = IBCoWPool(address(weightedPool)); - - uint256 ammWethInitialBalance = 1 ether; - uint256 ammDaiInitialBalance = 1000 ether; - - deal(address(WETH), address(pool), ammWethInitialBalance); - // NOTE: pool is 80-20 DAI-WETH, has 4xDAI balance than basic, same spot price - deal(address(DAI), address(pool), 4 * ammDaiInitialBalance); - - uint256 spotPrice = pool.getSpotPriceSansFee(address(WETH), address(DAI)); - assertEq(spotPrice, INITIAL_SPOT_PRICE); - - _testHelper(pool, ammWethInitialBalance, ammDaiInitialBalance); - - uint256 postSpotPrice = pool.getSpotPriceSansFee(address(WETH), address(DAI)); - assertEq(postSpotPrice, 1_052_631_578_947_368); - } - - function addressVecToIerc20Vec(address[] memory addrVec) private pure returns (IERC20[] memory ierc20vec) { - assembly { - ierc20vec := addrVec - } - } } From 441d117e169f3a93bdacfd07bb13dee6d1d39caf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wei=C3=9Fer=20Hase?= Date: Tue, 9 Jul 2024 12:16:41 +0200 Subject: [PATCH 10/41] feat: deprecated weights in helper --- src/contracts/BCoWHelper.sol | 7 +++++-- src/contracts/GetTradeableOrder.sol | 2 -- test/integration/BCoWHelper.t.sol | 19 ++++++++++++------- 3 files changed, 17 insertions(+), 11 deletions(-) diff --git a/src/contracts/BCoWHelper.sol b/src/contracts/BCoWHelper.sol index dcb1f43b..5374ae0a 100644 --- a/src/contracts/BCoWHelper.sol +++ b/src/contracts/BCoWHelper.sol @@ -30,6 +30,11 @@ contract BCoWHelper is ICOWAMMPoolHelper { tokens_ = IBCoWPool(pool).getFinalTokens(); if (tokens_.length != 2) revert PoolDoesNotExist(); + // NOTE: reverts in case pool is not supported (non-equal weights) + if (IBCoWPool(pool).getNormalizedWeight(tokens_[0]) != IBCoWPool(pool).getNormalizedWeight(tokens_[1])) { + revert PoolDoesNotExist(); + } + return tokens_; } @@ -54,8 +59,6 @@ contract BCoWHelper is ICOWAMMPoolHelper { pool: pool, token0: IERC20(tokens_[0]), token1: IERC20(tokens_[1]), - weightToken0: IBCoWPool(pool).getNormalizedWeight(tokens_[0]), - weightToken1: IBCoWPool(pool).getNormalizedWeight(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. diff --git a/src/contracts/GetTradeableOrder.sol b/src/contracts/GetTradeableOrder.sol index ec51bd8b..022e693d 100644 --- a/src/contracts/GetTradeableOrder.sol +++ b/src/contracts/GetTradeableOrder.sol @@ -11,8 +11,6 @@ library GetTradeableOrder { address pool; IERC20 token0; IERC20 token1; - uint256 weightToken0; - uint256 weightToken1; /// @dev The numerator of the price, expressed in amount of token1 per /// amount of token0. For example, if token0 is DAI and the price is /// 1 WETH (token1) for 3000 DAI, then this could be 1 (and the diff --git a/test/integration/BCoWHelper.t.sol b/test/integration/BCoWHelper.t.sol index 24e2a439..fd3ec8dd 100644 --- a/test/integration/BCoWHelper.t.sol +++ b/test/integration/BCoWHelper.t.sol @@ -7,6 +7,8 @@ import {IERC20} from '@cowprotocol/interfaces/IERC20.sol'; import {IBCoWPool} from 'interfaces/IBCoWPool.sol'; import {IBPool} from 'interfaces/IBPool.sol'; + +import {ICOWAMMPoolHelper} from 'interfaces/ICOWAMMPoolHelper.sol'; import {ISettlement} from 'interfaces/ISettlement.sol'; import {GPv2Interaction} from '@cowprotocol/libraries/GPv2Interaction.sol'; @@ -60,13 +62,13 @@ contract ConstantProductHelperForkedTest is Test { DAI.approve(address(weightedPool), type(uint256).max); WETH.approve(address(weightedPool), type(uint256).max); - weightedPool.bind(address(DAI), VALID_AMOUNT, 0.16e18); // 80% weight - weightedPool.bind(address(WETH), VALID_AMOUNT, 0.04e18); // 20% weight + weightedPool.bind(address(DAI), VALID_AMOUNT, 8e18); // 80% weight + weightedPool.bind(address(WETH), VALID_AMOUNT, 2e18); // 20% weight DAI.approve(address(basicPool), type(uint256).max); WETH.approve(address(basicPool), type(uint256).max); - basicPool.bind(address(DAI), VALID_AMOUNT, 1e18); // no weight - basicPool.bind(address(WETH), VALID_AMOUNT, 1e18); // no weight + basicPool.bind(address(DAI), VALID_AMOUNT, 4.2e18); // no weight + basicPool.bind(address(WETH), VALID_AMOUNT, 4.2e18); // no weight // finalize weightedPool.finalize(); @@ -110,10 +112,13 @@ contract ConstantProductHelperForkedTest is Test { uint256 spotPrice = pool.getSpotPriceSansFee(address(WETH), address(DAI)); assertEq(spotPrice, INITIAL_SPOT_PRICE); - _executeHelperOrder(pool, ammWethInitialBalance, ammDaiInitialBalance); + vm.expectRevert(ICOWAMMPoolHelper.PoolDoesNotExist.selector); + helper.order(address(pool), new uint256[](2)); - uint256 postSpotPrice = pool.getSpotPriceSansFee(address(WETH), address(DAI)); - assertEq(postSpotPrice, 1_052_631_578_947_368); + // NOTE: not supported + // _executeHelperOrder(pool, ammWethInitialBalance, ammDaiInitialBalance); + // uint256 postSpotPrice = pool.getSpotPriceSansFee(address(WETH), address(DAI)); + // assertEq(postSpotPrice, 1_052_631_578_947_368); } function addressVecToIerc20Vec(address[] memory addrVec) private pure returns (IERC20[] memory ierc20vec) { From 5630797f5a9af6ef16854eaf55edb7bf310e9c39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wei=C3=9Fer=20Hase?= Date: Tue, 9 Jul 2024 12:29:12 +0200 Subject: [PATCH 11/41] feat: updated buyAmount as per aaf44a3 --- src/contracts/GetTradeableOrder.sol | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/src/contracts/GetTradeableOrder.sol b/src/contracts/GetTradeableOrder.sol index 022e693d..e30e63e5 100644 --- a/src/contracts/GetTradeableOrder.sol +++ b/src/contracts/GetTradeableOrder.sol @@ -54,27 +54,16 @@ library GetTradeableOrder { // isn't the AMM best price. uint256 selfReserve0TimesPriceDenominator = selfReserve0 * params.priceDenominator; uint256 selfReserve1TimesPriceNumerator = selfReserve1 * params.priceNumerator; - if (selfReserve1TimesPriceNumerator < selfReserve0TimesPriceDenominator) { sellToken = params.token0; buyToken = params.token1; sellAmount = selfReserve0 / 2 - Math.ceilDiv(selfReserve1TimesPriceNumerator, 2 * params.priceDenominator); - buyAmount = Math.mulDiv( - sellAmount, - selfReserve1TimesPriceNumerator + (params.priceDenominator * sellAmount), - params.priceNumerator * selfReserve0, - Math.Rounding.Ceil - ); + buyAmount = Math.mulDiv(sellAmount, selfReserve1, selfReserve0 - sellAmount, Math.Rounding.Ceil); } else { sellToken = params.token1; buyToken = params.token0; sellAmount = selfReserve1 / 2 - Math.ceilDiv(selfReserve0TimesPriceDenominator, 2 * params.priceNumerator); - buyAmount = Math.mulDiv( - sellAmount, - selfReserve0TimesPriceDenominator + (params.priceNumerator * sellAmount), - params.priceDenominator * selfReserve1, - Math.Rounding.Ceil - ); + buyAmount = Math.mulDiv(sellAmount, selfReserve0, selfReserve1 - sellAmount, Math.Rounding.Ceil); } order_ = GPv2Order.Data( From 627d878db60209c0b6cdc9c054c5ce4ffd9d2877 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wei=C3=9Fer=20Hase?= Date: Tue, 9 Jul 2024 12:34:48 +0200 Subject: [PATCH 12/41] refactor: mv GetTradeableOrder to libraries dir --- remappings.txt | 1 + src/contracts/BCoWHelper.sol | 2 +- src/interfaces/ICOWAMMPoolHelper.sol | 1 + src/{contracts => libraries}/GetTradeableOrder.sol | 1 + test/integration/BCowPool.t.sol | 2 +- 5 files changed, 5 insertions(+), 2 deletions(-) rename src/{contracts => libraries}/GetTradeableOrder.sol (98%) diff --git a/remappings.txt b/remappings.txt index 5b011a43..976a835b 100644 --- a/remappings.txt +++ b/remappings.txt @@ -8,3 +8,4 @@ cowprotocol/=node_modules/@cowprotocol/contracts/src/ contracts/=src/contracts interfaces/=src/interfaces +libraries/=src/libraries diff --git a/src/contracts/BCoWHelper.sol b/src/contracts/BCoWHelper.sol index 5374ae0a..ce731c7f 100644 --- a/src/contracts/BCoWHelper.sol +++ b/src/contracts/BCoWHelper.sol @@ -2,11 +2,11 @@ pragma solidity 0.8.25; import {IERC20} from '@cowprotocol/interfaces/IERC20.sol'; -import {GetTradeableOrder} from 'contracts/GetTradeableOrder.sol'; import {IBCoWFactory} from 'interfaces/IBCoWFactory.sol'; import {IBCoWPool} from 'interfaces/IBCoWPool.sol'; import {IBFactory} from 'interfaces/IBFactory.sol'; import {GPv2Interaction, GPv2Order, ICOWAMMPoolHelper} from 'interfaces/ICOWAMMPoolHelper.sol'; +import {GetTradeableOrder} from 'libraries/GetTradeableOrder.sol'; /** * @title BCoWHelper diff --git a/src/interfaces/ICOWAMMPoolHelper.sol b/src/interfaces/ICOWAMMPoolHelper.sol index c7b7345c..3c3ae627 100644 --- a/src/interfaces/ICOWAMMPoolHelper.sol +++ b/src/interfaces/ICOWAMMPoolHelper.sol @@ -7,6 +7,7 @@ import {GPv2Order} from 'cowprotocol/contracts/libraries/GPv2Order.sol'; /** * @notice Pool-specific helper interface for AMM's operating in CoW Protocol. */ +// TODO: vendor interface from cowprotocol/cow-amm repo when available interface ICOWAMMPoolHelper { /** * All functions that take `pool` as an argument MUST revert with this error diff --git a/src/contracts/GetTradeableOrder.sol b/src/libraries/GetTradeableOrder.sol similarity index 98% rename from src/contracts/GetTradeableOrder.sol rename to src/libraries/GetTradeableOrder.sol index e30e63e5..8ac9f715 100644 --- a/src/contracts/GetTradeableOrder.sol +++ b/src/libraries/GetTradeableOrder.sol @@ -5,6 +5,7 @@ import {IERC20} from '@cowprotocol/interfaces/IERC20.sol'; import {GPv2Order} from '@cowprotocol/libraries/GPv2Order.sol'; import {Math} from '@openzeppelin/contracts/utils/math/Math.sol'; +// TODO: vendor library from cowprotocol/cow-amm repo when available library GetTradeableOrder { /// @dev Avoid stack too deep errors with `getTradeableOrder`. struct GetTradeableOrderParams { diff --git a/test/integration/BCowPool.t.sol b/test/integration/BCowPool.t.sol index 8219c67d..bdd9ed2d 100644 --- a/test/integration/BCowPool.t.sol +++ b/test/integration/BCowPool.t.sol @@ -8,11 +8,11 @@ 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 {GetTradeableOrder} from 'libraries/GetTradeableOrder.sol'; import {BCoWConst} from 'contracts/BCoWConst.sol'; import {BCoWFactory} from 'contracts/BCoWFactory.sol'; import {BPool} from 'contracts/BPool.sol'; -import {GetTradeableOrder} from 'contracts/GetTradeableOrder.sol'; import {IBCoWPool} from 'interfaces/IBCoWPool.sol'; import {IBFactory} from 'interfaces/IBFactory.sol'; From cb2fb52b4162b52f15cfbcc1440704954fe1c189 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wei=C3=9Fer=20Hase?= Date: Tue, 9 Jul 2024 12:36:09 +0200 Subject: [PATCH 13/41] fix: update comment on BCoWHelper --- src/contracts/BCoWHelper.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/contracts/BCoWHelper.sol b/src/contracts/BCoWHelper.sol index ce731c7f..78fbbcfd 100644 --- a/src/contracts/BCoWHelper.sol +++ b/src/contracts/BCoWHelper.sol @@ -11,7 +11,7 @@ import {GetTradeableOrder} from 'libraries/GetTradeableOrder.sol'; /** * @title BCoWHelper * @notice Helper contract that allows to trade on CoW Swap Protocol. - * @dev This contract supports only 2-token pools. + * @dev This contract supports only 2-token equal-weights pools. */ contract BCoWHelper is ICOWAMMPoolHelper { using GPv2Order for GPv2Order.Data; From 5207c18e969448d0509db7345ae1bbbe4aa69289 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wei=C3=9Fer=20Hase?= Date: Tue, 9 Jul 2024 12:39:16 +0200 Subject: [PATCH 14/41] fix: rm unused variable warning --- src/contracts/BCoWHelper.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/contracts/BCoWHelper.sol b/src/contracts/BCoWHelper.sol index 78fbbcfd..eb4e1975 100644 --- a/src/contracts/BCoWHelper.sol +++ b/src/contracts/BCoWHelper.sol @@ -51,8 +51,6 @@ contract BCoWHelper is ICOWAMMPoolHelper { bytes memory sig ) { - postInteractions; // NOTE: avoid unused var - address[] memory tokens_ = tokens(pool); GetTradeableOrder.GetTradeableOrderParams memory params = GetTradeableOrder.GetTradeableOrderParams({ @@ -84,5 +82,7 @@ contract BCoWHelper is ICOWAMMPoolHelper { value: 0, callData: abi.encodeWithSelector(IBCoWPool.commit.selector, orderCommitment) }); + + return (order_, preInteractions, postInteractions, sig); } } From 6f76f85586d5ef99d9f824c1ba8fc6261ccd94cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wei=C3=9Fer=20Hase?= Date: Tue, 9 Jul 2024 13:59:01 +0200 Subject: [PATCH 15/41] feat: adding BTT test for BCoWHelper --- test/manual-smock/MockBCoWFactory.sol | 59 ++++++++-- test/manual-smock/MockBCoWPool.sol | 11 ++ test/unit/BCoWHelper.t.sol | 152 ++++++++++++++++++++++++++ test/unit/BCoWHelper.tree | 26 +++++ 4 files changed, 238 insertions(+), 10 deletions(-) create mode 100644 test/unit/BCoWHelper.t.sol create mode 100644 test/unit/BCoWHelper.tree diff --git a/test/manual-smock/MockBCoWFactory.sol b/test/manual-smock/MockBCoWFactory.sol index 5e7baa5a..bf887e7d 100644 --- a/test/manual-smock/MockBCoWFactory.sol +++ b/test/manual-smock/MockBCoWFactory.sol @@ -7,32 +7,71 @@ import {Test} from 'forge-std/Test.sol'; contract MockBCoWFactory is BCoWFactory, Test { constructor(address solutionSettler, bytes32 appData) BCoWFactory(solutionSettler, appData) {} + 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()')); + } + function mock_call_logBCoWPool() public { vm.mockCall(address(this), abi.encodeWithSignature('logBCoWPool()'), abi.encode()); } - function mock_call__newBPool(IBPool bCoWPool) public { - vm.mockCall(address(this), abi.encodeWithSignature('_newBPool()'), abi.encode(bCoWPool)); + // 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__bLabs(address __bLabs) public { + _bLabs = __bLabs; + } + + function call__bLabs() public view returns (address) { + return _bLabs; + } + + function mock_call_newBPool(IBPool bPool) public { + vm.mockCall(address(this), abi.encodeWithSignature('newBPool()'), abi.encode(bPool)); } - function _newBPool() internal override returns (IBPool bCoWPool) { + function mock_call_setBLabs(address bLabs) public { + vm.mockCall(address(this), abi.encodeWithSignature('setBLabs(address)', bLabs), 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_getBLabs(address _returnParam0) public { + vm.mockCall(address(this), abi.encodeWithSignature('getBLabs()'), abi.encode(_returnParam0)); + } + + function mock_call__newBPool(IBPool bPool) public { + vm.mockCall(address(this), abi.encodeWithSignature('_newBPool()'), abi.encode(bPool)); + } + + function _newBPool() internal override returns (IBPool bPool) { (bool _success, bytes memory _data) = address(this).call(abi.encodeWithSignature('_newBPool()')); if (_success) return abi.decode(_data, (IBPool)); else return super._newBPool(); } - function call__newBPool() public returns (IBPool bCoWPool) { + function call__newBPool() public returns (IBPool bPool) { return _newBPool(); } function expectCall__newBPool() public { vm.expectCall(address(this), abi.encodeWithSignature('_newBPool()')); } - - // MockBFactory methods - - function set__isBPool(address _key0, bool _value) public { - _isBPool[_key0] = _value; - } } diff --git a/test/manual-smock/MockBCoWPool.sol b/test/manual-smock/MockBCoWPool.sol index de851269..11eaba95 100644 --- a/test/manual-smock/MockBCoWPool.sol +++ b/test/manual-smock/MockBCoWPool.sol @@ -21,6 +21,17 @@ contract MockBCoWPool is BCoWPool, Test { vm.expectCall(address(this), abi.encodeWithSignature('verify(GPv2Order.Data)', order)); } + // 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) {} diff --git a/test/unit/BCoWHelper.t.sol b/test/unit/BCoWHelper.t.sol new file mode 100644 index 00000000..12934182 --- /dev/null +++ b/test/unit/BCoWHelper.t.sol @@ -0,0 +1,152 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.25; + +import {BCoWHelper} from 'contracts/BCoWHelper.sol'; +import {Test} from 'forge-std/Test.sol'; + +import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; +import {IBCoWPool} from 'interfaces/IBCoWPool.sol'; +import {IBPool} from 'interfaces/IBPool.sol'; +import {GPv2Interaction, GPv2Order, ICOWAMMPoolHelper} from 'interfaces/ICOWAMMPoolHelper.sol'; + +import {ISettlement} from 'interfaces/ISettlement.sol'; + +import {MockBCoWFactory} from 'test/manual-smock/MockBCoWFactory.sol'; +import {MockBCoWPool} from 'test/manual-smock/MockBCoWPool.sol'; + +contract BCoWHelperTest is Test { + BCoWHelper helper; + + MockBCoWFactory factory; + MockBCoWPool pool; + address invalidPool = makeAddr('invalidPool'); + address[] tokens = new address[](2); + uint256[] priceVector = new uint256[](2); + + uint256 constant VALID_WEIGHT = 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] = 1e18; + + 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 BCoWHelper(address(factory)); + } + + function test_ConstructorWhenCalled(bytes32 _appData) external { + factory.expectCall_APP_DATA(); + factory.mock_call_APP_DATA(_appData); + helper = new BCoWHelper(address(factory)); + // it should set factory + assertEq(helper.factory(), address(factory)); + // it should set app data from factory + assertEq(helper.APP_DATA(), _appData); + } + + function test_TokensRevertWhen_PoolIsNotRegisteredInFactory() external { + // it should revert + vm.expectRevert(ICOWAMMPoolHelper.PoolDoesNotExist.selector); + helper.tokens(invalidPool); + } + + 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 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); + assertEq(bytes4(preInteractions[0].callData), IBCoWPool.commit.selector); + + // 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)); + } +} diff --git a/test/unit/BCoWHelper.tree b/test/unit/BCoWHelper.tree new file mode 100644 index 00000000..b4686c03 --- /dev/null +++ b/test/unit/BCoWHelper.tree @@ -0,0 +1,26 @@ +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 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 From de99df4115e7ab58bf32b6913c742d810fee4891 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wei=C3=9Fer=20Hase?= Date: Tue, 9 Jul 2024 14:13:42 +0200 Subject: [PATCH 16/41] fix: linter warnings --- src/contracts/BCoWHelper.sol | 30 +++++++++++++++--------------- test/integration/BCoWHelper.t.sol | 6 +++--- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/src/contracts/BCoWHelper.sol b/src/contracts/BCoWHelper.sol index eb4e1975..f3171d88 100644 --- a/src/contracts/BCoWHelper.sol +++ b/src/contracts/BCoWHelper.sol @@ -16,28 +16,14 @@ import {GetTradeableOrder} from 'libraries/GetTradeableOrder.sol'; contract BCoWHelper is ICOWAMMPoolHelper { using GPv2Order for GPv2Order.Data; - address public factory; bytes32 public immutable APP_DATA; + address public factory; constructor(address factory_) { factory = factory_; APP_DATA = IBCoWFactory(factory_).APP_DATA(); } - function tokens(address pool) public view returns (address[] memory tokens_) { - if (!IBFactory(factory).isBPool(pool)) revert PoolDoesNotExist(); - // NOTE: reverts in case pool is not finalized - tokens_ = IBCoWPool(pool).getFinalTokens(); - if (tokens_.length != 2) revert PoolDoesNotExist(); - - // NOTE: reverts in case pool is not supported (non-equal weights) - if (IBCoWPool(pool).getNormalizedWeight(tokens_[0]) != IBCoWPool(pool).getNormalizedWeight(tokens_[1])) { - revert PoolDoesNotExist(); - } - - return tokens_; - } - function order( address pool, uint256[] calldata prices @@ -85,4 +71,18 @@ contract BCoWHelper is ICOWAMMPoolHelper { return (order_, preInteractions, postInteractions, sig); } + + function tokens(address pool) public view returns (address[] memory tokens_) { + if (!IBFactory(factory).isBPool(pool)) revert PoolDoesNotExist(); + // NOTE: reverts in case pool is not finalized + tokens_ = IBCoWPool(pool).getFinalTokens(); + if (tokens_.length != 2) revert PoolDoesNotExist(); + + // NOTE: reverts in case pool is not supported (non-equal weights) + if (IBCoWPool(pool).getNormalizedWeight(tokens_[0]) != IBCoWPool(pool).getNormalizedWeight(tokens_[1])) { + revert PoolDoesNotExist(); + } + + return tokens_; + } } diff --git a/test/integration/BCoWHelper.t.sol b/test/integration/BCoWHelper.t.sol index fd3ec8dd..76d02c40 100644 --- a/test/integration/BCoWHelper.t.sol +++ b/test/integration/BCoWHelper.t.sol @@ -38,9 +38,9 @@ contract ConstantProductHelperForkedTest is Test { IBPool private weightedPool; IBPool private basicPool; - IERC20 private DAI = IERC20(0x6B175474E89094C44Da98b954EedeAC495271d0F); - IERC20 private USDC = IERC20(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48); - IERC20 private WETH = IERC20(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); + IERC20 private constant DAI = IERC20(0x6B175474E89094C44Da98b954EedeAC495271d0F); + IERC20 private constant USDC = IERC20(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48); + IERC20 private constant WETH = IERC20(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); uint256 constant VALID_AMOUNT = 1e6; uint256 constant TEN_PERCENT = 0.1 ether; From bd64dca7b61894bbdb17de32f417fae161953bfc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wei=C3=9Fer=20Hase?= Date: Tue, 9 Jul 2024 14:17:04 +0200 Subject: [PATCH 17/41] fix: rm remanent line change --- src/contracts/BConst.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/contracts/BConst.sol b/src/contracts/BConst.sol index 8feb125e..3a184a01 100644 --- a/src/contracts/BConst.sol +++ b/src/contracts/BConst.sol @@ -22,7 +22,7 @@ contract BConst { uint256 public constant EXIT_FEE = 0; /// @notice The minimum weight that a token can have. - uint256 public constant MIN_WEIGHT = 0.01e18; + uint256 public constant MIN_WEIGHT = BONE; /// @notice The maximum weight that a token can have. uint256 public constant MAX_WEIGHT = BONE * 50; /// @notice The maximum sum of weights of all tokens in a pool. From ac17f6922d7c22a8cb928afd6e5723a0076844ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wei=C3=9Fer=20Hase?= Date: Tue, 9 Jul 2024 14:18:30 +0200 Subject: [PATCH 18/41] fix: rm unused lines --- test/integration/BCowPool.t.sol | 2 -- test/integration/BPool.t.sol | 3 --- 2 files changed, 5 deletions(-) diff --git a/test/integration/BCowPool.t.sol b/test/integration/BCowPool.t.sol index bdd9ed2d..da711035 100644 --- a/test/integration/BCowPool.t.sol +++ b/test/integration/BCowPool.t.sol @@ -8,11 +8,9 @@ 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 {GetTradeableOrder} from 'libraries/GetTradeableOrder.sol'; import {BCoWConst} from 'contracts/BCoWConst.sol'; import {BCoWFactory} from 'contracts/BCoWFactory.sol'; -import {BPool} from 'contracts/BPool.sol'; import {IBCoWPool} from 'interfaces/IBCoWPool.sol'; import {IBFactory} from 'interfaces/IBFactory.sol'; diff --git a/test/integration/BPool.t.sol b/test/integration/BPool.t.sol index 0ee76785..36deb047 100644 --- a/test/integration/BPool.t.sol +++ b/test/integration/BPool.t.sol @@ -48,9 +48,6 @@ abstract contract BPoolIntegrationTest is Test, GasSnapshot { uint256 public constant DAI_AMOUNT = HUNDRED_UNITS; uint256 public constant WETH_AMOUNT_INVERSE = ONE_TENTH_UNIT; - uint256 public constant DAI_WEIGHT = 8e18; - uint256 public constant WETH_WEIGHT = 2e18; - // swap amounts OUT // NOTE: amounts OUT are hardcoded from test result uint256 public constant WETH_OUT_AMOUNT = 94_049_266_814_811_022; // 0.094 ETH From 6a14084b1c9d2f058f3c436c0f12038e19f42145 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wei=C3=9Fer=20Hase?= Date: Tue, 9 Jul 2024 14:30:54 +0200 Subject: [PATCH 19/41] fix: missing natspec --- .forge-snapshots/newBFactory.snap | 2 +- .forge-snapshots/newBPool.snap | 2 +- src/contracts/BCoWHelper.sol | 5 ++++ src/interfaces/ICOWAMMPoolHelper.sol | 40 ++++++++++++++++------------ src/libraries/GetTradeableOrder.sol | 15 ++++++++++- 5 files changed, 44 insertions(+), 20 deletions(-) diff --git a/.forge-snapshots/newBFactory.snap b/.forge-snapshots/newBFactory.snap index 4d65f71b..c54b6046 100644 --- a/.forge-snapshots/newBFactory.snap +++ b/.forge-snapshots/newBFactory.snap @@ -1 +1 @@ -4133545 \ No newline at end of file +4130633 \ No newline at end of file diff --git a/.forge-snapshots/newBPool.snap b/.forge-snapshots/newBPool.snap index f8d4cf46..0f67b99b 100644 --- a/.forge-snapshots/newBPool.snap +++ b/.forge-snapshots/newBPool.snap @@ -1 +1 @@ -3480192 \ No newline at end of file +3477592 \ No newline at end of file diff --git a/src/contracts/BCoWHelper.sol b/src/contracts/BCoWHelper.sol index f3171d88..0acec863 100644 --- a/src/contracts/BCoWHelper.sol +++ b/src/contracts/BCoWHelper.sol @@ -16,7 +16,10 @@ import {GetTradeableOrder} from 'libraries/GetTradeableOrder.sol'; contract BCoWHelper is ICOWAMMPoolHelper { using GPv2Order for GPv2Order.Data; + /// @notice The app data used by this helper's factory. bytes32 public immutable APP_DATA; + + /// @notice The factory contract to which this helper provides support. address public factory; constructor(address factory_) { @@ -24,6 +27,7 @@ contract BCoWHelper is ICOWAMMPoolHelper { APP_DATA = IBCoWFactory(factory_).APP_DATA(); } + /// @inheritdoc ICOWAMMPoolHelper function order( address pool, uint256[] calldata prices @@ -72,6 +76,7 @@ contract BCoWHelper is ICOWAMMPoolHelper { return (order_, preInteractions, postInteractions, sig); } + /// @inheritdoc ICOWAMMPoolHelper function tokens(address pool) public view returns (address[] memory tokens_) { if (!IBFactory(factory).isBPool(pool)) revert PoolDoesNotExist(); // NOTE: reverts in case pool is not finalized diff --git a/src/interfaces/ICOWAMMPoolHelper.sol b/src/interfaces/ICOWAMMPoolHelper.sol index 3c3ae627..ef3c2283 100644 --- a/src/interfaces/ICOWAMMPoolHelper.sol +++ b/src/interfaces/ICOWAMMPoolHelper.sol @@ -10,51 +10,57 @@ import {GPv2Order} from 'cowprotocol/contracts/libraries/GPv2Order.sol'; // TODO: vendor interface from cowprotocol/cow-amm repo when available interface ICOWAMMPoolHelper { /** - * All functions that take `pool` as an argument MUST revert with this error - * if the `pool` does not exist. + * @notice All functions that take `pool` as an argument MUST revert with this error + * if the `pool` does not exist. * @dev Indexers monitoring CoW AMM pools MAY use this as a signal to purge the * pool from their index. */ error PoolDoesNotExist(); /** - * All functions that take `pool` as an argument MUST revert with this error - * in the event that the pool is paused (ONLY applicable if the pool is pausable). + * @notice All functions that take `pool` as an argument MUST revert with this error + * in the event that the pool is paused (ONLY applicable if the pool is pausable). * @dev Indexers monitoring CoW AMM pools SHOULD use this as a signal to retain * the pool in the index with back-off on polling for orders. */ error PoolIsPaused(); /** - * All functions that take `pool` as an argument MUST revert with this error - * in the event that the pool is closed (ONLY applicable if the pool can be - * closed). + * @notice All functions that take `pool` as an argument MUST revert with this error + * in the event that the pool is closed (ONLY applicable if the pool can be + * closed). * @dev Indexers monitoring CoW AMM pools MAY use this as a signal to purge the * pool from their index. */ error PoolIsClosed(); /** - * Returned by the `order` function if there is no order matching the supplied - * parameters. + * @notice Returned by the `order` function if there is no order matching the supplied + * parameters. */ error NoOrder(); /** - * AMM Pool helpers MUST return the factory target for indexing of CoW AMM pools. + * @notice The factory contract to which this helper provides support. + * @dev AMM Pool helpers MUST return the factory target for indexing of CoW AMM pools. + * @return factory The factory contract address. */ - function factory() external view returns (address); + function factory() external view returns (address factory); /** - * AMM Pool helpers MUST return all tokens that may be traded on this pool. - * The order of the tokens is expected to be consistent and must be the same - * as that used for the input price vector in the `order` function. + * @notice Gets the tokens that may be traded on the given pool. + * @dev AMM Pool helpers MUST return all tokens that may be traded on this pool. + * The order of the tokens is expected to be consistent and must be the same + * as that used for the input price vector in the `order` function. + * @param pool The address of the pool to query. + * @return tokens The array of tokens that may be traded on the pool. */ - function tokens(address pool) external view returns (address[] memory); + function tokens(address pool) external view returns (address[] memory tokens); /** - * AMM Pool helpers MUST provide a method for returning the canonical order - * required to satisfy the pool's invariants, given a pricing vector. + * @notice Gets the pool's canonical order given a pricing vector + * @dev AMM Pool helpers MUST provide a method for returning the canonical order + * required to satisfy the pool's invariants, given a pricing vector. * @dev Reverts with `NoOrder` if the `pool` has no canonical order matching the * given price vector. * @param pool to calculate the order / signature for diff --git a/src/libraries/GetTradeableOrder.sol b/src/libraries/GetTradeableOrder.sol index 8ac9f715..c5e0385c 100644 --- a/src/libraries/GetTradeableOrder.sol +++ b/src/libraries/GetTradeableOrder.sol @@ -7,7 +7,15 @@ import {Math} from '@openzeppelin/contracts/utils/math/Math.sol'; // TODO: vendor library from cowprotocol/cow-amm repo when available library GetTradeableOrder { - /// @dev Avoid stack too deep errors with `getTradeableOrder`. + /** + * @notice Parameters for the `getTradeableOrder` function. + * @param pool The address of the pool to query. + * @param token0 The first token of the pool. + * @param token1 The second token of the pool. + * @param priceNumerator The numerator of the pricing vector. + * @param priceDenominator The denominator of the pricing vector. + * @dev Avoid stack too deep errors with `getTradeableOrder`. + */ struct GetTradeableOrderParams { address pool; IERC20 token0; @@ -28,6 +36,11 @@ library GetTradeableOrder { /// @notice The largest possible duration of any AMM order, starting from the current block timestamp. uint32 public constant MAX_ORDER_DURATION = 5 * 60; + /** + * @notice Method to calculate the canonical order of a pool given a pricing vector. + * @param params Encoded parameters for the order. + * @return order_ The canonical order of the pool. + */ function getTradeableOrder(GetTradeableOrderParams memory params) internal view From 96f0302368535aa6a8259be1ee84664298dba793 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wei=C3=9Fer=20Hase?= Date: Tue, 9 Jul 2024 14:36:04 +0200 Subject: [PATCH 20/41] fix: uncommenting assertion in test --- test/integration/BCoWHelper.t.sol | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/integration/BCoWHelper.t.sol b/test/integration/BCoWHelper.t.sol index 76d02c40..7f0d5416 100644 --- a/test/integration/BCoWHelper.t.sol +++ b/test/integration/BCoWHelper.t.sol @@ -98,7 +98,7 @@ contract ConstantProductHelperForkedTest is Test { assertEq(postSpotPrice, 1_052_631_578_947_368); } - // NOTE: failing test + // NOTE: reverting test, weighted pools are not supported function testWeightedOrder() public { IBCoWPool pool = IBCoWPool(address(weightedPool)); @@ -165,8 +165,8 @@ contract ConstantProductHelperForkedTest is Test { // 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, ammDaiInitialBalance * 25 / 1000, TEN_PERCENT); - // assertApproxEqRel(ammOrder.buyAmount, ammWethInitialBalance * 25 / 1000, TEN_PERCENT); + assertApproxEqRel(ammOrder.sellAmount, ammDaiInitialBalance * 25 / 1000, TEN_PERCENT); + assertApproxEqRel(ammOrder.buyAmount, ammWethInitialBalance * 25 / 1000, TEN_PERCENT); GPv2Trade.Data[] memory trades = new GPv2Trade.Data[](1); From e8c4d339ca0bef29e41c9d7fb15993b87d8d4b2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wei=C3=9Fer=20Hase?= Date: Tue, 9 Jul 2024 15:01:32 +0200 Subject: [PATCH 21/41] fix: missing natspec --- src/libraries/GetTradeableOrder.sol | 1 + 1 file changed, 1 insertion(+) diff --git a/src/libraries/GetTradeableOrder.sol b/src/libraries/GetTradeableOrder.sol index c5e0385c..239352e4 100644 --- a/src/libraries/GetTradeableOrder.sol +++ b/src/libraries/GetTradeableOrder.sol @@ -14,6 +14,7 @@ library GetTradeableOrder { * @param token1 The second token of the pool. * @param priceNumerator The numerator of the pricing vector. * @param priceDenominator The denominator of the pricing vector. + * @param appData The app data identifier to include in the order. * @dev Avoid stack too deep errors with `getTradeableOrder`. */ struct GetTradeableOrderParams { From 22480c5bb8f3ad30e03619331442fbf91b2b3b7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wei=C3=9Fer=20Hase?= Date: Tue, 9 Jul 2024 15:06:49 +0200 Subject: [PATCH 22/41] fix: safety checks comments --- src/contracts/BCoWHelper.sol | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/contracts/BCoWHelper.sol b/src/contracts/BCoWHelper.sol index 0acec863..d9b04534 100644 --- a/src/contracts/BCoWHelper.sol +++ b/src/contracts/BCoWHelper.sol @@ -78,12 +78,13 @@ contract BCoWHelper is ICOWAMMPoolHelper { /// @inheritdoc ICOWAMMPoolHelper function tokens(address pool) public view returns (address[] memory tokens_) { + // reverts in case pool is not deployed by the helper's factory if (!IBFactory(factory).isBPool(pool)) revert PoolDoesNotExist(); - // NOTE: reverts in case pool is not finalized + // 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(); - - // NOTE: reverts in case pool is not supported (non-equal weights) + // reverts in case pool is not supported (non-equal weights) if (IBCoWPool(pool).getNormalizedWeight(tokens_[0]) != IBCoWPool(pool).getNormalizedWeight(tokens_[1])) { revert PoolDoesNotExist(); } From 3e5bfeac343c6c1c0c941de925797d62586e6830 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wei=C3=9Fer=20Hase?= Date: Tue, 9 Jul 2024 16:19:00 +0200 Subject: [PATCH 23/41] feat: vendoring interface from cow-amm --- package.json | 1 + remappings.txt | 1 + src/contracts/BCoWHelper.sol | 9 ++- src/interfaces/ICOWAMMPoolHelper.sol | 94 ---------------------------- test/integration/BCoWHelper.t.sol | 3 +- test/unit/BCoWHelper.t.sol | 9 ++- yarn.lock | 4 ++ 7 files changed, 20 insertions(+), 101 deletions(-) delete mode 100644 src/interfaces/ICOWAMMPoolHelper.sol diff --git a/package.json b/package.json index 04721367..0a5496d8 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#2b5f973", "solmate": "github:transmissions11/solmate#c892309" }, "devDependencies": { diff --git a/remappings.txt b/remappings.txt index 976a835b..42bb073a 100644 --- a/remappings.txt +++ b/remappings.txt @@ -5,6 +5,7 @@ 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 contracts/=src/contracts interfaces/=src/interfaces diff --git a/src/contracts/BCoWHelper.sol b/src/contracts/BCoWHelper.sol index d9b04534..302f8bd9 100644 --- a/src/contracts/BCoWHelper.sol +++ b/src/contracts/BCoWHelper.sol @@ -1,11 +1,16 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity 0.8.25; -import {IERC20} from '@cowprotocol/interfaces/IERC20.sol'; import {IBCoWFactory} from 'interfaces/IBCoWFactory.sol'; import {IBCoWPool} from 'interfaces/IBCoWPool.sol'; import {IBFactory} from 'interfaces/IBFactory.sol'; -import {GPv2Interaction, GPv2Order, ICOWAMMPoolHelper} from 'interfaces/ICOWAMMPoolHelper.sol'; + +import {ICOWAMMPoolHelper} from '@cow-amm/interfaces/ICOWAMMPoolHelper.sol'; +import {GPv2Interaction} from '@cowprotocol/libraries/GPv2Interaction.sol'; +import {GPv2Order} from '@cowprotocol/libraries/GPv2Order.sol'; +import {IERC20} from '@cowprotocol/interfaces/IERC20.sol'; + +// TODO: vendor from cow-amm import {GetTradeableOrder} from 'libraries/GetTradeableOrder.sol'; /** diff --git a/src/interfaces/ICOWAMMPoolHelper.sol b/src/interfaces/ICOWAMMPoolHelper.sol deleted file mode 100644 index ef3c2283..00000000 --- a/src/interfaces/ICOWAMMPoolHelper.sol +++ /dev/null @@ -1,94 +0,0 @@ -// SPDX-License-Identifier: LGPL-3.0-only -pragma solidity ^0.8.24; - -import {GPv2Interaction} from 'cowprotocol/contracts/libraries/GPv2Interaction.sol'; -import {GPv2Order} from 'cowprotocol/contracts/libraries/GPv2Order.sol'; - -/** - * @notice Pool-specific helper interface for AMM's operating in CoW Protocol. - */ -// TODO: vendor interface from cowprotocol/cow-amm repo when available -interface ICOWAMMPoolHelper { - /** - * @notice All functions that take `pool` as an argument MUST revert with this error - * if the `pool` does not exist. - * @dev Indexers monitoring CoW AMM pools MAY use this as a signal to purge the - * pool from their index. - */ - error PoolDoesNotExist(); - - /** - * @notice All functions that take `pool` as an argument MUST revert with this error - * in the event that the pool is paused (ONLY applicable if the pool is pausable). - * @dev Indexers monitoring CoW AMM pools SHOULD use this as a signal to retain - * the pool in the index with back-off on polling for orders. - */ - error PoolIsPaused(); - - /** - * @notice All functions that take `pool` as an argument MUST revert with this error - * in the event that the pool is closed (ONLY applicable if the pool can be - * closed). - * @dev Indexers monitoring CoW AMM pools MAY use this as a signal to purge the - * pool from their index. - */ - error PoolIsClosed(); - - /** - * @notice Returned by the `order` function if there is no order matching the supplied - * parameters. - */ - error NoOrder(); - - /** - * @notice The factory contract to which this helper provides support. - * @dev AMM Pool helpers MUST return the factory target for indexing of CoW AMM pools. - * @return factory The factory contract address. - */ - function factory() external view returns (address factory); - - /** - * @notice Gets the tokens that may be traded on the given pool. - * @dev AMM Pool helpers MUST return all tokens that may be traded on this pool. - * The order of the tokens is expected to be consistent and must be the same - * as that used for the input price vector in the `order` function. - * @param pool The address of the pool to query. - * @return tokens The array of tokens that may be traded on the pool. - */ - function tokens(address pool) external view returns (address[] memory tokens); - - /** - * @notice Gets the pool's canonical order given a pricing vector - * @dev AMM Pool helpers MUST provide a method for returning the canonical order - * required to satisfy the pool's invariants, given a pricing vector. - * @dev Reverts with `NoOrder` if the `pool` has no canonical order matching the - * given price vector. - * @param pool to calculate the order / signature for - * @param prices supplied for determining the order, assumed to be in the - * same order as returned from `tokens(pool)`. Tokens prices are - * expressed relative to each other: for example, if tokens[0] is - * WETH, tokens[2] is DAI, and the price is 1 WETH per 3000 DAI, then - * a valid price vector is [3000, *, 1, ...]. If tokens[1] is another - * stablecoin with 18 decimals, then a valid price vector could be - * [3000, 3000, 1, ...]. - * This price vector is compatible with the price vector used in a - * call to `settle`, assuming the traded token array is in the same - * order as in `tokens(pool)`. - * @return order The CoW Protocol JIT order - * @return preInteractions The array array for any **PRE** interactions (empty if none) - * @return postInteractions The array array for any **POST** interactions (empty if none) - * @return sig A valid CoW-Protocol signature for the resulting order using the ERC-1271 signature scheme. - */ - 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 - ); -} diff --git a/test/integration/BCoWHelper.t.sol b/test/integration/BCoWHelper.t.sol index 7f0d5416..f441ac86 100644 --- a/test/integration/BCoWHelper.t.sol +++ b/test/integration/BCoWHelper.t.sol @@ -7,10 +7,9 @@ import {IERC20} from '@cowprotocol/interfaces/IERC20.sol'; import {IBCoWPool} from 'interfaces/IBCoWPool.sol'; import {IBPool} from 'interfaces/IBPool.sol'; - -import {ICOWAMMPoolHelper} from 'interfaces/ICOWAMMPoolHelper.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'; diff --git a/test/unit/BCoWHelper.t.sol b/test/unit/BCoWHelper.t.sol index 12934182..23af0299 100644 --- a/test/unit/BCoWHelper.t.sol +++ b/test/unit/BCoWHelper.t.sol @@ -4,13 +4,16 @@ pragma solidity 0.8.25; import {BCoWHelper} from 'contracts/BCoWHelper.sol'; import {Test} from 'forge-std/Test.sol'; -import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; import {IBCoWPool} from 'interfaces/IBCoWPool.sol'; import {IBPool} from 'interfaces/IBPool.sol'; -import {GPv2Interaction, GPv2Order, ICOWAMMPoolHelper} from 'interfaces/ICOWAMMPoolHelper.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'; diff --git a/yarn.lock b/yarn.lock index 27b5429f..2aaafe54 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#2b5f973": + version "0.0.0" + resolved "https://codeload.github.com/cowprotocol/cow-amm/tar.gz/2b5f9733ef7df905ce73fe2bed1be5f6cf407afc" + cross-spawn@^7.0.3: version "7.0.3" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" From 68e8368b94cc2ebbe4526cc3c2157765ef9be114 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wei=C3=9Fer=20Hase?= Date: Tue, 9 Jul 2024 16:19:38 +0200 Subject: [PATCH 24/41] fix: lint run --- src/contracts/BCoWHelper.sol | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/contracts/BCoWHelper.sol b/src/contracts/BCoWHelper.sol index 302f8bd9..fcd0f811 100644 --- a/src/contracts/BCoWHelper.sol +++ b/src/contracts/BCoWHelper.sol @@ -6,9 +6,10 @@ import {IBCoWPool} from 'interfaces/IBCoWPool.sol'; import {IBFactory} from 'interfaces/IBFactory.sol'; import {ICOWAMMPoolHelper} from '@cow-amm/interfaces/ICOWAMMPoolHelper.sol'; + +import {IERC20} from '@cowprotocol/interfaces/IERC20.sol'; import {GPv2Interaction} from '@cowprotocol/libraries/GPv2Interaction.sol'; import {GPv2Order} from '@cowprotocol/libraries/GPv2Order.sol'; -import {IERC20} from '@cowprotocol/interfaces/IERC20.sol'; // TODO: vendor from cow-amm import {GetTradeableOrder} from 'libraries/GetTradeableOrder.sol'; From 5afd282c0cdb22e55bc529220f2c056a3dad1b56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wei=C3=9Fer=20Hase?= Date: Tue, 9 Jul 2024 17:09:16 +0200 Subject: [PATCH 25/41] chore: merge dev --- script/DeployBCoWFactory.s.sol | 2 +- script/DeployBFactory.s.sol | 2 +- script/Params.s.sol | 4 +- src/contracts/BFactory.sol | 28 ++++----- src/interfaces/IBFactory.sol | 26 ++++----- test/manual-smock/MockBCoWFactory.sol | 60 ++++++++++---------- test/manual-smock/MockBCoWPool.sol | 81 ++++++++++++++++++++++++++- test/smock/MockBFactory.sol | 16 +++--- test/unit/BCoWFactory.t.sol | 8 +-- test/unit/BCoWFactory.tree | 2 +- test/unit/BFactory.t.sol | 47 ++++++++-------- test/unit/BFactory.tree | 18 +++--- 12 files changed, 185 insertions(+), 109 deletions(-) diff --git a/script/DeployBCoWFactory.s.sol b/script/DeployBCoWFactory.s.sol index a2abfbdd..57905392 100644 --- a/script/DeployBCoWFactory.s.sol +++ b/script/DeployBCoWFactory.s.sol @@ -11,7 +11,7 @@ contract DeployBCoWFactory is Script, Params { vm.startBroadcast(); BCoWFactory bCoWFactory = new BCoWFactory(params.settlement, params.appData); - bCoWFactory.setBLabs(params.bLabs); + bCoWFactory.setBDao(params.bDao); vm.stopBroadcast(); } } diff --git a/script/DeployBFactory.s.sol b/script/DeployBFactory.s.sol index 5d0c33bc..1d7a0157 100644 --- a/script/DeployBFactory.s.sol +++ b/script/DeployBFactory.s.sol @@ -11,7 +11,7 @@ contract DeployBFactory is Script, Params { vm.startBroadcast(); BFactory bFactory = new BFactory(); - bFactory.setBLabs(params.bLabs); + bFactory.setBDao(params.bDao); vm.stopBroadcast(); } } diff --git a/script/Params.s.sol b/script/Params.s.sol index b2f0eeab..9df9a8f0 100644 --- a/script/Params.s.sol +++ b/script/Params.s.sol @@ -3,11 +3,11 @@ pragma solidity 0.8.25; contract Params { struct BFactoryDeploymentParams { - address bLabs; + address bDao; } struct BCoWFactoryDeploymentParams { - address bLabs; + address bDao; address settlement; bytes32 appData; } diff --git a/src/contracts/BFactory.sol b/src/contracts/BFactory.sol index 60619bb6..19450439 100644 --- a/src/contracts/BFactory.sol +++ b/src/contracts/BFactory.sol @@ -13,11 +13,11 @@ import {IBPool} from 'interfaces/IBPool.sol'; contract BFactory is IBFactory { /// @dev Mapping indicating whether the address is a BPool. mapping(address => bool) internal _isBPool; - /// @dev bLabs address. - address internal _bLabs; + /// @dev bDao address. + address internal _bDao; constructor() { - _bLabs = msg.sender; + _bDao = msg.sender; } /// @inheritdoc IBFactory @@ -29,25 +29,25 @@ contract BFactory is IBFactory { } /// @inheritdoc IBFactory - function setBLabs(address bLabs) external { - if (bLabs == address(0)) { + function setBDao(address bDao) external { + if (bDao == address(0)) { revert BFactory_AddressZero(); } - if (msg.sender != _bLabs) { - revert BFactory_NotBLabs(); + if (msg.sender != _bDao) { + revert BFactory_NotBDao(); } - emit LOG_BLABS(msg.sender, bLabs); - _bLabs = bLabs; + emit LOG_BDAO(msg.sender, bDao); + _bDao = bDao; } /// @inheritdoc IBFactory function collect(IBPool bPool) external { - if (msg.sender != _bLabs) { - revert BFactory_NotBLabs(); + if (msg.sender != _bDao) { + revert BFactory_NotBDao(); } uint256 collected = bPool.balanceOf(address(this)); - SafeERC20.safeTransfer(bPool, _bLabs, collected); + SafeERC20.safeTransfer(bPool, _bDao, collected); } /// @inheritdoc IBFactory @@ -56,8 +56,8 @@ contract BFactory is IBFactory { } /// @inheritdoc IBFactory - function getBLabs() external view returns (address) { - return _bLabs; + function getBDao() external view returns (address) { + return _bDao; } /** diff --git a/src/interfaces/IBFactory.sol b/src/interfaces/IBFactory.sol index edaff060..672ba7ca 100644 --- a/src/interfaces/IBFactory.sol +++ b/src/interfaces/IBFactory.sol @@ -12,11 +12,11 @@ interface IBFactory { event LOG_NEW_POOL(address indexed caller, address indexed bPool); /** - * @notice Emitted when setting the BLabs address - * @param caller The caller of the set BLabs function - * @param bLabs The address of the new BLabs + * @notice Emitted when setting the BDao address + * @param caller The caller of the set BDao function + * @param bDao The address of the new BDao */ - event LOG_BLABS(address indexed caller, address indexed bLabs); + event LOG_BDAO(address indexed caller, address indexed bDao); /** * @notice Thrown when setting a variable to address zero @@ -24,9 +24,9 @@ interface IBFactory { error BFactory_AddressZero(); /** - * @notice Thrown when caller is not BLabs address + * @notice Thrown when caller is not BDao address */ - error BFactory_NotBLabs(); + error BFactory_NotBDao(); /** * @notice Creates a new BPool, assigning the caller as the pool controller @@ -35,13 +35,13 @@ interface IBFactory { function newBPool() external returns (IBPool bPool); /** - * @notice Sets the BLabs address in the factory - * @param bLabs The new BLabs address + * @notice Sets the BDao address in the factory + * @param bDao The new BDao address */ - function setBLabs(address bLabs) external; + function setBDao(address bDao) external; /** - * @notice Collects the fees of a pool and transfers it to BLabs address + * @notice Collects the fees of a pool and transfers it to BDao address * @param bPool The address of the pool to collect fees from */ function collect(IBPool bPool) external; @@ -54,8 +54,8 @@ interface IBFactory { function isBPool(address bPool) external view returns (bool isBPool); /** - * @notice Gets the BLabs address - * @return bLabs The address of the BLabs + * @notice Gets the BDao address + * @return bDao The address of the BDao */ - function getBLabs() external view returns (address bLabs); + function getBDao() external view returns (address bDao); } diff --git a/test/manual-smock/MockBCoWFactory.sol b/test/manual-smock/MockBCoWFactory.sol index bf887e7d..c4b2e427 100644 --- a/test/manual-smock/MockBCoWFactory.sol +++ b/test/manual-smock/MockBCoWFactory.sol @@ -5,8 +5,7 @@ import {BCoWFactory, BCoWPool, BFactory, IBCoWFactory, IBPool} from '../../src/c import {Test} from 'forge-std/Test.sol'; contract MockBCoWFactory is BCoWFactory, Test { - constructor(address solutionSettler, bytes32 appData) BCoWFactory(solutionSettler, appData) {} - + // 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)); } @@ -15,10 +14,32 @@ contract MockBCoWFactory is BCoWFactory, Test { vm.expectCall(address(this), abi.encodeWithSignature('APP_DATA()')); } + // BCoWFactory methods + constructor(address solutionSettler, bytes32 appData) BCoWFactory(solutionSettler, appData) {} + function mock_call_logBCoWPool() public { vm.mockCall(address(this), abi.encodeWithSignature('logBCoWPool()'), abi.encode()); } + function mock_call__newBPool(IBPool bCoWPool) public { + vm.mockCall(address(this), abi.encodeWithSignature('_newBPool()'), abi.encode(bCoWPool)); + } + + function _newBPool() internal override returns (IBPool bCoWPool) { + (bool _success, bytes memory _data) = address(this).call(abi.encodeWithSignature('_newBPool()')); + + if (_success) return abi.decode(_data, (IBPool)); + else return super._newBPool(); + } + + function call__newBPool() public returns (IBPool bCoWPool) { + return _newBPool(); + } + + function expectCall__newBPool() public { + vm.expectCall(address(this), abi.encodeWithSignature('_newBPool()')); + } + // MockBFactory methods function set__isBPool(address _key0, bool _value) public { _isBPool[_key0] = _value; @@ -28,20 +49,20 @@ contract MockBCoWFactory is BCoWFactory, Test { return _isBPool[_key0]; } - function set__bLabs(address __bLabs) public { - _bLabs = __bLabs; + function set__bDao(address __bDao) public { + _bDao = __bDao; } - function call__bLabs() public view returns (address) { - return _bLabs; + function call__bDao() public view returns (address) { + return _bDao; } function mock_call_newBPool(IBPool bPool) public { vm.mockCall(address(this), abi.encodeWithSignature('newBPool()'), abi.encode(bPool)); } - function mock_call_setBLabs(address bLabs) public { - vm.mockCall(address(this), abi.encodeWithSignature('setBLabs(address)', bLabs), abi.encode()); + function mock_call_setBDao(address bDao) public { + vm.mockCall(address(this), abi.encodeWithSignature('setBDao(address)', bDao), abi.encode()); } function mock_call_collect(IBPool bPool) public { @@ -52,26 +73,7 @@ contract MockBCoWFactory is BCoWFactory, Test { vm.mockCall(address(this), abi.encodeWithSignature('isBPool(address)', bPool), abi.encode(_returnParam0)); } - function mock_call_getBLabs(address _returnParam0) public { - vm.mockCall(address(this), abi.encodeWithSignature('getBLabs()'), abi.encode(_returnParam0)); - } - - function mock_call__newBPool(IBPool bPool) public { - vm.mockCall(address(this), abi.encodeWithSignature('_newBPool()'), abi.encode(bPool)); - } - - function _newBPool() internal override returns (IBPool bPool) { - (bool _success, bytes memory _data) = address(this).call(abi.encodeWithSignature('_newBPool()')); - - if (_success) return abi.decode(_data, (IBPool)); - else return super._newBPool(); - } - - function call__newBPool() public returns (IBPool bPool) { - return _newBPool(); - } - - function expectCall__newBPool() public { - vm.expectCall(address(this), abi.encodeWithSignature('_newBPool()')); + function mock_call_getBDao(address _returnParam0) public { + vm.mockCall(address(this), abi.encodeWithSignature('getBDao()'), abi.encode(_returnParam0)); } } diff --git a/test/manual-smock/MockBCoWPool.sol b/test/manual-smock/MockBCoWPool.sol index 11eaba95..1b9e7703 100644 --- a/test/manual-smock/MockBCoWPool.sol +++ b/test/manual-smock/MockBCoWPool.sol @@ -33,7 +33,6 @@ contract MockBCoWPool is BCoWPool, Test { } /// MockBCoWPool mock methods - constructor(address cowSolutionSettler, bytes32 appData) BCoWPool(cowSolutionSettler, appData) {} function mock_call_commit(bytes32 orderHash) public { @@ -376,6 +375,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)); } @@ -387,7 +464,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/smock/MockBFactory.sol b/test/smock/MockBFactory.sol index 7689e20e..ab754fa5 100644 --- a/test/smock/MockBFactory.sol +++ b/test/smock/MockBFactory.sol @@ -13,12 +13,12 @@ contract MockBFactory is BFactory, Test { return _isBPool[_key0]; } - function set__bLabs(address __bLabs) public { - _bLabs = __bLabs; + function set__bDao(address __bDao) public { + _bDao = __bDao; } - function call__bLabs() public view returns (address) { - return _bLabs; + function call__bDao() public view returns (address) { + return _bDao; } constructor() BFactory() {} @@ -27,8 +27,8 @@ contract MockBFactory is BFactory, Test { vm.mockCall(address(this), abi.encodeWithSignature('newBPool()'), abi.encode(bPool)); } - function mock_call_setBLabs(address bLabs) public { - vm.mockCall(address(this), abi.encodeWithSignature('setBLabs(address)', bLabs), abi.encode()); + function mock_call_setBDao(address bDao) public { + vm.mockCall(address(this), abi.encodeWithSignature('setBDao(address)', bDao), abi.encode()); } function mock_call_collect(IBPool bPool) public { @@ -39,8 +39,8 @@ contract MockBFactory is BFactory, Test { vm.mockCall(address(this), abi.encodeWithSignature('isBPool(address)', bPool), abi.encode(_returnParam0)); } - function mock_call_getBLabs(address _returnParam0) public { - vm.mockCall(address(this), abi.encodeWithSignature('getBLabs()'), abi.encode(_returnParam0)); + function mock_call_getBDao(address _returnParam0) public { + vm.mockCall(address(this), abi.encodeWithSignature('getBDao()'), abi.encode(_returnParam0)); } function mock_call__newBPool(IBPool bPool) public { diff --git a/test/unit/BCoWFactory.t.sol b/test/unit/BCoWFactory.t.sol index 1ff8070a..9c2bfca2 100644 --- a/test/unit/BCoWFactory.t.sol +++ b/test/unit/BCoWFactory.t.sol @@ -24,15 +24,15 @@ contract BCoWFactoryTest is Test { ); } - function test_ConstructorWhenCalled(address _blabs, address _newSettler, bytes32 _appData) external { - vm.prank(_blabs); + function test_ConstructorWhenCalled(address _bDao, address _newSettler, bytes32 _appData) external { + vm.prank(_bDao); MockBCoWFactory _newFactory = new MockBCoWFactory(_newSettler, _appData); // it should set solution settler assertEq(_newFactory.SOLUTION_SETTLER(), _newSettler); // it should set app data assertEq(_newFactory.APP_DATA(), _appData); - // it should set blabs - assertEq(_newFactory.getBLabs(), _blabs); + // it should set BDao + assertEq(_newFactory.getBDao(), _bDao); } function test__newBPoolWhenCalled() external { diff --git a/test/unit/BCoWFactory.tree b/test/unit/BCoWFactory.tree index 37160019..daa4549a 100644 --- a/test/unit/BCoWFactory.tree +++ b/test/unit/BCoWFactory.tree @@ -2,7 +2,7 @@ BCoWFactoryTest::constructor └── when called ├── it should set solution settler ├── it should set app data - └── it should set blabs + └── it should set BDao BCoWFactoryTest::_newBPool └── when called diff --git a/test/unit/BFactory.t.sol b/test/unit/BFactory.t.sol index b1426432..24e0ed78 100644 --- a/test/unit/BFactory.t.sol +++ b/test/unit/BFactory.t.sol @@ -21,11 +21,11 @@ contract BFactoryTest is Test { factory = new MockBFactory(); } - function test_ConstructorWhenCalled(address _blabs) external { - vm.prank(_blabs); + function test_ConstructorWhenCalled(address _bDao) external { + vm.prank(_bDao); MockBFactory newFactory = new MockBFactory(); - // it should set BLabs - assertEq(newFactory.getBLabs(), _blabs); + // it should set BDao + assertEq(newFactory.getBDao(), _bDao); } function test_NewBPoolWhenCalled(address _deployer, address _newBPool) external { @@ -60,55 +60,52 @@ contract BFactoryTest is Test { assertEq(_newBPool.code, _expectedCode); } - function test_SetBLabsRevertWhen_TheSenderIsNotTheCurrentBLabs(address _caller) external { + function test_SetBDaoRevertWhen_TheSenderIsNotTheCurrentBDao(address _caller) external { vm.assume(_caller != factoryDeployer); // it should revert - vm.expectRevert(IBFactory.BFactory_NotBLabs.selector); + vm.expectRevert(IBFactory.BFactory_NotBDao.selector); vm.prank(_caller); - factory.setBLabs(makeAddr('newBLabs')); + factory.setBDao(makeAddr('newBDao')); } - modifier whenTheSenderIsTheCurrentBLabs() { + modifier whenTheSenderIsTheCurrentBDao() { vm.startPrank(factoryDeployer); _; } - function test_SetBLabsRevertWhen_TheAddressIsZero() external whenTheSenderIsTheCurrentBLabs { + function test_SetBDaoRevertWhen_TheAddressIsZero() external whenTheSenderIsTheCurrentBDao { // it should revert vm.expectRevert(IBFactory.BFactory_AddressZero.selector); - factory.setBLabs(address(0)); + factory.setBDao(address(0)); } - function test_SetBLabsWhenTheAddressIsNotZero(address _newBLabs) external whenTheSenderIsTheCurrentBLabs { - vm.assume(_newBLabs != address(0)); + function test_SetBDaoWhenTheAddressIsNotZero(address _newBDao) external whenTheSenderIsTheCurrentBDao { + vm.assume(_newBDao != address(0)); - // it should emit a BLabsSet event + // it should emit a BDaoSet event vm.expectEmit(address(factory)); - emit IBFactory.LOG_BLABS(factoryDeployer, _newBLabs); + emit IBFactory.LOG_BDAO(factoryDeployer, _newBDao); - factory.setBLabs(_newBLabs); + factory.setBDao(_newBDao); - // it should set the new bLabs address - assertEq(factory.getBLabs(), _newBLabs); + // it should set the new bDao address + assertEq(factory.getBDao(), _newBDao); } - function test_CollectRevertWhen_TheSenderIsNotTheCurrentBLabs(address _caller) external { + function test_CollectRevertWhen_TheSenderIsNotTheCurrentBDao(address _caller) external { vm.assume(_caller != factoryDeployer); // it should revert - vm.expectRevert(IBFactory.BFactory_NotBLabs.selector); + vm.expectRevert(IBFactory.BFactory_NotBDao.selector); vm.prank(_caller); factory.collect(IBPool(makeAddr('pool'))); } - function test_CollectWhenTheSenderIsTheCurrentBLabs(uint256 _factoryBTBalance) - external - whenTheSenderIsTheCurrentBLabs - { + function test_CollectWhenTheSenderIsTheCurrentBDao(uint256 _factoryBTBalance) external whenTheSenderIsTheCurrentBDao { address _mockPool = makeAddr('pool'); vm.mockCall(_mockPool, abi.encodeCall(IERC20.balanceOf, address(factory)), abi.encode(_factoryBTBalance)); vm.mockCall(_mockPool, abi.encodeCall(IERC20.transfer, (factoryDeployer, _factoryBTBalance)), abi.encode(true)); @@ -116,7 +113,7 @@ contract BFactoryTest is Test { // it should get the pool's btoken balance of the factory vm.expectCall(_mockPool, abi.encodeCall(IERC20.balanceOf, address(factory))); - // it should transfer the btoken balance of the factory to BLabs + // it should transfer the btoken balance of the factory to BDao vm.expectCall(_mockPool, abi.encodeCall(IERC20.transfer, (factoryDeployer, _factoryBTBalance))); factory.collect(IBPool(_mockPool)); @@ -124,7 +121,7 @@ contract BFactoryTest is Test { function test_CollectRevertWhen_TheBtokenTransferFails(uint256 _factoryBTBalance) external - whenTheSenderIsTheCurrentBLabs + whenTheSenderIsTheCurrentBDao { address _mockPool = makeAddr('pool'); vm.mockCall(_mockPool, abi.encodeCall(IERC20.balanceOf, address(factory)), abi.encode(_factoryBTBalance)); diff --git a/test/unit/BFactory.tree b/test/unit/BFactory.tree index bd2dce21..02f1fd16 100644 --- a/test/unit/BFactory.tree +++ b/test/unit/BFactory.tree @@ -1,6 +1,6 @@ BFactoryTest::constructor └── when called - └── it should set the deployer as BLabs + └── it should set the deployer as BDao BFactoryTest::newBPool └── when called @@ -14,22 +14,22 @@ BFactoryTest::_newBPool └── when called └── it should deploy a new BPool -BFactoryTest::setBLabs -├── when the sender is not the current BLabs +BFactoryTest::setBDao +├── when the sender is not the current BDao │ └── it should revert -└── when the sender is the current BLabs +└── when the sender is the current BDao ├── when the address is zero │ └── it should revert └── when the address is not zero - ├── it should set the new BLabs address - └── it should emit a BLabsSet event + ├── it should set the new BDao address + └── it should emit a BDaoSet event BFactoryTest::collect -├── when the sender is not the current BLabs +├── when the sender is not the current BDao │ └── it should revert -└── when the sender is the current BLabs +└── when the sender is the current BDao ├── it should get the pool's btoken balance of the factory - ├── it should transfer the btoken balance of the factory to BLabs + ├── it should transfer the btoken balance of the factory to BDao └── when the btoken transfer fails └── it should revert From b1ba43a2e38c10642113e075918807962c559f88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wei=C3=9Fer=20Hase?= Date: Tue, 9 Jul 2024 17:10:56 +0200 Subject: [PATCH 26/41] fix: gas snapshot --- .forge-snapshots/newBFactory.snap | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.forge-snapshots/newBFactory.snap b/.forge-snapshots/newBFactory.snap index 95e984ca..c54b6046 100644 --- a/.forge-snapshots/newBFactory.snap +++ b/.forge-snapshots/newBFactory.snap @@ -1 +1 @@ -4130621 \ No newline at end of file +4130633 \ No newline at end of file From 9ec1c349b01918940da9e33e8700c7c81857ba2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wei=C3=9Fer=20Hase?= Date: Wed, 10 Jul 2024 15:12:39 +0200 Subject: [PATCH 27/41] refactor: reordered helper flow and cleanup --- src/contracts/BCoWHelper.sol | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/contracts/BCoWHelper.sol b/src/contracts/BCoWHelper.sol index fcd0f811..340f793b 100644 --- a/src/contracts/BCoWHelper.sol +++ b/src/contracts/BCoWHelper.sol @@ -3,7 +3,6 @@ pragma solidity 0.8.25; import {IBCoWFactory} from 'interfaces/IBCoWFactory.sol'; import {IBCoWPool} from 'interfaces/IBCoWPool.sol'; -import {IBFactory} from 'interfaces/IBFactory.sol'; import {ICOWAMMPoolHelper} from '@cow-amm/interfaces/ICOWAMMPoolHelper.sol'; @@ -25,8 +24,9 @@ contract BCoWHelper is ICOWAMMPoolHelper { /// @notice The app data used by this helper's factory. bytes32 public immutable APP_DATA; - /// @notice The factory contract to which this helper provides support. - address public factory; + /// @inheritdoc ICOWAMMPoolHelper + // solhint-disable-next-line style-guide-casing + address public immutable factory; constructor(address factory_) { factory = factory_; @@ -63,15 +63,16 @@ contract BCoWHelper is ICOWAMMPoolHelper { order_ = GetTradeableOrder.getTradeableOrder(params); + // 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); - // 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. - sig = abi.encodePacked(pool, eip1271sig); - preInteractions = new GPv2Interaction.Data[](1); preInteractions[0] = GPv2Interaction.Data({ target: pool, @@ -85,7 +86,7 @@ contract BCoWHelper is ICOWAMMPoolHelper { /// @inheritdoc ICOWAMMPoolHelper function tokens(address pool) public view returns (address[] memory tokens_) { // reverts in case pool is not deployed by the helper's factory - if (!IBFactory(factory).isBPool(pool)) revert PoolDoesNotExist(); + 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) From 05d6271077bb331ee4d213468d4e01267a245799 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wei=C3=9Fer=20Hase?= Date: Thu, 11 Jul 2024 13:36:29 +0200 Subject: [PATCH 28/41] feat: vendoring GetTradeableOrder from cow-amm --- package.json | 2 +- remappings.txt | 1 + src/contracts/BCoWHelper.sol | 4 +- src/libraries/GetTradeableOrder.sol | 99 ----------------------------- yarn.lock | 4 +- 5 files changed, 5 insertions(+), 105 deletions(-) delete mode 100644 src/libraries/GetTradeableOrder.sol diff --git a/package.json b/package.json index 0a5496d8..80bb0bda 100644 --- a/package.json +++ b/package.json @@ -42,7 +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#2b5f973", + "cow-amm": "github:cowprotocol/cow-amm.git#6566128", "solmate": "github:transmissions11/solmate#c892309" }, "devDependencies": { diff --git a/remappings.txt b/remappings.txt index 42bb073a..08fc002a 100644 --- a/remappings.txt +++ b/remappings.txt @@ -6,6 +6,7 @@ solmate/=node_modules/solmate/src 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 diff --git a/src/contracts/BCoWHelper.sol b/src/contracts/BCoWHelper.sol index 340f793b..7bfb651b 100644 --- a/src/contracts/BCoWHelper.sol +++ b/src/contracts/BCoWHelper.sol @@ -5,14 +5,12 @@ 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'; -// TODO: vendor from cow-amm -import {GetTradeableOrder} from 'libraries/GetTradeableOrder.sol'; - /** * @title BCoWHelper * @notice Helper contract that allows to trade on CoW Swap Protocol. diff --git a/src/libraries/GetTradeableOrder.sol b/src/libraries/GetTradeableOrder.sol deleted file mode 100644 index 239352e4..00000000 --- a/src/libraries/GetTradeableOrder.sol +++ /dev/null @@ -1,99 +0,0 @@ -// SPDX-License-Identifier: LGPL-3.0-only -pragma solidity ^0.8.24; - -import {IERC20} from '@cowprotocol/interfaces/IERC20.sol'; -import {GPv2Order} from '@cowprotocol/libraries/GPv2Order.sol'; -import {Math} from '@openzeppelin/contracts/utils/math/Math.sol'; - -// TODO: vendor library from cowprotocol/cow-amm repo when available -library GetTradeableOrder { - /** - * @notice Parameters for the `getTradeableOrder` function. - * @param pool The address of the pool to query. - * @param token0 The first token of the pool. - * @param token1 The second token of the pool. - * @param priceNumerator The numerator of the pricing vector. - * @param priceDenominator The denominator of the pricing vector. - * @param appData The app data identifier to include in the order. - * @dev Avoid stack too deep errors with `getTradeableOrder`. - */ - struct GetTradeableOrderParams { - address pool; - IERC20 token0; - IERC20 token1; - /// @dev The numerator of the price, expressed in amount of token1 per - /// amount of token0. For example, if token0 is DAI and the price is - /// 1 WETH (token1) for 3000 DAI, then this could be 1 (and the - /// denominator would be 3000). - uint256 priceNumerator; - /// @dev The denominator of the price, expressed in amount of token1 per - /// amount of token0. For example, if token0 is DAI and the price is - /// 1 WETH (token1) for 3000 DAI, then this could be 3000 (and the - /// denominator would be 1). - uint256 priceDenominator; - bytes32 appData; - } - - /// @notice The largest possible duration of any AMM order, starting from the current block timestamp. - uint32 public constant MAX_ORDER_DURATION = 5 * 60; - - /** - * @notice Method to calculate the canonical order of a pool given a pricing vector. - * @param params Encoded parameters for the order. - * @return order_ The canonical order of the pool. - */ - function getTradeableOrder(GetTradeableOrderParams memory params) - internal - view - returns (GPv2Order.Data memory order_) - { - (uint256 selfReserve0, uint256 selfReserve1) = - (params.token0.balanceOf(params.pool), params.token1.balanceOf(params.pool)); - - IERC20 sellToken; - IERC20 buyToken; - uint256 sellAmount; - uint256 buyAmount; - // Note on rounding: we want to round down the sell amount and up the - // buy amount. This is because the math for the order makes it lie - // precisely on the AMM curve, and a rounding error to the other way - // could cause a valid order to become invalid. - // Note on the if condition: it guarantees that sellAmount is positive - // in the corresponding branch (it would be negative in the other). This - // excludes rounding errors: in this case, the function could revert but - // the amounts involved would be just a few atoms, so we accept that no - // order will be available. - // Note on the order price: The buy amount is not optimal for the AMM - // given the sell amount. This is intended because we want to force - // solvers to maximize the surplus for this order with the price that - // isn't the AMM best price. - uint256 selfReserve0TimesPriceDenominator = selfReserve0 * params.priceDenominator; - uint256 selfReserve1TimesPriceNumerator = selfReserve1 * params.priceNumerator; - if (selfReserve1TimesPriceNumerator < selfReserve0TimesPriceDenominator) { - sellToken = params.token0; - buyToken = params.token1; - sellAmount = selfReserve0 / 2 - Math.ceilDiv(selfReserve1TimesPriceNumerator, 2 * params.priceDenominator); - buyAmount = Math.mulDiv(sellAmount, selfReserve1, selfReserve0 - sellAmount, Math.Rounding.Ceil); - } else { - sellToken = params.token1; - buyToken = params.token0; - sellAmount = selfReserve1 / 2 - Math.ceilDiv(selfReserve0TimesPriceDenominator, 2 * params.priceNumerator); - buyAmount = Math.mulDiv(sellAmount, selfReserve0, selfReserve1 - sellAmount, Math.Rounding.Ceil); - } - - order_ = GPv2Order.Data( - sellToken, - buyToken, - GPv2Order.RECEIVER_SAME_AS_OWNER, - sellAmount, - buyAmount, - uint32(block.timestamp) + MAX_ORDER_DURATION, - params.appData, - 0, - GPv2Order.KIND_SELL, - true, - GPv2Order.BALANCE_ERC20, - GPv2Order.BALANCE_ERC20 - ); - } -} diff --git a/yarn.lock b/yarn.lock index 2aaafe54..bba6419e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -603,9 +603,9 @@ cosmiconfig@^9.0.0: js-yaml "^4.1.0" parse-json "^5.2.0" -"cow-amm@github:cowprotocol/cow-amm.git#2b5f973": +"cow-amm@github:cowprotocol/cow-amm.git#6566128": version "0.0.0" - resolved "https://codeload.github.com/cowprotocol/cow-amm/tar.gz/2b5f9733ef7df905ce73fe2bed1be5f6cf407afc" + resolved "https://codeload.github.com/cowprotocol/cow-amm/tar.gz/6566128b6c73008062cf4a6d1957db602409b719" cross-spawn@^7.0.3: version "7.0.3" From 9923fc0692cf8750a860f1cf74b9ab1dc775fe92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wei=C3=9Fer=20Hase?= Date: Thu, 11 Jul 2024 13:40:10 +0200 Subject: [PATCH 29/41] feat: improving commitment expectation --- test/unit/BCoWHelper.t.sol | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/unit/BCoWHelper.t.sol b/test/unit/BCoWHelper.t.sol index 23af0299..065b4b1a 100644 --- a/test/unit/BCoWHelper.t.sol +++ b/test/unit/BCoWHelper.t.sol @@ -143,7 +143,8 @@ contract BCoWHelperTest is Test { assertEq(preInteractions.length, 1); assertEq(preInteractions[0].target, address(pool)); assertEq(preInteractions[0].value, 0); - assertEq(bytes4(preInteractions[0].callData), IBCoWPool.commit.selector); + 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); From 699996e95591703358523ff1624440a146376975 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wei=C3=9Fer=20Hase?= Date: Thu, 11 Jul 2024 18:48:12 +0200 Subject: [PATCH 30/41] fix: typos in comments --- test/integration/BCoWHelper.t.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/integration/BCoWHelper.t.sol b/test/integration/BCoWHelper.t.sol index f441ac86..3b541afb 100644 --- a/test/integration/BCoWHelper.t.sol +++ b/test/integration/BCoWHelper.t.sol @@ -76,7 +76,7 @@ contract ConstantProductHelperForkedTest is Test { vm.stopPrank(); } - // NOTE: 1 ETH = 1000e6 DAI + // NOTE: 1 ETH = 1000 DAI uint256 constant INITIAL_SPOT_PRICE = 0.001e18; function testBasicOrder() public { @@ -157,7 +157,7 @@ contract ConstantProductHelperForkedTest is Test { assertEq(preInteractions.length, 1); assertEq(postInteractions.length, 0); - // Because of how we changed the price, we expect to buy DAI + // Because of how we changed the price, we expect to buy WETH assertEq(address(ammOrder.sellToken), address(DAI)); assertEq(address(ammOrder.buyToken), address(WETH)); From 1bec1fdba511c0934e5712595359ffe5d51d67a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wei=C3=9Fer=20Hase?= Date: Mon, 15 Jul 2024 15:59:18 +0200 Subject: [PATCH 31/41] 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 --- src/contracts/BCoWHelper.sol | 24 +++++++++++++++++++++- test/integration/BCoWHelper.t.sol | 33 +++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 1 deletion(-) diff --git a/src/contracts/BCoWHelper.sol b/src/contracts/BCoWHelper.sol index 7bfb651b..d2e53534 100644 --- a/src/contracts/BCoWHelper.sol +++ b/src/contracts/BCoWHelper.sol @@ -11,12 +11,14 @@ 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 { +contract BCoWHelper is ICOWAMMPoolHelper, BMath { using GPv2Order for GPv2Order.Data; /// @notice The app data used by this helper's factory. @@ -61,6 +63,26 @@ contract BCoWHelper is ICOWAMMPoolHelper { 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; diff --git a/test/integration/BCoWHelper.t.sol b/test/integration/BCoWHelper.t.sol index 3b541afb..ceadecce 100644 --- a/test/integration/BCoWHelper.t.sol +++ b/test/integration/BCoWHelper.t.sol @@ -97,6 +97,39 @@ contract ConstantProductHelperForkedTest is Test { assertEq(postSpotPrice, 1_052_631_578_947_368); } + // NOTE: this test checks that the order generated by the helper is valid + function testValidOrder(uint256 balanceToken0, uint256 balanceToken1, uint256 priceSkewness) public { + IBCoWPool pool = IBCoWPool(address(basicPool)); + balanceToken0 = bound(balanceToken0, 1e18, 1e36); + balanceToken1 = bound(balanceToken1, 1e18, 1e36); + // skew the price by max 50% (more could result in reverts bc of max swap ratio) + priceSkewness = bound(priceSkewness, 5000, 15_000); + vm.assume(priceSkewness != 10_000); // avoids no-skewness revert + + vm.mockCall( + address(DAI), abi.encodeWithSelector(IERC20.balanceOf.selector, address(pool)), abi.encode(balanceToken0) + ); + vm.mockCall( + address(WETH), abi.encodeWithSelector(IERC20.balanceOf.selector, address(pool)), abi.encode(balanceToken1) + ); + + uint256[] memory prices = new uint256[](2); + prices[0] = balanceToken1; + prices[1] = balanceToken0 * priceSkewness / 10_000; + + // the helper generates the AMM order + (GPv2Order.Data memory ammOrder,,,) = helper.order(address(pool), prices); + + if (priceSkewness > 10_000) { + assertEq(address(ammOrder.buyToken), address(DAI)); + } else { + assertEq(address(ammOrder.buyToken), address(WETH)); + } + + // it should not revert + pool.verify(ammOrder); + } + // NOTE: reverting test, weighted pools are not supported function testWeightedOrder() public { IBCoWPool pool = IBCoWPool(address(weightedPool)); From 1ffda8b3b466b9b3ed21025547f40fb758ef748e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wei=C3=9Fer=20Hase?= Date: Tue, 16 Jul 2024 18:36:14 +0200 Subject: [PATCH 32/41] chore: addressing contract changes from PR comments --- src/contracts/BCoWHelper.sol | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/contracts/BCoWHelper.sol b/src/contracts/BCoWHelper.sol index d2e53534..09a5b20f 100644 --- a/src/contracts/BCoWHelper.sol +++ b/src/contracts/BCoWHelper.sol @@ -22,7 +22,7 @@ contract BCoWHelper is ICOWAMMPoolHelper, BMath { using GPv2Order for GPv2Order.Data; /// @notice The app data used by this helper's factory. - bytes32 public immutable APP_DATA; + bytes32 internal immutable _APP_DATA; /// @inheritdoc ICOWAMMPoolHelper // solhint-disable-next-line style-guide-casing @@ -30,7 +30,7 @@ contract BCoWHelper is ICOWAMMPoolHelper, BMath { constructor(address factory_) { factory = factory_; - APP_DATA = IBCoWFactory(factory_).APP_DATA(); + _APP_DATA = IBCoWFactory(factory_).APP_DATA(); } /// @inheritdoc ICOWAMMPoolHelper @@ -58,7 +58,7 @@ contract BCoWHelper is ICOWAMMPoolHelper, BMath { // expressed the other way around. priceNumerator: prices[1], priceDenominator: prices[0], - appData: APP_DATA + appData: _APP_DATA }); order_ = GetTradeableOrder.getTradeableOrder(params); @@ -106,16 +106,20 @@ contract BCoWHelper is ICOWAMMPoolHelper, BMath { /// @inheritdoc ICOWAMMPoolHelper function tokens(address pool) public view returns (address[] memory tokens_) { // reverts in case pool is not deployed by the helper's factory - if (!IBCoWFactory(factory).isBPool(pool)) revert PoolDoesNotExist(); + 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(); + 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(); } - - return tokens_; } } From 063d3f64f5aeb4c2a846a6f52e77be722337071c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wei=C3=9Fer=20Hase?= Date: Tue, 16 Jul 2024 18:51:34 +0200 Subject: [PATCH 33/41] chore: addressing comments in tests --- test/integration/BCoWHelper.t.sol | 8 ++++---- test/unit/BCoWHelper.t.sol | 10 +++++----- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/test/integration/BCoWHelper.t.sol b/test/integration/BCoWHelper.t.sol index ceadecce..b256128a 100644 --- a/test/integration/BCoWHelper.t.sol +++ b/test/integration/BCoWHelper.t.sol @@ -38,12 +38,15 @@ contract ConstantProductHelperForkedTest is Test { IBPool private basicPool; IERC20 private constant DAI = IERC20(0x6B175474E89094C44Da98b954EedeAC495271d0F); - IERC20 private constant USDC = IERC20(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48); IERC20 private constant WETH = IERC20(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); + // NOTE: minimum balance amount of any token to initialize a pool with uint256 constant VALID_AMOUNT = 1e6; uint256 constant TEN_PERCENT = 0.1 ether; + // NOTE: 1 ETH = 1000 DAI + uint256 constant INITIAL_SPOT_PRICE = 0.001e18; + function setUp() public { vm.createSelectFork('mainnet', 20_012_063); @@ -76,9 +79,6 @@ contract ConstantProductHelperForkedTest is Test { vm.stopPrank(); } - // NOTE: 1 ETH = 1000 DAI - uint256 constant INITIAL_SPOT_PRICE = 0.001e18; - function testBasicOrder() public { IBCoWPool pool = IBCoWPool(address(basicPool)); diff --git a/test/unit/BCoWHelper.t.sol b/test/unit/BCoWHelper.t.sol index 065b4b1a..61d9885b 100644 --- a/test/unit/BCoWHelper.t.sol +++ b/test/unit/BCoWHelper.t.sol @@ -1,8 +1,8 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity 0.8.25; -import {BCoWHelper} from 'contracts/BCoWHelper.sol'; 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'; @@ -18,7 +18,7 @@ import {MockBCoWFactory} from 'test/manual-smock/MockBCoWFactory.sol'; import {MockBCoWPool} from 'test/manual-smock/MockBCoWPool.sol'; contract BCoWHelperTest is Test { - BCoWHelper helper; + MockBCoWHelper helper; MockBCoWFactory factory; MockBCoWPool pool; @@ -57,17 +57,17 @@ contract BCoWHelperTest is Test { vm.mockCall(tokens[1], abi.encodePacked(IERC20.balanceOf.selector), abi.encode(priceVector[1])); factory.mock_call_APP_DATA(bytes32('appData')); - helper = new BCoWHelper(address(factory)); + helper = new MockBCoWHelper(address(factory)); } function test_ConstructorWhenCalled(bytes32 _appData) external { factory.expectCall_APP_DATA(); factory.mock_call_APP_DATA(_appData); - helper = new BCoWHelper(address(factory)); + helper = new MockBCoWHelper(address(factory)); // it should set factory assertEq(helper.factory(), address(factory)); // it should set app data from factory - assertEq(helper.APP_DATA(), _appData); + assertEq(helper.call__APP_DATA(), _appData); } function test_TokensRevertWhen_PoolIsNotRegisteredInFactory() external { From e0de51873affd207d08c2681798ba2aee3ae79e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wei=C3=9Fer=20Hase?= Date: Tue, 16 Jul 2024 18:54:30 +0200 Subject: [PATCH 34/41] fix: adding helper mock --- test/manual-smock/MockBCoWHelper.sol | 44 ++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 test/manual-smock/MockBCoWHelper.sol diff --git a/test/manual-smock/MockBCoWHelper.sol b/test/manual-smock/MockBCoWHelper.sol new file mode 100644 index 00000000..54158906 --- /dev/null +++ b/test/manual-smock/MockBCoWHelper.sol @@ -0,0 +1,44 @@ +// 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; + } + + // 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_)); + } +} From d0bba5c3379906f739b24fd869d323cc2cc80d15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wei=C3=9Fer=20Hase?= Date: Tue, 16 Jul 2024 21:40:13 +0200 Subject: [PATCH 35/41] fix: addressing comments from PR --- test/integration/BCoWHelper.t.sol | 83 +++++++++++++------------------ test/unit/BCoWHelper.t.sol | 26 +++++++++- test/unit/BCoWHelper.tree | 14 +++--- 3 files changed, 66 insertions(+), 57 deletions(-) diff --git a/test/integration/BCoWHelper.t.sol b/test/integration/BCoWHelper.t.sol index b256128a..cb83561c 100644 --- a/test/integration/BCoWHelper.t.sol +++ b/test/integration/BCoWHelper.t.sol @@ -40,11 +40,10 @@ contract ConstantProductHelperForkedTest is Test { IERC20 private constant DAI = IERC20(0x6B175474E89094C44Da98b954EedeAC495271d0F); IERC20 private constant WETH = IERC20(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); - // NOTE: minimum balance amount of any token to initialize a pool with - uint256 constant VALID_AMOUNT = 1e6; 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; function setUp() public { @@ -55,26 +54,27 @@ contract ConstantProductHelperForkedTest is Test { ammFactory = new BCoWFactory(address(settlement), bytes32('appData')); helper = new BCoWHelper(address(ammFactory)); - deal(address(DAI), lp, 2 * VALID_AMOUNT); - deal(address(WETH), lp, 2 * VALID_AMOUNT); + deal(address(DAI), lp, type(uint256).max, false); + deal(address(WETH), lp, type(uint256).max, false); vm.startPrank(lp); - weightedPool = ammFactory.newBPool(); basicPool = ammFactory.newBPool(); - - DAI.approve(address(weightedPool), type(uint256).max); - WETH.approve(address(weightedPool), type(uint256).max); - weightedPool.bind(address(DAI), VALID_AMOUNT, 8e18); // 80% weight - weightedPool.bind(address(WETH), VALID_AMOUNT, 2e18); // 20% weight + weightedPool = ammFactory.newBPool(); DAI.approve(address(basicPool), type(uint256).max); WETH.approve(address(basicPool), type(uint256).max); - basicPool.bind(address(DAI), VALID_AMOUNT, 4.2e18); // no weight - basicPool.bind(address(WETH), VALID_AMOUNT, 4.2e18); // no weight + 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 - weightedPool.finalize(); basicPool.finalize(); + weightedPool.finalize(); vm.stopPrank(); } @@ -82,40 +82,26 @@ contract ConstantProductHelperForkedTest is Test { function testBasicOrder() public { IBCoWPool pool = IBCoWPool(address(basicPool)); - uint256 ammWethInitialBalance = 1 ether; - uint256 ammDaiInitialBalance = 1000 ether; - - deal(address(WETH), address(pool), ammWethInitialBalance); - deal(address(DAI), address(pool), ammDaiInitialBalance); - uint256 spotPrice = pool.getSpotPriceSansFee(address(WETH), address(DAI)); assertEq(spotPrice, INITIAL_SPOT_PRICE); - _executeHelperOrder(pool, ammWethInitialBalance, ammDaiInitialBalance); + _executeHelperOrder(pool); uint256 postSpotPrice = pool.getSpotPriceSansFee(address(WETH), address(DAI)); assertEq(postSpotPrice, 1_052_631_578_947_368); } // NOTE: this test checks that the order generated by the helper is valid - function testValidOrder(uint256 balanceToken0, uint256 balanceToken1, uint256 priceSkewness) public { + function testValidOrder(uint256 priceSkewness) public { IBCoWPool pool = IBCoWPool(address(basicPool)); - balanceToken0 = bound(balanceToken0, 1e18, 1e36); - balanceToken1 = bound(balanceToken1, 1e18, 1e36); + // skew the price by max 50% (more could result in reverts bc of max swap ratio) priceSkewness = bound(priceSkewness, 5000, 15_000); vm.assume(priceSkewness != 10_000); // avoids no-skewness revert - vm.mockCall( - address(DAI), abi.encodeWithSelector(IERC20.balanceOf.selector, address(pool)), abi.encode(balanceToken0) - ); - vm.mockCall( - address(WETH), abi.encodeWithSelector(IERC20.balanceOf.selector, address(pool)), abi.encode(balanceToken1) - ); - uint256[] memory prices = new uint256[](2); - prices[0] = balanceToken1; - prices[1] = balanceToken0 * priceSkewness / 10_000; + prices[0] = INITIAL_WETH_BALANCE; + prices[1] = INITIAL_DAI_BALANCE * priceSkewness / 10_000; // the helper generates the AMM order (GPv2Order.Data memory ammOrder,,,) = helper.order(address(pool), prices); @@ -138,7 +124,6 @@ contract ConstantProductHelperForkedTest is Test { uint256 ammDaiInitialBalance = 1000 ether; deal(address(WETH), address(pool), ammWethInitialBalance); - // NOTE: pool is 80-20 DAI-WETH, has 4xDAI balance than basic, same spot price deal(address(DAI), address(pool), 4 * ammDaiInitialBalance); uint256 spotPrice = pool.getSpotPriceSansFee(address(WETH), address(DAI)); @@ -153,19 +138,13 @@ contract ConstantProductHelperForkedTest is Test { // assertEq(postSpotPrice, 1_052_631_578_947_368); } - function addressVecToIerc20Vec(address[] memory addrVec) private pure returns (IERC20[] memory ierc20vec) { - assembly { - ierc20vec := addrVec - } - } - - function _executeHelperOrder(IBPool pool, uint256 ammWethInitialBalance, uint256 ammDaiInitialBalance) internal { - IERC20[] memory tokens = addressVecToIerc20Vec(helper.tokens(address(pool))); + function _executeHelperOrder(IBPool pool) internal { + address[] memory tokens = helper.tokens(address(pool)); uint256 daiIndex = 0; uint256 wethIndex = 1; assertEq(tokens.length, 2); - assertEq(address(tokens[daiIndex]), address(DAI)); - assertEq(address(tokens[wethIndex]), address(WETH)); + 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 @@ -176,8 +155,8 @@ contract ConstantProductHelperForkedTest is Test { // 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] = ammWethInitialBalance; - prices[wethIndex] = ammDaiInitialBalance * 95 / 100; + prices[daiIndex] = INITIAL_WETH_BALANCE; + prices[wethIndex] = INITIAL_DAI_BALANCE * 95 / 100; // The helper generates the AMM order GPv2Order.Data memory ammOrder; @@ -197,8 +176,8 @@ contract ConstantProductHelperForkedTest is Test { // 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, ammDaiInitialBalance * 25 / 1000, TEN_PERCENT); - assertApproxEqRel(ammOrder.buyAmount, ammWethInitialBalance * 25 / 1000, TEN_PERCENT); + 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); @@ -222,8 +201,14 @@ contract ConstantProductHelperForkedTest is Test { interactions[0][0] = preInteractions[0]; + // cast tokens array to IERC20 array + IERC20[] memory ierc20vec; + assembly { + ierc20vec := tokens + } + // finally, settle vm.prank(solver); - settlement.settle(tokens, prices, trades, interactions); + settlement.settle(ierc20vec, prices, trades, interactions); } } diff --git a/test/unit/BCoWHelper.t.sol b/test/unit/BCoWHelper.t.sol index 61d9885b..7eeffcc2 100644 --- a/test/unit/BCoWHelper.t.sol +++ b/test/unit/BCoWHelper.t.sol @@ -51,7 +51,7 @@ contract BCoWHelperTest is Test { pool.set__finalized(true); priceVector[0] = 1e18; - priceVector[1] = 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])); @@ -71,9 +71,10 @@ contract BCoWHelperTest is Test { } function test_TokensRevertWhen_PoolIsNotRegisteredInFactory() external { + factory.mock_call_isBPool(address(pool), false); // it should revert vm.expectRevert(ICOWAMMPoolHelper.PoolDoesNotExist.selector); - helper.tokens(invalidPool); + helper.tokens(address(pool)); } function test_TokensRevertWhen_PoolHasLessThan2Tokens() external { @@ -153,4 +154,25 @@ contract BCoWHelperTest is Test { bytes memory validSig = abi.encodePacked(pool, abi.encode(order_)); assertEq(keccak256(validSig), keccak256(sig)); } + + function test_OrderGivenAPriceVector(uint256 priceSkewness, uint256 balanceToken0, uint256 balanceToken1) external { + // skew the price by max 50% (more could result in reverts bc of max swap ratio) + priceSkewness = bound(priceSkewness, 5000, 15_000); + vm.assume(priceSkewness != 10_000); // avoids no-skewness revert + + balanceToken0 = bound(balanceToken0, 1e18, 1e36); + balanceToken1 = bound(balanceToken1, 1e18, 1e36); + vm.mockCall(tokens[0], abi.encodePacked(IERC20.balanceOf.selector), abi.encode(balanceToken0)); + vm.mockCall(tokens[1], abi.encodePacked(IERC20.balanceOf.selector), abi.encode(balanceToken1)); + + uint256[] memory prices = new uint256[](2); + prices[0] = balanceToken1; + prices[1] = balanceToken0 * priceSkewness / 10_000; + + // it should return a valid pool order + (GPv2Order.Data memory ammOrder,,,) = helper.order(address(pool), prices); + + // this call should not revert + pool.verify(ammOrder); + } } diff --git a/test/unit/BCoWHelper.tree b/test/unit/BCoWHelper.tree index b4686c03..ebbb7387 100644 --- a/test/unit/BCoWHelper.tree +++ b/test/unit/BCoWHelper.tree @@ -18,9 +18,11 @@ BCoWHelperTest::tokens BCoWHelperTest::order ├── when the pool is not supported │ └── it should revert -└── when the pool is supported - ├── 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 +├── when the pool is supported +│ ├── 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 vector + └── it should return a valid pool order From d8c49e66968b04b65fe6b4fde9cd3b5e10b0e4df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wei=C3=9Fer=20Hase?= Date: Tue, 16 Jul 2024 22:02:55 +0200 Subject: [PATCH 36/41] refactor: deprecate fuzzed integration valid order test in favour of unit test --- test/integration/BCoWHelper.t.sol | 25 ------------------------- test/unit/BCoWHelper.t.sol | 2 ++ 2 files changed, 2 insertions(+), 25 deletions(-) diff --git a/test/integration/BCoWHelper.t.sol b/test/integration/BCoWHelper.t.sol index cb83561c..efb84907 100644 --- a/test/integration/BCoWHelper.t.sol +++ b/test/integration/BCoWHelper.t.sol @@ -91,31 +91,6 @@ contract ConstantProductHelperForkedTest is Test { assertEq(postSpotPrice, 1_052_631_578_947_368); } - // NOTE: this test checks that the order generated by the helper is valid - function testValidOrder(uint256 priceSkewness) public { - IBCoWPool pool = IBCoWPool(address(basicPool)); - - // skew the price by max 50% (more could result in reverts bc of max swap ratio) - priceSkewness = bound(priceSkewness, 5000, 15_000); - vm.assume(priceSkewness != 10_000); // avoids no-skewness revert - - uint256[] memory prices = new uint256[](2); - prices[0] = INITIAL_WETH_BALANCE; - prices[1] = INITIAL_DAI_BALANCE * priceSkewness / 10_000; - - // the helper generates the AMM order - (GPv2Order.Data memory ammOrder,,,) = helper.order(address(pool), prices); - - if (priceSkewness > 10_000) { - assertEq(address(ammOrder.buyToken), address(DAI)); - } else { - assertEq(address(ammOrder.buyToken), address(WETH)); - } - - // it should not revert - pool.verify(ammOrder); - } - // NOTE: reverting test, weighted pools are not supported function testWeightedOrder() public { IBCoWPool pool = IBCoWPool(address(weightedPool)); diff --git a/test/unit/BCoWHelper.t.sol b/test/unit/BCoWHelper.t.sol index 7eeffcc2..d776e012 100644 --- a/test/unit/BCoWHelper.t.sol +++ b/test/unit/BCoWHelper.t.sol @@ -172,6 +172,8 @@ contract BCoWHelperTest is Test { // it should return a valid pool order (GPv2Order.Data memory ammOrder,,,) = helper.order(address(pool), prices); + assertEq(address(ammOrder.buyToken), priceSkewness > 10_000 ? tokens[0] : tokens[1]); + // this call should not revert pool.verify(ammOrder); } From abe6f000706eeb1b206a4f6b93b660555800987c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wei=C3=9Fer=20Hase?= Date: Wed, 17 Jul 2024 16:10:14 +0200 Subject: [PATCH 37/41] feat: simplifying helper integration test --- test/integration/BCoWHelper.t.sol | 20 ++++++-------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/test/integration/BCoWHelper.t.sol b/test/integration/BCoWHelper.t.sol index efb84907..7c1fc536 100644 --- a/test/integration/BCoWHelper.t.sol +++ b/test/integration/BCoWHelper.t.sol @@ -20,7 +20,7 @@ import {GPv2TradeEncoder} from '@composable-cow/test/vendored/GPv2TradeEncoder.s import {BCoWFactory} from 'contracts/BCoWFactory.sol'; import {BCoWHelper} from 'contracts/BCoWHelper.sol'; -contract ConstantProductHelperForkedTest is Test { +contract BCoWHelperIntegrationTest is Test { using GPv2Order for GPv2Order.Data; BCoWHelper private helper; @@ -46,6 +46,9 @@ contract ConstantProductHelperForkedTest is Test { 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); @@ -88,29 +91,18 @@ contract ConstantProductHelperForkedTest is Test { _executeHelperOrder(pool); uint256 postSpotPrice = pool.getSpotPriceSansFee(address(WETH), address(DAI)); - assertEq(postSpotPrice, 1_052_631_578_947_368); + assertEq(postSpotPrice, EXPECTED_FINAL_SPOT_PRICE); } // NOTE: reverting test, weighted pools are not supported function testWeightedOrder() public { IBCoWPool pool = IBCoWPool(address(weightedPool)); - uint256 ammWethInitialBalance = 1 ether; - uint256 ammDaiInitialBalance = 1000 ether; - - deal(address(WETH), address(pool), ammWethInitialBalance); - deal(address(DAI), address(pool), 4 * ammDaiInitialBalance); - 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)); - - // NOTE: not supported - // _executeHelperOrder(pool, ammWethInitialBalance, ammDaiInitialBalance); - // uint256 postSpotPrice = pool.getSpotPriceSansFee(address(WETH), address(DAI)); - // assertEq(postSpotPrice, 1_052_631_578_947_368); } function _executeHelperOrder(IBPool pool) internal { @@ -131,7 +123,7 @@ contract ConstantProductHelperForkedTest is Test { // 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 * 95 / 100; + prices[wethIndex] = INITIAL_DAI_BALANCE * SKEWENESS_RATIO / 100; // The helper generates the AMM order GPv2Order.Data memory ammOrder; From b2056be22704e2222cd15076f0dd3416503dae26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wei=C3=9Fer=20Hase?= Date: Wed, 17 Jul 2024 16:12:52 +0200 Subject: [PATCH 38/41] feat: improving unit test for valid order --- test/unit/BCoWHelper.t.sol | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/test/unit/BCoWHelper.t.sol b/test/unit/BCoWHelper.t.sol index d776e012..871b66f9 100644 --- a/test/unit/BCoWHelper.t.sol +++ b/test/unit/BCoWHelper.t.sol @@ -157,8 +157,9 @@ contract BCoWHelperTest is Test { function test_OrderGivenAPriceVector(uint256 priceSkewness, uint256 balanceToken0, uint256 balanceToken1) external { // skew the price by max 50% (more could result in reverts bc of max swap ratio) - priceSkewness = bound(priceSkewness, 5000, 15_000); - vm.assume(priceSkewness != 10_000); // avoids no-skewness revert + uint256 base = 1e18; + priceSkewness = bound(priceSkewness, 0.5e18, 1.5e18); + vm.assume(priceSkewness != base); // avoids no-skewness revert balanceToken0 = bound(balanceToken0, 1e18, 1e36); balanceToken1 = bound(balanceToken1, 1e18, 1e36); @@ -167,12 +168,12 @@ contract BCoWHelperTest is Test { uint256[] memory prices = new uint256[](2); prices[0] = balanceToken1; - prices[1] = balanceToken0 * priceSkewness / 10_000; + prices[1] = balanceToken0 * priceSkewness / base; // it should return a valid pool order (GPv2Order.Data memory ammOrder,,,) = helper.order(address(pool), prices); - assertEq(address(ammOrder.buyToken), priceSkewness > 10_000 ? tokens[0] : tokens[1]); + assertEq(address(ammOrder.buyToken), priceSkewness > 1e18 ? tokens[0] : tokens[1]); // this call should not revert pool.verify(ammOrder); From ffb1bc0e4939360a7fb66bc57a5ab5a126d1d359 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wei=C3=9Fer=20Hase?= Date: Thu, 18 Jul 2024 12:10:43 +0200 Subject: [PATCH 39/41] feat: adding call tokens expectation --- src/contracts/BCoWHelper.sol | 2 +- test/manual-smock/MockBCoWHelper.sol | 13 +++++++++++++ test/unit/BCoWHelper.t.sol | 4 ++++ test/unit/BCoWHelper.tree | 1 + 4 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/contracts/BCoWHelper.sol b/src/contracts/BCoWHelper.sol index 09a5b20f..4562bcda 100644 --- a/src/contracts/BCoWHelper.sol +++ b/src/contracts/BCoWHelper.sol @@ -104,7 +104,7 @@ contract BCoWHelper is ICOWAMMPoolHelper, BMath { } /// @inheritdoc ICOWAMMPoolHelper - function tokens(address pool) public view returns (address[] memory tokens_) { + 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(); diff --git a/test/manual-smock/MockBCoWHelper.sol b/test/manual-smock/MockBCoWHelper.sol index 54158906..003b4ce2 100644 --- a/test/manual-smock/MockBCoWHelper.sol +++ b/test/manual-smock/MockBCoWHelper.sol @@ -20,6 +20,19 @@ contract MockBCoWHelper is BCoWHelper, Test { 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_) {} diff --git a/test/unit/BCoWHelper.t.sol b/test/unit/BCoWHelper.t.sol index 871b66f9..276cd68a 100644 --- a/test/unit/BCoWHelper.t.sol +++ b/test/unit/BCoWHelper.t.sol @@ -120,6 +120,10 @@ contract BCoWHelperTest is Test { } 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); diff --git a/test/unit/BCoWHelper.tree b/test/unit/BCoWHelper.tree index ebbb7387..1cb8d994 100644 --- a/test/unit/BCoWHelper.tree +++ b/test/unit/BCoWHelper.tree @@ -19,6 +19,7 @@ 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 From 7719c39d429e66f8c2ee2d41543df6110c0a8cb3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wei=C3=9Fer=20Hase?= Date: Mon, 22 Jul 2024 11:33:05 +0200 Subject: [PATCH 40/41] fix: branching different behaviours given skewness --- test/unit/BCoWHelper.t.sol | 47 +++++++++++++++++++++++++++++++++----- test/unit/BCoWHelper.tree | 6 ++++- 2 files changed, 46 insertions(+), 7 deletions(-) diff --git a/test/unit/BCoWHelper.t.sol b/test/unit/BCoWHelper.t.sol index 276cd68a..095a5fce 100644 --- a/test/unit/BCoWHelper.t.sol +++ b/test/unit/BCoWHelper.t.sol @@ -27,6 +27,7 @@ contract BCoWHelperTest is Test { uint256[] priceVector = new uint256[](2); uint256 constant VALID_WEIGHT = 1e18; + uint256 constant BASE = 1e18; function setUp() external { factory = new MockBCoWFactory(address(0), bytes32(0)); @@ -159,11 +160,14 @@ contract BCoWHelperTest is Test { assertEq(keccak256(validSig), keccak256(sig)); } - function test_OrderGivenAPriceVector(uint256 priceSkewness, uint256 balanceToken0, uint256 balanceToken1) external { + 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) - uint256 base = 1e18; - priceSkewness = bound(priceSkewness, 0.5e18, 1.5e18); - vm.assume(priceSkewness != base); // avoids no-skewness revert + // avoids no-skewness revert + priceSkewness = bound(priceSkewness, BASE + 1, 1.5e18); balanceToken0 = bound(balanceToken0, 1e18, 1e36); balanceToken1 = bound(balanceToken1, 1e18, 1e36); @@ -172,13 +176,44 @@ contract BCoWHelperTest is Test { uint256[] memory prices = new uint256[](2); prices[0] = balanceToken1; - prices[1] = balanceToken0 * priceSkewness / base; + prices[1] = balanceToken0 * priceSkewness / BASE; // it should return a valid pool order (GPv2Order.Data memory ammOrder,,,) = helper.order(address(pool), prices); - assertEq(address(ammOrder.buyToken), priceSkewness > 1e18 ? tokens[0] : tokens[1]); + // 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_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, 0.5e18, BASE - 1); + + balanceToken0 = bound(balanceToken0, 1e18, 1e36); + balanceToken1 = bound(balanceToken1, 1e18, 1e36); + vm.mockCall(tokens[0], abi.encodePacked(IERC20.balanceOf.selector), abi.encode(balanceToken0)); + vm.mockCall(tokens[1], abi.encodePacked(IERC20.balanceOf.selector), abi.encode(balanceToken1)); + + 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 index 1cb8d994..47ccf744 100644 --- a/test/unit/BCoWHelper.tree +++ b/test/unit/BCoWHelper.tree @@ -25,5 +25,9 @@ BCoWHelperTest::order │ ├── it should return a commit pre-interaction │ ├── it should return an empty post-interaction │ └── it should return a valid signature -└── given a price vector +├── given a price skeweness to token0 +│ ├── it should buy token0 +│ └── it should return a valid pool order +└── given a price skeweness to token1 + ├── it should buy token1 └── it should return a valid pool order From 7db4e8e12a5f343bcd3cdd659bac6bbaf74e6adf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wei=C3=9Fer=20Hase?= Date: Mon, 22 Jul 2024 15:58:46 +0200 Subject: [PATCH 41/41] fix: adding comments on the skewness sign --- test/unit/BCoWHelper.t.sol | 18 ++++++++++-------- test/unit/BCoWHelper.tree | 4 ++-- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/test/unit/BCoWHelper.t.sol b/test/unit/BCoWHelper.t.sol index 095a5fce..ef6d03b5 100644 --- a/test/unit/BCoWHelper.t.sol +++ b/test/unit/BCoWHelper.t.sol @@ -160,20 +160,21 @@ contract BCoWHelperTest is Test { assertEq(keccak256(validSig), keccak256(sig)); } - function test_OrderGivenAPriceSkewenessToToken0( + 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 + 1, 1.5e18); + priceSkewness = bound(priceSkewness, BASE + 0.0001e18, 1.5e18); - balanceToken0 = bound(balanceToken0, 1e18, 1e36); - balanceToken1 = bound(balanceToken1, 1e18, 1e36); + 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; @@ -189,20 +190,21 @@ contract BCoWHelperTest is Test { pool.verify(ammOrder); } - function test_OrderGivenAPriceSkewenessToToken1( + 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 - 1); + priceSkewness = bound(priceSkewness, 0.5e18, BASE - 0.0001e18); - balanceToken0 = bound(balanceToken0, 1e18, 1e36); - balanceToken1 = bound(balanceToken1, 1e18, 1e36); + 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; diff --git a/test/unit/BCoWHelper.tree b/test/unit/BCoWHelper.tree index 47ccf744..e27a5207 100644 --- a/test/unit/BCoWHelper.tree +++ b/test/unit/BCoWHelper.tree @@ -25,9 +25,9 @@ BCoWHelperTest::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 token0 +├── given a price skeweness to token1 │ ├── it should buy token0 │ └── it should return a valid pool order -└── given a price skeweness to token1 +└── given a price skeweness to token0 ├── it should buy token1 └── it should return a valid pool order